From 7f59dc7b7b619e4f4c3480368ae1a37d108eb502 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Fri, 27 Oct 2023 21:03:01 +0400 Subject: [PATCH 01/18] wip --- .../js/Pages/Galleries/MyGalleries/Create.tsx | 8 ++- .../Pages/Galleries/hooks/useGalleryDraft.ts | 70 +++++++++++++++++++ resources/types/generated.d.ts | 4 -- 3 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 resources/js/Pages/Galleries/hooks/useGalleryDraft.ts diff --git a/resources/js/Pages/Galleries/MyGalleries/Create.tsx b/resources/js/Pages/Galleries/MyGalleries/Create.tsx index 2d12ef6a6..ac4be9e0a 100644 --- a/resources/js/Pages/Galleries/MyGalleries/Create.tsx +++ b/resources/js/Pages/Galleries/MyGalleries/Create.tsx @@ -17,6 +17,7 @@ import { NoNftsOverlay } from "@/Components/Layout/NoNftsOverlay"; import { useMetaMaskContext } from "@/Contexts/MetaMaskContext"; import { useAuthorizedAction } from "@/Hooks/useAuthorizedAction"; import { GalleryNameInput } from "@/Pages/Galleries/Components/GalleryNameInput"; +import { useGalleryDraft } from "@/Pages/Galleries/hooks/useGalleryDraft"; import { useGalleryForm } from "@/Pages/Galleries/hooks/useGalleryForm"; import { assertUser, assertWallet } from "@/Utils/assertions"; import { isTruthy } from "@/Utils/is-truthy"; @@ -61,6 +62,8 @@ const Create = ({ gallery, }); + const { setCover, setTitle, setNfts } = useGalleryDraft(); + const totalValue = 0; assertUser(auth.user); @@ -155,7 +158,10 @@ const Create = ({ }} > { + updateSelectedNfts(nfts); + setNfts(nfts); + }} error={errors.nfts} /> diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDraft.ts b/resources/js/Pages/Galleries/hooks/useGalleryDraft.ts new file mode 100644 index 000000000..65a8709db --- /dev/null +++ b/resources/js/Pages/Galleries/hooks/useGalleryDraft.ts @@ -0,0 +1,70 @@ +import { useRef } from "react"; +import { useAuth } from "@/Contexts/AuthContext"; + +interface DraftNft { + nftId: number; + image: string; + collectionSlug: slug; +} + +interface GalleryDraft { + title: string | null; + cover: string | null; + nfts: DraftNft[]; +} + +const initialGalleryDraft: GalleryDraft = { + title: null, + cover: null, + nfts: [], +}; + +export const useGalleryDraft = (givenDraftId?: string) => { + const { wallet } = useAuth(); + + const draftIdReference = useRef(givenDraftId ?? `gallery-${wallet?.address}-${new Date().getTime()}`); + + const draftId = draftIdReference.current; + + const getDraft = (): GalleryDraft => { + try { + const rawDraft = localStorage.getItem(draftId); + return rawDraft != null ? (JSON.parse(rawDraft) as GalleryDraft) : initialGalleryDraft; + } catch (error) { + return initialGalleryDraft; + } + }; + + const draftReference = useRef(getDraft()); + + const draft = draftReference.current; + + const persistDraft = (): void => { + localStorage.setItem(draftId, JSON.stringify(draft)); + }; + + const setTitle = (title: string | null): void => { + draft.title = title; + persistDraft(); + }; + + const setCover = (image: string | null): void => { + draft.cover = image; + persistDraft(); + }; + + const setNfts = (nfts: App.Data.Gallery.GalleryNftData[]) => { + draft.nfts = nfts.map((nft) => ({ + nftId: nft.id, + image: nft.images.large ?? "", + collectionSlug: nft.collectionSlug, + })); + persistDraft(); + }; + + return { + setTitle, + setCover, + setNfts, + }; +}; diff --git a/resources/types/generated.d.ts b/resources/types/generated.d.ts index 212beec63..157bb0fa6 100644 --- a/resources/types/generated.d.ts +++ b/resources/types/generated.d.ts @@ -242,10 +242,6 @@ declare namespace App.Data.Gallery { isOwner: boolean; hasLiked: boolean; }; - export type GalleryLikeData = { - likes: number; - hasLiked: boolean; - }; export type GalleryNftData = { id: number; name: string | null; From beba28b476325775bac76d5f06bbe5167b95ccbc Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Mon, 30 Oct 2023 17:21:20 +0400 Subject: [PATCH 02/18] wip --- package.json | 1 + pnpm-lock.yaml | 21 ++- .../js/Pages/Galleries/MyGalleries/Create.tsx | 20 +-- .../Pages/Galleries/hooks/useGalleryDraft.ts | 70 ---------- .../Pages/Galleries/hooks/useGalleryDrafts.ts | 127 ++++++++++++++++++ .../Pages/Galleries/hooks/useGalleryForm.ts | 23 +++- resources/js/app.tsx | 4 + resources/js/databaseConfig.ts | 16 +++ 8 files changed, 197 insertions(+), 85 deletions(-) delete mode 100644 resources/js/Pages/Galleries/hooks/useGalleryDraft.ts create mode 100644 resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts create mode 100644 resources/js/databaseConfig.ts diff --git a/package.json b/package.json index b61f75e72..761d33187 100644 --- a/package.json +++ b/package.json @@ -137,6 +137,7 @@ "react-chartjs-2": "^5.2.0", "react-i18next": "^12.3.1", "react-in-viewport": "1.0.0-alpha.30", + "react-indexed-db-hook": "^1.0.14", "react-loading-skeleton": "^3.3.1", "react-popper": "^2.3.0", "react-resize-detector": "^8.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bed7746bc..10266bd51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - dependencies: '@ardenthq/sdk-helpers': specifier: ^1.2.7 @@ -71,6 +67,9 @@ dependencies: react-in-viewport: specifier: 1.0.0-alpha.30 version: 1.0.0-alpha.30(react-dom@18.2.0)(react@18.2.0) + react-indexed-db-hook: + specifier: ^1.0.14 + version: 1.0.14(react-dom@18.2.0)(react@18.2.0) react-loading-skeleton: specifier: ^3.3.1 version: 3.3.1(react@18.2.0) @@ -10351,6 +10350,16 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /react-indexed-db-hook@1.0.14(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-tQ6rWofgXUCBhZp9pRpWzthzPbjqcll5uXMo07lbQTKl47VyL9nw9wfVswRxxzS5yj5Sq/VHUkNUjamWbA/M/w==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-inspector@6.0.2(react@18.2.0): resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==} peerDependencies: @@ -12595,3 +12604,7 @@ packages: /yocto-queue@1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/resources/js/Pages/Galleries/MyGalleries/Create.tsx b/resources/js/Pages/Galleries/MyGalleries/Create.tsx index ac4be9e0a..31bf2b8e3 100644 --- a/resources/js/Pages/Galleries/MyGalleries/Create.tsx +++ b/resources/js/Pages/Galleries/MyGalleries/Create.tsx @@ -17,7 +17,6 @@ import { NoNftsOverlay } from "@/Components/Layout/NoNftsOverlay"; import { useMetaMaskContext } from "@/Contexts/MetaMaskContext"; import { useAuthorizedAction } from "@/Hooks/useAuthorizedAction"; import { GalleryNameInput } from "@/Pages/Galleries/Components/GalleryNameInput"; -import { useGalleryDraft } from "@/Pages/Galleries/hooks/useGalleryDraft"; import { useGalleryForm } from "@/Pages/Galleries/hooks/useGalleryForm"; import { assertUser, assertWallet } from "@/Utils/assertions"; import { isTruthy } from "@/Utils/is-truthy"; @@ -58,11 +57,10 @@ const Create = ({ const [showDeleteModal, setShowDeleteModal] = useState(false); const [busy, setBusy] = useState(false); - const { selectedNfts, data, setData, errors, submit, updateSelectedNfts, processing } = useGalleryForm({ - gallery, - }); - - const { setCover, setTitle, setNfts } = useGalleryDraft(); + const { selectedNfts, data, setData, errors, submit, updateSelectedNfts, processing, setDraftCover } = + useGalleryForm({ + gallery, + }); const totalValue = 0; @@ -158,10 +156,7 @@ const Create = ({ }} > { - updateSelectedNfts(nfts); - setNfts(nfts); - }} + onChange={updateSelectedNfts} error={errors.nfts} /> @@ -204,8 +199,13 @@ const Create = ({ setGalleryCoverImageUrl(imageDataURI); if (blob === undefined) { setData("coverImage", null); + setDraftCover(null); } else { setData("coverImage", new File([blob], blob.name, { type: blob.type })); + // eslint ignore + void blob.arrayBuffer().then((buf) => { + setDraftCover(buf); + }); } setIsGalleryFormSliderOpen(false); }} diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDraft.ts b/resources/js/Pages/Galleries/hooks/useGalleryDraft.ts deleted file mode 100644 index 65a8709db..000000000 --- a/resources/js/Pages/Galleries/hooks/useGalleryDraft.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useRef } from "react"; -import { useAuth } from "@/Contexts/AuthContext"; - -interface DraftNft { - nftId: number; - image: string; - collectionSlug: slug; -} - -interface GalleryDraft { - title: string | null; - cover: string | null; - nfts: DraftNft[]; -} - -const initialGalleryDraft: GalleryDraft = { - title: null, - cover: null, - nfts: [], -}; - -export const useGalleryDraft = (givenDraftId?: string) => { - const { wallet } = useAuth(); - - const draftIdReference = useRef(givenDraftId ?? `gallery-${wallet?.address}-${new Date().getTime()}`); - - const draftId = draftIdReference.current; - - const getDraft = (): GalleryDraft => { - try { - const rawDraft = localStorage.getItem(draftId); - return rawDraft != null ? (JSON.parse(rawDraft) as GalleryDraft) : initialGalleryDraft; - } catch (error) { - return initialGalleryDraft; - } - }; - - const draftReference = useRef(getDraft()); - - const draft = draftReference.current; - - const persistDraft = (): void => { - localStorage.setItem(draftId, JSON.stringify(draft)); - }; - - const setTitle = (title: string | null): void => { - draft.title = title; - persistDraft(); - }; - - const setCover = (image: string | null): void => { - draft.cover = image; - persistDraft(); - }; - - const setNfts = (nfts: App.Data.Gallery.GalleryNftData[]) => { - draft.nfts = nfts.map((nft) => ({ - nftId: nft.id, - image: nft.images.large ?? "", - collectionSlug: nft.collectionSlug, - })); - persistDraft(); - }; - - return { - setTitle, - setCover, - setNfts, - }; -}; diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts new file mode 100644 index 000000000..8f1a2a3bf --- /dev/null +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -0,0 +1,127 @@ +import { useEffect, useState } from "react"; +import { useIndexedDB } from "react-indexed-db-hook"; +import { useAuth } from "@/Contexts/AuthContext"; +import { useDebounce } from "@/Hooks/useDebounce"; + +interface DraftNft { + nftId: number; + image: string; + collectionSlug: string; +} + +interface GalleryDraft { + title: string; + cover: ArrayBuffer | null; + nfts: DraftNft[]; + walletAddress?: string; +} + +const initialGalleryDraft: GalleryDraft = { + title: "", + cover: null, + nfts: [], +}; + +export const useGalleryDrafts = (givenDraftId?: number) => { + const { wallet } = useAuth(); + + const database = useIndexedDB("gallery-drafts"); + + const [draft, setDraft] = useState(null); + const [draftId, setDraftId] = useState(givenDraftId ?? null); + + const [save, setSave] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + const [title, setTitle] = useState(""); + const [debouncedValue] = useDebounce(title, 400); + + // populate `draft` state if `draftId` is present + useEffect(() => { + if (draft === null && draftId !== null) { + const getDraft = async (): Promise => { + const draft: GalleryDraft = await database.getByID(draftId); + console.log(draft); + setDraft(draft); + }; + + void getDraft(); + } + }, []); + + // persist debounced title + useEffect(() => { + if (draft === null) return; + + setDraft({ ...draft, title: debouncedValue }); + setSave(true); + }, [debouncedValue]); + + // initialize an initial `draft` state + useEffect(() => { + if (save && draftId === null) { + const initializeDraft = async (): Promise => { + const data: GalleryDraft = { + ...initialGalleryDraft, + walletAddress: wallet?.address, + }; + + const id = await database.add(data); + + setDraftId(id); + setDraft(data); + }; + + void initializeDraft(); + } + }, [save, draftId]); + + // update persisted draft + useEffect(() => { + if (!save || draftId === null || isSaving) return; + + setIsSaving(true); + + const saveDraft = async (): Promise => { + await database.update({ + ...draft, + id: draftId, + }); + setSave(false); + setIsSaving(false); + }; + + void saveDraft(); + }, [save]); + + const setDraftTitle = (title: string): void => { + setTitle(title); + }; + + const setDraftCover = (image: ArrayBuffer | null): void => { + draft != null && setDraft({ ...draft, cover: image }); + setSave(true); + }; + + const setDraftNfts = (nfts: App.Data.Gallery.GalleryNftData[]): void => { + draft != null && + setDraft({ + ...draft, + nfts: nfts.map((nft) => ({ + nftId: nft.id, + image: nft.images.large ?? "", + collectionSlug: nft.collectionSlug, + })), + }); + + setSave(true); + }; + + return { + draftId, + draft, + setDraftCover, + setDraftNfts, + setDraftTitle, + }; +}; diff --git a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts index 402647db9..2ba699a56 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts @@ -1,8 +1,11 @@ import { useForm } from "@inertiajs/react"; -import { type FormEvent, useState } from "react"; +import { type FormEvent, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useToasts } from "@/Hooks/useToasts"; +import { useGalleryDrafts } from "@/Pages/Galleries/hooks/useGalleryDrafts"; +import { getQueryParameters } from "@/Utils/get-query-parameters"; import { isTruthy } from "@/Utils/is-truthy"; +import { replaceUrlQuery } from "@/Utils/replace-url-query"; interface UseGalleryFormProperties extends Record { id: number | null; @@ -24,11 +27,22 @@ export const useGalleryForm = ({ errors: Partial>; updateSelectedNfts: (nfts: App.Data.Gallery.GalleryNftData[]) => void; processing: boolean; + setDraftCover: (image: ArrayBuffer | null) => void; } => { const { t } = useTranslation(); const [selectedNfts, setSelectedNfts] = useState([]); const { showToast } = useToasts(); + const { draftId: givenDraftId } = getQueryParameters(); + + const { setDraftCover, setDraftTitle, setDraftNfts, draftId } = useGalleryDrafts( + givenDraftId !== "" ? Number(givenDraftId) : undefined, + ); + + useEffect(() => { + draftId != null && replaceUrlQuery({ draftId: draftId.toString() }); + }, [draftId]); + const { data, setData, post, processing, errors, ...form } = useForm({ id: gallery?.id ?? null, name: gallery?.name ?? "", @@ -99,6 +113,8 @@ export const useGalleryForm = ({ "nfts", nfts.map((nft) => nft.id), ); + + setDraftNfts(nfts); }; return { @@ -108,12 +124,17 @@ export const useGalleryForm = ({ submit, errors, processing, + setDraftCover, setData: (field, value) => { setData(field, value); if (field === "name" && validateName(field)) { form.setError("name", ""); } + + if (field === "name") { + setDraftTitle(typeof value === "string" ? value : ""); + } }, }; }; diff --git a/resources/js/app.tsx b/resources/js/app.tsx index 74bf4c2c4..05742e7df 100644 --- a/resources/js/app.tsx +++ b/resources/js/app.tsx @@ -21,12 +21,14 @@ import { import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers"; import { createRoot } from "react-dom/client"; import { I18nextProvider } from "react-i18next"; +import { initDB } from "react-indexed-db-hook"; import { AuthContextProvider } from "./Contexts/AuthContext"; import DarkModeContextProvider from "./Contexts/DarkModeContex"; import EnvironmentContextProvider from "./Contexts/EnvironmentContext"; import { CookieConsent } from "./cookieConsent"; import MetaMaskContextProvider from "@/Contexts/MetaMaskContext"; import { TransactionSliderProvider } from "@/Contexts/TransactionSliderContext"; +import { databaseConfig } from "@/databaseConfig"; import { i18n } from "@/I18n"; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access @@ -55,6 +57,8 @@ const appName = window.document.querySelector("title")?.innerText ?? "Dashbrd"; const queryClient = new QueryClient(); +initDB(databaseConfig); + void createInertiaApp({ title: (title) => (title !== "" ? title : appName), diff --git a/resources/js/databaseConfig.ts b/resources/js/databaseConfig.ts new file mode 100644 index 000000000..2a69fbfbd --- /dev/null +++ b/resources/js/databaseConfig.ts @@ -0,0 +1,16 @@ +export const databaseConfig = { + name: "dashbrd", + version: 1, + objectStoresMeta: [ + { + store: "gallery-drafts", + storeConfig: { keyPath: "id", autoIncrement: true }, + storeSchema: [ + { name: "walletAddress", keypath: "walletAddress", options: { unique: false } }, + { name: "title", keypath: "title", options: { unique: false } }, + { name: "cover", keypath: "cover", options: { unique: false } }, + { name: "nfts", keypath: "nfts", options: { unique: false } }, + ], + }, + ], +}; From 2b2b96a0657803f8c44b8e1bfe93bc0f379701f1 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Mon, 30 Oct 2023 18:41:33 +0400 Subject: [PATCH 03/18] wip --- .../js/Pages/Galleries/MyGalleries/Create.tsx | 4 +- .../Pages/Galleries/hooks/useGalleryDrafts.ts | 90 +++++++------------ .../Pages/Galleries/hooks/useGalleryForm.ts | 10 +-- 3 files changed, 40 insertions(+), 64 deletions(-) diff --git a/resources/js/Pages/Galleries/MyGalleries/Create.tsx b/resources/js/Pages/Galleries/MyGalleries/Create.tsx index 31bf2b8e3..aef36be03 100644 --- a/resources/js/Pages/Galleries/MyGalleries/Create.tsx +++ b/resources/js/Pages/Galleries/MyGalleries/Create.tsx @@ -199,12 +199,12 @@ const Create = ({ setGalleryCoverImageUrl(imageDataURI); if (blob === undefined) { setData("coverImage", null); - setDraftCover(null); + void setDraftCover(null); } else { setData("coverImage", new File([blob], blob.name, { type: blob.type })); // eslint ignore void blob.arrayBuffer().then((buf) => { - setDraftCover(buf); + void setDraftCover(buf); }); } setIsGalleryFormSliderOpen(false); diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts index 8f1a2a3bf..c99ca807c 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -14,12 +14,14 @@ interface GalleryDraft { cover: ArrayBuffer | null; nfts: DraftNft[]; walletAddress?: string; + id: number | null; } const initialGalleryDraft: GalleryDraft = { title: "", cover: null, nfts: [], + id: null, }; export const useGalleryDrafts = (givenDraftId?: number) => { @@ -27,10 +29,11 @@ export const useGalleryDrafts = (givenDraftId?: number) => { const database = useIndexedDB("gallery-drafts"); - const [draft, setDraft] = useState(null); - const [draftId, setDraftId] = useState(givenDraftId ?? null); + const [draft, setDraft] = useState({ + ...initialGalleryDraft, + walletAddress: wallet?.address, + }); - const [save, setSave] = useState(false); const [isSaving, setIsSaving] = useState(false); const [title, setTitle] = useState(""); @@ -38,10 +41,9 @@ export const useGalleryDrafts = (givenDraftId?: number) => { // populate `draft` state if `draftId` is present useEffect(() => { - if (draft === null && draftId !== null) { + if (draft.id === null && givenDraftId != null) { const getDraft = async (): Promise => { - const draft: GalleryDraft = await database.getByID(draftId); - console.log(draft); + const draft: GalleryDraft = await database.getByID(givenDraftId); setDraft(draft); }; @@ -51,74 +53,48 @@ export const useGalleryDrafts = (givenDraftId?: number) => { // persist debounced title useEffect(() => { - if (draft === null) return; - setDraft({ ...draft, title: debouncedValue }); - setSave(true); + void saveDraft(); }, [debouncedValue]); - // initialize an initial `draft` state - useEffect(() => { - if (save && draftId === null) { - const initializeDraft = async (): Promise => { - const data: GalleryDraft = { - ...initialGalleryDraft, - walletAddress: wallet?.address, - }; - - const id = await database.add(data); - - setDraftId(id); - setDraft(data); - }; - - void initializeDraft(); - } - }, [save, draftId]); - - // update persisted draft - useEffect(() => { - if (!save || draftId === null || isSaving) return; + const saveDraft = async (): Promise => { + if (isSaving) return; setIsSaving(true); - const saveDraft = async (): Promise => { - await database.update({ - ...draft, - id: draftId, - }); - setSave(false); - setIsSaving(false); - }; + if (draft.id === null) { + const id = await database.add(draft); + setDraft({ ...draft, id }); + } else { + await database.update(draft); + } - void saveDraft(); - }, [save]); + setIsSaving(false); + }; const setDraftTitle = (title: string): void => { setTitle(title); }; - const setDraftCover = (image: ArrayBuffer | null): void => { - draft != null && setDraft({ ...draft, cover: image }); - setSave(true); + const setDraftCover = async (image: ArrayBuffer | null): Promise => { + setDraft({ ...draft, cover: image }); + await saveDraft(); }; - const setDraftNfts = (nfts: App.Data.Gallery.GalleryNftData[]): void => { - draft != null && - setDraft({ - ...draft, - nfts: nfts.map((nft) => ({ - nftId: nft.id, - image: nft.images.large ?? "", - collectionSlug: nft.collectionSlug, - })), - }); - - setSave(true); + const setDraftNfts = async (nfts: App.Data.Gallery.GalleryNftData[]): Promise => { + setDraft({ + ...draft, + nfts: nfts.map((nft) => ({ + nftId: nft.id, + image: nft.images.large ?? "", + collectionSlug: nft.collectionSlug, + })), + }); + + await saveDraft(); }; return { - draftId, draft, setDraftCover, setDraftNfts, diff --git a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts index 2ba699a56..1e3fa6254 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts @@ -27,7 +27,7 @@ export const useGalleryForm = ({ errors: Partial>; updateSelectedNfts: (nfts: App.Data.Gallery.GalleryNftData[]) => void; processing: boolean; - setDraftCover: (image: ArrayBuffer | null) => void; + setDraftCover: (image: ArrayBuffer | null) => Promise; } => { const { t } = useTranslation(); const [selectedNfts, setSelectedNfts] = useState([]); @@ -35,13 +35,13 @@ export const useGalleryForm = ({ const { draftId: givenDraftId } = getQueryParameters(); - const { setDraftCover, setDraftTitle, setDraftNfts, draftId } = useGalleryDrafts( + const { setDraftCover, setDraftTitle, setDraftNfts, draft } = useGalleryDrafts( givenDraftId !== "" ? Number(givenDraftId) : undefined, ); useEffect(() => { - draftId != null && replaceUrlQuery({ draftId: draftId.toString() }); - }, [draftId]); + draft.id != null && replaceUrlQuery({ draftId: draft.id.toString() }); + }, [draft.id]); const { data, setData, post, processing, errors, ...form } = useForm({ id: gallery?.id ?? null, @@ -114,7 +114,7 @@ export const useGalleryForm = ({ nfts.map((nft) => nft.id), ); - setDraftNfts(nfts); + void setDraftNfts(nfts); }; return { From 694e033aabf5a502b7235a8da13462042909401c Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Mon, 30 Oct 2023 18:55:07 +0400 Subject: [PATCH 04/18] wip --- .../Pages/Galleries/hooks/useGalleryDrafts.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts index c99ca807c..ebcd3a4ae 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { useIndexedDB } from "react-indexed-db-hook"; import { useAuth } from "@/Contexts/AuthContext"; import { useDebounce } from "@/Hooks/useDebounce"; +import { useIsFirstRender } from "@/Hooks/useIsFirstRender"; interface DraftNft { nftId: number; @@ -39,20 +40,25 @@ export const useGalleryDrafts = (givenDraftId?: number) => { const [title, setTitle] = useState(""); const [debouncedValue] = useDebounce(title, 400); + const isFirstRender = useIsFirstRender(); + // populate `draft` state if `draftId` is present useEffect(() => { - if (draft.id === null && givenDraftId != null) { - const getDraft = async (): Promise => { - const draft: GalleryDraft = await database.getByID(givenDraftId); + if (givenDraftId === undefined) return; + const getDraft = async (): Promise => { + const draft: GalleryDraft = await database.getByID(givenDraftId); + + if (draft.walletAddress === wallet?.address) { setDraft(draft); - }; + } + }; - void getDraft(); - } + void getDraft(); }, []); // persist debounced title useEffect(() => { + if (isFirstRender) return; setDraft({ ...draft, title: debouncedValue }); void saveDraft(); }, [debouncedValue]); From 1f9281788ec0f8cff96c289f6240580f42efedb8 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Mon, 30 Oct 2023 20:54:13 +0400 Subject: [PATCH 05/18] wip --- .../Pages/Galleries/hooks/useGalleryDrafts.ts | 36 +++++++++++-------- .../Pages/Galleries/hooks/useGalleryForm.ts | 2 +- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts index ebcd3a4ae..3394cf976 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -42,9 +42,10 @@ export const useGalleryDrafts = (givenDraftId?: number) => { const isFirstRender = useIsFirstRender(); - // populate `draft` state if `draftId` is present + // populate `draft` state if `givenDraftId` is present useEffect(() => { if (givenDraftId === undefined) return; + console.log("getting the draft", givenDraftId); const getDraft = async (): Promise => { const draft: GalleryDraft = await database.getByID(givenDraftId); @@ -59,17 +60,23 @@ export const useGalleryDrafts = (givenDraftId?: number) => { // persist debounced title useEffect(() => { if (isFirstRender) return; - setDraft({ ...draft, title: debouncedValue }); - void saveDraft(); + + const updatedDraft = { ...draft, title: debouncedValue }; + + setDraft(updatedDraft); + void saveDraft(updatedDraft); }, [debouncedValue]); - const saveDraft = async (): Promise => { + const saveDraft = async (draft: GalleryDraft): Promise => { if (isSaving) return; setIsSaving(true); if (draft.id === null) { - const id = await database.add(draft); + const draftToCreate: Partial = { ...draft }; + delete draftToCreate.id; + + const id = await database.add(draftToCreate); setDraft({ ...draft, id }); } else { await database.update(draft); @@ -78,32 +85,31 @@ export const useGalleryDrafts = (givenDraftId?: number) => { setIsSaving(false); }; - const setDraftTitle = (title: string): void => { - setTitle(title); - }; - const setDraftCover = async (image: ArrayBuffer | null): Promise => { - setDraft({ ...draft, cover: image }); - await saveDraft(); + const updatedDraft = { ...draft, cover: image }; + setDraft(updatedDraft); + await saveDraft(updatedDraft); }; const setDraftNfts = async (nfts: App.Data.Gallery.GalleryNftData[]): Promise => { - setDraft({ + const updatedDraft = { ...draft, nfts: nfts.map((nft) => ({ nftId: nft.id, image: nft.images.large ?? "", collectionSlug: nft.collectionSlug, })), - }); + }; + + setDraft(updatedDraft); - await saveDraft(); + await saveDraft(updatedDraft); }; return { draft, setDraftCover, setDraftNfts, - setDraftTitle, + setDraftTitle: setTitle, }; }; diff --git a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts index 1e3fa6254..bd5763ca5 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts @@ -36,7 +36,7 @@ export const useGalleryForm = ({ const { draftId: givenDraftId } = getQueryParameters(); const { setDraftCover, setDraftTitle, setDraftNfts, draft } = useGalleryDrafts( - givenDraftId !== "" ? Number(givenDraftId) : undefined, + isTruthy(givenDraftId) ? Number(givenDraftId) : undefined, ); useEffect(() => { From 8cc74186bc1a6903983cd3f8a7bcff9e153a7ec0 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Mon, 30 Oct 2023 21:14:16 +0400 Subject: [PATCH 06/18] wip --- resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts index 3394cf976..476fe8be7 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -67,7 +67,10 @@ export const useGalleryDrafts = (givenDraftId?: number) => { void saveDraft(updatedDraft); }, [debouncedValue]); + console.log("rendered", draft); + const saveDraft = async (draft: GalleryDraft): Promise => { + console.log("saving draft"); if (isSaving) return; setIsSaving(true); From b03b234046bf708f386c55fc37ba593519213249 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Mon, 30 Oct 2023 21:38:05 +0400 Subject: [PATCH 07/18] wip --- .../Pages/Galleries/hooks/useGalleryDrafts.ts | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts index 476fe8be7..4ba4c3013 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -35,6 +35,7 @@ export const useGalleryDrafts = (givenDraftId?: number) => { walletAddress: wallet?.address, }); + const [save, setSave] = useState(false); const [isSaving, setIsSaving] = useState(false); const [title, setTitle] = useState(""); @@ -45,7 +46,6 @@ export const useGalleryDrafts = (givenDraftId?: number) => { // populate `draft` state if `givenDraftId` is present useEffect(() => { if (givenDraftId === undefined) return; - console.log("getting the draft", givenDraftId); const getDraft = async (): Promise => { const draft: GalleryDraft = await database.getByID(givenDraftId); @@ -57,22 +57,21 @@ export const useGalleryDrafts = (givenDraftId?: number) => { void getDraft(); }, []); - // persist debounced title + // handle debounced title useEffect(() => { if (isFirstRender) return; - const updatedDraft = { ...draft, title: debouncedValue }; - - setDraft(updatedDraft); - void saveDraft(updatedDraft); + setDraft({ ...draft, title: debouncedValue }); + setSave(true); }, [debouncedValue]); - console.log("rendered", draft); + useEffect(() => { + if (!save || isSaving) return; - const saveDraft = async (draft: GalleryDraft): Promise => { - console.log("saving draft"); - if (isSaving) return; + void saveDraft(); + }, [save]); + const saveDraft = async (): Promise => { setIsSaving(true); if (draft.id === null) { @@ -85,28 +84,25 @@ export const useGalleryDrafts = (givenDraftId?: number) => { await database.update(draft); } + setSave(false); setIsSaving(false); }; - const setDraftCover = async (image: ArrayBuffer | null): Promise => { - const updatedDraft = { ...draft, cover: image }; - setDraft(updatedDraft); - await saveDraft(updatedDraft); + const setDraftCover = (image: ArrayBuffer | null): void => { + setDraft({ ...draft, cover: image }); + setSave(true); }; - const setDraftNfts = async (nfts: App.Data.Gallery.GalleryNftData[]): Promise => { - const updatedDraft = { + const setDraftNfts = (nfts: App.Data.Gallery.GalleryNftData[]): void => { + setDraft({ ...draft, nfts: nfts.map((nft) => ({ nftId: nft.id, image: nft.images.large ?? "", collectionSlug: nft.collectionSlug, })), - }; - - setDraft(updatedDraft); - - await saveDraft(updatedDraft); + }); + setSave(true); }; return { From 3bcd7fc1dd78241e51f29392c9e49cf457996385 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Tue, 31 Oct 2023 15:08:19 +0400 Subject: [PATCH 08/18] wip --- .../Pages/Galleries/hooks/useGalleryDrafts.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts index 4ba4c3013..3daf1a170 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -25,7 +25,17 @@ const initialGalleryDraft: GalleryDraft = { id: null, }; -export const useGalleryDrafts = (givenDraftId?: number) => { +const MAX_DRAFT_LIMIT_PER_WALLET = 6; + +interface GalleryDraftsState { + reachedLimit: boolean; + draft: GalleryDraft; + setDraftCover: (image: ArrayBuffer | null) => void; + setDraftNfts: (nfts: App.Data.Gallery.GalleryNftData[]) => void; + setDraftTitle: (title: string) => void; +} + +export const useGalleryDrafts = (givenDraftId?: number): GalleryDraftsState => { const { wallet } = useAuth(); const database = useIndexedDB("gallery-drafts"); @@ -43,6 +53,8 @@ export const useGalleryDrafts = (givenDraftId?: number) => { const isFirstRender = useIsFirstRender(); + const [reachedLimit, setReachedLimit] = useState(false); + // populate `draft` state if `givenDraftId` is present useEffect(() => { if (givenDraftId === undefined) return; @@ -66,7 +78,7 @@ export const useGalleryDrafts = (givenDraftId?: number) => { }, [debouncedValue]); useEffect(() => { - if (!save || isSaving) return; + if (!save || isSaving || reachedLimit) return; void saveDraft(); }, [save]); @@ -75,6 +87,13 @@ export const useGalleryDrafts = (givenDraftId?: number) => { setIsSaving(true); if (draft.id === null) { + const walletDrafts = await getWalletDrafts(); + + if (walletDrafts.length >= MAX_DRAFT_LIMIT_PER_WALLET) { + setReachedLimit(true); + return; + } + const draftToCreate: Partial = { ...draft }; delete draftToCreate.id; @@ -88,6 +107,12 @@ export const useGalleryDrafts = (givenDraftId?: number) => { setIsSaving(false); }; + const getWalletDrafts = async (): Promise => { + const allDrafts: GalleryDraft[] = await database.getAll(); + + return allDrafts.filter((draft) => draft.walletAddress === wallet?.address); + }; + const setDraftCover = (image: ArrayBuffer | null): void => { setDraft({ ...draft, cover: image }); setSave(true); @@ -106,6 +131,7 @@ export const useGalleryDrafts = (givenDraftId?: number) => { }; return { + reachedLimit, draft, setDraftCover, setDraftNfts, From 4a49115d478ff8279ec5c9a55c2437a0faa599ea Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Tue, 31 Oct 2023 15:10:44 +0400 Subject: [PATCH 09/18] wip --- .../js/Pages/Galleries/MyGalleries/Create.tsx | 12 +++------- .../Pages/Galleries/hooks/useGalleryForm.ts | 23 +------------------ 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/resources/js/Pages/Galleries/MyGalleries/Create.tsx b/resources/js/Pages/Galleries/MyGalleries/Create.tsx index aef36be03..2d12ef6a6 100644 --- a/resources/js/Pages/Galleries/MyGalleries/Create.tsx +++ b/resources/js/Pages/Galleries/MyGalleries/Create.tsx @@ -57,10 +57,9 @@ const Create = ({ const [showDeleteModal, setShowDeleteModal] = useState(false); const [busy, setBusy] = useState(false); - const { selectedNfts, data, setData, errors, submit, updateSelectedNfts, processing, setDraftCover } = - useGalleryForm({ - gallery, - }); + const { selectedNfts, data, setData, errors, submit, updateSelectedNfts, processing } = useGalleryForm({ + gallery, + }); const totalValue = 0; @@ -199,13 +198,8 @@ const Create = ({ setGalleryCoverImageUrl(imageDataURI); if (blob === undefined) { setData("coverImage", null); - void setDraftCover(null); } else { setData("coverImage", new File([blob], blob.name, { type: blob.type })); - // eslint ignore - void blob.arrayBuffer().then((buf) => { - void setDraftCover(buf); - }); } setIsGalleryFormSliderOpen(false); }} diff --git a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts index bd5763ca5..402647db9 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts @@ -1,11 +1,8 @@ import { useForm } from "@inertiajs/react"; -import { type FormEvent, useEffect, useState } from "react"; +import { type FormEvent, useState } from "react"; import { useTranslation } from "react-i18next"; import { useToasts } from "@/Hooks/useToasts"; -import { useGalleryDrafts } from "@/Pages/Galleries/hooks/useGalleryDrafts"; -import { getQueryParameters } from "@/Utils/get-query-parameters"; import { isTruthy } from "@/Utils/is-truthy"; -import { replaceUrlQuery } from "@/Utils/replace-url-query"; interface UseGalleryFormProperties extends Record { id: number | null; @@ -27,22 +24,11 @@ export const useGalleryForm = ({ errors: Partial>; updateSelectedNfts: (nfts: App.Data.Gallery.GalleryNftData[]) => void; processing: boolean; - setDraftCover: (image: ArrayBuffer | null) => Promise; } => { const { t } = useTranslation(); const [selectedNfts, setSelectedNfts] = useState([]); const { showToast } = useToasts(); - const { draftId: givenDraftId } = getQueryParameters(); - - const { setDraftCover, setDraftTitle, setDraftNfts, draft } = useGalleryDrafts( - isTruthy(givenDraftId) ? Number(givenDraftId) : undefined, - ); - - useEffect(() => { - draft.id != null && replaceUrlQuery({ draftId: draft.id.toString() }); - }, [draft.id]); - const { data, setData, post, processing, errors, ...form } = useForm({ id: gallery?.id ?? null, name: gallery?.name ?? "", @@ -113,8 +99,6 @@ export const useGalleryForm = ({ "nfts", nfts.map((nft) => nft.id), ); - - void setDraftNfts(nfts); }; return { @@ -124,17 +108,12 @@ export const useGalleryForm = ({ submit, errors, processing, - setDraftCover, setData: (field, value) => { setData(field, value); if (field === "name" && validateName(field)) { form.setError("name", ""); } - - if (field === "name") { - setDraftTitle(typeof value === "string" ? value : ""); - } }, }; }; From b0aef6f6ee1df39eeca9aa80870047da483abd7f Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Tue, 31 Oct 2023 15:43:05 +0400 Subject: [PATCH 10/18] wip --- resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts | 8 +++++--- resources/js/databaseConfig.ts | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts index 3daf1a170..b7b17d099 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -13,6 +13,7 @@ interface DraftNft { interface GalleryDraft { title: string; cover: ArrayBuffer | null; + coverType: string | null; nfts: DraftNft[]; walletAddress?: string; id: number | null; @@ -21,6 +22,7 @@ interface GalleryDraft { const initialGalleryDraft: GalleryDraft = { title: "", cover: null, + coverType: null, nfts: [], id: null, }; @@ -30,7 +32,7 @@ const MAX_DRAFT_LIMIT_PER_WALLET = 6; interface GalleryDraftsState { reachedLimit: boolean; draft: GalleryDraft; - setDraftCover: (image: ArrayBuffer | null) => void; + setDraftCover: (image: ArrayBuffer | null, type: string | null) => void; setDraftNfts: (nfts: App.Data.Gallery.GalleryNftData[]) => void; setDraftTitle: (title: string) => void; } @@ -113,8 +115,8 @@ export const useGalleryDrafts = (givenDraftId?: number): GalleryDraftsState => { return allDrafts.filter((draft) => draft.walletAddress === wallet?.address); }; - const setDraftCover = (image: ArrayBuffer | null): void => { - setDraft({ ...draft, cover: image }); + const setDraftCover = (image: ArrayBuffer | null, type: string | null): void => { + setDraft({ ...draft, cover: image, coverType: type }); setSave(true); }; diff --git a/resources/js/databaseConfig.ts b/resources/js/databaseConfig.ts index 2a69fbfbd..cd14e6ea3 100644 --- a/resources/js/databaseConfig.ts +++ b/resources/js/databaseConfig.ts @@ -9,6 +9,7 @@ export const databaseConfig = { { name: "walletAddress", keypath: "walletAddress", options: { unique: false } }, { name: "title", keypath: "title", options: { unique: false } }, { name: "cover", keypath: "cover", options: { unique: false } }, + { name: "coverType", keypath: "coverType", options: { unique: false } }, { name: "nfts", keypath: "nfts", options: { unique: false } }, ], }, From 69e3f557c0a366c95afe449a1991b05a7044809d Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Wed, 1 Nov 2023 20:41:28 +0400 Subject: [PATCH 11/18] wip --- .../Galleries/hooks/useGalleryDrafts.test.ts | 175 ++++++++++++++++++ .../Pages/Galleries/hooks/useGalleryDrafts.ts | 44 ++--- 2 files changed, 191 insertions(+), 28 deletions(-) create mode 100644 resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts new file mode 100644 index 000000000..7a3e3ecba --- /dev/null +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts @@ -0,0 +1,175 @@ +import { expect, type SpyInstance } from "vitest"; +import { useGalleryDrafts } from "./useGalleryDrafts"; +import * as AuthContextMock from "@/Contexts/AuthContext"; + +import GalleryNftDataFactory from "@/Tests/Factories/Gallery/GalleryNftDataFactory"; +import { act, renderHook, waitFor } from "@/Tests/testing-library"; + +let useAuthSpy: SpyInstance; +vi.mock("@/Contexts/AuthContext", () => ({ + useAuth: () => ({ wallet: { address: "mockedWalletAddress" } }), +})); + +const defaultGalleryDraft = { + id: 1, + walletAddress: "mockedAddress", + nfts: [], + title: "", + cover: null, + coverTye: null, +}; + +const indexedDBMocks = { + add: vi.fn(), + getAll: vi.fn().mockResolvedValue([]), + update: vi.fn(), + deleteRecord: vi.fn(), + openCursor: vi.fn(), + getByIndex: vi.fn(), + clear: vi.fn(), + getByID: vi.fn().mockResolvedValue(defaultGalleryDraft), +}; + +const mocks = vi.hoisted(() => ({ + useIndexedDB: () => indexedDBMocks, +})); + +vi.mock("react-indexed-db-hook", () => ({ + useIndexedDB: mocks.useIndexedDB, +})); + +describe("useGalleryDrafts custom hook", () => { + beforeAll(() => { + useAuthSpy = vi.spyOn(AuthContextMock, "useAuth").mockReturnValue({ + user: null, + wallet: { + address: "mockedAddress", + domain: null, + totalUsd: 1, + totalBalanceInCurrency: "1", + totalTokens: 1, + collectionCount: 1, + galleryCount: 1, + timestamps: { + tokens_fetched_at: null, + native_balances_fetched_at: null, + }, + isRefreshingCollections: false, + canRefreshCollections: false, + avatar: { + small: null, + default: null, + small2x: null, + }, + }, + authenticated: false, + signed: false, + logout: vi.fn(), + setAuthData: vi.fn(), + }); + }); + + afterAll(() => { + useAuthSpy.mockRestore(); + }); + + it("should populate draft when givenDraftId is provided", async () => { + const givenDraftId = 1; + + const { result } = renderHook(() => useGalleryDrafts(givenDraftId)); + + await waitFor(() => { + expect(result.current.draft.id).toBe(givenDraftId); + }); + }); + + it("should keep draft in initial state when givenDraftId is irrelevant", async () => { + mocks.useIndexedDB().getByID.mockResolvedValue({ + ...defaultGalleryDraft, + walletAddress: "unrelatedAddress", + }); + + const givenDraftId = 1; + + const { result } = renderHook(() => useGalleryDrafts(givenDraftId)); + + await waitFor(() => { + expect(result.current.draft.id).toBe(null); + }); + }); + + it("should try to create a new row if draft hasn't been created yet", async () => { + mocks.useIndexedDB().add.mockResolvedValue(2); + + const { result } = renderHook(() => useGalleryDrafts()); + + act(() => { + result.current.setDraftTitle("hello"); + }); + + await waitFor(() => { + expect(result.current.draft.id).toBe(2); + }); + }); + + it("should try to update the row if draft is present", async () => { + const givenDraftId = 2; + const updateMock = vi.fn(); + + mocks.useIndexedDB().getByID.mockResolvedValue({ ...defaultGalleryDraft, id: givenDraftId }); + mocks.useIndexedDB().update.mockImplementation(updateMock); + + const { result } = renderHook(() => useGalleryDrafts(givenDraftId)); + + await waitFor(() => { + expect(result.current.draft.id).toBe(givenDraftId); + }); + + act(() => { + result.current.setDraftTitle("hello"); + }); + + await waitFor(() => { + expect(updateMock).toHaveBeenCalledOnce(); + }); + }); + + it("should update the title", async () => { + const { result } = renderHook(() => useGalleryDrafts(1)); + + act(() => { + result.current.setDraftTitle("hello"); + }); + + await waitFor(() => { + expect(result.current.draft.title).toBe("hello"); + }); + }); + + it("should update the nfts", async () => { + const { result } = renderHook(() => useGalleryDrafts(1)); + + const nft = new GalleryNftDataFactory().create(); + + act(() => { + result.current.setDraftNfts([nft]); + }); + + await waitFor(() => { + expect(result.current.draft.nfts.length).toBe(1); + }); + }); + + it("should update the cover", async () => { + const { result } = renderHook(() => useGalleryDrafts(1)); + + act(() => { + result.current.setDraftCover(new ArrayBuffer(8), "png"); + }); + + await waitFor(() => { + expect(result.current.draft.cover).not.toBeNull(); + expect(result.current.draft.coverType).toBe("png"); + }); + }); +}); diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts index b7b17d099..01c06de05 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -1,9 +1,8 @@ import { useEffect, useState } from "react"; import { useIndexedDB } from "react-indexed-db-hook"; import { useAuth } from "@/Contexts/AuthContext"; -import { useDebounce } from "@/Hooks/useDebounce"; -import { useIsFirstRender } from "@/Hooks/useIsFirstRender"; +const MAX_DRAFT_LIMIT_PER_WALLET = 6; interface DraftNft { nftId: number; image: string; @@ -19,16 +18,6 @@ interface GalleryDraft { id: number | null; } -const initialGalleryDraft: GalleryDraft = { - title: "", - cover: null, - coverType: null, - nfts: [], - id: null, -}; - -const MAX_DRAFT_LIMIT_PER_WALLET = 6; - interface GalleryDraftsState { reachedLimit: boolean; draft: GalleryDraft; @@ -37,6 +26,14 @@ interface GalleryDraftsState { setDraftTitle: (title: string) => void; } +const initialGalleryDraft: GalleryDraft = { + title: "", + cover: null, + coverType: null, + nfts: [], + id: null, +}; + export const useGalleryDrafts = (givenDraftId?: number): GalleryDraftsState => { const { wallet } = useAuth(); @@ -50,11 +47,6 @@ export const useGalleryDrafts = (givenDraftId?: number): GalleryDraftsState => { const [save, setSave] = useState(false); const [isSaving, setIsSaving] = useState(false); - const [title, setTitle] = useState(""); - const [debouncedValue] = useDebounce(title, 400); - - const isFirstRender = useIsFirstRender(); - const [reachedLimit, setReachedLimit] = useState(false); // populate `draft` state if `givenDraftId` is present @@ -62,22 +54,13 @@ export const useGalleryDrafts = (givenDraftId?: number): GalleryDraftsState => { if (givenDraftId === undefined) return; const getDraft = async (): Promise => { const draft: GalleryDraft = await database.getByID(givenDraftId); - if (draft.walletAddress === wallet?.address) { setDraft(draft); } }; void getDraft(); - }, []); - - // handle debounced title - useEffect(() => { - if (isFirstRender) return; - - setDraft({ ...draft, title: debouncedValue }); - setSave(true); - }, [debouncedValue]); + }, [givenDraftId, wallet?.address]); useEffect(() => { if (!save || isSaving || reachedLimit) return; @@ -132,11 +115,16 @@ export const useGalleryDrafts = (givenDraftId?: number): GalleryDraftsState => { setSave(true); }; + const setDraftTitle = (title: string): void => { + setDraft({ ...draft, title }); + setSave(true); + }; + return { reachedLimit, draft, setDraftCover, setDraftNfts, - setDraftTitle: setTitle, + setDraftTitle, }; }; From 228ea5961c0cd99fbfa43d1c0491bb0257a45271 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Wed, 1 Nov 2023 20:51:09 +0400 Subject: [PATCH 12/18] wip --- .../Galleries/hooks/useGalleryDrafts.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts index 7a3e3ecba..8b0aab1f5 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts @@ -172,4 +172,22 @@ describe("useGalleryDrafts custom hook", () => { expect(result.current.draft.coverType).toBe("png"); }); }); + + it("should not add new draft if reached to the limit", async () => { + const addMock = vi.fn(); + + mocks.useIndexedDB().getAll.mockReturnValue(Array.from({ length: 6 }).fill({ walletAddress: "mockedAddress" })); + mocks.useIndexedDB().add.mockImplementation(addMock); + + const { result } = renderHook(() => useGalleryDrafts()); + + act(() => { + result.current.setDraftTitle("hello"); + }); + + await waitFor(() => { + expect(addMock).not.toHaveBeenCalled(); + expect(result.current.reachedLimit).toBe(true); + }); + }); }); From 79f5d9b90bf08df7247f06be33cdad3d82fad768 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Wed, 1 Nov 2023 20:51:31 +0400 Subject: [PATCH 13/18] wip --- .../js/{Pages/Galleries/hooks => Hooks}/useGalleryDrafts.test.ts | 0 resources/js/{Pages/Galleries/hooks => Hooks}/useGalleryDrafts.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename resources/js/{Pages/Galleries/hooks => Hooks}/useGalleryDrafts.test.ts (100%) rename resources/js/{Pages/Galleries/hooks => Hooks}/useGalleryDrafts.ts (100%) diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts b/resources/js/Hooks/useGalleryDrafts.test.ts similarity index 100% rename from resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts rename to resources/js/Hooks/useGalleryDrafts.test.ts diff --git a/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts b/resources/js/Hooks/useGalleryDrafts.ts similarity index 100% rename from resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts rename to resources/js/Hooks/useGalleryDrafts.ts From 19b5d549532ba88c28caddb88f154e95cfafbe61 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Wed, 1 Nov 2023 20:53:29 +0400 Subject: [PATCH 14/18] wip --- resources/js/Hooks/useGalleryDrafts.test.ts | 24 ++++++++++----------- resources/js/Hooks/useGalleryDrafts.ts | 1 + 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/resources/js/Hooks/useGalleryDrafts.test.ts b/resources/js/Hooks/useGalleryDrafts.test.ts index 8b0aab1f5..a72e33593 100644 --- a/resources/js/Hooks/useGalleryDrafts.test.ts +++ b/resources/js/Hooks/useGalleryDrafts.test.ts @@ -1,11 +1,11 @@ import { expect, type SpyInstance } from "vitest"; import { useGalleryDrafts } from "./useGalleryDrafts"; import * as AuthContextMock from "@/Contexts/AuthContext"; - import GalleryNftDataFactory from "@/Tests/Factories/Gallery/GalleryNftDataFactory"; import { act, renderHook, waitFor } from "@/Tests/testing-library"; let useAuthSpy: SpyInstance; + vi.mock("@/Contexts/AuthContext", () => ({ useAuth: () => ({ wallet: { address: "mockedWalletAddress" } }), })); @@ -19,19 +19,17 @@ const defaultGalleryDraft = { coverTye: null, }; -const indexedDBMocks = { - add: vi.fn(), - getAll: vi.fn().mockResolvedValue([]), - update: vi.fn(), - deleteRecord: vi.fn(), - openCursor: vi.fn(), - getByIndex: vi.fn(), - clear: vi.fn(), - getByID: vi.fn().mockResolvedValue(defaultGalleryDraft), -}; - const mocks = vi.hoisted(() => ({ - useIndexedDB: () => indexedDBMocks, + useIndexedDB: () => ({ + add: vi.fn(), + getAll: vi.fn().mockResolvedValue([]), + update: vi.fn(), + deleteRecord: vi.fn(), + openCursor: vi.fn(), + getByIndex: vi.fn(), + clear: vi.fn(), + getByID: vi.fn().mockResolvedValue(defaultGalleryDraft), + }), })); vi.mock("react-indexed-db-hook", () => ({ diff --git a/resources/js/Hooks/useGalleryDrafts.ts b/resources/js/Hooks/useGalleryDrafts.ts index 01c06de05..179b5cff4 100644 --- a/resources/js/Hooks/useGalleryDrafts.ts +++ b/resources/js/Hooks/useGalleryDrafts.ts @@ -3,6 +3,7 @@ import { useIndexedDB } from "react-indexed-db-hook"; import { useAuth } from "@/Contexts/AuthContext"; const MAX_DRAFT_LIMIT_PER_WALLET = 6; + interface DraftNft { nftId: number; image: string; From 0c373ce352b9ce3dd9c1907a2e6029450c2787a5 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Wed, 1 Nov 2023 21:48:22 +0400 Subject: [PATCH 15/18] wip --- .../Galleries/hooks}/useGalleryDrafts.test.ts | 64 +++++++++++++++---- .../Galleries/hooks}/useGalleryDrafts.ts | 13 +++- 2 files changed, 64 insertions(+), 13 deletions(-) rename resources/js/{Hooks => Pages/Galleries/hooks}/useGalleryDrafts.test.ts (77%) rename resources/js/{Hooks => Pages/Galleries/hooks}/useGalleryDrafts.ts (89%) diff --git a/resources/js/Hooks/useGalleryDrafts.test.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts similarity index 77% rename from resources/js/Hooks/useGalleryDrafts.test.ts rename to resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts index a72e33593..a1b63c8bc 100644 --- a/resources/js/Hooks/useGalleryDrafts.test.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.test.ts @@ -19,24 +19,26 @@ const defaultGalleryDraft = { coverTye: null, }; +const indexedDBMocks = { + add: vi.fn(), + getAll: vi.fn().mockResolvedValue([]), + update: vi.fn(), + deleteRecord: vi.fn(), + openCursor: vi.fn(), + getByIndex: vi.fn(), + clear: vi.fn(), + getByID: vi.fn().mockResolvedValue(defaultGalleryDraft), +}; + const mocks = vi.hoisted(() => ({ - useIndexedDB: () => ({ - add: vi.fn(), - getAll: vi.fn().mockResolvedValue([]), - update: vi.fn(), - deleteRecord: vi.fn(), - openCursor: vi.fn(), - getByIndex: vi.fn(), - clear: vi.fn(), - getByID: vi.fn().mockResolvedValue(defaultGalleryDraft), - }), + useIndexedDB: () => indexedDBMocks, })); vi.mock("react-indexed-db-hook", () => ({ useIndexedDB: mocks.useIndexedDB, })); -describe("useGalleryDrafts custom hook", () => { +describe("useGalleryDrafts", () => { beforeAll(() => { useAuthSpy = vi.spyOn(AuthContextMock, "useAuth").mockReturnValue({ user: null, @@ -188,4 +190,44 @@ describe("useGalleryDrafts custom hook", () => { expect(result.current.reachedLimit).toBe(true); }); }); + + it("should delete the draft if id is present", async () => { + const givenDraftId = 2; + + const deleteMock = vi.fn(); + + mocks.useIndexedDB().getByID.mockResolvedValue({ ...defaultGalleryDraft, id: givenDraftId }); + mocks.useIndexedDB().deleteRecord.mockImplementation(deleteMock); + + const { result } = renderHook(() => useGalleryDrafts(givenDraftId)); + + await waitFor(() => { + expect(result.current.draft.id).toBe(givenDraftId); + }); + + await act(async () => { + await result.current.deleteDraft(); + }); + + await waitFor(() => { + expect(deleteMock).toHaveBeenCalled(); + expect(result.current.reachedLimit).toBe(false); + }); + }); + + it("should not delete the draft if id is not present", async () => { + const deleteMock = vi.fn(); + + mocks.useIndexedDB().deleteRecord.mockImplementation(deleteMock); + + const { result } = renderHook(() => useGalleryDrafts()); + + await act(async () => { + await result.current.deleteDraft(); + }); + + await waitFor(() => { + expect(deleteMock).not.toHaveBeenCalled(); + }); + }); }); diff --git a/resources/js/Hooks/useGalleryDrafts.ts b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts similarity index 89% rename from resources/js/Hooks/useGalleryDrafts.ts rename to resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts index 179b5cff4..cb4fefcdf 100644 --- a/resources/js/Hooks/useGalleryDrafts.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryDrafts.ts @@ -25,6 +25,7 @@ interface GalleryDraftsState { setDraftCover: (image: ArrayBuffer | null, type: string | null) => void; setDraftNfts: (nfts: App.Data.Gallery.GalleryNftData[]) => void; setDraftTitle: (title: string) => void; + deleteDraft: () => Promise; } const initialGalleryDraft: GalleryDraft = { @@ -54,8 +55,8 @@ export const useGalleryDrafts = (givenDraftId?: number): GalleryDraftsState => { useEffect(() => { if (givenDraftId === undefined) return; const getDraft = async (): Promise => { - const draft: GalleryDraft = await database.getByID(givenDraftId); - if (draft.walletAddress === wallet?.address) { + const draft: GalleryDraft | undefined = await database.getByID(givenDraftId); + if (draft !== undefined && draft.walletAddress === wallet?.address) { setDraft(draft); } }; @@ -121,11 +122,19 @@ export const useGalleryDrafts = (givenDraftId?: number): GalleryDraftsState => { setSave(true); }; + const deleteDraft = async (): Promise => { + if (draft.id === null) return; + await database.deleteRecord(draft.id); + + setReachedLimit(false); + }; + return { reachedLimit, draft, setDraftCover, setDraftNfts, setDraftTitle, + deleteDraft, }; }; From d013354442b3dddd1b0c3937e8cf5f7beb41aa12 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Thu, 2 Nov 2023 15:51:20 +0400 Subject: [PATCH 16/18] fix tests --- resources/js/Components/Sidebar/SidebarItem.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/js/Components/Sidebar/SidebarItem.test.tsx b/resources/js/Components/Sidebar/SidebarItem.test.tsx index 73a962fbe..fbc9dc36c 100644 --- a/resources/js/Components/Sidebar/SidebarItem.test.tsx +++ b/resources/js/Components/Sidebar/SidebarItem.test.tsx @@ -8,6 +8,7 @@ describe("SidebarItem", () => { , ); @@ -20,6 +21,7 @@ describe("SidebarItem", () => { icon="Cog" title="General" rightText="1234" + href="/hello" />, ); From cc12a80d037ec28707bc6fa0b791567ac3e66816 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Thu, 2 Nov 2023 16:00:57 +0400 Subject: [PATCH 17/18] fix tests --- resources/js/Components/Sidebar/SidebarItem.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/resources/js/Components/Sidebar/SidebarItem.test.tsx b/resources/js/Components/Sidebar/SidebarItem.test.tsx index fbc9dc36c..5d4db1a54 100644 --- a/resources/js/Components/Sidebar/SidebarItem.test.tsx +++ b/resources/js/Components/Sidebar/SidebarItem.test.tsx @@ -13,6 +13,8 @@ describe("SidebarItem", () => { ); expect(screen.getByTestId("SidebarItem")).toBeInTheDocument(); + + expect(screen.queryByText("1234")).not.toBeInTheDocument(); }); it("should render with rightText", () => { From 39491b1cb0a8e5fd8785f8df02c7dfa79656da90 Mon Sep 17 00:00:00 2001 From: Shahin Safaraliyev Date: Thu, 2 Nov 2023 16:53:05 +0400 Subject: [PATCH 18/18] fix tests --- .../js/Components/Sidebar/SidebarItem.test.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/resources/js/Components/Sidebar/SidebarItem.test.tsx b/resources/js/Components/Sidebar/SidebarItem.test.tsx index 5d4db1a54..029c94a9f 100644 --- a/resources/js/Components/Sidebar/SidebarItem.test.tsx +++ b/resources/js/Components/Sidebar/SidebarItem.test.tsx @@ -31,4 +31,18 @@ describe("SidebarItem", () => { expect(screen.getByText("1234")).toBeInTheDocument(); }); + + it("should render disabled with rightText", () => { + render( + , + ); + + expect(screen.getByTestId("SidebarItem__disabled")).toBeInTheDocument(); + + expect(screen.getByText("1234")).toBeInTheDocument(); + }); });