mirror of
https://github.com/AykutSarac/jsoncrack.com.git
synced 2025-01-12 19:02:53 +08:00
feat: remove supabase & cloud related actions
This commit is contained in:
parent
99f4ad3d04
commit
38a9287b0a
2
.env
2
.env
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
93
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
@ -2,7 +2,6 @@ User-agent: *
|
||||
Allow: /
|
||||
|
||||
User-agent: *
|
||||
Disallow: /forgot-password
|
||||
Disallow: /widget
|
||||
|
||||
Sitemap: https://jsoncrack.com/sitemap.txt
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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'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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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'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>
|
||||
);
|
||||
};
|
@ -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 };
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 />}
|
||||
|
@ -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" }}>
|
||||
|
@ -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 = () => {
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
@ -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 };
|
@ -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} />
|
||||
|
@ -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 "json" 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
|
||||
|
@ -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'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'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't have an account?
|
||||
<Text component="span" fz="sm" c="blue" ml={4}>
|
||||
Sign up
|
||||
</Text>
|
||||
</Anchor>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</AuthLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForgotPassword;
|
@ -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;
|
@ -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't have an account?
|
||||
<Text component="span" fz="sm" c="blue" ml={4}>
|
||||
Sign up
|
||||
</Text>
|
||||
</Anchor>
|
||||
</Center>
|
||||
</AuthLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignIn;
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user