diff --git a/src/client/assets/icons/account_tree.tsx b/src/client/assets/icons/account_tree.tsx new file mode 100644 index 0000000..f9584fc --- /dev/null +++ b/src/client/assets/icons/account_tree.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +interface IconProps { + size?: number; + color?: string; +} + +const AccountTreeIcon: React.FC = ({ size = 18, color = '#2B2542' }) => { + return ( + + + + + + + + + ); +}; + +export default AccountTreeIcon; \ No newline at end of file diff --git a/src/client/assets/icons/cached.tsx b/src/client/assets/icons/cached.tsx new file mode 100644 index 0000000..c313de0 --- /dev/null +++ b/src/client/assets/icons/cached.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +interface IconProps { + size?: number; + color?: string; +} + +const CachedIcon: React.FC = ({ size = 18, color = '#2B2542' }) => { + return ( + + + + + + + + + ); +}; + +export default CachedIcon; \ No newline at end of file diff --git a/src/client/assets/icons/folder_open.tsx b/src/client/assets/icons/folder_open.tsx new file mode 100644 index 0000000..867cbe1 --- /dev/null +++ b/src/client/assets/icons/folder_open.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +interface IconProps { + size?: number; + color?: string; +} + +const FolderOpenIcon: React.FC = ({ size = 18, color = '#2B2542' }) => { + return ( + + + + + + + + + ); +}; + +export default FolderOpenIcon; \ No newline at end of file diff --git a/src/client/assets/icons/index.ts b/src/client/assets/icons/index.ts new file mode 100644 index 0000000..5e5f9ca --- /dev/null +++ b/src/client/assets/icons/index.ts @@ -0,0 +1,4 @@ +export { default as AccountTreeIcon } from './account_tree'; +export { default as FolderOpenIcon } from './folder_open'; +export { default as CachedIcon } from './cached'; +export { default as LogoutIcon } from './logout'; \ No newline at end of file diff --git a/src/client/assets/icons/logout.tsx b/src/client/assets/icons/logout.tsx new file mode 100644 index 0000000..02cc88c --- /dev/null +++ b/src/client/assets/icons/logout.tsx @@ -0,0 +1,12 @@ +export default function LogoutIcon() { + return ( + + + + + + + + + ); +} \ No newline at end of file diff --git a/src/client/components/button.module.css b/src/client/components/button.module.css index 4143a3b..b1d10f8 100644 --- a/src/client/components/button.module.css +++ b/src/client/components/button.module.css @@ -1,14 +1,44 @@ +.buttonPrimary { + font-family: 'Recursive', sans-serif; + font-weight: 600; + font-size: 16px; + line-height: 24px; + letter-spacing: 0%; + width: 100%; + height: 36px; + padding: 6px 16px; + outline: none; + border-radius: 8px; + border: none; + background-color: #BEDDE3; + cursor: pointer; + color: #1E1A2E; + display: flex; + align-items: center; + justify-content: flex-start; + vertical-align: middle; +} + +.buttonPrimary:hover { + background-color: #A5D0D7; + color: #1E1A2E; + transition: all 0.2s; +} + .button { - font-family: 'Commissioner', sans-serif; - font-size: 14px; - line-height: 1.25rem; + font-family: 'Recursive', sans-serif; + font-weight: 600; + font-size: 16px; + line-height: 24px; + letter-spacing: 0%; padding: 0.5rem; outline: none; border-radius: 0.375rem; border: none; background-color: #E80962; cursor: pointer; - color: white; + color: white; + vertical-align: middle; } .button:hover { @@ -31,8 +61,41 @@ border-color: rgb(207, 207, 211); } -.flexContainer { +.buttonLogout { + font-family: 'Recursive', sans-serif; + font-weight: 500; + font-size: 14px; + line-height: 24px; + letter-spacing: 0%; + width: 100%; + height: 52px; + padding: 14px; + outline: none; + border-radius: 6px; + border: none; + background-color: transparent; + cursor: pointer; + color: #444746; display: flex; align-items: center; + justify-content: flex-start; gap: 8px; + opacity: 1; + vertical-align: middle; + transition: all 0.2s ease; } + +.buttonLogout:hover { + background-color: rgba(68, 71, 70, 0.1); + color: #1E1A2E; + transition: all 0.2s ease; +} + +.buttonLogout:active { + background-color: rgba(68, 71, 70, 0.2); + color: #1E1A2E; + transform: translateY(1px); + transition: all 0.1s ease; +} + + diff --git a/src/client/components/button.tsx b/src/client/components/button.tsx index 117e8ea..74e041c 100644 --- a/src/client/components/button.tsx +++ b/src/client/components/button.tsx @@ -8,6 +8,8 @@ interface ButtonProps { disabled?: boolean; style?: React.CSSProperties; loading?: boolean; + variant?: 'primary' | 'secondary' | 'logout'; + icon?: React.ReactNode; } const Button: FunctionComponent = ({ @@ -16,18 +18,26 @@ const Button: FunctionComponent = ({ disabled, style, loading = false, + variant = 'secondary', + icon, ...rest }) => { + const getButtonClassName = () => { + if (variant === 'primary') return styles.buttonPrimary; + if (variant === 'logout') return styles.buttonLogout; + return styles.button; + }; + return ( diff --git a/src/client/sidebar/components/Sidebar.tsx b/src/client/sidebar/components/Sidebar.tsx index c5109b7..1818d8e 100644 --- a/src/client/sidebar/components/Sidebar.tsx +++ b/src/client/sidebar/components/Sidebar.tsx @@ -4,19 +4,17 @@ import { Container, Typography, Button as MuiButton, - Divider, Box, - Tabs, - Tab, AlertColor, } from '@mui/material'; import { serverFunctions } from '../../utils/serverFunctions'; -import { buildUrl } from '../../utils/helpers'; +import { buildUrl, compressBase64Image } from '../../utils/helpers'; import useAuth from '../../hooks/useAuth'; import Button from '../../components/button'; import { showAlertDialog } from '../../utils/alert'; import analytics from '../../../analytics/analytics'; import Toast from '../../components/toast'; +import { AccountTreeIcon, FolderOpenIcon, CachedIcon, LogoutIcon } from '../../assets/icons'; @@ -27,6 +25,8 @@ interface ChartImage { const Sidebar = () => { const [tab, setTab] = useState(0); + const [tabRefreshCount, setTabRefreshCount] = useState(0); + const [iframeLoading, setIframeLoading] = useState(true); const [overlayEnabled, setOverlayEnabled] = useState(false); const intervalRef = useRef(null); const [diagramsUrl, setDiagramsUrl] = useState(''); @@ -35,6 +35,8 @@ const Sidebar = () => { const [createDiagramState, setCreateDiagramState] = useState('idle'); const [selectDiagramState, setSelectDiagramState] = useState('idle'); const [updateDiagramsState, setUpdateDiagramsState] = useState('idle'); + const [editingDiagram, setEditingDiagram] = useState(null); + const [removingDiagram, setRemovingDiagram] = useState(null); const { authState, authStatus, getAuth, signOut } = useAuth(); // State for background insertion processing @@ -42,6 +44,7 @@ const Sidebar = () => { const [toastOpen, setToastOpen] = useState(false); const [toastMessage, setToastMessage] = useState(''); const [toastSeverity, setToastSeverity] = useState('success'); + const [logoutLoading, setLogoutLoading] = useState(false); const insertionPollingRef = useRef(null); useEffect(() => { @@ -51,6 +54,7 @@ const Sidebar = () => { authState.token ); setDiagramsUrl(url); + setIframeLoading(true); if (intervalRef.current !== null) { clearInterval(intervalRef?.current); intervalRef.current = null; @@ -91,7 +95,7 @@ const Sidebar = () => { if (result.success) { setToastMessage('Diagram inserted successfully!'); setToastSeverity('success'); - getImages(); + getImages(); } else { setToastMessage(result.message || 'Error inserting diagram'); setToastSeverity('error'); @@ -127,11 +131,14 @@ const Sidebar = () => { setToastSeverity('info'); setToastOpen(true); + // Ensure image is compressed (in case it wasn't compressed before sending) + const compressedImage = await compressBase64Image(data.image); + if (data.operation === 'replace') { - await serverFunctions.replaceSelectedImageWithBase64AndSize(data.image, data.metadata); + await serverFunctions.replaceSelectedImageWithBase64AndSize(compressedImage, data.metadata); setToastMessage('Diagram updated successfully!'); } else { - await serverFunctions.insertBase64ImageWithMetadata(data.image, data.metadata); + await serverFunctions.insertBase64ImageWithMetadata(compressedImage, data.metadata); setToastMessage('Diagram inserted successfully!'); } @@ -180,6 +187,7 @@ const Sidebar = () => { const actionData = e.data; if (action === 'save') { + console.log('Received save action from dialog'); const data = actionData.data; if (!data) return; const metadata = new URLSearchParams({ @@ -189,14 +197,28 @@ const Sidebar = () => { minor: data.minor, }); try { + setIsProcessingInsertion(true); + setToastMessage('Inserting diagram...'); + setToastSeverity('info'); + setToastOpen(true); + + const compressedImage = await compressBase64Image(data.diagramImage); await serverFunctions.insertBase64ImageWithMetadata( - data.diagramImage, + compressedImage, metadata.toString() ); + console.log('Diagram inserted successfully from dialog'); + + setToastMessage('Diagram inserted successfully!'); + setToastSeverity('success'); getImages(); } catch (error) { console.error('Error inserting image with metadata', error); + setToastMessage('Error inserting diagram'); + setToastSeverity('error'); showAlertDialog('Error inserting image, please try again'); + } finally { + setIsProcessingInsertion(false); } return; } @@ -230,7 +252,20 @@ const Sidebar = () => { }; }, [getImages]); + const handleLogout = async () => { + setLogoutLoading(true); + try { + await signOut(); + } finally { + setLogoutLoading(false); + } + }; + const handleTabSwitch = (tabIndex: number) => { + if (tabIndex === 0 && tab !== 0) { + setTabRefreshCount((prev) => prev + 1); + setIframeLoading(true); + } setTab(tabIndex); if (chartImagesState !== 'loading') { getImages(); @@ -248,7 +283,7 @@ const Sidebar = () => { options += ',left=' + left; try { - analytics.trackLogin(); + analytics.trackLogin(); const authUrl = await serverFunctions.getOAuthURL(); const windowObjectReference = window.open( authUrl, @@ -277,7 +312,7 @@ const Sidebar = () => { }; const handleSelectDiagram = async () => { - analytics.trackBrowseDiagram(); + analytics.trackBrowseDiagram(); try { setSelectDiagramState('loading'); await serverFunctions.openSelectDiagramDialog(); @@ -313,16 +348,20 @@ const Sidebar = () => { const handleEditDiagram = async (altDescription: string) => { analytics.trackEditDiagram(); try { + setEditingDiagram(altDescription); await serverFunctions.selectChartImage(altDescription); await serverFunctions.openEditDiagramDialog(); } catch (error) { console.error('Error editing diagram', error); showAlertDialog('Error editing diagram, please try again'); + } finally { + setEditingDiagram(null); } }; const handleRemoveDiagram = async (altDescription: string) => { try { + setRemovingDiagram(altDescription); const result = await serverFunctions.removeDiagramByAltDescription( altDescription ); @@ -335,6 +374,8 @@ const Sidebar = () => { } catch (error) { console.error('Error removing diagram', error); showAlertDialog('Error removing diagram, please try again'); + } finally { + setRemovingDiagram(null); } }; @@ -345,7 +386,8 @@ const Sidebar = () => { display: 'flex', alignItems: 'center', justifyContent: 'center', - height: 'calc(100vh - 114px)', + height: '100vh', + overflow: 'hidden', }} > @@ -361,7 +403,8 @@ const Sidebar = () => { flexDirection: 'column', alignItems: 'center', justifyContent: 'center', - height: 'calc(100vh - 114px)', + height: '100vh', + overflow: 'hidden', }} > @@ -375,7 +418,7 @@ const Sidebar = () => { } return ( - <> +
{(overlayEnabled || isProcessingInsertion) && ( { )} )} - - - - -
+
{!authState?.authorized ? ( { sx={{ maxWidth: '344px', fontFamily: 'Recursive', - fontSize: '20px', + fontSize: '24px', marginTop: '12px', - fontWeight: 420, + fontWeight: 600, color: '#1E1A2E', marginBottom: '14px', - lineHeight: '28px', + lineHeight: '36px', letterSpacing: 'normal', }} > - Welcome to
- the Mermaid + Welcome to the
+ official Mermaid Plugin { marginBottom: '28px', }} > - Create and edit diagrams in Mermaid Chart and easily synchronize + Create and edit diagrams in Mermaid and easily synchronize documents with Google Docs. @@ -497,125 +533,187 @@ const Sidebar = () => { }, }} > - Login - - - - Don’t have an account? - - - - window.open( - 'https://mermaidchart.com/app/sign-up', - '_blank' - ) - } - sx={{ - textTransform: 'none', - color: '#0071e3', - padding: 0, - minWidth: 'auto', - fontSize: '14px', - marginTop: '4px', - fontFamily: - 'Recursive, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - '&:hover': { - textDecoration: 'underline', - background: 'none', - }, - }} - > - Sign up + Sign in
) : ( <> - - Create a new diagram - - - Insert a diagram from Mermaid Chart - - - Update all diagrams in document to most recent version - - - - handleTabSwitch(newValue)} + + + handleTabSwitch(0)} + sx={{ + flex: '0 0 auto', + padding: '8px 32px', + cursor: 'pointer', + fontSize: '13px', + fontWeight: tab === 0 ? 600 : 400, + backgroundColor: tab === 0 ? '#FFFFFF' : 'transparent', + color: tab === 0 ? '#1E1A2E' : '#666', + borderRadius: '6px', + transition: 'all 0.2s ease', + fontFamily: 'Recursive', + lineHeight: '20px', + textAlign: 'center', + whiteSpace: 'nowrap', + overflow: 'hidden', + boxShadow: tab === 0 ? '0 2px 4px rgba(0,0,0,0.1)' : 'none', + '&:hover': { + backgroundColor: tab === 0 ? '#FFFFFF' : 'rgba(255,255,255,0.5)', + color: '#1E1A2E' + } + }} > - - - + Recent + + handleTabSwitch(1)} + sx={{ + flex: '1 1 auto', + padding: '8px 8px', + cursor: 'pointer', + fontSize: '13px', + fontWeight: tab === 1 ? 600 : 400, + backgroundColor: tab === 1 ? '#FFFFFF' : 'transparent', + color: tab === 1 ? '#1E1A2E' : '#666', + borderRadius: '6px', + transition: 'all 0.2s ease', + fontFamily: 'Recursive', + lineHeight: '20px', + textAlign: 'center', + whiteSpace: 'nowrap', + overflow: 'hidden', + boxShadow: tab === 1 ? '0 2px 4px rgba(0,0,0,0.1)' : 'none', + '&:hover': { + backgroundColor: tab === 1 ? '#FFFFFF' : 'rgba(255,255,255,0.5)', + color: '#1E1A2E' + } + }} + > + In this Document + + + {tab === 0 && iframeLoading && ( + + + + Loading recent diagrams... + + + )}