feat: UI improvements

This commit is contained in:
AykutSarac 2024-05-01 13:17:29 +03:00
parent b83917c6bb
commit e6cb91bb74
No known key found for this signature in database
13 changed files with 158 additions and 318 deletions

View File

@ -0,0 +1,32 @@
import React from "react";
import { useRouter } from "next/router";
import { LoadingOverlay } from "@mantine/core";
export const Loading = () => {
const router = useRouter();
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
const handleStart = (url: string) => {
return url !== router.asPath && url === "/editor" && setLoading(true);
};
const handleComplete = (url: string) => url === router.asPath && setLoading(false);
router.events.on("routeChangeStart", handleStart);
router.events.on("routeChangeComplete", handleComplete);
router.events.on("routeChangeError", handleComplete);
return () => {
router.events.off("routeChangeStart", handleStart);
router.events.off("routeChangeComplete", handleComplete);
router.events.off("routeChangeError", handleComplete);
};
});
if (loading) {
return <LoadingOverlay visible loaderProps={{ color: "orange", type: "oval" }} />;
}
return null;
};

View File

@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { LoadingOverlay } from "@mantine/core";
import styled from "styled-components"; import styled from "styled-components";
import Editor, { loader, useMonaco } from "@monaco-editor/react"; import Editor, { loader, useMonaco } from "@monaco-editor/react";
import { Loading } from "src/layout/Loading";
import useConfig from "src/store/useConfig"; import useConfig from "src/store/useConfig";
import useFile from "src/store/useFile"; import useFile from "src/store/useFile";
@ -81,7 +81,7 @@ export const MonacoEditor = () => {
options={editorOptions} options={editorOptions}
onValidate={errors => setError(errors[0]?.message)} onValidate={errors => setError(errors[0]?.message)}
onChange={contents => setContents({ contents, skipUpdate: true })} onChange={contents => setContents({ contents, skipUpdate: true })}
loading={<Loading message="Loading Monaco Editor..." loading />} loading={<LoadingOverlay visible />}
/> />
</StyledWrapper> </StyledWrapper>
); );

View File

@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import { Menu, Text } from "@mantine/core";
import styled from "styled-components"; import styled from "styled-components";
import { firaMono } from "src/constants/fonts"; import { firaMono } from "src/constants/fonts";
import { Graph } from "src/containers/Views/GraphView"; import { Graph } from "src/containers/Views/GraphView";
@ -32,44 +31,8 @@ const View = () => {
}; };
const LiveEditor: React.FC = () => { const LiveEditor: React.FC = () => {
const [contextOpened, setContextOpened] = React.useState(false);
const [contextPosition, setContextPosition] = React.useState({
x: 0,
y: 0,
});
return ( return (
<StyledLiveEditor <StyledLiveEditor onContextMenuCapture={e => e.preventDefault()}>
onContextMenuCapture={e => {
e.preventDefault();
setContextOpened(true);
setContextPosition({ x: e.pageX, y: e.pageY });
}}
onClick={() => setContextOpened(false)}
>
<div
style={{
position: "fixed",
top: contextPosition.y,
left: contextPosition.x,
zIndex: 100,
}}
>
<Menu opened={false} shadow="sm">
<Menu.Dropdown>
<Menu.Item>
<Text size="xs">Download as Image</Text>
</Menu.Item>
<Menu.Item>
<Text size="xs">Zoom to Fit</Text>
</Menu.Item>
<Menu.Item>
<Text size="xs">Rotate</Text>
</Menu.Item>
</Menu.Dropdown>
</Menu>
</div>
<View /> <View />
</StyledLiveEditor> </StyledLiveEditor>
); );

View File

@ -55,41 +55,34 @@ export const TypeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
return typeOptions[typeOptions.findIndex(o => o.value === selectedType)]?.lang; return typeOptions[typeOptions.findIndex(o => o.value === selectedType)]?.lang;
}, [selectedType]); }, [selectedType]);
const transformer = React.useCallback(
async ({ value }) => {
const { run } = await import("json_typegen_wasm");
return run(
"Root",
value,
JSON.stringify({
output_mode: selectedType,
})
);
},
[selectedType]
);
React.useEffect(() => { React.useEffect(() => {
if (opened) { if (opened) {
try { (async () => {
setLoading(true); try {
if (selectedType === Language.Go) { setLoading(true);
import("src/lib/utils/json2go").then(jtg => { const json = getJson();
import("gofmt.js").then(gofmt => {
const types = jtg.default(getJson()); if (selectedType === Language.Go) {
setType(gofmt.default(types.go)); const jtg = await import("src/lib/utils/json2go");
}); const gofmt = await import("gofmt.js");
});
} else { const types = jtg.default(json);
transformer({ value: getJson() }).then(setType); setType(gofmt.default(types.go));
} else {
const { run } = await import("json_typegen_wasm");
const output_mode = selectedType;
const types = run("Root", json, JSON.stringify({ output_mode }));
setType(types);
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
} }
} catch (error) { })();
console.error(error);
} finally {
setLoading(false);
}
} }
}, [getJson, opened, selectedType, transformer]); }, [getJson, opened, selectedType]);
return ( return (
<Modal title="Generate Types" size="md" opened={opened} onClose={onClose} centered> <Modal title="Generate Types" size="md" opened={opened} onClose={onClose} centered>

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { Badge, Flex, Group, Select, Text } from "@mantine/core"; import { Badge, Flex, Group, Indicator, Select, Text } from "@mantine/core";
import { useSessionStorage } from "@mantine/hooks";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { AiOutlineFullscreen } from "react-icons/ai"; import { AiOutlineFullscreen } from "react-icons/ai";
import { AiFillGift } from "react-icons/ai"; import { AiFillGift } from "react-icons/ai";
@ -35,6 +36,10 @@ export const Toolbar: React.FC<{ isWidget?: boolean }> = ({ isWidget = false })
const setFormat = useFile(state => state.setFormat); const setFormat = useFile(state => state.setFormat);
const format = useFile(state => state.format); const format = useFile(state => state.format);
const premium = useUser(state => state.premium); const premium = useUser(state => state.premium);
const [seenPremium, setSeenPremium] = useSessionStorage({
key: "seenPremium",
defaultValue: false,
});
return ( return (
<Styles.StyledTools> <Styles.StyledTools>
@ -74,11 +79,30 @@ export const Toolbar: React.FC<{ isWidget?: boolean }> = ({ isWidget = false })
)} )}
<Group gap="xs" justify="right" w="100%" style={{ flexWrap: "nowrap" }}> <Group gap="xs" justify="right" w="100%" style={{ flexWrap: "nowrap" }}>
{!premium && !isWidget && ( {!premium && !isWidget && (
<Styles.StyledToolElement onClick={() => setVisible("premium")(true)}> <Styles.StyledToolElement
<Text display="flex" c="teal" fz="xs" fw={600} style={{ textAlign: "center", gap: 4 }}> onClick={() => {
<AiFillGift size="18" /> setSeenPremium(true);
Get Premium setVisible("premium")(true);
</Text> }}
>
<Indicator
size={5}
color="green"
position="top-start"
processing
disabled={seenPremium}
>
<Text
display="flex"
c="teal"
fz="xs"
fw={600}
style={{ textAlign: "center", gap: 4 }}
>
<AiFillGift size="18" />
Get Premium
</Text>
</Indicator>
</Styles.StyledToolElement> </Styles.StyledToolElement>
)} )}

View File

@ -1,10 +1,5 @@
import React from "react"; import React from "react";
import dynamic from "next/dynamic"; import { Edge, EdgeProps } from "reaflow";
import { EdgeProps } from "reaflow/dist/symbols/Edge";
const Edge = dynamic(() => import("reaflow").then(r => r.Edge), {
ssr: false,
});
const CustomEdgeWrapper = (props: EdgeProps) => { const CustomEdgeWrapper = (props: EdgeProps) => {
return <Edge containerClassName={`edge-${props.id}`} {...props} />; return <Edge containerClassName={`edge-${props.id}`} {...props} />;

View File

@ -1,16 +1,11 @@
import React from "react"; import React from "react";
import dynamic from "next/dynamic"; import { Node, NodeProps } from "reaflow";
import { NodeProps } from "reaflow";
import useGraph from "src/store/useGraph"; import useGraph from "src/store/useGraph";
import useModal from "src/store/useModal"; import useModal from "src/store/useModal";
import { NodeData } from "src/types/graph"; import { NodeData } from "src/types/graph";
import { ObjectNode } from "./ObjectNode"; import { ObjectNode } from "./ObjectNode";
import { TextNode } from "./TextNode"; import { TextNode } from "./TextNode";
const Node = dynamic(() => import("reaflow").then(r => r.Node), {
ssr: false,
});
export interface CustomNodeProps { export interface CustomNodeProps {
node: NodeData; node: NodeData;
x: number; x: number;

View File

@ -1,22 +1,18 @@
import React from "react"; import React from "react";
import dynamic from "next/dynamic"; import { LoadingOverlay } from "@mantine/core";
import styled from "styled-components"; import styled from "styled-components";
import debounce from "lodash.debounce"; import debounce from "lodash.debounce";
import { Space } from "react-zoomable-ui"; import { Space } from "react-zoomable-ui";
import { Canvas } from "reaflow";
import { ElkRoot } from "reaflow/dist/layout/useLayout"; import { ElkRoot } from "reaflow/dist/layout/useLayout";
import { useLongPress } from "use-long-press"; import { useLongPress } from "use-long-press";
import { CustomNode } from "src/containers/Views/GraphView/CustomNode"; import { CustomNode } from "src/containers/Views/GraphView/CustomNode";
import useToggleHide from "src/hooks/useToggleHide"; import useToggleHide from "src/hooks/useToggleHide";
import { Loading } from "src/layout/Loading";
import useConfig from "src/store/useConfig"; import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph"; import useGraph from "src/store/useGraph";
import { CustomEdge } from "./CustomEdge"; import { CustomEdge } from "./CustomEdge";
import { PremiumView } from "./PremiumView"; import { PremiumView } from "./PremiumView";
const Canvas = dynamic(() => import("reaflow").then(r => r.Canvas), {
ssr: false,
});
interface GraphProps { interface GraphProps {
isWidget?: boolean; isWidget?: boolean;
} }
@ -175,7 +171,7 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
return ( return (
<> <>
<Loading loading={loading} message="Painting graph..." /> <LoadingOverlay visible={loading} />
<StyledEditorWrapper <StyledEditorWrapper
$widget={isWidget} $widget={isWidget}
onContextMenu={e => e.preventDefault()} onContextMenu={e => e.preventDefault()}

View File

@ -1,57 +0,0 @@
import React from "react";
import { Center, Stack, Text } from "@mantine/core";
import styled, { keyframes } from "styled-components";
import { JSONCrackLogo } from "../JsonCrackLogo";
interface LoadingProps {
loading?: boolean;
message?: string;
}
const fadeIn = keyframes`
99% {
visibility: hidden;
}
100% {
visibility: visible;
}
`;
const StyledLoading = styled.div<{ $visible: boolean }>`
display: ${({ $visible }) => ($visible ? "grid" : "none")};
position: fixed;
top: 0;
left: 0;
place-content: center;
width: 100%;
height: 100vh;
text-align: center;
z-index: 100;
pointer-events: visiblePainted;
animation: 200ms ${fadeIn};
animation-fill-mode: forwards;
visibility: hidden;
background: ${({ theme }) => theme.BACKGROUND_NODE};
opacity: 0.8;
color: ${({ theme }) => theme.INTERACTIVE_HOVER};
cursor: wait;
img {
transform: rotate(45deg);
}
`;
export const Loading = ({ loading = false, message }: LoadingProps) => {
return (
<Center mx="auto">
<StyledLoading $visible={loading}>
<Stack>
<JSONCrackLogo fontSize="3rem" />
<Text fz="lg" fw="bold">
{message ?? "Preparing the environment for you..."}
</Text>
</Stack>
</StyledLoading>
</Center>
);
};

View File

@ -1,6 +1,6 @@
import React from "react"; import React from "react";
import Link from "next/link"; import Link from "next/link";
import { Box, Burger, Button, Flex, Overlay } from "@mantine/core"; import { Button } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks"; import { useDisclosure } from "@mantine/hooks";
import styled from "styled-components"; import styled from "styled-components";
import useUser from "src/store/useUser"; import useUser from "src/store/useUser";
@ -26,18 +26,6 @@ const StyledNavbar = styled.nav`
@media only screen and (max-width: 768px) { @media only screen and (max-width: 768px) {
padding: 16px 24px; padding: 16px 24px;
} }
@media only screen and (max-width: 1024px) {
.desktop {
display: none;
}
}
@media only screen and (max-width: 768px) {
.hide-mobile {
display: none;
}
}
`; `;
const Left = styled.div``; const Left = styled.div``;
@ -98,74 +86,28 @@ export const Navbar = () => {
</Button> </Button>
</Left> </Left>
<Right> <Right>
{!hasSession && ( <Button
<> component="a"
<Button href="https://app.jsoncrack.com/sign-in"
component="a" variant="subtle"
href="https://app.jsoncrack.com/sign-in" color="gray"
variant="outline" radius="xl"
color="gray" visibleFrom="sm"
className="hide-mobile" size="md"
radius="md" >
visibleFrom="sm" Login
size="md" </Button>
> <Button
Login component={Link}
</Button> prefetch={false}
<Button href={premium ? "https://app.jsoncrack.com/editor" : "/editor"}
component={Link} color="dark"
prefetch={false} visibleFrom="sm"
href={premium ? "https://app.jsoncrack.com/editor" : "/editor"} radius="xl"
color="dark" size="md"
className="hide-mobile" >
visibleFrom="sm" Editor
radius="md" </Button>
size="md"
>
Editor
</Button>
</>
)}
{hasSession && (
<Button
color="dark"
size="md"
radius="md"
component={Link}
href="/editor"
prefetch={false}
visibleFrom="sm"
>
Editor
</Button>
)}
<Burger opened={opened} onClick={toggle} aria-label="Toggle navigation" hiddenFrom="sm" />
{opened && (
<Overlay top={56} h="100dvh">
<Box
bg="white"
top={56}
left={0}
pos="fixed"
w="100%"
pb="lg"
style={{ zIndex: 3, borderBottom: "1px solid black" }}
>
<Flex pt="lg" direction="column" align="center" justify="center" gap="lg">
<Button
component={Link}
href="/pricing"
variant="transparent"
color="dark"
radius="md"
onClick={toggle}
>
Pricing
</Button>
</Flex>
</Box>
</Overlay>
)}
</Right> </Right>
</StyledNavbar> </StyledNavbar>
</StyledNavbarWrapper> </StyledNavbarWrapper>

View File

@ -8,12 +8,14 @@ import "@mantine/core/styles.css";
import "@mantine/code-highlight/styles.css"; import "@mantine/code-highlight/styles.css";
import { ThemeProvider } from "styled-components"; import { ThemeProvider } from "styled-components";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import { Loading } from "src/components/Loading";
import GlobalStyle from "src/constants/globalStyle"; import GlobalStyle from "src/constants/globalStyle";
import { lightTheme } from "src/constants/theme"; import { lightTheme } from "src/constants/theme";
import { supabase } from "src/lib/api/supabase"; import { supabase } from "src/lib/api/supabase";
import useUser from "src/store/useUser"; import useUser from "src/store/useUser";
const Toaster = dynamic(() => import("react-hot-toast").then(c => c.Toaster)); const Toaster = dynamic(() => import("react-hot-toast").then(c => c.Toaster));
const ExternalMode = dynamic(() => import("src/layout/ExternalMode"));
const mantineTheme = createTheme({ const mantineTheme = createTheme({
primaryShade: 8, primaryShade: 8,
@ -24,8 +26,6 @@ const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;
ReactGA.initialize(GA_TRACKING_ID, { testMode: isDevelopment }); ReactGA.initialize(GA_TRACKING_ID, { testMode: isDevelopment });
const ExternalMode = dynamic(() => import("src/layout/ExternalMode"));
function JsonCrack({ Component, pageProps }: AppProps) { function JsonCrack({ Component, pageProps }: AppProps) {
const router = useRouter(); const router = useRouter();
const setSession = useUser(state => state.setSession); const setSession = useUser(state => state.setSession);
@ -72,6 +72,7 @@ function JsonCrack({ Component, pageProps }: AppProps) {
}} }}
/> />
<GlobalStyle /> <GlobalStyle />
<Loading />
<Component {...pageProps} /> <Component {...pageProps} />
<ExternalMode /> <ExternalMode />
</ThemeProvider> </ThemeProvider>

View File

@ -1,16 +1,12 @@
import React from "react"; import React from "react";
import dynamic from "next/dynamic";
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import styled from "styled-components"; import styled from "styled-components";
import { BottomBar } from "src/containers/Editor/BottomBar"; import { BottomBar } from "src/containers/Editor/BottomBar";
import Panes from "src/containers/Editor/Panes";
import { Toolbar } from "src/containers/Toolbar"; import { Toolbar } from "src/containers/Toolbar";
import { EditorWrapper } from "src/layout/EditorWrapper"; import { EditorWrapper } from "src/layout/EditorWrapper";
import { Loading } from "src/layout/Loading";
import useFile from "src/store/useFile"; import useFile from "src/store/useFile";
import useJson from "src/store/useJson";
const Panes = dynamic(() => import("src/containers/Editor/Panes"));
export const StyledPageWrapper = styled.div` export const StyledPageWrapper = styled.div`
height: calc(100vh - 27px); height: calc(100vh - 27px);
@ -29,7 +25,6 @@ export const StyledEditorWrapper = styled.div`
const EditorPage: React.FC = () => { const EditorPage: React.FC = () => {
const { query, isReady } = useRouter(); const { query, isReady } = useRouter();
const loading = useJson(state => state.loading);
const hasQuery = React.useMemo(() => Object.keys(query).length > 0, [query]); const hasQuery = React.useMemo(() => Object.keys(query).length > 0, [query]);
const checkEditorSession = useFile(state => state.checkEditorSession); const checkEditorSession = useFile(state => state.checkEditorSession);
@ -37,24 +32,13 @@ const EditorPage: React.FC = () => {
if (isReady) checkEditorSession(query?.json); if (isReady) checkEditorSession(query?.json);
}, [checkEditorSession, isReady, query]); }, [checkEditorSession, isReady, query]);
if (loading) {
return (
<StyledEditorWrapper>
<Head>
<title>Editor | JSON Crack</title>
{hasQuery && <meta name="robots" content="noindex,nofollow" />}
</Head>
<Loading message="Preparing the editor for you..." loading />
</StyledEditorWrapper>
);
}
return ( return (
<EditorWrapper> <EditorWrapper>
<StyledEditorWrapper> <StyledEditorWrapper>
<Head> <Head>
<title>Editor | JSON Crack</title> <title>Editor | JSON Crack</title>
<link rel="canonical" href="https://jsoncrack.com/editor" /> <link rel="canonical" href="https://jsoncrack.com/editor" />
{hasQuery && <meta name="robots" content="noindex,nofollow" />}
</Head> </Head>
<StyledPageWrapper> <StyledPageWrapper>
<Toolbar /> <Toolbar />

View File

@ -8,7 +8,6 @@ import {
Center, Center,
Flex, Flex,
Grid, Grid,
Group,
Image, Image,
Paper, Paper,
Stack, Stack,
@ -21,7 +20,6 @@ import { Carousel } from "@mantine/carousel";
import "@mantine/carousel/styles.css"; import "@mantine/carousel/styles.css";
import styled from "styled-components"; import styled from "styled-components";
import { BiChevronDown } from "react-icons/bi"; import { BiChevronDown } from "react-icons/bi";
import { FaRocket } from "react-icons/fa";
import { import {
MdChevronRight, MdChevronRight,
MdCompare, MdCompare,
@ -78,19 +76,19 @@ const StyledHeroSectionBody = styled.div`
const StyledHeroText = styled.p` const StyledHeroText = styled.p`
font-size: 0.8rem; font-size: 0.8rem;
color: #414141; color: #5b5b5b;
font-weight: 400; font-weight: 400;
max-width: 100%; max-width: 100%;
min-width: 400px; min-width: 400px;
text-align: center; text-align: center;
@media only screen and (min-width: 576px) { @media only screen and (min-width: 576px) {
font-size: 1.3rem; font-size: 1.2rem;
max-width: 80%; max-width: 80%;
} }
@media only screen and (min-width: 1400px) { @media only screen and (min-width: 1400px) {
font-size: 1.4rem; font-size: 1.3rem;
max-width: 60%; max-width: 60%;
} }
`; `;
@ -212,77 +210,51 @@ export const HomePage = () => {
Experience the ultimate online editor designed to empower you in visualizing, Experience the ultimate online editor designed to empower you in visualizing,
refining, and formatting data effortlessly. refining, and formatting data effortlessly.
</StyledHeroText> </StyledHeroText>
<Group justify="center">
<Button
component={Link}
prefetch={false}
href="/editor"
size="xl"
fw="bold"
color="indigo"
rightSection={<MdChevronRight size={30} />}
visibleFrom="sm"
radius="md"
>
Go To Editor
</Button>
<Button
component={Link}
prefetch={false}
href="/editor"
fw="bold"
size="md"
color="indigo"
rightSection={<MdChevronRight size={24} />}
hiddenFrom="sm"
radius="md"
>
Go To Editor
</Button>
<Button
component="a"
href="/#features"
size="xl"
fw="bold"
variant="outline"
color="gray.7"
leftSection={<FaRocket />}
visibleFrom="sm"
radius="md"
>
Explore Premium
</Button>
<Button
component="a"
href="/#features"
fw="bold"
size="md"
variant="outline"
color="gray.7"
leftSection={<FaRocket />}
hiddenFrom="sm"
radius="md"
>
Explore Premium
</Button>
</Group>
<Flex gap="xs"> <Flex gap="xs">
<Badge color="dark" radius="sm" variant="light"> <Badge size="xs" color="dark" radius="sm" variant="light">
JSON JSON
</Badge> </Badge>
<Badge color="dark" radius="sm" variant="light"> <Badge size="xs" color="dark" radius="sm" variant="light">
YAML YAML
</Badge> </Badge>
<Badge color="dark" radius="sm" variant="light"> <Badge size="xs" color="dark" radius="sm" variant="light">
CSV CSV
</Badge> </Badge>
<Badge color="dark" radius="sm" variant="light"> <Badge size="xs" color="dark" radius="sm" variant="light">
XML XML
</Badge> </Badge>
<Badge color="dark" radius="sm" variant="light"> <Badge size="xs" color="dark" radius="sm" variant="light">
TOML TOML
</Badge> </Badge>
</Flex> </Flex>
<Button
component={Link}
prefetch={false}
href="/editor"
size="xl"
fw="bold"
color="orange"
rightSection={<MdChevronRight size={30} />}
visibleFrom="sm"
radius="xl"
mt="lg"
>
Go to Editor
</Button>
<Button
component={Link}
prefetch={false}
href="/editor"
fw="bold"
size="md"
color="indigo"
rightSection={<MdChevronRight size={24} />}
hiddenFrom="sm"
radius="xl"
mt="lg"
>
Go to Editor
</Button>
</Stack> </Stack>
</StyledHeroSectionBody> </StyledHeroSectionBody>
</StyledHeroSection> </StyledHeroSection>