feat: remove supabase & cloud related actions

This commit is contained in:
AykutSarac 2024-10-02 13:38:00 +03:00
parent 99f4ad3d04
commit 38a9287b0a
No known key found for this signature in database
31 changed files with 54 additions and 1345 deletions

2
.env
View File

@ -1,3 +1 @@
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-JKZEHMJBMH
NEXT_PUBLIC_SUPABASE_URL=https://bxkgqurwqjmvrqekcbws.supabase.co
NEXT_PUBLIC_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJ4a2dxdXJ3cWptdnJxZWtjYndzIiwicm9sZSI6ImFub24iLCJpYXQiOjE2OTA2NDU0MjUsImV4cCI6MjAwNjIyMTQyNX0.3nZ0yhuFjnI3yHbAL8S9UtK-Ny-6F5AylNHgo1tymTU

View File

@ -7,7 +7,7 @@ Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approacha
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
## Getting Started
JSON Crack is built with React, Reaflow for visualization, Mantine UI for components, Zustand for state management, and Supabase for backend integration. If you are not familiar with these technologies, we recommend you to read their documentation to get started. You can find the links to the respective documentations below:
JSON Crack is built with React, Reaflow for visualization, Mantine UI for components, Zustand for state management. If you are not familiar with these technologies, we recommend you to read their documentation to get started. You can find the links to the respective documentations below:
* [React](https://reactjs.org/docs/getting-started.html)
* [Reaflow](https://github.com/reaviz/reaflow)

View File

@ -59,7 +59,6 @@ JSON Crack is a free, open-source data visualization app capable of visualizing
- [Next.js](https://nextjs.org/?ref=jsoncrack.com)
- [React.js](https://reactjs.org/?ref=jsoncrack.com)
- [Supabase](https://supabase.com/?ref=jsoncrack.com)
- [Reaflow](https://reaflow.dev/?ref=jsoncrack.com)
- [Monaco Editor](https://github.com/suren-atoyan/monaco-react)

View File

@ -14,12 +14,4 @@ server {
location /docs {
try_files $uri /docs.html;
}
location /forgot-password {
try_files $uri /forgot-password.html;
}
location /sign-in {
try_files $uri /sign-in.html;
}
}

View File

@ -8,8 +8,8 @@
},
"homepage": "https://jsoncrack.com",
"bugs": {
"url": "https://github.com/AykutSarac/jsoncrack.com/issues"
},
"url": "https://github.com/AykutSarac/jsoncrack.com/issues"
},
"scripts": {
"dev": "next dev",
"build": "next build",
@ -25,7 +25,6 @@
"@mantine/hooks": "^7.11.2",
"@monaco-editor/react": "^4.6.0",
"@sentry/nextjs": "^7.118.0",
"@supabase/supabase-js": "^2.44.4",
"@tanstack/react-query": "^4.36.1",
"allotment": "^1.20.2",
"axios": "^1.7.2",

93
pnpm-lock.yaml generated
View File

@ -26,9 +26,6 @@ importers:
'@sentry/nextjs':
specifier: ^7.118.0
version: 7.118.0(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@supabase/supabase-js':
specifier: ^2.44.4
version: 2.44.4
'@tanstack/react-query':
specifier: ^4.36.1
version: 4.36.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -618,28 +615,6 @@ packages:
resolution: {integrity: sha512-x0PYIMWcsTauqxgl7vWUY6sANl+XGKtx7DCVnnY7aOIIlIna0jChTAPANTfA2QrK+VK+4I/4JxatCEZBnXh3Og==}
engines: {node: '>= 8'}
'@supabase/auth-js@2.64.4':
resolution: {integrity: sha512-9ITagy4WP4FLl+mke1rchapOH0RQpf++DI+WSG2sO1OFOZ0rW3cwAM0nCrMOxu+Zw4vJ4zObc08uvQrXx590Tg==}
'@supabase/functions-js@2.4.1':
resolution: {integrity: sha512-8sZ2ibwHlf+WkHDUZJUXqqmPvWQ3UHN0W30behOJngVh/qHHekhJLCFbh0AjkE9/FqqXtf9eoVvmYgfCLk5tNA==}
'@supabase/node-fetch@2.6.15':
resolution: {integrity: sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==}
engines: {node: 4.x || >=6.0.0}
'@supabase/postgrest-js@1.15.8':
resolution: {integrity: sha512-YunjXpoQjQ0a0/7vGAvGZA2dlMABXFdVI/8TuVKtlePxyT71sl6ERl6ay1fmIeZcqxiuFQuZw/LXUuStUG9bbg==}
'@supabase/realtime-js@2.10.2':
resolution: {integrity: sha512-qyCQaNg90HmJstsvr2aJNxK2zgoKh9ZZA8oqb7UT2LCh3mj9zpa3Iwu167AuyNxsxrUE8eEJ2yH6wLCij4EApA==}
'@supabase/storage-js@2.6.0':
resolution: {integrity: sha512-REAxr7myf+3utMkI2oOmZ6sdplMZZ71/2NEIEMBZHL9Fkmm3/JnaOZVSRqvG4LStYj2v5WhCruCzuMn6oD/Drw==}
'@supabase/supabase-js@2.44.4':
resolution: {integrity: sha512-vqtUp8umqcgj+RPUc7LiEcQmgsEWFDPJdJizRJF/5tf2zSlVB+3YbUwyQE/hLagYA8TLvGXe7oAqtYyFde6llw==}
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
@ -706,9 +681,6 @@ packages:
'@types/node@20.14.11':
resolution: {integrity: sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==}
'@types/phoenix@1.6.5':
resolution: {integrity: sha512-xegpDuR+z0UqG9fwHqNoy3rI7JDlvaPh2TY47Fl80oq6g+hXT+c/LEuE43X48clZ6lOfANl5WrPur9fYO1RJ/w==}
'@types/prop-types@15.7.12':
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==}
@ -724,9 +696,6 @@ packages:
'@types/stylis@4.2.5':
resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==}
'@types/ws@8.5.11':
resolution: {integrity: sha512-4+q7P5h3SpJxaBft0Dzpbr6lmMaqh0Jr2tbhJZ/luAwvD7ohSCniYkwz/pLxuT2h0EOa6QADgJj1Ko+TzRfZ+w==}
'@typescript-eslint/eslint-plugin@7.17.0':
resolution: {integrity: sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==}
engines: {node: ^18.18.0 || >=20.0.0}
@ -2793,18 +2762,6 @@ packages:
utf-8-validate:
optional: true
ws@8.18.0:
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
xmldom@0.1.31:
resolution: {integrity: sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==}
engines: {node: '>=0.1'}
@ -3332,48 +3289,6 @@ snapshots:
- encoding
- supports-color
'@supabase/auth-js@2.64.4':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/functions-js@2.4.1':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/node-fetch@2.6.15':
dependencies:
whatwg-url: 5.0.0
'@supabase/postgrest-js@1.15.8':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/realtime-js@2.10.2':
dependencies:
'@supabase/node-fetch': 2.6.15
'@types/phoenix': 1.6.5
'@types/ws': 8.5.11
ws: 8.18.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@supabase/storage-js@2.6.0':
dependencies:
'@supabase/node-fetch': 2.6.15
'@supabase/supabase-js@2.44.4':
dependencies:
'@supabase/auth-js': 2.64.4
'@supabase/functions-js': 2.4.1
'@supabase/node-fetch': 2.6.15
'@supabase/postgrest-js': 1.15.8
'@supabase/realtime-js': 2.10.2
'@supabase/storage-js': 2.6.0
transitivePeerDependencies:
- bufferutil
- utf-8-validate
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.5':
@ -3431,8 +3346,6 @@ snapshots:
dependencies:
undici-types: 5.26.5
'@types/phoenix@1.6.5': {}
'@types/prop-types@15.7.12': {}
'@types/react-dom@18.3.0':
@ -3449,10 +3362,6 @@ snapshots:
'@types/stylis@4.2.5': {}
'@types/ws@8.5.11':
dependencies:
'@types/node': 20.14.11
'@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@8.56.0)(typescript@5.3.3))(eslint@8.56.0)(typescript@5.3.3)':
dependencies:
'@eslint-community/regexpp': 4.11.0
@ -5771,8 +5680,6 @@ snapshots:
ws@7.5.10: {}
ws@8.18.0: {}
xmldom@0.1.31: {}
yn@3.1.1: {}

View File

@ -2,7 +2,6 @@ User-agent: *
Allow: /
User-agent: *
Disallow: /forgot-password
Disallow: /widget
Sitemap: https://jsoncrack.com/sitemap.txt

View File

@ -1,6 +1,4 @@
https://jsoncrack.com
https://jsoncrack.com/sign-in
https://jsoncrack.com/sign-up
https://jsoncrack.com/editor
https://jsoncrack.com/docs
https://jsoncrack.com/widget

View File

@ -9,14 +9,12 @@ import {
VscError,
VscFeedback,
VscRunAll,
VscSourceControl,
VscSync,
VscSyncIgnored,
} from "react-icons/vsc";
import useGraph from "src/containers/Editor/components/views/GraphView/stores/useGraph";
import useConfig from "src/store/useConfig";
import useFile from "src/store/useFile";
import useModal from "src/store/useModal";
const StyledBottomBar = styled.div`
position: relative;
@ -88,12 +86,9 @@ export const BottomBar = () => {
const error = useFile(state => state.error);
const setContents = useFile(state => state.setContents);
const nodeCount = useGraph(state => state.nodes.length);
const fileName = useFile(state => state.fileData?.name);
const toggleFullscreen = useGraph(state => state.toggleFullscreen);
const fullscreen = useGraph(state => state.fullscreen);
const setVisible = useModal(state => state.setVisible);
const toggleEditor = () => {
toggleFullscreen(!fullscreen);
gaEvent("toggle_fullscreen");
@ -109,12 +104,6 @@ export const BottomBar = () => {
<StyledBottomBarItem onClick={toggleEditor}>
<BiSolidDockLeft />
</StyledBottomBarItem>
{fileName && (
<StyledBottomBarItem onClick={() => setVisible("cloud")(true)}>
<VscSourceControl />
{fileName}
</StyledBottomBarItem>
)}
<StyledBottomBarItem>
{error ? (
<Popover width="auto" shadow="md" position="top" withArrow>

View File

@ -1,68 +0,0 @@
import React from "react";
import type { ModalProps } from "@mantine/core";
import { Modal, Group, Button, Avatar, Text, Divider, Paper, Badge, Anchor } from "@mantine/core";
import useUser from "src/store/useUser";
export const AccountModal = ({ opened, onClose }: ModalProps) => {
const user = useUser(state => state.user);
const logout = useUser(state => state.logout);
const username =
user?.user_metadata.full_name || user?.user_metadata.display_name || user?.user_metadata.name;
return (
<Modal title={`Hello, ${username}!`} opened={opened} onClose={onClose} centered>
<Paper p="md">
<Group>
<Avatar src={user?.user_metadata.avatar_url} size={94}>
JC
</Avatar>
<div>
<Text fz="lg" tt="uppercase" fw={700}>
{username}
</Text>
<Group gap={10} mt={3}>
<Text fz="xs" c="dimmed">
{user?.email}
</Text>
</Group>
<Group gap={10} mt={5}>
<Text fz="xs" c="dimmed">
<Badge
size="sm"
variant="dot"
color="dark"
gradient={{ from: "#8800fe", to: "#ff00cc", deg: 35 }}
>
Free
</Badge>
</Text>
</Group>
</div>
</Group>
</Paper>
<Text fz="xs" c="dimmed">
If you&apos;re already a premium user, please login at{" "}
<Anchor inherit href="https://todiagram.com" target="_blank">
ToDiagram
</Anchor>
.
</Text>
<Divider my="xs" />
<Group justify="right">
<Button
variant="light"
color="red"
onClick={() => {
logout();
onClose();
}}
>
Log Out
</Button>
</Group>
</Modal>
);
};

View File

@ -1,35 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
import type { ModalProps } from "@mantine/core";
import { Modal, Group, Button, Text, Divider } from "@mantine/core";
import { documentSvc } from "src/lib/api/document.service";
import useJson from "src/store/useJson";
export const ClearModal = ({ opened, onClose }: ModalProps) => {
const setJson = useJson(state => state.setJson);
const { query, replace } = useRouter();
const handleClear = () => {
setJson("{}");
onClose();
if (typeof query.json === "string") {
documentSvc.delete(query.json);
replace("/editor");
}
};
return (
<Modal title="Delete JSON" opened={opened} onClose={onClose} centered>
<Group py="sm">
<Text>Are you sure you want to delete JSON?</Text>
</Group>
<Divider py="xs" />
<Group justify="right">
<Button color="red" onClick={handleClear}>
Confirm
</Button>
</Group>
</Modal>
);
};

View File

@ -1,260 +0,0 @@
import React from "react";
import { useRouter } from "next/router";
import type { ModalProps, DefaultMantineColor } from "@mantine/core";
import {
Text,
ScrollArea,
Table,
ActionIcon,
Badge,
Paper,
Flex,
Button,
RingProgress,
Drawer,
LoadingOverlay,
Menu,
TextInput,
Alert,
} from "@mantine/core";
import { useDebouncedValue } from "@mantine/hooks";
import { useQuery } from "@tanstack/react-query";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import { event as gaEvent } from "nextjs-google-analytics";
import toast from "react-hot-toast";
import { BiSearch } from "react-icons/bi";
import { FaTrash } from "react-icons/fa";
import { LuDownload } from "react-icons/lu";
import { SlOptionsVertical } from "react-icons/sl";
import { VscAdd, VscWarning } from "react-icons/vsc";
import type { FileFormat } from "src/enums/file.enum";
import { documentSvc } from "src/lib/api/document.service";
import type { File } from "src/store/useFile";
import useFile from "src/store/useFile";
import useModal from "src/store/useModal";
dayjs.extend(relativeTime);
const colorByFormat: Record<FileFormat, DefaultMantineColor> = {
json: "orange",
yaml: "blue",
xml: "red",
toml: "dark",
csv: "grape",
};
const TOTAL_QUOTA = 25;
export const CloudModal = ({ opened, onClose }: ModalProps) => {
const setVisible = useModal(state => state.setVisible);
const setFile = useFile(state => state.setFile);
const [searchValue, setSearchValue] = React.useState("");
const [debouncedSearchValue] = useDebouncedValue(searchValue, 1000);
const { isReady, replace } = useRouter();
const { data, isLoading, refetch } = useQuery(
["allJson", debouncedSearchValue],
() => documentSvc.getAll(debouncedSearchValue),
{ enabled: isReady && opened }
);
const isCreateDisabled = React.useMemo(() => {
if (!data?.length) return false;
return data.length >= TOTAL_QUOTA;
}, [data?.length]);
const onCreate = async () => {
replace({ query: undefined });
onClose();
};
const openFile = React.useCallback(
async (id: string) => {
try {
const { data, error } = await documentSvc.getById(id);
if (error) throw new Error(error.message);
if (data[0]) setFile(data[0]);
gaEvent("open_cloud_file");
} catch (error) {
if (error instanceof Error) toast.error(error.message);
} finally {
onClose();
}
},
[onClose, setFile]
);
const downloadFile = React.useCallback(async (id: string) => {
try {
// it will fetch the file first, then download it with corresponsing format
const { data, error } = await documentSvc.getById(id);
if (error) throw new Error(error.message);
const blob = new Blob([data[0].content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `${data[0].name}.${data[0].format}`;
a.click();
URL.revokeObjectURL(url);
gaEvent("download_cloud_file");
return true;
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
}
return false;
}
}, []);
const onDeleteClick = React.useCallback(
async (file: File) => {
try {
toast.loading("Deleting file...", { id: "delete-file" });
const { error } = await documentSvc.delete(file.id);
if (error) throw new Error(error.message);
await refetch();
toast.success(`Deleted ${file.name}!`, { id: "delete-file" });
gaEvent("delete_cloud_file");
} catch (error) {
if (error instanceof Error) {
toast.error(error.message, { id: "delete-file" });
}
} finally {
toast.dismiss("delete-file");
}
},
[refetch]
);
const rows = React.useMemo(
() =>
data?.map(element => (
<Table.Tr
key={element.id}
onClick={() => openFile(element.id)}
style={{ cursor: "pointer" }}
>
<Table.Td>
<Badge
variant="transparent"
color={colorByFormat[element.format]}
size="sm"
radius="xs"
>
{element.format.toUpperCase()}
</Badge>
</Table.Td>
<Table.Td>{element.name}</Table.Td>
<Table.Td fz="10" c="dimmed" title={dayjs(element.updated_at).format("DD MMM, YYYY")}>
{dayjs(element.updated_at).fromNow()}
</Table.Td>
<Table.Td onClick={e => e.stopPropagation()}>
<Flex gap="xs">
<Menu position="bottom" withArrow shadow="md">
<Menu.Target>
<ActionIcon variant="subtle" color="gray">
<SlOptionsVertical />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
fz="xs"
onClick={() => downloadFile(element.id)}
leftSection={<LuDownload />}
>
Download
</Menu.Item>
<Menu.Divider />
<Menu.Item
c="red"
fz="xs"
onClick={() => onDeleteClick(element)}
leftSection={<FaTrash />}
>
Delete
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Flex>
</Table.Td>
</Table.Tr>
)),
[data, downloadFile, onDeleteClick, openFile]
);
return (
<Drawer
title="Saved On The Cloud"
opened={opened}
size="xl"
onClose={onClose}
transitionProps={{ duration: 300, timingFunction: "ease", transition: "slide-left" }}
pos="relative"
position="right"
>
<Alert color="red" icon={<VscWarning />}>
Cloud storage will be terminated on <b>1 October 2024</b>. Please download your data before
the deadline.
<Button color="red" variant="light" mt="sm" onClick={() => setVisible("notice")(true)}>
Read More
</Button>
</Alert>
<Text fz="xs" py="lg">
The Cloud Save feature is primarily designed for convenient access and is not advisable for
storing sensitive data.
</Text>
<Paper>
<TextInput
value={searchValue}
onChange={e => setSearchValue(e.currentTarget.value)}
size="xs"
mb="sm"
placeholder="Search"
leftSection={<BiSearch />}
/>
<ScrollArea h="calc(100vh - 345px)" offsetScrollbars>
<LoadingOverlay visible={isLoading} />
<Table fz="xs" verticalSpacing="4" highlightOnHover>
<Table.Tbody>{rows}</Table.Tbody>
</Table>
</ScrollArea>
{data && (
<Flex py="lg" gap="md">
<Button
size="compact-xs"
leftSection={<VscAdd />}
disabled={isCreateDisabled}
onClick={onCreate}
variant="outline"
>
Open New
</Button>
<Flex align="center" gap="xs">
<RingProgress
size={20}
roundCaps
thickness={2}
sections={[
{
value: (data.length * 100) / TOTAL_QUOTA,
color: data.length > TOTAL_QUOTA / 1.5 ? "red" : "blue",
},
]}
/>
<Text fw={700} size="xs">
{data.length} / {TOTAL_QUOTA}
</Text>
</Flex>
</Flex>
)}
</Paper>
</Drawer>
);
};

View File

@ -1,24 +0,0 @@
import React from "react";
import Link from "next/link";
import type { ModalProps } from "@mantine/core";
import { Modal, Stack, Button } from "@mantine/core";
export const LoginModal = ({ opened, onClose }: ModalProps) => {
return (
<Modal title="Sign In" opened={opened} onClose={onClose} centered>
<Stack py="sm">
<Button
variant="default"
component={Link}
prefetch={false}
href="/sign-in"
rel="noreferrer"
size="md"
fullWidth
>
Sign in to continue
</Button>
</Stack>
</Modal>
);
};

View File

@ -1,85 +0,0 @@
import React from "react";
import type { ModalProps } from "@mantine/core";
import { Anchor, Button, Center, Divider, Group, Modal, Text } from "@mantine/core";
import { useLocalStorage } from "@mantine/hooks";
import useModal from "src/store/useModal";
import useUser from "src/store/useUser";
export const NoticeModal = (props: ModalProps) => {
const isAuthenticated = useUser(state => state.isAuthenticated);
const setVisible = useModal(state => state.setVisible);
const [seenModal, setSeenModal] = useLocalStorage({
key: "seenNoticeModal",
defaultValue: false,
getInitialValueInEffect: false,
});
const closeModal = () => {
setSeenModal(true);
props.onClose?.();
};
if (!isAuthenticated) return null;
return (
<Modal
title={
<Text c="red" fw={600} fz="lg">
Important Notice: Changes to Free Version
</Text>
}
opened={!seenModal || props.opened}
onClose={closeModal}
centered
zIndex={1000}
size="lg"
>
<Center></Center>
<Text>
Hello,
<br />
<br />
We want to inform you that starting from <b>1 October 2024</b>, the following features will
be removed from the free version of our service:
<br />
<br />
Sign-in / Sign-up
<br />
Cloud Storage
<br />
Share
<br />
<br />
<Text fw={500}>
Any cloud-stored data that has not been accessed in the past 4 months will be
automatically deleted at 1 October 2024.
</Text>
<br />
To ensure you don&apos;t lose any important data, please download your data before the
deadline. If you wish to continue using these features, we recommend upgrading to{" "}
<Anchor href="https://todiagram.com" target="_blank">
ToDiagram
</Anchor>
, which will allow you to retain full access to these services and more.
<br />
<br />
Thank you for your understanding and continued support.
</Text>
<Divider my="xs" />
<Group justify="right">
<Button color="gray" variant="subtle" onClick={closeModal}>
Close
</Button>
<Button
variant="default"
onClick={() => {
setVisible("cloud")(true);
closeModal();
}}
>
Open Cloud
</Button>
</Group>
</Modal>
);
};

View File

@ -1,31 +1,22 @@
export { CloudModal } from "./CloudModal";
export { ClearModal } from "./ClearModal";
export { DownloadModal } from "./DownloadModal";
export { ImportModal } from "./ImportModal";
export { AccountModal } from "./AccountModal";
export { NodeModal } from "./NodeModal";
export { LoginModal } from "./LoginModal";
export { UpgradeModal } from "./UpgradeModal";
export { JWTModal } from "./JWTModal";
export { SchemaModal } from "./SchemaModal";
export { JQModal } from "./JQModal";
export { TypeModal } from "./TypeModal";
export { JPathModal } from "./JPathModal";
export { NoticeModal } from "./NoticeModal";
type Modal =
| "cloud"
| "download"
| "import"
| "account"
| "node"
| "login"
| "upgrade"
| "jwt"
| "schema"
| "jq"
| "type"
| "jpath"
| "notice";
| "jpath";
export type { Modal };

View File

@ -1,56 +1,28 @@
import React from "react";
import Link from "next/link";
import { Menu, Avatar, Text } from "@mantine/core";
import { VscSignIn, VscFeedback, VscSignOut } from "react-icons/vsc";
import useModal from "src/store/useModal";
import useUser from "src/store/useUser";
import { FaRegCircleUser } from "react-icons/fa6";
import { VscSignIn } from "react-icons/vsc";
import { StyledToolElement } from "./styles";
export const AccountMenu = () => {
const user = useUser(state => state.user?.user_metadata);
const logout = useUser(state => state.logout);
const setVisible = useModal(state => state.setVisible);
const username = user?.full_name || user?.display_name || user?.name;
return (
<Menu shadow="md" trigger="click" closeOnItemClick={false} withArrow>
<Menu.Target>
<StyledToolElement>
<Avatar color={user ? "teal" : "indigo"} variant="filled" size={20} radius="xl">
{user && "JC"}
<Avatar color="blue" variant="filled" size={20} radius="xl">
<FaRegCircleUser size="12" />
</Avatar>
</StyledToolElement>
</Menu.Target>
<Menu.Dropdown>
{user ? (
<Menu.Item
leftSection={<Avatar color="indigo" alt={username} size={20} radius="xl" />}
onClick={() => setVisible("account")(true)}
closeMenuOnClick
>
<Text size="xs">{username ?? "Account"}</Text>
</Menu.Item>
) : (
<Link href="/sign-in" prefetch={false}>
<Menu.Item leftSection={<VscSignIn />}>
<Text size="xs">Sign in</Text>
</Menu.Item>
</Link>
)}
{user && (
<>
<Menu.Divider />
<Link href="https://github.com/AykutSarac/jsoncrack.com/discussions" target="_blank">
<Menu.Item leftSection={<VscFeedback />} closeMenuOnClick>
<Text size="xs">Feedback</Text>
</Menu.Item>
</Link>
<Menu.Item leftSection={<VscSignOut />} onClick={() => logout()} closeMenuOnClick>
<Text size="xs">Log out</Text>
</Menu.Item>
</>
)}
<Menu.Item
component="a"
href="https://todiagram.com/sign-up?utm_source=signup&utm_medium=app&utm_content=toolbar"
target="_blank"
leftSection={<VscSignIn />}
>
<Text size="xs">Sign up</Text>
</Menu.Item>
</Menu.Dropdown>
</Menu>
);

View File

@ -6,6 +6,7 @@ import toast from "react-hot-toast";
import { CgChevronDown } from "react-icons/cg";
import { FaRandom } from "react-icons/fa";
import { FaWandMagicSparkles } from "react-icons/fa6";
import { LuGlobe } from "react-icons/lu";
import { MdCompare, MdFilterListAlt } from "react-icons/md";
import { SiJsonwebtokens } from "react-icons/si";
import { VscSearchFuzzy, VscJson, VscGroupByRefType, VscLock } from "react-icons/vsc";
@ -86,17 +87,6 @@ export const ToolsMenu = () => {
JSON Path
</Menu.Item>
<Menu.Divider />
<Menu.Item
fz={12}
leftSection={<FaWandMagicSparkles />}
rightSection={<VscLock />}
onClick={() => {
setVisible("upgrade")(true);
gaEvent("open_ai_filter_modal");
}}
>
AI-Powered Filter
</Menu.Item>
<Menu.Item
fz={12}
leftSection={<SiJsonwebtokens />}
@ -120,6 +110,28 @@ export const ToolsMenu = () => {
<Menu.Item fz={12} leftSection={<FaRandom />} onClick={randomizeData}>
Randomize Data
</Menu.Item>
<Menu.Item
fz={12}
leftSection={<LuGlobe />}
rightSection={<VscLock />}
onClick={() => {
setVisible("upgrade")(true);
gaEvent("rest_client_modal");
}}
>
REST Client
</Menu.Item>
<Menu.Item
fz={12}
leftSection={<FaWandMagicSparkles />}
rightSection={<VscLock />}
onClick={() => {
setVisible("upgrade")(true);
gaEvent("open_ai_filter_modal");
}}
>
AI-Powered Filter
</Menu.Item>
<Menu.Item
fz={12}
leftSection={<MdCompare />}

View File

@ -10,7 +10,6 @@ import { FileFormat } from "src/enums/file.enum";
import { JSONCrackLogo } from "src/layout/JsonCrackLogo";
import useFile from "src/store/useFile";
import useModal from "src/store/useModal";
import useUser from "src/store/useUser";
import { AccountMenu } from "./AccountMenu";
import { FileMenu } from "./FileMenu";
import { Logo } from "./Logo";
@ -57,7 +56,6 @@ export const Toolbar = ({ isWidget = false }: ToolbarProps) => {
const setVisible = useModal(state => state.setVisible);
const setFormat = useFile(state => state.setFormat);
const format = useFile(state => state.format);
const isAuthenticated = useUser(state => state.isAuthenticated);
return (
<StyledTools>
@ -90,11 +88,6 @@ export const Toolbar = ({ isWidget = false }: ToolbarProps) => {
<FileMenu />
<ViewMenu />
<ToolsMenu />
{isAuthenticated && (
<StyledToolElement title="Cloud" onClick={() => setVisible("cloud")(true)}>
Cloud
</StyledToolElement>
)}
</Group>
)}
<Group gap="xs" justify="right" w="100%" style={{ flexWrap: "nowrap" }}>

View File

@ -9,17 +9,13 @@ type ModalComponent = { key: Modal; component: React.FC<ModalProps> };
const modalComponents: ModalComponent[] = [
{ key: "import", component: Modals.ImportModal },
{ key: "download", component: Modals.DownloadModal },
{ key: "cloud", component: Modals.CloudModal },
{ key: "account", component: Modals.AccountModal },
{ key: "upgrade", component: Modals.UpgradeModal },
{ key: "login", component: Modals.LoginModal },
{ key: "jwt", component: Modals.JWTModal },
{ key: "node", component: Modals.NodeModal },
{ key: "schema", component: Modals.SchemaModal },
{ key: "jq", component: Modals.JQModal },
{ key: "type", component: Modals.TypeModal },
{ key: "jpath", component: Modals.JPathModal },
{ key: "notice", component: Modals.NoticeModal },
];
const ModalController = () => {

View File

@ -58,16 +58,6 @@ export const Navbar = () => {
<JSONCrackLogo fontSize="1.2rem" />
</Left>
<Center>
<Button
component="a"
href="https://todiagram.com"
variant="subtle"
color="black"
size="md"
radius="md"
>
Upgrade
</Button>
<Button
component="a"
href="https://marketplace.visualstudio.com/items?itemName=AykutSarac.jsoncrack-vscode"
@ -90,18 +80,28 @@ export const Navbar = () => {
>
Open Source
</Button>
</Center>
<Right>
<Button
variant="subtle"
color="black"
component={Link}
href="/sign-in"
href="/docs"
visibleFrom="sm"
size="md"
radius="md"
>
Log in
Docs
</Button>
</Center>
<Right>
<Button
component="a"
href="https://todiagram.com"
variant="subtle"
color="black"
size="md"
radius="md"
>
Upgrade
</Button>
<Button
radius="md"

View File

@ -1,50 +0,0 @@
import type { PostgrestSingleResponse } from "@supabase/supabase-js";
import toast from "react-hot-toast";
import { FileFormat } from "src/enums/file.enum";
import { supabase } from "src/lib/api/supabase";
import type { File } from "src/store/useFile";
import useUser from "src/store/useUser";
type CloudSave = {
id?: string;
contents: string;
format: FileFormat;
};
export const documentSvc = {
upsert: async (args: CloudSave): Promise<PostgrestSingleResponse<string>> => {
const { id: p_id = "", contents: p_content, format: p_format = FileFormat.JSON } = args;
return await supabase.rpc("upsert_document", {
p_content,
p_format,
p_id,
});
},
getById: async (doc_id: string): Promise<PostgrestSingleResponse<File[]>> => {
return await supabase.rpc("get_document_by_id", { doc_id });
},
getAll: async (searchText?: string): Promise<File[]> => {
const userEmail = useUser.getState().user?.email;
if (!userEmail) return [];
const { data, error } = await supabase
.from("document")
.select()
.eq("owner_email", userEmail)
.ilike("name", `%${searchText}%`)
.order("created_at", { ascending: false });
if (error) {
toast.error(error.message);
return [];
}
return data;
},
update: async (id: string, data: object) => {
return await supabase.from("document").update(data).eq("id", id).select("private");
},
delete: async (id: string) => {
return await supabase.from("document").delete().eq("id", id);
},
};

View File

@ -1,8 +0,0 @@
import { createClient } from "@supabase/supabase-js";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL as string;
const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_KEY as string;
const supabase = createClient(supabaseUrl, supabaseKey);
export { supabase };

View File

@ -10,8 +10,6 @@ import { Toaster } from "react-hot-toast";
import GlobalStyle from "src/constants/globalStyle";
import { SEO } from "src/constants/seo";
import { lightTheme } from "src/constants/theme";
import { supabase } from "src/lib/api/supabase";
import useUser from "src/store/useUser";
const theme = createTheme({
autoContrast: true,
@ -55,14 +53,6 @@ const theme = createTheme({
const IS_PROD = process.env.NODE_ENV === "production";
function JsonCrack({ Component, pageProps }: AppProps) {
const setSession = useUser(state => state.setSession);
React.useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
if (session) setSession(session);
});
}, [setSession]);
return (
<>
<NextSeo {...SEO} />

View File

@ -80,27 +80,6 @@ const Docs = () => {
</StyledFrame>
</StyledContentBody>
</Paper>
<Paper bg="white" c="black" p="md" radius="md" withBorder>
<Title mb="sm" order={3} c="dark">
# Embed Saved JSON
</Title>
<StyledContentBody>
<Text>
Just like fetching from URL above, you can embed saved public json by adding the json
id to &quot;json&quot; query{" "}
<StyledHighlight>?json=639b65c5a82efc29a24b2de2</StyledHighlight>
</Text>
<StyledFrame
title="Untitled"
src="https://codepen.io/AykutSarac/embed/vYaORgM?default-tab=html%2Cresult"
loading="lazy"
>
See the Pen <a href="https://codepen.io/AykutSarac/pen/vYaORgM">Untitled</a> by Aykut
Saraç (<a href="https://codepen.io/AykutSarac">@AykutSarac</a>) on{" "}
<a href="https://codepen.io">CodePen</a>.
</StyledFrame>
</StyledContentBody>
</Paper>
<Paper bg="white" c="black" p="md" radius="md" withBorder>
<Title mb="sm" order={3} c="dark">
# Communicating with API

View File

@ -1,225 +0,0 @@
import React from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import {
Button,
Group,
Stack,
TextInput,
Text,
Anchor,
PasswordInput,
Title,
FocusTrap,
Alert,
} from "@mantine/core";
import { NextSeo } from "next-seo";
import { MdErrorOutline } from "react-icons/md";
import { SEO } from "src/constants/seo";
import { AuthLayout } from "src/layout/AuthLayout";
import { supabase } from "src/lib/api/supabase";
function ResetPassword() {
const { push } = useRouter();
const [loading, setLoading] = React.useState(false);
const [password, setPassword] = React.useState("");
const [password2, setPassword2] = React.useState("");
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [success, setSuccess] = React.useState(false);
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
try {
e.preventDefault();
setLoading(true);
if (password !== password2) throw new Error("Passwords do not match");
const { error } = await supabase.auth.updateUser({ password });
if (error) throw new Error(error.message);
setSuccess(true);
push("/sign-in");
} catch (error) {
if (error instanceof Error) setErrorMessage(error.message);
} finally {
setLoading(false);
}
};
if (success) {
return (
<Text>
Password updated successfully.{" "}
<Anchor component={Link} href="/sign-in">
Sign in
</Anchor>
</Text>
);
}
return (
<FocusTrap>
{errorMessage && (
<Alert
onClose={() => setErrorMessage(null)}
color="red"
py="xs"
mb="lg"
icon={<MdErrorOutline color="red" />}
withCloseButton
>
<Text fz="sm" c="red">
{errorMessage}
</Text>
</Alert>
)}
<form onSubmit={onSubmit}>
<Stack>
<PasswordInput
name="password"
value={password}
onChange={e => setPassword(e.target.value)}
required
label="New Password"
radius="sm"
placeholder=""
style={{ color: "black" }}
autoComplete="new-password"
data-autofocus
/>
<PasswordInput
name="password"
value={password2}
onChange={e => setPassword2(e.target.value)}
required
label="Confirm Password"
autoComplete="new-password"
radius="sm"
placeholder=""
style={{ color: "black" }}
/>
</Stack>
<Group justify="apart" mt="xl">
<Button color="dark" type="submit" radius="sm" loading={loading} fullWidth>
Reset Password
</Button>
</Group>
</form>
<Stack mt="lg" align="center">
<Anchor component={Link} prefetch={false} href="/sign-up" c="dark" fz="sm">
Don&apos;t have an account?
<Text component="span" fz="sm" c="blue" ml={4}>
Sign up
</Text>
</Anchor>
</Stack>
</FocusTrap>
);
}
const ForgotPassword = () => {
const { query } = useRouter();
const [loading, setLoading] = React.useState(false);
const [email, setEmail] = React.useState("");
const [success, setSuccess] = React.useState(false);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const isPasswordReset = query?.type === "recovery" && !query?.error;
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
try {
e.preventDefault();
setLoading(true);
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${window.location.origin}/forgot-password`,
});
if (error) throw new Error(error.message);
setSuccess(true);
} catch (error) {
if (error instanceof Error) setErrorMessage(error.message);
console.error(error);
} finally {
setLoading(false);
}
};
return (
<AuthLayout>
<NextSeo
{...SEO}
title="Reset Password - JSON Crack"
noindex
nofollow
canonical="https://jsoncrack.com/forgot-password"
/>
<Title c="dark.4" order={2} fz="xl" mb={25} fw={600}>
{isPasswordReset ? "Create New Password" : "Forgot Password"}
</Title>
{isPasswordReset ? (
<ResetPassword />
) : (
<>
{success ? (
<>
<Text>We&apos;ve sent an email to you, please check your inbox.</Text>
<Button component={Link} href="/sign-in" color="dark" radius="sm" mt="lg" fullWidth>
Back to Login
</Button>
</>
) : (
<form onSubmit={onSubmit}>
{errorMessage && (
<Alert
onClose={() => setErrorMessage(null)}
color="red"
py="xs"
mb="lg"
icon={<MdErrorOutline color="red" />}
withCloseButton
>
<Text fz="sm" c="red">
{errorMessage}
</Text>
</Alert>
)}
<FocusTrap>
<Stack>
<TextInput
name="email"
type="email"
value={email}
onChange={e => setEmail(e.target.value)}
required
label="Email"
placeholder="hello@jsoncrack.com"
radius="sm"
style={{ color: "black" }}
data-autofocus
/>
</Stack>
</FocusTrap>
<Group justify="apart" mt="md">
<Button color="dark" type="submit" radius="sm" loading={loading} fullWidth>
Reset Password
</Button>
</Group>
</form>
)}
<Stack mt="lg" align="center">
<Anchor component={Link} prefetch={false} href="/sign-up" c="dark" fz="sm">
Don&apos;t have an account?
<Text component="span" fz="sm" c="blue" ml={4}>
Sign up
</Text>
</Anchor>
</Stack>
</>
)}
</AuthLayout>
);
};
export default ForgotPassword;

View File

@ -1,25 +0,0 @@
import { useEffect } from "react";
import { useRouter } from "next/router";
import { NextSeo } from "next-seo";
import { SEO } from "src/constants/seo";
import { supabase } from "src/lib/api/supabase";
const Oauth = () => {
const { push } = useRouter();
useEffect(() => {
supabase.auth.getSession().then(({ data, error }) => {
if (error) return console.error(error);
if (!data.session) return;
push("/editor");
});
}, [push]);
return (
<>
<NextSeo {...SEO} canonical="https://jsoncrack.com/oauth" nofollow noindex />
</>
);
};
export default Oauth;

View File

@ -1,198 +0,0 @@
import React, { useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import {
TextInput,
PasswordInput,
Button,
Anchor,
Stack,
Center,
Text,
FocusTrap,
Alert,
Box,
Flex,
} from "@mantine/core";
import { NextSeo } from "next-seo";
import { AiOutlineGithub } from "react-icons/ai";
import { FcGoogle } from "react-icons/fc";
import { MdErrorOutline } from "react-icons/md";
import { SEO } from "src/constants/seo";
import { AuthLayout } from "src/layout/AuthLayout";
import { supabase } from "src/lib/api/supabase";
import useUser from "src/store/useUser";
export function AuthenticationForm() {
const { push } = useRouter();
const setSession = useUser(state => state.setSession);
const isAuthenticated = useUser(state => state.isAuthenticated);
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
const [loading, setLoading] = React.useState(false);
const [userData, setUserData] = React.useState({
name: "",
email: "",
password: "",
});
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
const { data, error } = await supabase.auth.signInWithPassword({
email: userData.email,
password: userData.password,
});
if (error) {
setLoading(false);
return setErrorMessage("Incorrect email or password.");
}
setSession(data.session);
push("/editor");
};
const handleLoginClick = async (provider: "github" | "google") => {
await supabase.auth.signInWithOAuth({
provider,
options: { redirectTo: `${window.location.origin}/oauth` },
});
};
if (isAuthenticated) {
return (
<Box mt="lg">
<Text fz="sm" c="dark">
You are already signed in. Click the button below to go to the editor.
</Text>
<Link href="/editor">
<Button mt="lg" color="dark" size="md" radius="md" fullWidth>
Go to Editor
</Button>
</Link>
</Box>
);
}
return (
<Box>
{errorMessage && (
<Alert
onClose={() => setErrorMessage(null)}
color="red"
py="xs"
mb="lg"
icon={<MdErrorOutline color="red" />}
withCloseButton
>
<Text fz="sm" c="red">
{errorMessage}
</Text>
</Alert>
)}
<FocusTrap>
<form onSubmit={onSubmit}>
<Stack>
<TextInput
name="email"
type="email"
required
label="Email"
placeholder="hello@jsoncrack.com"
value={userData.email}
onChange={event => setUserData(d => ({ ...d, email: event.target.value }))}
radius="sm"
style={{ color: "black" }}
withAsterisk={false}
/>
<PasswordInput
name="password"
required
label="Password"
placeholder=""
value={userData.password}
onChange={event => setUserData(d => ({ ...d, password: event.target.value }))}
radius="sm"
style={{ color: "black" }}
withAsterisk={false}
/>
<Anchor
component={Link}
prefetch={false}
href="/forgot-password"
c="dimmed"
size="xs"
ta="right"
mt="-sm"
>
Forgot password?
</Anchor>
<Button color="dark" type="submit" radius="sm" loading={loading}>
Sign in
</Button>
</Stack>
</form>
</FocusTrap>
<Flex mt="lg" gap="sm">
<Button
radius="sm"
fullWidth
leftSection={<FcGoogle size="20" />}
onClick={() => handleLoginClick("google")}
variant="default"
disabled={loading}
>
Google
</Button>
<Button
radius="sm"
leftSection={<AiOutlineGithub size="20" />}
onClick={() => handleLoginClick("github")}
variant="default"
fullWidth
disabled={loading}
>
GitHub
</Button>
</Flex>
</Box>
);
}
const SignIn = () => {
const { push, query, pathname } = useRouter();
useEffect(() => {
supabase.auth.getSession().then(({ data, error }) => {
if (error) return console.error(error);
if (!data.session) return;
push("/editor");
});
}, [pathname, push, query.code]);
return (
<AuthLayout>
<NextSeo
{...SEO}
title="Sign In - JSON Crack"
description="Sign in to your JSON Crack account to create and edit diagrams with ease."
canonical="https://jsoncrack.com/sign-in"
/>
<AuthenticationForm />
<Center mt={80}>
<Anchor component={Link} prefetch={false} href="/sign-up" c="dark" fz="sm">
Don&apos;t have an account?
<Text component="span" fz="sm" c="blue" ml={4}>
Sign up
</Text>
</Anchor>
</Center>
</AuthLayout>
);
};
export default SignIn;

View File

@ -1,54 +0,0 @@
import React, { useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/router";
import { Anchor, Button, Center, Text } from "@mantine/core";
import { NextSeo } from "next-seo";
import { SEO } from "src/constants/seo";
import { AuthLayout } from "src/layout/AuthLayout";
import { supabase } from "src/lib/api/supabase";
const SignUp = () => {
const { push } = useRouter();
useEffect(() => {
supabase.auth.getSession().then(({ data }) => {
if (data.session) push("/editor");
});
}, [push]);
return (
<AuthLayout>
<NextSeo
{...SEO}
title="Sign Up - JSON Crack"
description="Create an account to start creating graphs and visualizing your data."
canonical="https://jsoncrack.com/sign-up"
/>
<Text fw={500}>JSON Crack is no longer accepting new sign-ups.</Text>
<Text fz="sm" mt="md" c="gray.6">
For advanced features, please visit{" "}
<Anchor td="underline" href="https://todiagram.com" inherit>
ToDiagram
</Anchor>
, or you can continue using JSON Crack without an account.
</Text>
<Center my="md">
<Link href="/editor" prefetch={false} passHref>
<Button size="md" color="dark" radius="md">
Go to editor
</Button>
</Link>
</Center>
<Center mt={50}>
<Anchor component={Link} prefetch={false} href="/sign-in" c="dark" fz="sm">
Already have an account?
<Text component="span" fz="sm" c="blue" ml={4}>
Sign in
</Text>
</Anchor>
</Center>
</AuthLayout>
);
};
export default SignUp;

View File

@ -3,7 +3,6 @@ import { event as gaEvent } from "nextjs-google-analytics";
import { toast } from "react-hot-toast";
import { create } from "zustand";
import { FileFormat } from "src/enums/file.enum";
import { documentSvc } from "src/lib/api/document.service";
import { isIframe } from "src/lib/utils/helpers";
import { contentToJson, jsonToContent } from "src/lib/utils/jsonAdapter";
import useGraph from "../containers/Editor/components/views/GraphView/stores/useGraph";
@ -64,7 +63,6 @@ interface JsonActions {
setError: (error: string | null) => void;
setHasChanges: (hasChanges: boolean) => void;
setContents: (data: SetContents) => void;
fetchFile: (fileId: string) => void;
fetchUrl: (url: string) => void;
setFormat: (format: FileFormat) => void;
clear: () => void;
@ -182,7 +180,6 @@ const useFile = create<FileStates & JsonActions>()((set, get) => ({
checkEditorSession: (url, widget) => {
if (url && typeof url === "string") {
if (isURL(url)) return get().fetchUrl(url);
return get().fetchFile(url);
}
let contents = defaultJson;
@ -193,18 +190,6 @@ const useFile = create<FileStates & JsonActions>()((set, get) => ({
if (format) set({ format });
get().setContents({ contents, hasChanges: false });
},
fetchFile: async id => {
try {
const { data, error } = await documentSvc.getById(id);
if (error) throw error;
if (data?.length) get().setFile(data[0]);
if (data?.length === 0) throw new Error("Document not found");
} catch (error: any) {
if (error?.message) toast.error(error?.message);
get().setContents({ contents: defaultJson, hasChanges: false });
}
},
}));
export default useFile;

View File

@ -1,6 +1,5 @@
import { create } from "zustand";
import type { Modal } from "src/containers/Modals";
import useUser from "./useUser";
type ModalState = {
[key in Modal]: boolean;
@ -11,34 +10,20 @@ interface ModalActions {
}
const initialStates: ModalState = {
cloud: false,
download: false,
import: false,
account: false,
node: false,
login: false,
upgrade: false,
jwt: false,
schema: false,
jq: false,
type: false,
jpath: false,
notice: false,
};
const authModals: Modal[] = ["cloud", "account"];
const useModal = create<ModalState & ModalActions>()(set => ({
...initialStates,
setVisible: modal => visible => {
const user = useUser.getState();
if (authModals.includes(modal) && !user.isAuthenticated) {
return set({ login: true });
}
set({ [modal]: visible });
},
setVisible: modal => visible => set({ [modal]: visible }),
}));
export default useModal;

View File

@ -1,43 +0,0 @@
import type { Session, User } from "@supabase/supabase-js";
import { event as gaEvent } from "nextjs-google-analytics";
import toast from "react-hot-toast";
import { create } from "zustand";
import { supabase } from "src/lib/api/supabase";
interface UserActions {
logout: () => void;
setSession: (session: Session) => void;
}
interface UserStates {
user: User | null;
isAuthenticated: boolean;
}
const initialStates: UserStates = {
user: null,
isAuthenticated: false,
};
const useUser = create<UserStates & UserActions>()(set => ({
...initialStates,
setSession: async session => {
gaEvent("login");
set({ user: session.user, isAuthenticated: true });
},
logout: async () => {
toast.loading("Logging out...", { id: "logout" });
const { error } = await supabase.auth.signOut({ scope: "local" });
if (error) {
toast.error("Failed to log out.");
return;
}
gaEvent("logout");
set(initialStates);
toast.success("Logged out.", { id: "logout" });
},
}));
export default useUser;