refactor codebase

This commit is contained in:
AykutSarac 2024-05-14 21:24:07 +03:00
parent b4e2fcabf6
commit 900e6897b9
No known key found for this signature in database
42 changed files with 96 additions and 189 deletions

View File

@ -1,5 +1,4 @@
NEXT_PUBLIC_BASE_URL=http://localhost:3000
NEXT_PUBLIC_PAYMENT_URL=https://herowand.lemonsqueezy.com/checkout/buy/ce30521f-c7cc-44f3-9435-995d3260ba22
NEXT_PUBLIC_GA_ID=G-JKZEHMJBMH
NEXT_PUBLIC_PAYMENT_URL=https://herowand.lemonsqueezy.com/checkout/buy/ce30521f-c7cc-44f3-9435-995d3260ba22
NEXT_PUBLIC_SUPABASE_URL=https://bxkgqurwqjmvrqekcbws.supabase.co
NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4a2dxdXJ3cWptdnJxZWtjYndzIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTA2NDU0MjUsImV4cCI6MjAwNjIyMTQyNX0.3nZ0yhuFjnI3yHbAL8S9UtK-Ny-6F5AylNHgo1tymTU

View File

@ -1,5 +0,0 @@
NEXT_PUBLIC_BASE_URL=https://jsoncrack.com
NEXT_PUBLIC_PAYMENT_URL=https://herowand.lemonsqueezy.com/checkout/buy/ce30521f-c7cc-44f3-9435-995d3260ba22
NEXT_PUBLIC_GA_ID=G-JKZEHMJBMH
NEXT_PUBLIC_SUPABASE_URL=https://bxkgqurwqjmvrqekcbws.supabase.co
NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4a2dxdXJ3cWptdnJxZWtjYndzIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTA2NDU0MjUsImV4cCI6MjAwNjIyMTQyNX0.3nZ0yhuFjnI3yHbAL8S9UtK-Ny-6F5AylNHgo1tymTU

View File

@ -1,80 +0,0 @@
import React from "react";
import styled from "styled-components";
const StyledInputWrapper = styled.div`
background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
width: 100%;
border-radius: 4px;
`;
const StyledForm = styled.div`
display: flex;
flex: 1;
`;
const StyledInput = styled.input`
background: ${({ theme }) => theme.BACKGROUND_TERTIARY};
color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
outline: none;
border: none;
border-radius: 4px;
line-height: 32px;
padding: 10px;
width: 100%;
height: 40px;
`;
const StyledButton = styled.button`
display: flex;
align-items: center;
background: none;
color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
padding: 0 10px;
min-height: unset;
text-transform: uppercase;
&:hover {
box-shadow: none;
}
&.active {
background: ${({ theme }) => theme.PRIMARY};
color: white;
outline: 3px solid ${({ theme }) => theme.BACKGROUND_TERTIARY};
border-radius: 10px;
}
`;
export interface InputProps {
value: string | number | string[];
extensions: string[];
activeExtension: number;
onChange?: React.ChangeEventHandler<HTMLInputElement>;
setExtension: (value: number) => void;
}
export const FileInput: React.FC<InputProps> = ({
setExtension,
activeExtension,
onChange,
extensions,
value,
}) => {
return (
<StyledInputWrapper>
<StyledForm>
<StyledInput type="text" onChange={onChange} value={value} placeholder="File Name" />
{extensions.map((ext, key) => (
<StyledButton
className={`${activeExtension === key && "active"}`}
key={key}
aria-label="search"
onClick={() => setExtension(key)}
>
{ext}
</StyledButton>
))}
</StyledForm>
</StyledInputWrapper>
);
};

View File

@ -1,5 +1,3 @@
export const baseURL = process.env.NEXT_PUBLIC_BASE_URL as string;
// Example taken from https://mdn.github.io/learning-area/javascript/oojs/json/superheroes.json
const sampleJson = Object.freeze({
squadName: "Super hero squad",

View File

@ -208,11 +208,7 @@ export const BottomBar = () => {
</Text>
</Flex>
</Popover.Target>
<Popover.Dropdown
style={{
pointerEvents: "none",
}}
>
<Popover.Dropdown style={{ pointerEvents: "none" }}>
<Text size="xs">{error}</Text>
</Popover.Dropdown>
</Popover>

View File

@ -1,20 +0,0 @@
import React from "react";
import styled from "styled-components";
import { MonacoEditor } from "src/components/MonacoEditor";
const StyledEditorWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
user-select: none;
`;
export const JsonEditor: React.FC = () => {
return (
<StyledEditorWrapper>
<MonacoEditor />
</StyledEditorWrapper>
);
};
export default JsonEditor;

View File

@ -1,5 +1,5 @@
import React from "react";
import type { CustomNodeProps } from "src/containers/Views/GraphView/CustomNode";
import type { CustomNodeProps } from "src/containers/Editor/LiveEditor/GraphView/CustomNode";
import { TextRenderer } from "./TextRenderer";
import * as Styled from "./styles";
@ -25,7 +25,7 @@ const Row = ({ val, x, y, index }: RowProps) => {
);
};
const Node: React.FC<CustomNodeProps> = ({ node, x, y }) => (
const Node = ({ node, x, y }: CustomNodeProps) => (
<Styled.StyledForeignObject width={node.width} height={node.height} x={0} y={0} $isObject>
{(node.text as Value[]).map((val, idx) => (
<Row val={val} index={idx} x={x} y={y} key={idx} />

View File

@ -1,7 +1,7 @@
import React from "react";
import styled from "styled-components";
import { MdLink, MdLinkOff } from "react-icons/md";
import type { CustomNodeProps } from "src/containers/Views/GraphView/CustomNode";
import type { CustomNodeProps } from "src/containers/Editor/LiveEditor/GraphView/CustomNode";
import useToggleHide from "src/hooks/useToggleHide";
import { isContentImage } from "src/lib/utils/graph/calculateNodeSize";
import useConfig from "src/store/useConfig";
@ -43,7 +43,7 @@ const StyledImage = styled.img`
background: ${({ theme }) => theme.BACKGROUND_MODIFIER_ACCENT};
`;
const Node: React.FC<CustomNodeProps> = ({ node, x, y, hasCollapse = false }) => {
const Node = ({ node, x, y, hasCollapse = false }: CustomNodeProps) => {
const {
id,
text,

View File

@ -11,20 +11,14 @@ const StyledRow = styled.span`
vertical-align: middle;
`;
function isColorFormat(colorString: string) {
const hexCodeRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
const rgbRegex = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
const rgbaRegex = /^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(0|1|0\.\d+)\s*\)$/;
return (
hexCodeRegex.test(colorString) || rgbRegex.test(colorString) || rgbaRegex.test(colorString)
);
}
const isURL =
/(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/gi;
export const TextRenderer: React.FC<{ children: string }> = ({ children }) => {
interface TextRendererProps {
children: string;
}
export const TextRenderer = ({ children }: TextRendererProps) => {
if (isURL.test(children?.replaceAll('"', ""))) {
return <Styled.StyledLinkItUrl>{children}</Styled.StyledLinkItUrl>;
}
@ -39,3 +33,13 @@ export const TextRenderer: React.FC<{ children: string }> = ({ children }) => {
}
return <>{children}</>;
};
function isColorFormat(colorString: string) {
const hexCodeRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
const rgbRegex = /^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/;
const rgbaRegex = /^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*(0|1|0\.\d+)\s*\)$/;
return (
hexCodeRegex.test(colorString) || rgbRegex.test(colorString) || rgbaRegex.test(colorString)
);
}

View File

@ -6,17 +6,13 @@ import { Space } from "react-zoomable-ui";
import { Canvas } from "reaflow";
import type { ElkRoot } from "reaflow/dist/layout/useLayout";
import { useLongPress } from "use-long-press";
import { CustomNode } from "src/containers/Views/GraphView/CustomNode";
import { CustomNode } from "src/containers/Editor/LiveEditor/GraphView/CustomNode";
import useToggleHide from "src/hooks/useToggleHide";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import { CustomEdge } from "./CustomEdge";
import { NotSupported } from "./NotSupported";
interface GraphProps {
isWidget?: boolean;
}
const StyledEditorWrapper = styled.div<{ $widget: boolean; $showRulers: boolean }>`
position: absolute;
width: 100%;
@ -77,6 +73,10 @@ const layoutOptions = {
"elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX",
};
interface GraphProps {
isWidget?: boolean;
}
const GraphCanvas = ({ isWidget }: GraphProps) => {
const { validateHiddenNodes } = useToggleHide();
const setLoading = useGraph(state => state.setLoading);

View File

@ -1,7 +1,7 @@
import React from "react";
import type { DefaultTheme } from "styled-components";
import { useTheme } from "styled-components";
import { TextRenderer } from "src/containers/Views/GraphView/CustomNode/TextRenderer";
import { TextRenderer } from "src/containers/Editor/LiveEditor/GraphView/CustomNode/TextRenderer";
type TextColorFn = {
theme: DefaultTheme;

View File

@ -1,7 +1,7 @@
import React from "react";
import styled from "styled-components";
import { Graph } from "src/containers/Views/GraphView";
import { TreeView } from "src/containers/Views/TreeView";
import { Graph } from "src/containers/Editor/LiveEditor/GraphView";
import { TreeView } from "src/containers/Editor/LiveEditor/TreeView";
import { ViewMode } from "src/enums/viewMode.enum";
import useConfig from "src/store/useConfig";
@ -29,7 +29,7 @@ const View = () => {
return null;
};
const LiveEditor: React.FC = () => {
const LiveEditor = () => {
return (
<StyledLiveEditor onContextMenuCapture={e => e.preventDefault()}>
<View />

View File

@ -19,14 +19,7 @@ const editorOptions = {
},
};
const StyledWrapper = styled.div`
display: grid;
height: calc(100vh - 67px);
grid-template-columns: 100%;
grid-template-rows: minmax(0, 1fr);
`;
export const MonacoEditor = () => {
const TextEditor = () => {
const monaco = useMonaco();
const contents = useFile(state => state.contents);
const setContents = useFile(state => state.setContents);
@ -72,17 +65,35 @@ export const MonacoEditor = () => {
}, [getHasChanges]);
return (
<StyledWrapper>
<Editor
height="100%"
language={fileType}
theme={theme}
value={contents}
options={editorOptions}
onValidate={errors => setError(errors[0]?.message)}
onChange={contents => setContents({ contents, skipUpdate: true })}
loading={<LoadingOverlay visible />}
/>
</StyledWrapper>
<StyledEditorWrapper>
<StyledWrapper>
<Editor
height="100%"
language={fileType}
theme={theme}
value={contents}
options={editorOptions}
onValidate={errors => setError(errors[0]?.message)}
onChange={contents => setContents({ contents, skipUpdate: true })}
loading={<LoadingOverlay visible />}
/>
</StyledWrapper>
</StyledEditorWrapper>
);
};
export default TextEditor;
const StyledEditorWrapper = styled.div`
display: flex;
flex-direction: column;
height: 100%;
user-select: none;
`;
const StyledWrapper = styled.div`
display: grid;
height: calc(100vh - 67px);
grid-template-columns: 100%;
grid-template-rows: minmax(0, 1fr);
`;

View File

@ -16,7 +16,7 @@ export const StyledEditor = styled(Allotment)`
}
`;
const JsonEditor = dynamic(() => import("src/containers/Editor/JsonEditor"), {
const TextEditor = dynamic(() => import("src/containers/Editor/TextEditor"), {
ssr: false,
});
@ -24,7 +24,7 @@ const LiveEditor = dynamic(() => import("src/containers/Editor/LiveEditor"), {
ssr: false,
});
const Panes: React.FC = () => {
export const Editor = () => {
const fullscreen = useGraph(state => state.fullscreen);
return (
@ -35,7 +35,7 @@ const Panes: React.FC = () => {
maxSize={800}
visible={!fullscreen}
>
<JsonEditor />
<TextEditor />
</Allotment.Pane>
<Allotment.Pane minSize={0}>
<LiveEditor />
@ -43,5 +43,3 @@ const Panes: React.FC = () => {
</StyledEditor>
);
};
export default Panes;

View File

@ -6,7 +6,7 @@ import { gaEvent } from "src/lib/utils/gaEvent";
import useModal from "src/store/useModal";
import useUser from "src/store/useUser";
export const AccountModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const AccountModal = ({ opened, onClose }: ModalProps) => {
const user = useUser(state => state.user);
const setVisible = useModal(state => state.setVisible);
const logout = useUser(state => state.logout);

View File

@ -5,7 +5,7 @@ import { Modal, Group, Button, Text, Divider } from "@mantine/core";
import { documentSvc } from "src/services/document.service";
import useJson from "src/store/useJson";
export const ClearModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const ClearModal = ({ opened, onClose }: ModalProps) => {
const setJson = useJson(state => state.setJson);
const { query, replace } = useRouter();

View File

@ -44,11 +44,13 @@ const colorByFormat: Record<FileFormat, DefaultMantineColor> = {
csv: "grape",
};
const UpdateNameModal: React.FC<{
interface UpdateNameModalProps {
file: File | null;
onClose: () => void;
refetch: () => void;
}> = ({ file, onClose, refetch }) => {
}
const UpdateNameModal = ({ file, onClose, refetch }: UpdateNameModalProps) => {
const [name, setName] = React.useState("");
React.useEffect(() => {
@ -95,7 +97,7 @@ const UpdateNameModal: React.FC<{
const TOTAL_QUOTA = 25;
export const CloudModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const CloudModal = ({ opened, onClose }: ModalProps) => {
const setFile = useFile(state => state.setFile);
const [currentFile, setCurrentFile] = React.useState<File | null>(null);
const [searchValue, setSearchValue] = React.useState("");

View File

@ -62,7 +62,7 @@ function downloadURI(uri: string, name: string) {
document.body.removeChild(link);
}
export const DownloadModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const DownloadModal = ({ opened, onClose }: ModalProps) => {
const [extension, setExtension] = React.useState(Extensions.PNG);
const [fileDetails, setFileDetails] = React.useState({
filename: "jsoncrack.com",

View File

@ -8,7 +8,7 @@ import type { FileFormat } from "src/enums/file.enum";
import { gaEvent } from "src/lib/utils/gaEvent";
import useFile from "src/store/useFile";
export const ImportModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const ImportModal = ({ opened, onClose }: ModalProps) => {
const [url, setURL] = React.useState("");
const [file, setFile] = React.useState<File | null>(null);

View File

@ -4,7 +4,7 @@ import { Stack, Modal, Button, Text, Anchor, Group, TextInput, Divider } from "@
import { VscLinkExternal } from "react-icons/vsc";
import useJsonQuery from "src/hooks/useJsonQuery";
export const JQModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const JQModal = ({ opened, onClose }: ModalProps) => {
const { updateJson } = useJsonQuery();
const [query, setQuery] = React.useState("");

View File

@ -5,7 +5,7 @@ import { decode } from "jsonwebtoken";
import { gaEvent } from "src/lib/utils/gaEvent";
import useFile from "src/store/useFile";
export const JWTModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const JWTModal = ({ opened, onClose }: ModalProps) => {
const setContents = useFile(state => state.setContents);
const [token, setToken] = React.useState("");

View File

@ -2,7 +2,7 @@ import React from "react";
import type { ModalProps } from "@mantine/core";
import { Modal, Stack, Button, Text } from "@mantine/core";
export const LoginModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const LoginModal = ({ opened, onClose }: ModalProps) => {
return (
<Modal title="Sign In" opened={opened} onClose={onClose} centered>
<Stack py="sm">

View File

@ -17,7 +17,7 @@ const dataToString = (data: any) => {
return JSON.stringify(text, replacer, 2);
};
export const NodeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const NodeModal = ({ opened, onClose }: ModalProps) => {
const setVisible = useModal(state => state.setVisible);
const nodeData = useGraph(state => dataToString(state.selectedNode?.text));
const path = useGraph(state => state.selectedNode?.path || "");

View File

@ -4,7 +4,7 @@ import { Button, Modal, Rating, Text, Textarea } from "@mantine/core";
import { toast } from "react-hot-toast";
import { supabase } from "src/lib/api/supabase";
export const ReviewModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const ReviewModal = ({ opened, onClose }: ModalProps) => {
const [stars, setStars] = React.useState(0);
const [review, setReview] = React.useState("");

View File

@ -8,7 +8,7 @@ import { gaEvent } from "src/lib/utils/gaEvent";
import useConfig from "src/store/useConfig";
import useFile from "src/store/useFile";
export const SchemaModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const SchemaModal = ({ opened, onClose }: ModalProps) => {
const setJsonSchema = useFile(state => state.setJsonSchema);
const [schema, setSchema] = React.useState(
JSON.stringify(

View File

@ -15,7 +15,7 @@ import { FiExternalLink } from "react-icons/fi";
import { MdCheck, MdCopyAll } from "react-icons/md";
import { gaEvent } from "src/lib/utils/gaEvent";
export const ShareModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const ShareModal = ({ opened, onClose }: ModalProps) => {
const { query } = useRouter();
const shareURL = `https://jsoncrack.com/editor?json=${query.json}`;

View File

@ -47,7 +47,7 @@ const typeOptions = [
},
];
export const TypeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const TypeModal = ({ opened, onClose }: ModalProps) => {
const getJson = useJson(state => state.getJson);
const [type, setType] = React.useState("");
const [selectedType, setSelectedType] = React.useState<Language>(Language.TypeScript);

View File

@ -24,7 +24,7 @@ const overlayLinks = {
"https://herowand.lemonsqueezy.com/buy/577928ea-fb09-4076-9307-3e5931b35ad0?embed=1&media=0&logo=0&desc=0&discount=0&enabled=82417",
};
export const UpgradeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
export const UpgradeModal = ({ opened, onClose }: ModalProps) => {
const [plan, setPlan] = React.useState<string>("annual");
const user = useUser(state => state.user);

View File

@ -4,7 +4,7 @@ import { getHotkeyHandler } from "@mantine/hooks";
import { AiOutlineSearch } from "react-icons/ai";
import { useFocusNode } from "src/hooks/useFocusNode";
export const SearchInput: React.FC = () => {
export const SearchInput = () => {
const [searchValue, setValue, skip, nodeCount, currentNode] = useFocusNode();
return (

View File

@ -5,7 +5,7 @@ import toast from "react-hot-toast";
import { AiOutlineFullscreen } from "react-icons/ai";
import { AiFillGift } from "react-icons/ai";
import { FiDownload } from "react-icons/fi";
import { SearchInput } from "src/components/SearchInput";
import { SearchInput } from "src/containers/Toolbar/SearchInput";
import { FileFormat } from "src/enums/file.enum";
import { JSONCrackLogo } from "src/layout/JsonCrackLogo";
import { gaEvent } from "src/lib/utils/gaEvent";
@ -30,7 +30,11 @@ function fullscreenBrowser() {
}
}
export const Toolbar: React.FC<{ isWidget?: boolean }> = ({ isWidget = false }) => {
interface ToolbarProps {
isWidget?: boolean;
}
export const Toolbar = ({ isWidget = false }: ToolbarProps) => {
const setVisible = useModal(state => state.setVisible);
const setFormat = useFile(state => state.setFormat);
const format = useFile(state => state.format);

View File

@ -19,7 +19,7 @@ interface LogoProps extends React.ComponentPropsWithoutRef<"a"> {
fontSize?: string;
}
export const JSONCrackLogo: React.FC<LogoProps> = ({ fontSize = "1.2rem", ...props }) => {
export const JSONCrackLogo = ({ fontSize = "1.2rem", ...props }: LogoProps) => {
const logoText = React.useMemo(() => {
if (typeof window === "undefined") return "JSON CRACK";
return isIframe() ? "JC" : "JSON CRACK";

View File

@ -8,7 +8,7 @@ const StyledLayoutWrapper = styled.div`
padding-bottom: 48px;
`;
const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const Layout = ({ children }: React.PropsWithChildren) => {
return (
<ThemeProvider theme={lightTheme}>
<StyledLayoutWrapper>

View File

@ -8,9 +8,9 @@ import "@mantine/core/styles.css";
import "@mantine/code-highlight/styles.css";
import { ThemeProvider } from "styled-components";
import ReactGA from "react-ga4";
import { Loading } from "src/components/Loading";
import GlobalStyle from "src/constants/globalStyle";
import { lightTheme } from "src/constants/theme";
import { Loading } from "src/layout/Loading";
import { supabase } from "src/lib/api/supabase";
import useUser from "src/store/useUser";

View File

@ -6,8 +6,8 @@ import { useMantineColorScheme } from "@mantine/core";
import styled, { ThemeProvider } from "styled-components";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { darkTheme, lightTheme } from "src/constants/theme";
import { Editor } from "src/containers/Editor";
import { BottomBar } from "src/containers/Editor/BottomBar";
import Panes from "src/containers/Editor/Panes";
import { Toolbar } from "src/containers/Toolbar";
import useConfig from "src/store/useConfig";
import useFile from "src/store/useFile";
@ -39,7 +39,7 @@ export const StyledEditorWrapper = styled.div`
overflow: hidden;
`;
const EditorPage: React.FC = () => {
const EditorPage = () => {
const { query, isReady } = useRouter();
const { setColorScheme } = useMantineColorScheme();
const checkEditorSession = useFile(state => state.checkEditorSession);
@ -67,7 +67,7 @@ const EditorPage: React.FC = () => {
<StyledPageWrapper>
<Toolbar />
<StyledEditorWrapper>
<Panes />
<Editor />
</StyledEditorWrapper>
</StyledPageWrapper>
<BottomBar />

View File

@ -20,7 +20,7 @@ interface EmbedMessage {
};
}
const Graph = dynamic(() => import("src/containers/Views/GraphView").then(c => c.Graph), {
const Graph = dynamic(() => import("src/containers/Editor/LiveEditor/GraphView").then(c => c.Graph), {
ssr: false,
});