index.tsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import React from "react";
  2. import { useRouter } from "next/router";
  3. import { useQuery } from "@tanstack/react-query";
  4. import dayjs from "dayjs";
  5. import relativeTime from "dayjs/plugin/relativeTime";
  6. import toast from "react-hot-toast";
  7. import {
  8. AiOutlineEdit,
  9. AiOutlineInfoCircle,
  10. AiOutlineLock,
  11. AiOutlinePlus,
  12. AiOutlineUnlock,
  13. } from "react-icons/ai";
  14. import { FaTrash } from "react-icons/fa";
  15. import { IoRocketSharp } from "react-icons/io5";
  16. import { Button } from "src/components/Button";
  17. import { Modal, ModalProps } from "src/components/Modal";
  18. import { Spinner } from "src/components/Spinner";
  19. import { deleteJson, getAllJson, saveJson, updateJson } from "src/services/db/json";
  20. import useJson from "src/store/useJson";
  21. import useUser from "src/store/useUser";
  22. import { Json } from "src/typings/altogic";
  23. import styled from "styled-components";
  24. dayjs.extend(relativeTime);
  25. const StyledModalContent = styled.div`
  26. display: flex;
  27. flex-direction: column;
  28. gap: 14px;
  29. overflow: auto;
  30. `;
  31. const StyledJsonCard = styled.a<{ active?: boolean; create?: boolean }>`
  32. display: ${({ create }) => (create ? "block" : "flex")};
  33. align-items: center;
  34. justify-content: space-between;
  35. background: ${({ theme }) => theme.BLACK_SECONDARY};
  36. border: 2px solid ${({ theme, active }) => (active ? theme.SEAGREEN : theme.BLACK_SECONDARY)};
  37. border-radius: 5px;
  38. overflow: hidden;
  39. flex: 1;
  40. height: 160px;
  41. `;
  42. const StyledInfo = styled.div`
  43. padding: 4px 6px;
  44. `;
  45. const StyledTitle = styled.div`
  46. display: flex;
  47. align-items: center;
  48. gap: 4px;
  49. font-size: 14px;
  50. font-weight: 500;
  51. width: fit-content;
  52. cursor: pointer;
  53. span {
  54. overflow: hidden;
  55. text-overflow: ellipsis;
  56. }
  57. `;
  58. const StyledDetils = styled.div`
  59. display: flex;
  60. align-items: center;
  61. font-size: 12px;
  62. gap: 4px;
  63. `;
  64. const StyledModal = styled(Modal)`
  65. #modal-view {
  66. display: none;
  67. }
  68. `;
  69. const StyledDeleteButton = styled(Button)`
  70. background: transparent;
  71. `;
  72. const StyledCreateWrapper = styled.div`
  73. display: flex;
  74. height: 100%;
  75. gap: 6px;
  76. align-items: center;
  77. justify-content: center;
  78. opacity: 0.6;
  79. height: 45px;
  80. font-size: 14px;
  81. cursor: pointer;
  82. `;
  83. const StyledNameInput = styled.input`
  84. background: transparent;
  85. border: none;
  86. outline: none;
  87. width: 90%;
  88. color: ${({ theme }) => theme.SEAGREEN};
  89. font-weight: 600;
  90. `;
  91. const StyledInfoText = styled.span`
  92. font-size: 10px;
  93. color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
  94. svg {
  95. vertical-align: text-top;
  96. margin-right: 4px;
  97. }
  98. `;
  99. const GraphCard: React.FC<{ data: Json; refetch: () => void; active: boolean }> = ({
  100. data,
  101. refetch,
  102. active,
  103. ...props
  104. }) => {
  105. const [editMode, setEditMode] = React.useState(false);
  106. const [name, setName] = React.useState(data.name);
  107. const onSubmit = () => {
  108. toast
  109. .promise(updateJson(data._id, { name }), {
  110. loading: "Updating document...",
  111. error: "Error occured while updating document!",
  112. success: `Renamed document to ${name}`,
  113. })
  114. .then(refetch);
  115. setEditMode(false);
  116. };
  117. const onDeleteClick = (e: React.MouseEvent<HTMLButtonElement>) => {
  118. e.preventDefault();
  119. toast
  120. .promise(deleteJson(data._id), {
  121. loading: "Deleting JSON file...",
  122. error: "An error occured while deleting the file!",
  123. success: `Deleted ${name}!`,
  124. })
  125. .then(refetch);
  126. };
  127. return (
  128. <StyledJsonCard
  129. href={`?json=${data._id}`}
  130. as={editMode ? "div" : "a"}
  131. active={active}
  132. {...props}
  133. >
  134. <StyledInfo>
  135. {editMode ? (
  136. <form onSubmit={onSubmit}>
  137. <StyledNameInput
  138. value={name}
  139. onChange={e => setName(e.currentTarget.value)}
  140. onClick={e => e.preventDefault()}
  141. autoFocus
  142. />
  143. <input type="submit" hidden />
  144. </form>
  145. ) : (
  146. <StyledTitle
  147. onClick={e => {
  148. e.preventDefault();
  149. setEditMode(true);
  150. }}
  151. >
  152. <span>{name}</span>
  153. <AiOutlineEdit />
  154. </StyledTitle>
  155. )}
  156. <StyledDetils>
  157. {data.private ? <AiOutlineLock /> : <AiOutlineUnlock />}
  158. Last modified {dayjs(data.updatedAt).fromNow()}
  159. </StyledDetils>
  160. </StyledInfo>
  161. <StyledDeleteButton onClick={onDeleteClick}>
  162. <FaTrash />
  163. </StyledDeleteButton>
  164. </StyledJsonCard>
  165. );
  166. };
  167. const CreateCard: React.FC<{ reachedLimit: boolean }> = ({ reachedLimit }) => {
  168. const { replace } = useRouter();
  169. const isPremium = useUser(state => state.isPremium());
  170. const getJson = useJson(state => state.getJson);
  171. const setHasChanges = useJson(state => state.setHasChanges);
  172. const onCreate = async () => {
  173. try {
  174. toast.loading("Saving JSON...", { id: "jsonSave" });
  175. const res = await saveJson({ data: getJson() });
  176. if (res.errors && res.errors.items.length > 0) throw res.errors;
  177. toast.success("JSON saved to cloud", { id: "jsonSave" });
  178. setHasChanges(false);
  179. replace({ query: { json: res.data._id } });
  180. } catch (error: any) {
  181. if (error?.items?.length > 0) {
  182. return toast.error(error.items[0].message, { id: "jsonSave", duration: 7000 });
  183. }
  184. toast.error("Failed to save JSON!", { id: "jsonSave" });
  185. }
  186. };
  187. if (!isPremium && reachedLimit)
  188. return (
  189. <StyledJsonCard href="/pricing" create>
  190. <StyledCreateWrapper>
  191. <IoRocketSharp size="18" />
  192. You reached max limit, upgrade to premium for more!
  193. </StyledCreateWrapper>
  194. </StyledJsonCard>
  195. );
  196. return (
  197. <StyledJsonCard onClick={onCreate} create>
  198. <StyledCreateWrapper>
  199. <AiOutlinePlus size="24" />
  200. Create New JSON
  201. </StyledCreateWrapper>
  202. </StyledJsonCard>
  203. );
  204. };
  205. export const CloudModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
  206. const { isReady, query } = useRouter();
  207. const { data, isFetching, refetch } = useQuery(["allJson", query], () => getAllJson(), {
  208. enabled: isReady && visible,
  209. });
  210. return (
  211. <StyledModal visible={visible} setVisible={setVisible}>
  212. <Modal.Header>Saved On The Cloud</Modal.Header>
  213. <Modal.Content>
  214. <StyledModalContent>
  215. {isFetching ? (
  216. <Spinner />
  217. ) : (
  218. <>
  219. <CreateCard reachedLimit={data ? data?.data.result.length > 15 : false} />
  220. {data?.data?.result?.map(json => (
  221. <GraphCard
  222. data={json}
  223. key={json._id}
  224. refetch={refetch}
  225. active={query.json === json._id}
  226. />
  227. ))}
  228. </>
  229. )}
  230. </StyledModalContent>
  231. </Modal.Content>
  232. <Modal.Controls setVisible={setVisible}>
  233. <StyledInfoText>
  234. <AiOutlineInfoCircle />
  235. Cloud Save feature is for ease-of-access only and not recommended to store sensitive data,
  236. we don&apos;t guarantee protection of your data.
  237. </StyledInfoText>
  238. </Modal.Controls>
  239. </StyledModal>
  240. );
  241. };