diff --git a/.eslintrc.json b/.eslintrc.json index 53b55f7..1b29e45 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,11 +11,6 @@ "linebreak-style": ["error", "unix"], "quotes": ["error", "double", { "avoidEscape": true }], "semi": ["error", "always"], - "padding-line-between-statements": [ - "error", - { "blankLine": "always", "prev": ["const", "let", "var"], "next": "*" }, - { "blankLine": "any", "prev": ["const", "let", "var"], "next": ["const", "let", "var"] } - ], "space-before-function-paren": [ "error", { diff --git a/package.json b/package.json index 306bfd6..2dc26e0 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ "@mantine/prism": "^6.0.17", "@monaco-editor/react": "^4.5.1", "@sentry/nextjs": "^7.60.0", + "@supabase/auth-helpers-nextjs": "^0.7.3", + "@supabase/auth-helpers-react": "^0.4.1", + "@supabase/supabase-js": "^2.31.0", "@tanstack/react-query": "^4.32.0", "allotment": "^1.19.2", "altogic": "^2.3.9", diff --git a/src/containers/Editor/BottomBar.tsx b/src/containers/Editor/BottomBar.tsx index e36cfbc..4f70d27 100644 --- a/src/containers/Editor/BottomBar.tsx +++ b/src/containers/Editor/BottomBar.tsx @@ -30,6 +30,7 @@ const StyledBottomBar = styled.div` max-height: 27px; height: 27px; padding: 0 6px; + z-index: 35; @media screen and (max-width: 320px) { display: none; @@ -73,7 +74,7 @@ const StyledBottomBarItem = styled.button<{ $error?: boolean }>` } &:disabled { - opacity: 0.4; + opacity: 0.6; cursor: default; } `; @@ -116,19 +117,20 @@ export const BottomBar = () => { try { setIsUpdating(true); toast.loading("Saving document...", { id: "fileSave" }); - const res = await saveToCloud(query?.json ?? null, getContents(), getFormat()); - if (res.errors && res.errors.items.length > 0) throw res.errors; - if (res.data._id) replace({ query: { json: res.data._id } }); + const { data, error } = await saveToCloud({ + id: query?.json, + contents: getContents(), + format: getFormat(), + }); + + if (error) throw error; + if (data[0].id) replace({ query: { json: data[0].id } }); toast.success("Document saved to cloud", { id: "fileSave" }); setHasChanges(false); } catch (error: any) { - if (error?.items?.length > 0) { - return toast.error(error.items[0].message, { id: "fileSave", duration: 5000 }); - } - - toast.error("Failed to save document!", { id: "fileSave" }); + toast.error(error.message, { id: "fileSave" }); } finally { setIsUpdating(false); } @@ -155,14 +157,18 @@ export const BottomBar = () => { if (!query.json) return handleSaveJson(); setIsUpdating(true); - const res = await updateJson(query.json as string, { private: !isPrivate }); + const { data: updatedJsonData, error } = await updateJson(query.json as string, { + private: !isPrivate, + }); - if (!res.errors?.items.length) { - setIsPrivate(res.data.private); + if (error) return toast.error(error.message); + + if (updatedJsonData[0]) { + setIsPrivate(updatedJsonData[0].private); toast.success(`Document set to ${isPrivate ? "public" : "private"}.`); - } else throw res.errors; + } else throw error; } catch (error) { - toast.error("An error occurred while updating document!"); + console.error(error); } finally { setIsUpdating(false); } @@ -178,7 +184,7 @@ export const BottomBar = () => { - {user ? user.name : "Login"} + {user?.user_metadata.name ?? "Login"} {premium && ( PREMIUM @@ -211,24 +217,25 @@ export const BottomBar = () => { )} - - {hasChanges ? : } - {hasChanges ? (query?.json ? "Unsaved Changes" : "Create Document") : "Saved"} - - {data && ( - <> - {typeof data.private !== "undefined" && ( - - {isPrivate ? : } - {isPrivate ? "Private" : "Public"} - - )} - setVisible("share")(true)} disabled={isPrivate}> - - Share - - + {(data?.owner_id === user?.id || (!data && user)) && ( + + {hasChanges ? : } + {hasChanges ? (query?.json ? "Unsaved Changes" : "Create Document") : "Saved"} + )} + {data?.owner_id === user?.id && ( + + {isPrivate ? : } + {isPrivate ? "Private" : "Public"} + + )} + setVisible("share")(true)} + disabled={isPrivate || !data} + > + + Share + {liveTransform ? ( toggleLiveTransform(false)}> diff --git a/src/containers/Modals/AccountModal/index.tsx b/src/containers/Modals/AccountModal/index.tsx index f718ccf..353d6f0 100644 --- a/src/containers/Modals/AccountModal/index.tsx +++ b/src/containers/Modals/AccountModal/index.tsx @@ -1,6 +1,8 @@ import React from "react"; import styled from "styled-components"; import { Modal, Group, Button, Badge, Avatar, Grid, Divider, ModalProps } from "@mantine/core"; +import { useUser as useSupaUser } from "@supabase/auth-helpers-react"; +import dayjs from "dayjs"; import { IoRocketSharp } from "react-icons/io5"; import useModal from "src/store/useModal"; import useUser from "src/store/useUser"; @@ -43,7 +45,7 @@ const StyledContainer = styled.div` `; export const AccountModal: React.FC = ({ opened, onClose }) => { - const user = useUser(state => state.user); + const user = useSupaUser(); const isPremium = useUser(state => state.premium); const premiumCancelled = useUser(state => state.premiumCancelled); const setVisible = useModal(state => state.setVisible); @@ -51,16 +53,21 @@ export const AccountModal: React.FC = ({ opened, onClose }) => { return ( - Hello, {user?.name}! + Hello, {user?.user_metadata.name}! - + USERNAME -
{user?.name}
+
{user?.user_metadata.name}
@@ -82,15 +89,17 @@ export const AccountModal: React.FC = ({ opened, onClose }) => { EMAIL -
{user?.email}
-
-
- - - REGISTRATION -
{user?.signUpAt && new Date(user.signUpAt).toDateString()}
+
{user?.user_metadata.email}
+ {user?.created_at && ( + + + REGISTRATION +
{dayjs(user.created_at).format("DD MMMM YYYY")}
+
+
+ )}
diff --git a/src/containers/Modals/CloudModal/index.tsx b/src/containers/Modals/CloudModal/index.tsx index 1e7259b..9e2bbb0 100644 --- a/src/containers/Modals/CloudModal/index.tsx +++ b/src/containers/Modals/CloudModal/index.tsx @@ -1,252 +1,242 @@ import React from "react"; +import Link from "next/link"; import { useRouter } from "next/router"; -import styled from "styled-components"; import { Modal, - Group, - Button, Text, - Stack, - Loader, - Center, Divider, ScrollArea, ModalProps, + Table, + ActionIcon, + Badge, Paper, + Flex, + DefaultMantineColor, + Input, + Button, + Group, + Stack, + RingProgress, + UnstyledButton, } from "@mantine/core"; import { useQuery } from "@tanstack/react-query"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import toast from "react-hot-toast"; -import { AiOutlineEdit, AiOutlineLock, AiOutlinePlus, AiOutlineUnlock } from "react-icons/ai"; +import { AiOutlineLink } from "react-icons/ai"; import { FaTrash } from "react-icons/fa"; -import { IoRocketSharp } from "react-icons/io5"; +import { MdFileOpen } from "react-icons/md"; +import { VscAdd, VscEdit } from "react-icons/vsc"; import { deleteJson, getAllJson, saveToCloud, updateJson } from "src/services/json"; import useFile, { File } from "src/store/useFile"; -import useModal from "src/store/useModal"; import useUser from "src/store/useUser"; +import { FileFormat } from "src/types/models"; dayjs.extend(relativeTime); -const StyledJsonCard = styled.a<{ active?: boolean; create?: boolean }>` - display: ${({ create }) => (create ? "block" : "flex")}; - align-items: center; - justify-content: space-between; - line-height: 20px; - padding: 6px; - border-radius: 3px; - overflow: hidden; - flex: 1; - height: 60px; - background: ${({ active }) => active && "rgb(48, 98, 197)"}; -`; - -const StyledInfo = styled.div``; - -const StyledTitle = styled.div` - display: flex; - align-items: center; - gap: 4px; - font-size: 14px; - font-weight: 500; - width: fit-content; - cursor: pointer; - - span { - overflow: hidden; - text-overflow: ellipsis; - } -`; - -const StyledDetils = styled.div` - display: flex; - align-items: center; - font-size: 12px; - gap: 4px; -`; - -const StyledCreateWrapper = styled.div` - display: flex; - height: 100%; - gap: 6px; - align-items: center; - justify-content: center; - opacity: 0.6; - height: 30px; - font-weight: 500; - font-size: 14px; - cursor: pointer; -`; - -const StyledNameInput = styled.input` - background: transparent; - border: none; - outline: none; - width: 90%; - color: ${({ theme }) => theme.SEAGREEN}; - font-weight: 600; -`; - -const GraphCard: React.FC<{ data: File; refetch: () => void; active: boolean }> = ({ - data, - refetch, - active, - ...props -}) => { - const [editMode, setEditMode] = React.useState(false); - const [name, setName] = React.useState(data.name); +const colorByFormat: Record = { + json: "orange", + yaml: "blue", + xml: "red", + toml: "dark", + csv: "grape", +}; +const UpdateNameModal: React.FC<{ + file: File | null; + onClose: () => void; + refetch: () => void; +}> = ({ file, onClose, refetch }) => { + const [name, setName] = React.useState(""); const onSubmit = () => { + if (!file) return; toast - .promise(updateJson(data._id, { name }), { + .promise(updateJson(file.id, { name }), { loading: "Updating document...", error: "Error occurred while updating document!", success: `Renamed document to ${name}`, }) - .then(refetch); + .then(() => refetch()); - setEditMode(false); - }; - - const onDeleteClick = (e: React.MouseEvent) => { - e.preventDefault(); - - toast - .promise(deleteJson(data._id), { - loading: "Deleting file...", - error: "An error occurred while deleting the file!", - success: `Deleted ${name}!`, - }) - .then(refetch); + onClose(); }; return ( - - - - {editMode ? ( -
- setName(e.currentTarget.value)} - onClick={e => e.preventDefault()} - autoFocus - /> - - - ) : ( - { - e.preventDefault(); - setEditMode(true); - }} - > - {name} - - - )} - - {data.private ? : } - Last modified {dayjs(data.updatedAt).fromNow()} - -
- -
-
- ); -}; - -const CreateCard: React.FC<{ reachedLimit: boolean }> = ({ reachedLimit }) => { - const { replace } = useRouter(); - const isPremium = useUser(state => state.premium); - const getContents = useFile(state => state.getContents); - const setHasChanges = useFile(state => state.setHasChanges); - const setVisible = useModal(state => state.setVisible); - const getFormat = useFile(state => state.getFormat); - - const onCreate = async () => { - try { - toast.loading("Saving document...", { id: "fileSave" }); - const res = await saveToCloud(null, getContents(), getFormat()); - - if (res.errors && res.errors.items.length > 0) throw res.errors; - - toast.success("Document saved to cloud", { id: "fileSave" }); - setHasChanges(false); - replace({ query: { json: res.data._id } }); - } catch (error: any) { - if (error?.items?.length > 0) { - return toast.error(error.items[0].message, { id: "fileSave", duration: 7000 }); - } - toast.error("Failed to save document!", { id: "fileSave" }); - } - }; - - if (!isPremium && reachedLimit) - return ( - setVisible("premium")(true)}> - - - You reached max limit, upgrade to premium for more! - - - ); - - return ( - - - - Create New File - - + + + setName(e.currentTarget.value)} + /> + + + + + + ); }; export const CloudModal: React.FC = ({ opened, onClose }) => { - const { isReady, query } = useRouter(); - - const { data, isFetching, refetch } = useQuery(["allJson", query], () => getAllJson(), { + const totalQuota = useUser(state => (state.premium ? 200 : 25)); + const getContents = useFile(state => state.getContents); + const setHasChanges = useFile(state => state.setHasChanges); + const getFormat = useFile(state => state.getFormat); + const [currentFile, setCurrentFile] = React.useState(null); + const { isReady, query, replace } = useRouter(); + const { data, refetch } = useQuery(["allJson", query], () => getAllJson(), { enabled: isReady && opened, }); - return ( - - - - {isFetching ? ( -
- -
- ) : ( - <> - 15 : false} /> - {data?.data?.result?.map(json => ( - - ))} - - )} -
-
+ const onCreate = async () => { + try { + toast.loading("Saving document...", { id: "fileSave" }); + const { data, error } = await saveToCloud({ contents: getContents(), format: getFormat() }); + if (error) throw error; + + toast.success("Document saved to cloud", { id: "fileSave" }); + setHasChanges(false); + replace({ query: { json: data[0].id } }); + onClose(); + } catch (error: any) { + toast.error("Failed to save document!", { id: "fileSave" }); + console.error(error); + } + }; + + const onDeleteClick = React.useCallback( + (file: File) => { + toast + .promise(deleteJson(file.id), { + loading: "Deleting file...", + error: "An error occurred while deleting the file!", + success: `Deleted ${file.name}!`, + }) + .then(() => refetch()); + }, + [refetch] + ); + + const copyShareLink = React.useCallback((fileId: string) => { + const shareLink = `${window.location.origin}/?json=${fileId}`; + navigator.clipboard.writeText(shareLink); + toast.success("Copied share link to clipboard!"); + }, []); + + const rows = React.useMemo( + () => + data?.map(element => ( + + {element.id} + + + {element.name} + setCurrentFile(element)}> + + + + + {dayjs(element.created_at).format("DD.MM.YYYY")} + + + {element.format.toUpperCase()} + + + {element.views.toLocaleString("en-US")} + + + + + + + + onDeleteClick(element)}> + + + copyShareLink(element.id)}> + + + + + + )), + [data, copyShareLink, onClose, onDeleteClick] + ); + + return ( + ); }; diff --git a/src/containers/Modals/PremiumModal/index.tsx b/src/containers/Modals/PremiumModal/index.tsx index 360fe46..d85e925 100644 --- a/src/containers/Modals/PremiumModal/index.tsx +++ b/src/containers/Modals/PremiumModal/index.tsx @@ -13,7 +13,6 @@ import { Badge, } from "@mantine/core"; import { BsCheck } from "react-icons/bs"; -import { paymentURL } from "src/constants/data"; export const PremiumModal: React.FC = ({ opened, onClose }) => { return ( @@ -53,7 +52,7 @@ export const PremiumModal: React.FC = ({ opened, onClose }) => { diff --git a/src/layout/Navbar/index.tsx b/src/layout/Navbar/index.tsx index 5541e2e..b4231f7 100644 --- a/src/layout/Navbar/index.tsx +++ b/src/layout/Navbar/index.tsx @@ -3,6 +3,7 @@ import Link from "next/link"; import styled from "styled-components"; import { Button } from "@mantine/core"; import { FaStar } from "react-icons/fa"; +import useUser from "src/store/useUser"; import { JSONCrackLogo } from "../JsonCrackLogo"; const StyledNavbarWrapper = styled.div` @@ -43,6 +44,8 @@ const Right = styled.div` `; export const Navbar = () => { + const isAuthenticated = useUser(state => state.isAuthenticated); + return ( @@ -86,11 +89,13 @@ export const Navbar = () => { > Star us on GitHub - - - + {!isAuthenticated && ( + + + + )}