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

View File

@ -1,5 +1,4 @@
import React from "react";
import { Menu, Text } from "@mantine/core";
import styled from "styled-components";
import { firaMono } from "src/constants/fonts";
import { Graph } from "src/containers/Views/GraphView";
@ -32,44 +31,8 @@ const View = () => {
};
const LiveEditor: React.FC = () => {
const [contextOpened, setContextOpened] = React.useState(false);
const [contextPosition, setContextPosition] = React.useState({
x: 0,
y: 0,
});
return (
<StyledLiveEditor
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>
<StyledLiveEditor onContextMenuCapture={e => e.preventDefault()}>
<View />
</StyledLiveEditor>
);

View File

@ -55,41 +55,34 @@ export const TypeModal: React.FC<ModalProps> = ({ opened, onClose }) => {
return typeOptions[typeOptions.findIndex(o => o.value === selectedType)]?.lang;
}, [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(() => {
if (opened) {
try {
setLoading(true);
if (selectedType === Language.Go) {
import("src/lib/utils/json2go").then(jtg => {
import("gofmt.js").then(gofmt => {
const types = jtg.default(getJson());
setType(gofmt.default(types.go));
});
});
} else {
transformer({ value: getJson() }).then(setType);
(async () => {
try {
setLoading(true);
const json = getJson();
if (selectedType === Language.Go) {
const jtg = await import("src/lib/utils/json2go");
const gofmt = await import("gofmt.js");
const types = jtg.default(json);
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 (
<Modal title="Generate Types" size="md" opened={opened} onClose={onClose} centered>

View File

@ -1,5 +1,6 @@
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 { AiOutlineFullscreen } 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 format = useFile(state => state.format);
const premium = useUser(state => state.premium);
const [seenPremium, setSeenPremium] = useSessionStorage({
key: "seenPremium",
defaultValue: false,
});
return (
<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" }}>
{!premium && !isWidget && (
<Styles.StyledToolElement onClick={() => setVisible("premium")(true)}>
<Text display="flex" c="teal" fz="xs" fw={600} style={{ textAlign: "center", gap: 4 }}>
<AiFillGift size="18" />
Get Premium
</Text>
<Styles.StyledToolElement
onClick={() => {
setSeenPremium(true);
setVisible("premium")(true);
}}
>
<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>
)}

View File

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

View File

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

View File

@ -1,22 +1,18 @@
import React from "react";
import dynamic from "next/dynamic";
import { LoadingOverlay } from "@mantine/core";
import styled from "styled-components";
import debounce from "lodash.debounce";
import { Space } from "react-zoomable-ui";
import { Canvas } from "reaflow";
import { ElkRoot } from "reaflow/dist/layout/useLayout";
import { useLongPress } from "use-long-press";
import { CustomNode } from "src/containers/Views/GraphView/CustomNode";
import useToggleHide from "src/hooks/useToggleHide";
import { Loading } from "src/layout/Loading";
import useConfig from "src/store/useConfig";
import useGraph from "src/store/useGraph";
import { CustomEdge } from "./CustomEdge";
import { PremiumView } from "./PremiumView";
const Canvas = dynamic(() => import("reaflow").then(r => r.Canvas), {
ssr: false,
});
interface GraphProps {
isWidget?: boolean;
}
@ -175,7 +171,7 @@ export const Graph = ({ isWidget = false }: GraphProps) => {
return (
<>
<Loading loading={loading} message="Painting graph..." />
<LoadingOverlay visible={loading} />
<StyledEditorWrapper
$widget={isWidget}
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 Link from "next/link";
import { Box, Burger, Button, Flex, Overlay } from "@mantine/core";
import { Button } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import styled from "styled-components";
import useUser from "src/store/useUser";
@ -26,18 +26,6 @@ const StyledNavbar = styled.nav`
@media only screen and (max-width: 768px) {
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``;
@ -98,74 +86,28 @@ export const Navbar = () => {
</Button>
</Left>
<Right>
{!hasSession && (
<>
<Button
component="a"
href="https://app.jsoncrack.com/sign-in"
variant="outline"
color="gray"
className="hide-mobile"
radius="md"
visibleFrom="sm"
size="md"
>
Login
</Button>
<Button
component={Link}
prefetch={false}
href={premium ? "https://app.jsoncrack.com/editor" : "/editor"}
color="dark"
className="hide-mobile"
visibleFrom="sm"
radius="md"
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>
)}
<Button
component="a"
href="https://app.jsoncrack.com/sign-in"
variant="subtle"
color="gray"
radius="xl"
visibleFrom="sm"
size="md"
>
Login
</Button>
<Button
component={Link}
prefetch={false}
href={premium ? "https://app.jsoncrack.com/editor" : "/editor"}
color="dark"
visibleFrom="sm"
radius="xl"
size="md"
>
Editor
</Button>
</Right>
</StyledNavbar>
</StyledNavbarWrapper>

View File

@ -8,12 +8,14 @@ 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 { supabase } from "src/lib/api/supabase";
import useUser from "src/store/useUser";
const Toaster = dynamic(() => import("react-hot-toast").then(c => c.Toaster));
const ExternalMode = dynamic(() => import("src/layout/ExternalMode"));
const mantineTheme = createTheme({
primaryShade: 8,
@ -24,8 +26,6 @@ const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;
ReactGA.initialize(GA_TRACKING_ID, { testMode: isDevelopment });
const ExternalMode = dynamic(() => import("src/layout/ExternalMode"));
function JsonCrack({ Component, pageProps }: AppProps) {
const router = useRouter();
const setSession = useUser(state => state.setSession);
@ -72,6 +72,7 @@ function JsonCrack({ Component, pageProps }: AppProps) {
}}
/>
<GlobalStyle />
<Loading />
<Component {...pageProps} />
<ExternalMode />
</ThemeProvider>

View File

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

View File

@ -8,7 +8,6 @@ import {
Center,
Flex,
Grid,
Group,
Image,
Paper,
Stack,
@ -21,7 +20,6 @@ import { Carousel } from "@mantine/carousel";
import "@mantine/carousel/styles.css";
import styled from "styled-components";
import { BiChevronDown } from "react-icons/bi";
import { FaRocket } from "react-icons/fa";
import {
MdChevronRight,
MdCompare,
@ -78,19 +76,19 @@ const StyledHeroSectionBody = styled.div`
const StyledHeroText = styled.p`
font-size: 0.8rem;
color: #414141;
color: #5b5b5b;
font-weight: 400;
max-width: 100%;
min-width: 400px;
text-align: center;
@media only screen and (min-width: 576px) {
font-size: 1.3rem;
font-size: 1.2rem;
max-width: 80%;
}
@media only screen and (min-width: 1400px) {
font-size: 1.4rem;
font-size: 1.3rem;
max-width: 60%;
}
`;
@ -212,77 +210,51 @@ export const HomePage = () => {
Experience the ultimate online editor designed to empower you in visualizing,
refining, and formatting data effortlessly.
</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">
<Badge color="dark" radius="sm" variant="light">
<Badge size="xs" color="dark" radius="sm" variant="light">
JSON
</Badge>
<Badge color="dark" radius="sm" variant="light">
<Badge size="xs" color="dark" radius="sm" variant="light">
YAML
</Badge>
<Badge color="dark" radius="sm" variant="light">
<Badge size="xs" color="dark" radius="sm" variant="light">
CSV
</Badge>
<Badge color="dark" radius="sm" variant="light">
<Badge size="xs" color="dark" radius="sm" variant="light">
XML
</Badge>
<Badge color="dark" radius="sm" variant="light">
<Badge size="xs" color="dark" radius="sm" variant="light">
TOML
</Badge>
</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>
</StyledHeroSectionBody>
</StyledHeroSection>