| import React from "react"; |
| import Link from "next/link"; |
| import { useRouter } from "next/router"; |
| import { |
| Modal, |
| Text, |
| Divider, |
| ScrollArea, |
| ModalProps, |
| Table, |
| ActionIcon, |
| Badge, |
| Paper, |
| Flex, |
| DefaultMantineColor, |
| Input, |
| Button, |
| Group, |
| Stack, |
| RingProgress, |
| UnstyledButton, |
| Drawer, |
| LoadingOverlay, |
| } 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 { AiOutlineLink } from "react-icons/ai"; |
| import { FaTrash } from "react-icons/fa"; |
| import { MdFileOpen } from "react-icons/md"; |
| import { VscAdd, VscEdit, VscLock, VscUnlock } from "react-icons/vsc"; |
| import { FileFormat } from "src/enums/file.enum"; |
| import { documentSvc } from "src/services/document.service"; |
| import useFile, { File } from "src/store/useFile"; |
| import useUser from "src/store/useUser"; |
|
|
| dayjs.extend(relativeTime); |
|
|
| const colorByFormat: Record<FileFormat, DefaultMantineColor> = { |
| 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(documentSvc.update(file.id, { name }), { |
| loading: "Updating document...", |
| error: "Error occurred while updating document!", |
| success: `Renamed document to ${name}`, |
| }) |
| .then(() => { |
| refetch(); |
| setName(""); |
| }); |
|
|
| onClose(); |
| }; |
|
|
| return ( |
| <Modal title="Update Document name" opened={!!file} onClose={onClose} centered zIndex={202}> |
| <Stack> |
| <Input |
| value={name} |
| placeholder={file?.name} |
| onChange={e => setName(e.currentTarget.value)} |
| /> |
| <Group justify="right"> |
| <Button variant="outline" onClick={onClose}> |
| Cancel |
| </Button> |
| <Button onClick={onSubmit}>Update</Button> |
| </Group> |
| </Stack> |
| </Modal> |
| ); |
| }; |
|
|
| export const CloudModal: React.FC<ModalProps> = ({ opened, onClose }) => { |
| const totalQuota = useUser(state => (state.premium ? 200 : 25)); |
| const isPremium = useUser(state => state.premium); |
| const getContents = useFile(state => state.getContents); |
| const setHasChanges = useFile(state => state.setHasChanges); |
| const getFormat = useFile(state => state.getFormat); |
| const [currentFile, setCurrentFile] = React.useState<File | null>(null); |
| const { isReady, query, replace } = useRouter(); |
|
|
| const { data, isLoading, refetch } = useQuery(["allJson", query], () => documentSvc.getAll(), { |
| enabled: isReady && opened, |
| }); |
|
|
| const isCreateDisabled = React.useMemo(() => { |
| if (!data?.length) return false; |
| return isPremium ? data.length >= 200 : data.length >= 25; |
| }, [isPremium, data?.length]); |
|
|
| const onCreate = async () => { |
| try { |
| toast.loading("Saving document...", { id: "fileSave" }); |
| const { data, error } = await documentSvc.upsert({ |
| contents: getContents(), |
| format: getFormat(), |
| }); |
|
|
| if (error) throw error; |
|
|
| toast.success("Document saved to cloud", { id: "fileSave" }); |
| setHasChanges(false); |
| replace({ query: { json: data } }); |
| onClose(); |
| } catch (error: any) { |
| toast.error("Failed to save document!", { id: "fileSave" }); |
| console.error(error); |
| } |
| }; |
|
|
| const onDeleteClick = React.useCallback( |
| (file: File) => { |
| toast |
| .promise(documentSvc.delete(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}/editor?json=${fileId}`; |
| navigator.clipboard.writeText(shareLink); |
| toast.success("Copied share link to clipboard!"); |
| }, []); |
|
|
| const rows = React.useMemo( |
| () => |
| data?.map(element => ( |
| <Table.Tr key={element.id}> |
| <Table.Td> |
| <Flex align="center" gap="xs"> |
| {element.private ? <VscLock /> : <VscUnlock />} |
| {element.id} |
| </Flex> |
| </Table.Td> |
| <Table.Td> |
| <Flex align="center" justify="space-between"> |
| {element.name} |
| <ActionIcon |
| variant="transparent" |
| color="cyan" |
| onClick={() => setCurrentFile(element)} |
| > |
| <VscEdit /> |
| </ActionIcon> |
| </Flex> |
| </Table.Td> |
| <Table.Td>{dayjs(element.created_at).format("DD.MM.YYYY")}</Table.Td> |
| <Table.Td> |
| <Badge variant="light" color={colorByFormat[element.format]} size="sm"> |
| {element.format.toUpperCase()} |
| </Badge> |
| </Table.Td> |
| <Table.Td>{element.views.toLocaleString("en-US")}</Table.Td> |
| <Table.Td> |
| <Flex gap="xs"> |
| <ActionIcon.Group> |
| <ActionIcon |
| variant="transparent" |
| component={Link} |
| href={`?json=${element.id}`} |
| prefetch={false} |
| color="blue" |
| onClick={onClose} |
| > |
| <MdFileOpen size="18" /> |
| </ActionIcon> |
| <ActionIcon |
| variant="transparent" |
| color="red" |
| onClick={() => onDeleteClick(element)} |
| > |
| <FaTrash size="18" /> |
| </ActionIcon> |
| <ActionIcon |
| variant="transparent" |
| color="green" |
| onClick={() => copyShareLink(element.id)} |
| > |
| <AiOutlineLink /> |
| </ActionIcon> |
| </ActionIcon.Group> |
| </Flex> |
| </Table.Td> |
| </Table.Tr> |
| )), |
| [data, copyShareLink, onClose, onDeleteClick] |
| ); |
|
|
| return ( |
| <Drawer |
| title="Saved On The Cloud" |
| opened={opened} |
| size="xl" |
| onClose={onClose} |
| transitionProps={{ duration: 300, timingFunction: "ease", transition: "slide-right" }} |
| pos="relative" |
| > |
| <LoadingOverlay visible={isLoading} /> |
| {data && ( |
| <Flex gap="md"> |
| <Paper my="lg" withBorder radius="md" p="xs" w="100%"> |
| <Group> |
| <RingProgress |
| size={40} |
| roundCaps |
| thickness={6} |
| sections={[ |
| { |
| value: (data.length * 100) / totalQuota, |
| color: data.length > totalQuota / 1.5 ? "red" : "blue", |
| }, |
| ]} |
| /> |
| <div> |
| <Text c="dimmed" fz="xs" fw={700} style={{ textTransform: "uppercase" }}> |
| Total Quota |
| </Text> |
| <Text fw={700} size="lg"> |
| {data.length} / {totalQuota} |
| </Text> |
| </div> |
| </Group> |
| </Paper> |
| <Paper my="lg" withBorder radius="md" p="xs" w={250}> |
| <UnstyledButton |
| fw="bold" |
| w="100%" |
| h="100%" |
| onClick={onCreate} |
| disabled={isCreateDisabled} |
| > |
| <Text |
| fz="sm" |
| fw="bold" |
| c="blue" |
| style={{ |
| display: "flex", |
| alignItems: "center", |
| justifyContent: "space-between", |
| textAlign: "center", |
| }} |
| > |
| <VscAdd size="18" strokeWidth={1} /> |
| Create New Document |
| </Text> |
| </UnstyledButton> |
| </Paper> |
| </Flex> |
| )} |
| <Text fz="xs" pb="lg"> |
| The Cloud Save feature is primarily designed for convenient access and is not advisable for |
| storing sensitive data. |
| </Text> |
| <Divider py="xs" /> |
| <Paper> |
| <ScrollArea h="100%" offsetScrollbars> |
| <Table fz="xs" verticalSpacing="xs" striped withTableBorder> |
| <Table.Thead> |
| <Table.Tr> |
| <Table.Th>ID</Table.Th> |
| <Table.Th>Name</Table.Th> |
| <Table.Th>Create Date</Table.Th> |
| <Table.Th>Format</Table.Th> |
| <Table.Th>Views</Table.Th> |
| <Table.Th>Actions</Table.Th> |
| </Table.Tr> |
| </Table.Thead> |
| <Table.Tbody>{rows}</Table.Tbody> |
| </Table> |
| </ScrollArea> |
| </Paper> |
| <UpdateNameModal file={currentFile} onClose={() => setCurrentFile(null)} refetch={refetch} /> |
| </Drawer> |
| ); |
| }; |
|
|