some mobile & bundle optimizations (#262)

This commit is contained in:
Guy Ben-Aharon
2024-10-08 16:14:10 +03:00
committed by GitHub
parent 72a8a5fc9c
commit 689f589b10
19 changed files with 548 additions and 339 deletions

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@ dist-ssr
*.sw?
.env
stats/

View File

@@ -22,6 +22,7 @@
property="og:image"
content="https://app.chartdb.io/ChartDB.png"
/>
<meta property="og:url" content="https://app.chartdb.io" />
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image" />
<meta

86
package-lock.json generated
View File

@@ -83,6 +83,7 @@
"rollup-plugin-visualizer": "^5.12.0",
"tailwindcss": "^3.4.7",
"typescript": "^5.2.2",
"unplugin-inject-preload": "^3.0.0",
"vite": "^5.3.4"
}
},
@@ -7844,6 +7845,29 @@
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -10276,6 +10300,51 @@
"dev": true,
"license": "MIT"
},
"node_modules/unplugin": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.14.1.tgz",
"integrity": "sha512-lBlHbfSFPToDYp9pjXlUEFVxYLaue9f9T1HC+4OHlmj+HnMDdz9oZY+erXfoCe/5V/7gKUSY2jpXPb9S7f0f/w==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.12.1",
"webpack-virtual-modules": "^0.6.2"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"webpack-sources": "^3"
},
"peerDependenciesMeta": {
"webpack-sources": {
"optional": true
}
}
},
"node_modules/unplugin-inject-preload": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/unplugin-inject-preload/-/unplugin-inject-preload-3.0.0.tgz",
"integrity": "sha512-VwHhjdaGo/CISu5ZZhlN74n3ioUjYGgWBwVwzpQjiCybusZajbT+vsL88sxK/xkH5Ypn2QUc1FA01Ne8K6TJHQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-types": "^2.1.35",
"unplugin": "^1.12.2",
"webpack-sources": "^3.2.3"
},
"engines": {
"node": "^14.18.0 || >=16.0.0"
},
"peerDependencies": {
"html-webpack-plugin": ">=5.0.0"
},
"peerDependenciesMeta": {
"html-webpack-plugin": {
"optional": true
}
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
@@ -10479,6 +10548,23 @@
}
}
},
"node_modules/webpack-sources": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/webpack-virtual-modules": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz",
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true,
"license": "MIT"
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -87,6 +87,7 @@
"rollup-plugin-visualizer": "^5.12.0",
"tailwindcss": "^3.4.7",
"typescript": "^5.2.2",
"unplugin-inject-preload": "^3.0.0",
"vite": "^5.3.4"
}
}

View File

@@ -1,27 +1,30 @@
import { cn } from '@/lib/utils';
import React from 'react';
import { CopyBlock, atomOneDark } from 'react-code-blocks';
import type { CodeBlockProps } from 'react-code-blocks/dist/components/CodeBlock';
import React, { Suspense } from 'react';
import type { CopyBlockProps } from 'react-code-blocks/dist/components/CopyBlock';
import { Spinner } from '../spinner/spinner';
export interface CodeSnippetProps {
className?: string;
codeProps?: CodeBlockProps;
codeProps?: CopyBlockProps;
code: string;
language?: 'sql' | 'bash';
}
export const CodeSnippet: React.FC<CodeSnippetProps> = ({
className,
codeProps,
code,
language = 'sql',
}) => {
return (
<div className={cn('flex flex-1', className)}>
const CopyBlock = React.lazy(() =>
import('react-code-blocks').then((module) => ({
default: (props: CopyBlockProps) => (
<module.CopyBlock {...props} theme={module.atomOneDark} />
),
}))
);
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
({ className, codeProps, code, language = 'sql' }) => (
<div className={cn('flex flex-1 justify-center', className)}>
<Suspense fallback={<Spinner />}>
<CopyBlock
language={language}
text={code}
theme={atomOneDark}
customStyle={{
display: 'flex',
flex: '1',
@@ -30,6 +33,9 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = ({
}}
{...codeProps}
/>
</Suspense>
</div>
)
);
};
CodeSnippet.displayName = 'CodeSnippet';

View File

@@ -12,7 +12,6 @@ import { DatabaseType } from '@/lib/domain/database-type';
import { databaseSecondaryLogoMap } from '@/lib/databases';
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
import { Textarea } from '@/components/textarea/textarea';
import { importMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import {
databaseEditionToImageMap,
@@ -33,6 +32,7 @@ import {
databaseTypeToClientsMap,
} from '@/lib/domain/database-clients';
import { isDatabaseMetadata } from '@/lib/data/import-metadata/metadata-types/database-metadata';
import type { ImportMetadataScripts } from '@/lib/data/import-metadata/scripts/scripts';
const errorScriptOutputMessage =
'Invalid JSON. Please correct it or contact us at chartdb.io@gmail.com for help.';
@@ -70,6 +70,23 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
DatabaseClient | undefined
>();
const { t } = useTranslation();
const [importMetadataScripts, setImportMetadataScripts] =
useState<ImportMetadataScripts>(
Object.values(DatabaseType).reduce((acc, val) => {
acc[val] = () => '';
return acc;
}, {} as ImportMetadataScripts)
);
useEffect(() => {
const loadScripts = async () => {
const { importMetadataScripts } = await import(
'@/lib/data/import-metadata/scripts/scripts'
);
setImportMetadataScripts(importMetadataScripts);
};
loadScripts();
}, []);
useEffect(() => {
if (scriptResult.trim().length === 0) {
@@ -280,6 +297,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
setDatabaseEdition,
databaseClients,
databaseClient,
importMetadataScripts,
t,
]);

View File

@@ -0,0 +1,27 @@
import React, { useMemo } from 'react';
import { ToggleGroupItem } from '@/components/toggle/toggle-group';
import type { DatabaseType } from '@/lib/domain/database-type';
import { databaseTypeToLabelMap, getDatabaseLogo } from '@/lib/databases';
import { useTheme } from '@/hooks/use-theme';
export interface DatabaseOptionProps {
type: DatabaseType;
}
export const DatabaseOption: React.FC<DatabaseOptionProps> = ({ type }) => {
const { effectiveTheme } = useTheme();
const logo = useMemo(
() => getDatabaseLogo(type, effectiveTheme),
[type, effectiveTheme]
);
return (
<ToggleGroupItem
value={type}
aria-label="Toggle bold"
className="flex size-20 md:size-32"
>
<img src={logo} alt={databaseTypeToLabelMap[type]} />
</ToggleGroupItem>
);
};

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { Link } from '@/components/link/link';
import { LayoutGrid } from 'lucide-react';
import { useTranslation } from 'react-i18next';
export interface ExampleOptionProps {}
export const ExampleOption: React.FC<ExampleOptionProps> = () => {
const { t } = useTranslation();
return (
<Link href="/examples" className="text-primary hover:text-primary">
<div className="flex size-20 cursor-pointer flex-col items-center rounded-md border py-3 text-center md:size-32">
<div className="flex flex-1 items-center">
<LayoutGrid size={34} />
</div>
<div className="flex flex-col-reverse">
<div className="hidden text-sm text-primary md:flex">
{t(
'new_diagram_dialog.database_selection.check_examples_long'
)}
</div>
<div className="flex text-xs text-primary md:hidden">
{t(
'new_diagram_dialog.database_selection.check_examples_short'
)}
</div>
</div>
</div>
</Link>
);
};

View File

@@ -0,0 +1,42 @@
import React from 'react';
import { ToggleGroup } from '@/components/toggle/toggle-group';
import { DatabaseType } from '@/lib/domain/database-type';
import { DatabaseOption } from './database-option';
import { ExampleOption } from './example-option';
export interface SelectDatabaseContentProps {
databaseType: DatabaseType;
setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>;
onContinue: () => void;
}
export const SelectDatabaseContent: React.FC<SelectDatabaseContentProps> = ({
databaseType,
setDatabaseType,
onContinue,
}) => {
return (
<div className="flex flex-1 items-center justify-center">
<ToggleGroup
value={databaseType}
onValueChange={(value: DatabaseType) => {
if (!value) {
setDatabaseType(DatabaseType.GENERIC);
} else {
setDatabaseType(value);
onContinue();
}
}}
type="single"
className="grid grid-flow-row grid-cols-3 gap-6"
>
<DatabaseOption type={DatabaseType.MYSQL} />
<DatabaseOption type={DatabaseType.POSTGRESQL} />
<DatabaseOption type={DatabaseType.MARIADB} />
<DatabaseOption type={DatabaseType.SQLITE} />
<DatabaseOption type={DatabaseType.SQL_SERVER} />
<ExampleOption />
</ToggleGroup>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import React, { useCallback } from 'react';
import React from 'react';
import { Button } from '@/components/button/button';
import {
DialogClose,
@@ -7,14 +7,9 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/dialog/dialog';
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
import { DatabaseType } from '@/lib/domain/database-type';
import { databaseTypeToLabelMap, getDatabaseLogo } from '@/lib/databases';
import { Link } from '@/components/link/link';
import { LayoutGrid } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useTheme } from '@/hooks/use-theme';
import { SelectDatabaseContent } from './select-database-content';
export interface SelectDatabaseProps {
onContinue: () => void;
@@ -32,50 +27,9 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
createNewDiagram,
}) => {
const { t } = useTranslation();
const { effectiveTheme } = useTheme();
const renderDatabaseOption = useCallback(
(type: DatabaseType) => {
const logo = getDatabaseLogo(type, effectiveTheme);
return (
<ToggleGroupItem
value={type}
aria-label="Toggle bold"
className="flex size-20 md:size-32"
>
<img src={logo} alt={databaseTypeToLabelMap[type]} />
</ToggleGroupItem>
);
},
[effectiveTheme]
);
const renderExamplesOption = useCallback(
() => (
<Link href="/examples" className="text-primary hover:text-primary">
<div className="flex size-20 cursor-pointer flex-col items-center rounded-md border py-3 text-center md:size-32">
<div className="flex flex-1 items-center">
<LayoutGrid size={34} />
</div>
<div className="flex flex-col-reverse">
<div className="hidden text-sm text-primary md:flex">
{t(
'new_diagram_dialog.database_selection.check_examples_long'
)}
</div>
<div className="flex text-xs text-primary md:hidden">
{t(
'new_diagram_dialog.database_selection.check_examples_short'
)}
</div>
</div>
</div>
</Link>
),
[t]
);
const renderHeader = useCallback(() => {
return (
<>
<DialogHeader>
<DialogTitle>
{t('new_diagram_dialog.database_selection.title')}
@@ -84,44 +38,11 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
{t('new_diagram_dialog.database_selection.description')}
</DialogDescription>
</DialogHeader>
);
}, [t]);
const renderContent = useCallback(() => {
return (
<div className="flex flex-1 items-center justify-center">
<ToggleGroup
value={databaseType}
onValueChange={(value: DatabaseType) => {
if (!value) {
setDatabaseType(DatabaseType.GENERIC);
} else {
setDatabaseType(value);
onContinue();
}
}}
type="single"
className="grid grid-flow-row grid-cols-3 gap-6"
>
{renderDatabaseOption(DatabaseType.MYSQL)}
{renderDatabaseOption(DatabaseType.POSTGRESQL)}
{renderDatabaseOption(DatabaseType.MARIADB)}
{renderDatabaseOption(DatabaseType.SQLITE)}
{renderDatabaseOption(DatabaseType.SQL_SERVER)}
{renderExamplesOption()}
</ToggleGroup>
</div>
);
}, [
databaseType,
renderDatabaseOption,
renderExamplesOption,
setDatabaseType,
onContinue,
]);
const renderFooter = useCallback(() => {
return (
<SelectDatabaseContent
databaseType={databaseType}
onContinue={onContinue}
setDatabaseType={setDatabaseType}
/>
<DialogFooter className="mt-4 flex !justify-between gap-2">
{hasExistingDiagram ? (
<DialogClose asChild>
@@ -150,14 +71,6 @@ export const SelectDatabase: React.FC<SelectDatabaseProps> = ({
</Button>
</div>
</DialogFooter>
);
}, [createNewDiagram, databaseType, hasExistingDiagram, onContinue, t]);
return (
<>
{renderHeader()}
{renderContent()}
{renderFooter()}
</>
);
};

View File

@@ -7,13 +7,15 @@ import { mariaDBQuery } from './maria-script';
import type { DatabaseEdition } from '@/lib/domain/database-edition';
import type { DatabaseClient } from '@/lib/domain/database-clients';
export const importMetadataScripts: Record<
export type ImportMetadataScripts = Record<
DatabaseType,
(options?: {
databaseEdition?: DatabaseEdition;
databaseClient?: DatabaseClient;
}) => string
> = {
>;
export const importMetadataScripts: ImportMetadataScripts = {
[DatabaseType.GENERIC]: () => '',
[DatabaseType.POSTGRESQL]: getPostgresQuery,
[DatabaseType.MYSQL]: getMySQLQuery,

View File

@@ -0,0 +1,42 @@
import React from 'react';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/resizable/resizable';
import { SidePanel } from './side-panel/side-panel';
import { Canvas } from './canvas/canvas';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useLayout } from '@/hooks/use-layout';
import type { Diagram } from '@/lib/domain/diagram';
export interface EditorDesktopLayoutProps {
initialDiagram?: Diagram;
}
export const EditorDesktopLayout: React.FC<EditorDesktopLayoutProps> = ({
initialDiagram,
}) => {
const { isSidePanelShowed } = useLayout();
const { isLg } = useBreakpoint('lg');
const { isXl } = useBreakpoint('xl');
return (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
defaultSize={isXl ? 25 : isLg ? 35 : 50}
minSize={isXl ? 25 : isLg ? 35 : 50}
maxSize={isSidePanelShowed ? 99 : 0}
// eslint-disable-next-line
className="transition-[flex-grow] duration-200"
>
<SidePanel />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={isXl ? 75 : isLg ? 65 : 50}>
<Canvas initialTables={initialDiagram?.tables ?? []} />
</ResizablePanel>
</ResizablePanelGroup>
);
};
export default EditorDesktopLayout;

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { SidePanel } from './side-panel/side-panel';
import { Canvas } from './canvas/canvas';
import { useLayout } from '@/hooks/use-layout';
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from '@/components/drawer/drawer';
import { Separator } from '@/components/separator/separator';
import type { Diagram } from '@/lib/domain/diagram';
export interface EditorMobileLayoutProps {
initialDiagram?: Diagram;
}
export const EditorMobileLayout: React.FC<EditorMobileLayoutProps> = ({
initialDiagram,
}) => {
const { isSidePanelShowed, hideSidePanel } = useLayout();
return (
<>
<Drawer open={isSidePanelShowed} onClose={() => hideSidePanel()}>
<DrawerContent className="h-full" fullScreen>
<DrawerHeader>
<DrawerTitle>Manage Diagram</DrawerTitle>
<DrawerDescription>
Manage your diagram objects
</DrawerDescription>
</DrawerHeader>
<Separator orientation="horizontal" />
<SidePanel data-vaul-no-drag />
</DrawerContent>
</Drawer>
<Canvas initialTables={initialDiagram?.tables ?? []} />
</>
);
};
export default EditorMobileLayout;

View File

@@ -1,12 +1,11 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, {
Suspense,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { TopNavbar } from './top-navbar/top-navbar';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/resizable/resizable';
import { SidePanel } from './side-panel/side-panel';
import { Canvas } from './canvas/canvas';
import { useNavigate, useParams } from 'react-router-dom';
import { useConfig } from '@/hooks/use-config';
import { useChartDB } from '@/hooks/use-chartdb';
@@ -17,14 +16,6 @@ import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useLayout } from '@/hooks/use-layout';
import { useToast } from '@/components/toast/use-toast';
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
} from '@/components/drawer/drawer';
import { Separator } from '@/components/separator/separator';
import type { Diagram } from '@/lib/domain/diagram';
import { ToastAction } from '@/components/toast/toast';
import { useLocalConfig } from '@/hooks/use-local-config';
@@ -42,27 +33,31 @@ import { ReactFlowProvider } from '@xyflow/react';
import { ExportImageProvider } from '@/context/export-image-context/export-image-provider';
import { DialogProvider } from '@/context/dialog-context/dialog-provider';
import { KeyboardShortcutsProvider } from '@/context/keyboard-shortcuts-context/keyboard-shortcuts-provider';
// import { EditorMobileLayout } from './editor-mobile-layout';
import { Spinner } from '@/components/spinner/spinner';
// import { EditorDesktopLayout } from './editor-desktop-layout';
const OPEN_STAR_US_AFTER_SECONDS = 30;
const SHOW_STAR_US_AGAIN_AFTER_DAYS = 1;
export const EditorDesktopLayoutLazy = React.lazy(
() => import('./editor-desktop-layout')
);
export const EditorMobileLayoutLazy = React.lazy(
() => import('./editor-mobile-layout')
);
const EditorPageComponent: React.FC = () => {
const { loadDiagram, currentDiagram, schemas, filteredSchemas } =
useChartDB();
const {
isSidePanelShowed,
hideSidePanel,
openSelectSchema,
showSidePanel,
} = useLayout();
const { openSelectSchema, showSidePanel } = useLayout();
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
const { showLoader, hideLoader } = useFullScreenLoader();
const { openCreateDiagramDialog, openStarUsDialog } = useDialog();
const { diagramId } = useParams<{ diagramId: string }>();
const { config, updateConfig } = useConfig();
const navigate = useNavigate();
const { isLg } = useBreakpoint('lg');
const { isXl } = useBreakpoint('xl');
const { isMd: isDesktop } = useBreakpoint('md');
const [initialDiagram, setInitialDiagram] = useState<Diagram | undefined>();
const {
@@ -221,46 +216,23 @@ const EditorPageComponent: React.FC = () => {
className={`bg-background ${isDesktop ? 'h-screen w-screen' : 'h-dvh w-dvw'} flex select-none flex-col overflow-x-hidden`}
>
<TopNavbar />
<Suspense
fallback={
<div className="flex flex-1 items-center justify-center">
<Spinner size={isDesktop ? 'large' : 'medium'} />
</div>
}
>
{isDesktop ? (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel
defaultSize={isXl ? 25 : isLg ? 35 : 50}
minSize={isXl ? 25 : isLg ? 35 : 50}
maxSize={isSidePanelShowed ? 99 : 0}
// eslint-disable-next-line
className="transition-[flex-grow] duration-200"
>
<SidePanel />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel
defaultSize={isXl ? 75 : isLg ? 65 : 50}
>
<Canvas
initialTables={initialDiagram?.tables ?? []}
<EditorDesktopLayoutLazy
initialDiagram={initialDiagram}
/>
</ResizablePanel>
</ResizablePanelGroup>
) : (
<>
<Drawer
open={isSidePanelShowed}
onClose={() => hideSidePanel()}
>
<DrawerContent className="h-full" fullScreen>
<DrawerHeader>
<DrawerTitle>Manage Diagram</DrawerTitle>
<DrawerDescription>
Manage your diagram objects
</DrawerDescription>
</DrawerHeader>
<Separator orientation="horizontal" />
<SidePanel data-vaul-no-drag />
</DrawerContent>
</Drawer>
<Canvas initialTables={initialDiagram?.tables ?? []} />
</>
<EditorMobileLayoutLazy
initialDiagram={initialDiagram}
/>
)}
</Suspense>
</section>
<Toaster />
</>
@@ -268,15 +240,15 @@ const EditorPageComponent: React.FC = () => {
};
export const EditorPage: React.FC = () => (
<LocalConfigProvider>
<ThemeProvider>
<FullScreenLoaderProvider>
<LayoutProvider>
<LocalConfigProvider>
<StorageProvider>
<ConfigProvider>
<RedoUndoStackProvider>
<ChartDBProvider>
<HistoryProvider>
<ThemeProvider>
<ReactFlowProvider>
<ExportImageProvider>
<DialogProvider>
@@ -286,13 +258,13 @@ export const EditorPage: React.FC = () => (
</DialogProvider>
</ExportImageProvider>
</ReactFlowProvider>
</ThemeProvider>
</HistoryProvider>
</ChartDBProvider>
</RedoUndoStackProvider>
</ConfigProvider>
</StorageProvider>
</LocalConfigProvider>
</LayoutProvider>
</FullScreenLoaderProvider>
</ThemeProvider>
</LocalConfigProvider>
);

View File

@@ -0,0 +1,92 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Label } from '@/components/label/label';
import { Button } from '@/components/button/button';
import { Check, Pencil } from 'lucide-react';
import { Input } from '@/components/input/input';
import { useChartDB } from '@/hooks/use-chartdb';
import { useClickAway, useKeyPressEvent } from 'react-use';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { DiagramIcon } from '@/components/diagram-icon/diagram-icon';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { labelVariants } from '@/components/label/label-variants';
export interface DiagramNameProps {}
export const DiagramName: React.FC<DiagramNameProps> = () => {
const { diagramName, updateDiagramName, currentDiagram } = useChartDB();
const { t } = useTranslation();
const { isMd: isDesktop } = useBreakpoint('md');
const [editMode, setEditMode] = useState(false);
const [editedDiagramName, setEditedDiagramName] =
React.useState(diagramName);
const inputRef = React.useRef<HTMLInputElement>(null);
useEffect(() => {
setEditedDiagramName(diagramName);
}, [diagramName]);
const editDiagramName = useCallback(() => {
if (!editMode) return;
if (editedDiagramName.trim()) {
updateDiagramName(editedDiagramName.trim());
}
setEditMode(false);
}, [editedDiagramName, updateDiagramName, editMode]);
useClickAway(inputRef, editDiagramName);
useKeyPressEvent('Enter', editDiagramName);
const enterEditMode = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.stopPropagation();
setEditMode(true);
};
return (
<>
<DiagramIcon diagram={currentDiagram} />
<div className="flex">
{isDesktop ? <Label>{t('diagrams')}/</Label> : null}
</div>
<div className="flex flex-row items-center gap-1">
{editMode ? (
<>
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={diagramName}
value={editedDiagramName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setEditedDiagramName(e.target.value)
}
className="ml-1 h-7 focus-visible:ring-0"
/>
<Button
variant="ghost"
className="hidden size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 group-hover:flex dark:text-slate-400 dark:hover:text-slate-300"
onClick={editDiagramName}
>
<Check />
</Button>
</>
) : (
<>
<h1 className={cn(labelVariants())}>{diagramName}</h1>
<Button
variant="ghost"
className="hidden size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 group-hover:flex dark:text-slate-400 dark:hover:text-slate-300"
onClick={enterEditMode}
>
<Pencil />
</Button>
</>
)}
</div>
</>
);
};

View File

@@ -0,0 +1,31 @@
import React from 'react';
import TimeAgo from 'timeago-react';
import { useChartDB } from '@/hooks/use-chartdb';
import { Badge } from '@/components/badge/badge';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { useTranslation } from 'react-i18next';
export interface LastSavedProps {}
export const LastSaved: React.FC<LastSavedProps> = () => {
const { currentDiagram } = useChartDB();
const { t } = useTranslation();
const { isMd: isDesktop } = useBreakpoint('md');
return (
<Tooltip>
<TooltipTrigger>
<Badge variant="secondary" className="flex gap-1">
{isDesktop ? t('last_saved') : t('saved')}
<TimeAgo datetime={currentDiagram.updatedAt} />
</Badge>
</TooltipTrigger>
<TooltipContent>
{currentDiagram.updatedAt.toLocaleString()}
</TooltipContent>
</Tooltip>
);
};

View File

@@ -1,5 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react';
import TimeAgo from 'timeago-react';
import React, { useCallback } from 'react';
import {
Menubar,
MenubarCheckboxItem,
@@ -13,28 +12,16 @@ import {
MenubarSubTrigger,
MenubarTrigger,
} from '@/components/menubar/menubar';
import { Label } from '@/components/label/label';
import { Button } from '@/components/button/button';
import { Check, Pencil } from 'lucide-react';
import { Input } from '@/components/input/input';
import { useChartDB } from '@/hooks/use-chartdb';
import { useClickAway, useKeyPressEvent } from 'react-use';
import ChartDBLogo from '@/assets/logo.png';
import ChartDBLogo from '@/assets/logo-light.png';
import ChartDBDarkLogo from '@/assets/logo-dark.png';
import { useDialog } from '@/hooks/use-dialog';
import { Badge } from '@/components/badge/badge';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/tooltip/tooltip';
import { useExportImage } from '@/hooks/use-export-image';
import { databaseTypeToLabelMap } from '@/lib/databases';
import { DatabaseType } from '@/lib/domain/database-type';
import { useConfig } from '@/hooks/use-config';
import { IS_CHARTDB_IO } from '@/lib/env';
import { useBreakpoint } from '@/hooks/use-breakpoint';
import { DiagramIcon } from '@/components/diagram-icon/diagram-icon';
import {
KeyboardShortcutAction,
keyboardShortcutsForOS,
@@ -49,20 +36,14 @@ import { deMetadata } from '@/i18n/locales/de';
import { jaMetadata } from '@/i18n/locales/ja';
import { useLocalConfig } from '@/hooks/use-local-config';
import { frMetadata } from '@/i18n/locales/fr';
import { cn } from '@/lib/utils';
import { labelVariants } from '@/components/label/label-variants';
import { DiagramName } from './diagram-name';
import { LastSaved } from './last-saved';
export interface TopNavbarProps {}
export const TopNavbar: React.FC<TopNavbarProps> = () => {
const {
diagramName,
updateDiagramName,
currentDiagram,
clearDiagramData,
deleteDiagram,
updateDiagramUpdatedAt,
} = useChartDB();
const { clearDiagramData, deleteDiagram, updateDiagramUpdatedAt } =
useChartDB();
const {
openCreateDiagramDialog,
openOpenDiagramDialog,
@@ -86,26 +67,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
const { redo, undo, hasRedo, hasUndo } = useHistory();
const { isMd: isDesktop } = useBreakpoint('md');
const { config, updateConfig } = useConfig();
const [editMode, setEditMode] = useState(false);
const { exportImage } = useExportImage();
const [editedDiagramName, setEditedDiagramName] =
React.useState(diagramName);
const inputRef = React.useRef<HTMLInputElement>(null);
useEffect(() => {
setEditedDiagramName(diagramName);
}, [diagramName]);
const editDiagramName = useCallback(() => {
if (!editMode) return;
if (editedDiagramName.trim()) {
updateDiagramName(editedDiagramName.trim());
}
setEditMode(false);
}, [editedDiagramName, updateDiagramName, editMode]);
useClickAway(inputRef, editDiagramName);
useKeyPressEvent('Enter', editDiagramName);
const createNewDiagram = () => {
openCreateDiagramDialog();
@@ -115,13 +77,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
openOpenDiagramDialog();
};
const enterEditMode = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.stopPropagation();
setEditMode(true);
};
const exportSVG = useCallback(() => {
exportImage('svg', 1);
}, [exportImage]);
@@ -220,79 +175,6 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
);
}, [isDesktop]);
const renderLastSaved = useCallback(() => {
return (
<Tooltip>
<TooltipTrigger>
<Badge variant="secondary" className="flex gap-1">
{isDesktop ? t('last_saved') : t('saved')}
<TimeAgo datetime={currentDiagram.updatedAt} />
</Badge>
</TooltipTrigger>
<TooltipContent>
{currentDiagram.updatedAt.toLocaleString()}
</TooltipContent>
</Tooltip>
);
}, [currentDiagram.updatedAt, isDesktop, t]);
const renderDiagramName = useCallback(() => {
return (
<>
<DiagramIcon diagram={currentDiagram} />
<div className="flex">
{isDesktop ? <Label>{t('diagrams')}/</Label> : null}
</div>
<div className="flex flex-row items-center gap-1">
{editMode ? (
<>
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={diagramName}
value={editedDiagramName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setEditedDiagramName(e.target.value)
}
className="ml-1 h-7 focus-visible:ring-0"
/>
<Button
variant="ghost"
className="hidden size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 group-hover:flex dark:text-slate-400 dark:hover:text-slate-300"
onClick={editDiagramName}
>
<Check />
</Button>
</>
) : (
<>
<h1 className={cn(labelVariants())}>
{diagramName}
</h1>
<Button
variant="ghost"
className="hidden size-7 p-2 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 group-hover:flex dark:text-slate-400 dark:hover:text-slate-300"
onClick={enterEditMode}
>
<Pencil />
</Button>
</>
)}
</div>
</>
);
}, [
currentDiagram,
diagramName,
editDiagramName,
editMode,
editedDiagramName,
isDesktop,
t,
]);
const showOrHideSidePanel = useCallback(() => {
if (isSidePanelShowed) {
hideSidePanel();
@@ -762,19 +644,21 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
{isDesktop ? (
<>
<div className="group flex flex-1 flex-row items-center justify-center">
{renderDiagramName()}
<DiagramName />
</div>
<div className="hidden flex-1 items-center justify-end gap-2 sm:flex">
{renderLastSaved()}
<LastSaved />
{renderStars()}
</div>
</>
) : (
<div className="flex flex-1 flex-row justify-between gap-2">
<div className="group flex flex-1 flex-row items-center">
{renderDiagramName()}
<DiagramName />
</div>
<div className="flex items-center">
<LastSaved />
</div>
<div className="flex items-center">{renderLastSaved()}</div>
<div className="flex items-center">{renderStars()}</div>
</div>
)}

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import ChartDBLogo from '@/assets/logo.png';
import ChartDBLogo from '@/assets/logo-light.png';
import ChartDBDarkLogo from '@/assets/logo-dark.png';
import { examples } from './examples-data/examples-data';
import { ExampleCard } from './example-card';

View File

@@ -1,11 +1,30 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// import { visualizer } from 'rollup-plugin-visualizer';
import { visualizer } from 'rollup-plugin-visualizer';
import path from 'path';
import UnpluginInjectPreload from 'unplugin-inject-preload/vite';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react() /*, visualizer()*/],
plugins: [
react(),
visualizer({
filename: './stats/stats.html',
open: false,
}),
UnpluginInjectPreload({
files: [
{
entryMatch: /logo-light.png$/,
outputMatch: /logo-light-.*.png$/,
},
{
entryMatch: /logo-dark.png$/,
outputMatch: /logo-dark-.*.png$/,
},
],
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),