Explorar el Código

api improvements & document count limit

AykutSarac hace 2 años
padre
commit
8070c7c280

+ 6 - 1
src/api/altogic.ts

@@ -1,8 +1,13 @@
-import { createClient } from "altogic";
+import { APIError, createClient } from "altogic";
 
 let envUrl = process.env.NEXT_PUBLIC_ALTOGIC_ENV_URL as string;
 let clientKey = process.env.NEXT_PUBLIC_ALTOGIC_CLIENT_KEY as string;
 
 const altogic = createClient(envUrl, clientKey);
 
+export interface AltogicResponse<T> {
+  data: T;
+  errors: APIError | null;
+}
+
 export { altogic };

+ 1 - 0
src/components/Button/index.tsx

@@ -4,6 +4,7 @@ import styled, { DefaultTheme } from "styled-components";
 enum ButtonType {
   PRIMARY = "PRIMARY",
   SECONDARY = "BLURPLE",
+  TERTIARY = "PURPLE",
   DANGER = "DANGER",
   SUCCESS = "SEAGREEN",
   WARNING = "ORANGE",

+ 3 - 0
src/components/Modal/styles.tsx

@@ -36,6 +36,9 @@ export const ModalInnerWrapper = styled.div<{ size: "sm" | "md" | "lg" }>`
 `;
 
 export const Title = styled.h2`
+  display: flex;
+  align-items: center;
+  gap: 5px;
   color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
   font-size: 20px !important;
   margin: 0 !important;

+ 4 - 0
src/constants/globalStyle.ts

@@ -10,6 +10,10 @@ const GlobalStyle = createGlobalStyle`
     font-stretch: 75% 125%;
   }
 
+  svg {
+    vertical-align: top;
+  }
+
   h1, h2, h3, h4, p {
     font-family: 'Mona Sans';
   }

+ 1 - 0
src/constants/theme.ts

@@ -1,6 +1,7 @@
 const fixedColors = {
   CRIMSON: "#DC143C",
   BLURPLE: "#5865F2",
+  PURPLE: "#9036AF",
   FULL_WHITE: "#FFFFFF",
   BLACK: "#202225",
   BLACK_DARK: "#2C2F33",

+ 16 - 11
src/containers/Editor/BottomBar.tsx

@@ -79,21 +79,26 @@ export const BottomBar = () => {
     setIsPrivate(data?.private ?? false);
   }, [data]);
 
-  const handleSaveJson = React.useCallback(() => {
+  const handleSaveJson = React.useCallback(async () => {
     if (!user) return setVisible("login")(true);
 
     if (hasChanges) {
-      toast.promise(
-        saveJson({ id: query.json, data: getJson() }).then(res => {
-          if (res.data._id) replace({ query: { json: res.data._id } });
-          setHasChanges(false);
-        }),
-        {
-          loading: "Saving JSON...",
-          success: "JSON saved to cloud",
-          error: "Failed to save JSON to cloud",
+      try {
+        toast.loading("Saving JSON...", { id: "jsonSave" });
+        const res = await saveJson({ id: query.json, data: getJson() });
+
+        if (res.errors && res.errors.items.length > 0) throw res.errors;
+        if (res.data._id) replace({ query: { json: res.data._id } });
+
+        toast.success("JSON saved to cloud", { id: "jsonSave" });
+        setHasChanges(false);
+      } catch (error: any) {
+        if (error?.items?.length > 0) {
+          return toast.error(error.items[0].message, { id: "jsonSave", duration: 5000 });
         }
-      );
+
+        toast.error("Failed to save JSON!", { id: "jsonSave" });
+      }
     }
   }, [getJson, hasChanges, query.json, replace, setHasChanges, setVisible, user]);
 

+ 27 - 7
src/containers/Modals/AccountModal/index.tsx

@@ -1,5 +1,6 @@
 import React from "react";
 import { IoRocketSharp } from "react-icons/io5";
+import { MdVerified } from "react-icons/md";
 import { Button } from "src/components/Button";
 import { Modal, ModalProps } from "src/components/Modal";
 import useUser from "src/store/useUser";
@@ -36,7 +37,6 @@ const StyledAccountWrapper = styled.div`
 
   button {
     flex-basis: 100%;
-    background: #9036af;
   }
 `;
 
@@ -49,24 +49,26 @@ const StyledContainer = styled.div`
   flex-direction: column;
   gap: 8px;
   padding: 12px 0;
-  font-size: 14px;
+  font-size: 12px;
   line-height: 16px;
   font-weight: 600;
   color: ${({ theme }) => theme.INTERACTIVE_NORMAL};
 
   & > div {
     font-weight: 400;
+    font-size: 14px;
     color: ${({ theme }) => theme.INTERACTIVE_ACTIVE};
   }
 `;
 
 const AccountView: React.FC<Pick<ModalProps, "setVisible">> = ({ setVisible }) => {
   const user = useUser(state => state.user);
+  const isPremium = useUser(state => state.isPremium());
   const logout = useUser(state => state.logout);
 
   return (
     <>
-      <Modal.Header>Account (FREE)</Modal.Header>
+      <Modal.Header>Account</Modal.Header>
       <Modal.Content>
         <StyledTitle>Hello, {user?.name}!</StyledTitle>
         <StyledAccountWrapper>
@@ -75,14 +77,32 @@ const AccountView: React.FC<Pick<ModalProps, "setVisible">> = ({ setVisible }) =
             USERNAME
             <div>{user?.name}</div>
           </StyledContainer>
+          <StyledContainer>
+            ACCOUNT STATUS
+            <div>
+              {isPremium ? "PREMIUM " : "Normal"}
+              {isPremium && <MdVerified />}
+            </div>
+          </StyledContainer>
           <StyledContainer>
             EMAIL
             <div>{user?.email}</div>
           </StyledContainer>
-          <Button block>
-            <IoRocketSharp />
-            UPDATE TO PREMIUM!
-          </Button>
+          <StyledContainer>
+            REGISTRATION
+            <div>{user?.signUpAt && new Date(user.signUpAt).toDateString()}</div>
+          </StyledContainer>
+          {isPremium ? (
+            <Button status="DANGER" block>
+              <IoRocketSharp />
+              Cancel Subscription
+            </Button>
+          ) : (
+            <Button href="/pricing" status="TERTIARY" block link>
+              <IoRocketSharp />
+              UPDATE TO PREMIUM!
+            </Button>
+          )}
         </StyledAccountWrapper>
       </Modal.Content>
       <Modal.Controls setVisible={setVisible}>

+ 26 - 10
src/containers/Modals/CloudModal/index.tsx

@@ -8,7 +8,9 @@ import { AiOutlineEdit, AiOutlineLock, AiOutlinePlus, AiOutlineUnlock } from "re
 import { Modal, ModalProps } from "src/components/Modal";
 import { Spinner } from "src/components/Spinner";
 import { getAllJson, updateJson } from "src/services/db/json";
+import useUser from "src/store/useUser";
 import styled from "styled-components";
+import { IoRocketSharp } from "react-icons/io5";
 
 dayjs.extend(relativeTime);
 
@@ -142,20 +144,34 @@ const GraphCard: React.FC<{ data: any; refetch: () => void; active: boolean }> =
   );
 };
 
-const CreateCard: React.FC = () => (
-  <StyledJsonCard href="/editor">
-    <StyledCreateWrapper>
-      <AiOutlinePlus size="24" />
-      Create New JSON
-    </StyledCreateWrapper>
-  </StyledJsonCard>
-);
+const CreateCard: React.FC<{ reachedLimit: boolean }> = ({ reachedLimit }) => {
+  const isPremium = useUser(state => state.isPremium());
+
+  if (!isPremium && reachedLimit)
+    return (
+      <StyledJsonCard href="/pricing">
+        <StyledCreateWrapper>
+          <IoRocketSharp size="18" />
+          You reached max limit, upgrade to premium for more!
+        </StyledCreateWrapper>
+      </StyledJsonCard>
+    );
+
+  return (
+    <StyledJsonCard href="/editor">
+      <StyledCreateWrapper>
+        <AiOutlinePlus size="24" />
+        Create New JSON
+      </StyledCreateWrapper>
+    </StyledJsonCard>
+  );
+};
 
 export const CloudModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
   const { isReady, query } = useRouter();
 
   const { data, isFetching, refetch } = useQuery(["allJson", query], () => getAllJson(), {
-    enabled: isReady && visible
+    enabled: isReady && visible,
   });
 
   return (
@@ -167,7 +183,7 @@ export const CloudModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
             <Spinner />
           ) : (
             <>
-              <CreateCard />
+              <CreateCard reachedLimit={data ? data?.data.result.length > 15 : false} />
               {data?.data?.result?.map(json => (
                 <GraphCard
                   data={json}

+ 4 - 3
src/services/db/json.tsx

@@ -1,5 +1,5 @@
 import { compressToBase64 } from "lz-string";
-import { altogic } from "src/api/altogic";
+import { altogic, AltogicResponse } from "src/api/altogic";
 
 type JSON = {
   _id: string;
@@ -10,7 +10,7 @@ type JSON = {
   json: string;
 };
 
-const saveJson = async ({ id, data }): Promise<{ data: { _id: string } }> => {
+const saveJson = async ({ id, data }): Promise<AltogicResponse<{ _id: string }>> => {
   const compressedData = compressToBase64(data);
 
   if (id) {
@@ -24,7 +24,8 @@ const saveJson = async ({ id, data }): Promise<{ data: { _id: string } }> => {
   });
 };
 
-const getAllJson = async (): Promise<{ data: { result: JSON[] } }> => await altogic.endpoint.get(`json`);
+const getAllJson = async (): Promise<AltogicResponse<{ result: JSON[] }>> =>
+  await altogic.endpoint.get(`json`);
 
 const updateJson = async (id: string, data: object) =>
   await altogic.endpoint.put(`json/${id}`, {

+ 0 - 1
src/store/useJson.tsx

@@ -62,7 +62,6 @@ const useJson = create<JsonStates & JsonActions>()((set, get) => ({
         const decompressedData = decompressFromBase64(data.json);
         if (decompressedData) {
           useGraph.getState().setGraph(decompressedData);
-          console.log(data);
           return set({
             data,
             json: decompressedData ?? undefined,

+ 13 - 15
src/store/useUser.tsx

@@ -1,24 +1,15 @@
 import toast from "react-hot-toast";
 import { altogic } from "src/api/altogic";
-import { AltogicAuth } from "src/typings/altogic";
+import { AltogicAuth, User } from "src/typings/altogic";
 import create from "zustand";
 import useModal from "./useModal";
 
-type User = {
-  _id: string;
-  provider: string;
-  providerUserId: string;
-  email: string;
-  name: string;
-  profilePicture: string;
-  signUpAt: Date;
-  lastLoginAt: Date;
-};
 interface UserActions {
   login: (response: AltogicAuth) => void;
   logout: () => void;
   setUser: (key: keyof typeof initialStates, value: any) => void;
   checkSession: () => void;
+  isPremium: () => boolean;
 }
 
 const initialStates = {
@@ -28,9 +19,15 @@ const initialStates = {
 
 export type UserStates = typeof initialStates;
 
-const useUser = create<UserStates & UserActions>()(set => ({
+const useUser = create<UserStates & UserActions>()((set, get) => ({
   ...initialStates,
   setUser: (key, value) => set({ [key]: value }),
+  isPremium: () => {
+    const user = get().user;
+
+    if (user) return user.type > 0;
+    return false;
+  },
   logout: () => {
     altogic.auth.signOut();
     toast.success("Logged out.");
@@ -38,15 +35,16 @@ const useUser = create<UserStates & UserActions>()(set => ({
     set(initialStates);
   },
   login: response => {
-    set({ user: response.user, isAuthenticated: true });
+    set({ user: response.user as any, isAuthenticated: true });
   },
   checkSession: async () => {
     const currentSession = altogic.auth.getSession();
-    const currentUser = altogic.auth.getUser();
 
     if (currentSession) {
+      const dbUser = await altogic.auth.getUserFromDB();
+      
       altogic.auth.setSession(currentSession);
-      set({ user: currentUser as any, isAuthenticated: true });
+      set({ user: dbUser.user as any, isAuthenticated: true });
     } else {
       if (!new URLSearchParams(window.location.search).get("access_token")) return;
 

+ 8 - 1
src/typings/altogic.ts

@@ -1,4 +1,4 @@
-interface User {
+export interface User {
   _id: string;
   provider: string;
   providerUserId: string;
@@ -7,6 +7,13 @@ interface User {
   profilePicture: string;
   signUpAt: Date;
   lastLoginAt: Date;
+  type: UserType;
+}
+
+
+export enum UserType {
+  "DEFAULT" = 0,
+  "PREMIUM" = 1
 }
 
 interface Device {