mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
feat: single page sharing support (#1805)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
40
packages/component/src/components/share-menu/Export.tsx
Normal file
40
packages/component/src/components/share-menu/Export.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ContentParser } from '@blocksuite/blocks/content-parser';
|
||||
import type { FC } from 'react';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { Button } from '../..';
|
||||
import type { ShareMenuProps } from './index';
|
||||
import { actionsStyle, descriptionStyle, menuItemStyle } from './index.css';
|
||||
|
||||
export const Export: FC<ShareMenuProps> = props => {
|
||||
const contentParserRef = useRef<ContentParser>();
|
||||
return (
|
||||
<div className={menuItemStyle}>
|
||||
<div className={descriptionStyle}>
|
||||
Download a static copy of your page to share with others.
|
||||
</div>
|
||||
<div className={actionsStyle}>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!contentParserRef.current) {
|
||||
contentParserRef.current = new ContentParser(props.currentPage);
|
||||
}
|
||||
return contentParserRef.current.onExportHtml();
|
||||
}}
|
||||
>
|
||||
Export to HTML
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (!contentParserRef.current) {
|
||||
contentParserRef.current = new ContentParser(props.currentPage);
|
||||
}
|
||||
return contentParserRef.current.onExportMarkdown();
|
||||
}}
|
||||
>
|
||||
Export to Markdown
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
70
packages/component/src/components/share-menu/SharePage.tsx
Normal file
70
packages/component/src/components/share-menu/SharePage.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { getEnvironment } from '@affine/env';
|
||||
import type { LocalWorkspace } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import { useBlockSuiteWorkspacePageIsPublic } from '@toeverything/hooks/use-blocksuite-workspace-page-is-public';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { Button } from '../..';
|
||||
import type { ShareMenuProps } from './index';
|
||||
import { buttonStyle, descriptionStyle, menuItemStyle } from './index.css';
|
||||
|
||||
export const LocalSharePage: FC<ShareMenuProps> = props => {
|
||||
return (
|
||||
<div className={menuItemStyle}>
|
||||
<div className={descriptionStyle}>
|
||||
Sharing page publicly requires AFFiNE Cloud service.
|
||||
</div>
|
||||
<Button
|
||||
data-testid="share-menu-enable-affine-cloud-button"
|
||||
className={buttonStyle}
|
||||
type="light"
|
||||
shape="round"
|
||||
onClick={() => {
|
||||
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
|
||||
}}
|
||||
>
|
||||
Enable AFFiNE Cloud
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AffineSharePage: FC<ShareMenuProps> = props => {
|
||||
const [isPublic, setIsPublic] = useBlockSuiteWorkspacePageIsPublic(
|
||||
props.currentPage
|
||||
);
|
||||
const sharingUrl = useMemo(() => {
|
||||
const env = getEnvironment();
|
||||
if (env.isBrowser) {
|
||||
return `${env.origin}/public-workspace/${props.workspace.id}/${props.currentPage.id}`;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}, [props.workspace.id, props.currentPage.id]);
|
||||
const onClickCreateLink = useCallback(() => {
|
||||
setIsPublic(true);
|
||||
}, [isPublic]);
|
||||
const onClickCopyLink = useCallback(() => {
|
||||
navigator.clipboard.writeText(sharingUrl);
|
||||
}, []);
|
||||
return (
|
||||
<div className={menuItemStyle}>
|
||||
<div className={descriptionStyle}>
|
||||
Create a link you can easily share with anyone.
|
||||
</div>
|
||||
<span>{isPublic ? sharingUrl : 'not public'}</span>
|
||||
{!isPublic && <Button onClick={onClickCreateLink}>Create</Button>}
|
||||
{isPublic && <Button onClick={onClickCopyLink}>Copy Link</Button>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SharePage: FC<ShareMenuProps> = props => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return <LocalSharePage {...props} />;
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||
return <AffineSharePage {...props} />;
|
||||
}
|
||||
throw new Error('Unreachable');
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { Button } from '../..';
|
||||
import type { ShareMenuProps } from '.';
|
||||
import { buttonStyle, descriptionStyle, menuItemStyle } from './index.css';
|
||||
|
||||
const ShareLocalWorkspace: FC<ShareMenuProps<LocalWorkspace>> = props => {
|
||||
return (
|
||||
<div className={menuItemStyle}>
|
||||
<div className={descriptionStyle}>
|
||||
Sharing page publicly requires AFFiNE Cloud service.
|
||||
</div>
|
||||
<Button
|
||||
data-testid="share-menu-enable-affine-cloud-button"
|
||||
className={buttonStyle}
|
||||
type="light"
|
||||
shape="circle"
|
||||
onClick={() => {
|
||||
props.onEnableAffineCloud(props.workspace as LocalWorkspace);
|
||||
}}
|
||||
>
|
||||
Enable AFFiNE Cloud
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ShareAffineWorkspace: FC<ShareMenuProps<AffineWorkspace>> = props => {
|
||||
const isPublicWorkspace = props.workspace.public;
|
||||
return (
|
||||
<div className={menuItemStyle}>
|
||||
<div className={descriptionStyle}>
|
||||
{isPublicWorkspace
|
||||
? `Current workspace has been published to the web as a public workspace.`
|
||||
: `Invite others to join the Workspace or publish it to web`}
|
||||
</div>
|
||||
<Button
|
||||
data-testid="share-menu-publish-to-web-button"
|
||||
onClick={() => {
|
||||
props.onOpenWorkspaceSettings(props.workspace);
|
||||
}}
|
||||
type="light"
|
||||
shape="circle"
|
||||
>
|
||||
Open Workspace Settings
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ShareWorkspace: FC<ShareMenuProps> = props => {
|
||||
if (props.workspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||
return (
|
||||
<ShareLocalWorkspace {...(props as ShareMenuProps<LocalWorkspace>)} />
|
||||
);
|
||||
} else if (props.workspace.flavour === WorkspaceFlavour.AFFINE) {
|
||||
return (
|
||||
<ShareAffineWorkspace {...(props as ShareMenuProps<AffineWorkspace>)} />
|
||||
);
|
||||
}
|
||||
throw new Error('Unreachable');
|
||||
};
|
||||
34
packages/component/src/components/share-menu/index.css.ts
Normal file
34
packages/component/src/components/share-menu/index.css.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const tabStyle = style({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
alignItems: 'center',
|
||||
position: 'relative',
|
||||
marginTop: '4px',
|
||||
marginLeft: '10px',
|
||||
marginRight: '10px',
|
||||
});
|
||||
|
||||
export const menuItemStyle = style({
|
||||
marginLeft: '20px',
|
||||
marginRight: '20px',
|
||||
marginTop: '30px',
|
||||
});
|
||||
|
||||
export const descriptionStyle = style({
|
||||
fontSize: '1rem',
|
||||
});
|
||||
|
||||
export const buttonStyle = style({
|
||||
marginTop: '18px',
|
||||
// todo: new color scheme should be used
|
||||
});
|
||||
|
||||
export const actionsStyle = style({
|
||||
display: 'flex',
|
||||
gap: '9px',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'start',
|
||||
});
|
||||
100
packages/component/src/components/share-menu/index.tsx
Normal file
100
packages/component/src/components/share-menu/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { AffineWorkspace, LocalWorkspace } from '@affine/workspace/type';
|
||||
import { ExportIcon } from '@blocksuite/icons';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import type { FC } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { Menu } from '../..';
|
||||
import { Export } from './Export';
|
||||
import { tabStyle } from './index.css';
|
||||
import { SharePage } from './SharePage';
|
||||
import { ShareWorkspace } from './ShareWorkspace';
|
||||
import { StyledIndicator, StyledShareButton, TabItem } from './styles';
|
||||
|
||||
type SharePanel = 'SharePage' | 'Export' | 'ShareWorkspace';
|
||||
const MenuItems: Record<SharePanel, FC<ShareMenuProps>> = {
|
||||
SharePage: SharePage,
|
||||
Export: Export,
|
||||
ShareWorkspace: ShareWorkspace,
|
||||
};
|
||||
|
||||
export type ShareMenuProps<
|
||||
Workspace extends AffineWorkspace | LocalWorkspace =
|
||||
| AffineWorkspace
|
||||
| LocalWorkspace
|
||||
> = {
|
||||
workspace: Workspace;
|
||||
currentPage: Page;
|
||||
onEnableAffineCloud: (workspace: LocalWorkspace) => void;
|
||||
onOpenWorkspaceSettings: (workspace: Workspace) => void;
|
||||
togglePagePublic: (page: Page, publish: boolean) => Promise<void>;
|
||||
toggleWorkspacePublish: (
|
||||
workspace: Workspace,
|
||||
publish: boolean
|
||||
) => Promise<void>;
|
||||
};
|
||||
|
||||
export const ShareMenu: FC<ShareMenuProps> = props => {
|
||||
const [activeItem, setActiveItem] = useState<SharePanel>('SharePage');
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleMenuChange = useCallback((selectedItem: SharePanel) => {
|
||||
setActiveItem(selectedItem);
|
||||
}, []);
|
||||
|
||||
const ActiveComponent = MenuItems[activeItem];
|
||||
interface ShareMenuProps {
|
||||
activeItem: SharePanel;
|
||||
onChangeTab: (selectedItem: SharePanel) => void;
|
||||
}
|
||||
const ShareMenu: FC<ShareMenuProps> = ({ activeItem, onChangeTab }) => {
|
||||
const handleButtonClick = (itemName: SharePanel) => {
|
||||
onChangeTab(itemName);
|
||||
setActiveItem(itemName);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={tabStyle}>
|
||||
{Object.keys(MenuItems).map(item => (
|
||||
<TabItem
|
||||
isActive={activeItem === item}
|
||||
key={item}
|
||||
onClick={() => handleButtonClick(item as SharePanel)}
|
||||
>
|
||||
{item}
|
||||
</TabItem>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const activeIndex = Object.keys(MenuItems).indexOf(activeItem);
|
||||
const Share = (
|
||||
<>
|
||||
<ShareMenu activeItem={activeItem} onChangeTab={handleMenuChange} />
|
||||
<StyledIndicator activeIndex={activeIndex} />
|
||||
<ActiveComponent {...props} />
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<Menu
|
||||
content={Share}
|
||||
visible={open}
|
||||
width={439}
|
||||
placement="bottom-end"
|
||||
trigger={['click']}
|
||||
disablePortal={true}
|
||||
onClickAway={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<StyledShareButton
|
||||
data-testid="share-menu-button"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
>
|
||||
<ExportIcon />
|
||||
<div>Share</div>
|
||||
</StyledShareButton>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
64
packages/component/src/components/share-menu/styles.ts
Normal file
64
packages/component/src/components/share-menu/styles.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { displayFlex, styled, TextButton } from '../..';
|
||||
|
||||
export const StyledShareButton = styled(TextButton)(({ theme }) => {
|
||||
return {
|
||||
padding: '4px 8px',
|
||||
marginLeft: '4px',
|
||||
marginRight: '16px',
|
||||
border: `1px solid ${theme.colors.primaryColor}`,
|
||||
color: theme.colors.primaryColor,
|
||||
borderRadius: '8px',
|
||||
span: {
|
||||
...displayFlex('center', 'center'),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const StyledTabsWrapper = styled('div')(() => {
|
||||
return {
|
||||
...displayFlex('space-around', 'center'),
|
||||
position: 'relative',
|
||||
};
|
||||
});
|
||||
|
||||
export const TabItem = styled('li')<{ isActive?: boolean }>(
|
||||
({ theme, isActive }) => {
|
||||
{
|
||||
return {
|
||||
...displayFlex('center', 'center'),
|
||||
width: 'calc(100% / 3)',
|
||||
height: '34px',
|
||||
color: theme.colors.textColor,
|
||||
opacity: isActive ? 1 : 0.2,
|
||||
fontWeight: '500',
|
||||
fontSize: theme.font.h6,
|
||||
lineHeight: theme.font.lineHeight,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.15s ease',
|
||||
':after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
bottom: '-2px',
|
||||
left: '-2px',
|
||||
width: 'calc(100% + 4px)',
|
||||
height: '2px',
|
||||
background: theme.colors.textColor,
|
||||
opacity: 0.2,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
export const StyledIndicator = styled('div')<{ activeIndex: number }>(
|
||||
({ theme, activeIndex }) => {
|
||||
return {
|
||||
height: '2px',
|
||||
margin: '0 10px',
|
||||
background: theme.colors.textColor,
|
||||
position: 'absolute',
|
||||
left: `calc(${activeIndex * 100}% / 3)`,
|
||||
width: `calc(100% / 3)`,
|
||||
transition: 'left .3s, width .3s',
|
||||
};
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user