diff --git a/resources/js/Components/Drafts/NftGalleryDraftCard.test.tsx b/resources/js/Components/Drafts/NftGalleryDraftCard.test.tsx index 713cc2bd7..9c5f7ca28 100644 --- a/resources/js/Components/Drafts/NftGalleryDraftCard.test.tsx +++ b/resources/js/Components/Drafts/NftGalleryDraftCard.test.tsx @@ -19,9 +19,9 @@ describe("NftGalleryDraftCard", () => { coverType: null, coverFileName: null, walletAddress: "0x22Fd644149ea87ca26237183ad6A66f91dfcFB87", + collectionsCount: 1, nfts: [], value: "0", - collectionsCount: 0, updatedAt: 123, }; diff --git a/resources/js/Components/Drafts/NftGalleryDraftCard.tsx b/resources/js/Components/Drafts/NftGalleryDraftCard.tsx index fc6e75a43..291946aa3 100644 --- a/resources/js/Components/Drafts/NftGalleryDraftCard.tsx +++ b/resources/js/Components/Drafts/NftGalleryDraftCard.tsx @@ -21,8 +21,7 @@ export const NftGalleryDraftCard = ({ return ( => { + const redirectToNewDraft = async (existingDraft: GalleryDraftUnsaved): Promise => { try { const newDraft = await add({ ...existingDraft, walletAddress: auth.wallet?.address, nfts: [] }); reset(newDraft); diff --git a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts index a3a812446..f9890229d 100644 --- a/resources/js/Pages/Galleries/hooks/useGalleryForm.ts +++ b/resources/js/Pages/Galleries/hooks/useGalleryForm.ts @@ -1,7 +1,7 @@ import { useForm } from "@inertiajs/react"; import { type FormEvent, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { type GalleryDraft } from "./useWalletDraftGalleries"; +import { type GalleryDraftUnsaved } from "./useWalletDraftGalleries"; import { useToasts } from "@/Hooks/useToasts"; import { arrayBufferToFile } from "@/Utils/array-buffer-to-file"; import { isTruthy } from "@/Utils/is-truthy"; @@ -20,7 +20,7 @@ export const useGalleryForm = ({ deleteDraft, }: { gallery?: App.Data.Gallery.GalleryData; - draft?: GalleryDraft; + draft?: GalleryDraftUnsaved; setDraftNfts?: (nfts: App.Data.Gallery.GalleryNftData[]) => void; deleteDraft?: () => void; }): { diff --git a/resources/js/Pages/Galleries/hooks/useWalletDraftGalleries.test.ts b/resources/js/Pages/Galleries/hooks/useWalletDraftGalleries.test.ts index fdf2b4b14..81c3a2c65 100644 --- a/resources/js/Pages/Galleries/hooks/useWalletDraftGalleries.test.ts +++ b/resources/js/Pages/Galleries/hooks/useWalletDraftGalleries.test.ts @@ -1,6 +1,6 @@ import { act } from "react-dom/test-utils"; import { expect } from "vitest"; -import { type GalleryDraft, useWalletDraftGalleries } from "./useWalletDraftGalleries"; +import { type GalleryDraft, type GalleryDraftUnsaved, useWalletDraftGalleries } from "./useWalletDraftGalleries"; import { renderHook, waitFor } from "@/Tests/testing-library"; const defaultGalleryDraft = { @@ -12,12 +12,12 @@ const defaultGalleryDraft = { nfts: [], walletAddress: "mockedAddress", value: "test", - collectionsCount: 1, updatedAt: new Date().getTime(), + collectionsCount: 1, }; const expiredGalleryDraft = { - id: null, + id: undefined, title: "", cover: null, coverType: null, @@ -25,8 +25,8 @@ const expiredGalleryDraft = { nfts: [], walletAddress: "mockedAddress", value: "test", - collectionsCount: 1, updatedAt: 169901639000, + collectionsCount: 1, }; interface IndexedDBMockResponse { @@ -41,15 +41,15 @@ interface IndexedDBMockResponse { } const useIndexedDBMock = (): IndexedDBMockResponse => { - const drafts: GalleryDraft[] = [defaultGalleryDraft, expiredGalleryDraft]; + const drafts: GalleryDraft | GalleryDraftUnsaved[] = [defaultGalleryDraft, expiredGalleryDraft]; return { - add: async (draft: GalleryDraft): Promise => { + add: async (draft: GalleryDraftUnsaved): Promise => { const id = drafts.length + 1; drafts.push({ ...draft, id }); return await Promise.resolve(id); }, - getAll: async (): Promise => await Promise.resolve(drafts), + getAll: async (): Promise => (await Promise.resolve(drafts)) as GalleryDraft[], update: async (draft: GalleryDraft): Promise => { const index = drafts.findIndex((savedDraft) => savedDraft.id === draft.id); drafts.splice(index, 1, draft); @@ -62,7 +62,8 @@ const useIndexedDBMock = (): IndexedDBMockResponse => { await Promise.resolve(); }, - getByID: async (id: number | null) => await Promise.resolve(drafts.find((draft) => draft.id === id)), + getByID: async (id: number | null) => + (await Promise.resolve(drafts.find((draft) => draft.id === id))) as GalleryDraft, openCursor: vi.fn(), getByIndex: vi.fn(), clear: vi.fn(), @@ -98,7 +99,6 @@ describe("useWalletDraftGalleries", () => { nfts: [], walletAddress: "mockedAddress", value: "test", - collectionsCount: 1, updatedAt: new Date().getTime(), }); }); @@ -131,7 +131,6 @@ describe("useWalletDraftGalleries", () => { nfts: [], walletAddress: "mockedAddress", value: "test", - collectionsCount: 1, updatedAt: new Date().getTime(), }); }); diff --git a/resources/js/Pages/Galleries/hooks/useWalletDraftGalleries.ts b/resources/js/Pages/Galleries/hooks/useWalletDraftGalleries.ts index 169f90046..bd10e9b2b 100644 --- a/resources/js/Pages/Galleries/hooks/useWalletDraftGalleries.ts +++ b/resources/js/Pages/Galleries/hooks/useWalletDraftGalleries.ts @@ -1,3 +1,4 @@ +import uniqBy from "lodash/uniqBy"; import { useCallback, useEffect, useState } from "react"; import { useIndexedDB } from "react-indexed-db-hook"; import { isTruthy } from "@/Utils/is-truthy"; @@ -16,7 +17,7 @@ export interface DraftNft { collectionSlug: string; } -export interface GalleryDraft { +export interface GalleryDraftUnsaved { title: string; cover: ArrayBuffer | null; coverType: string | null; @@ -24,28 +25,47 @@ export interface GalleryDraft { walletAddress?: string; id?: number | null; value: string | null; - collectionsCount: number; updatedAt: number | null; coverFileName: string | null; } -interface GallerySavedDraft extends GalleryDraft { +export interface GalleryDraft extends GalleryDraftUnsaved { id: number; + collectionsCount: number; } interface WalletDraftGalleriesState { - upsert: (draft: GalleryDraft) => Promise; - add: (draft: GalleryDraft) => Promise; + upsert: (draft: GalleryDraftUnsaved) => Promise; + add: (draft: GalleryDraftUnsaved) => Promise; remove: (id?: number | null) => Promise; removeExpired: () => Promise; drafts: GalleryDraft[]; - findWalletDraftById: (id: number | string) => Promise; + findWalletDraftById: (id: number | string) => Promise; isLoading: boolean; isSaving: boolean; hasReachedLimit: boolean; - allDrafts: () => Promise; + allDrafts: () => Promise; } +/** + * Calculate collections count based on saved nfts. + * + * @param {GalleryDraft} draft + * @returns {number} + */ +const calculateCollectionsCount = (draft: GalleryDraftUnsaved): number => uniqBy(draft.nfts, "collectionSlug").length; + +/** + * Determine if gallery is expired. + * + * @param {GalleryDraft} draft + * @returns {boolean} + */ +const isExpired = (draft: GalleryDraft): boolean => { + const thresholdDaysAgo = new Date().getTime() - DRAFT_TTL_DAYS * 86400 * 1000; + return (draft.updatedAt ?? 0) < thresholdDaysAgo; +}; + /** * Note: The react-indexed-db-hook package that is used under the hood in this hook * is not reactive. That means that if this hook is used in multiple components, @@ -84,8 +104,9 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal * @param {GalleryDraft} draft * @returns {Promise} */ - const add = async (draft: GalleryDraft): Promise => { + const add = async (draft: GalleryDraftUnsaved): Promise => { const allDraftsCount = await allDrafts(); + if (allDraftsCount.length >= MAX_DRAFT_LIMIT_PER_WALLET) { throw new Error("[useWalletDraftGalleries:upsert] Reached limit"); } @@ -97,6 +118,7 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal const id = await database.add({ ...draftToSave, updatedAt: new Date().getTime(), + collectionsCount: calculateCollectionsCount(draft), }); setIsSaving(false); @@ -110,7 +132,7 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal * @param {GalleryDraft} draft * @returns {Promise} */ - const update = async (draft: GalleryDraft): Promise => { + const update = async (draft: GalleryDraftUnsaved): Promise => { if (!isTruthy(draft.id)) { throw new Error("[useWalletDraftGalleries:update] Missing Id"); } @@ -120,6 +142,7 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal await database.update({ ...draft, updatedAt: new Date().getTime(), + collectionsCount: calculateCollectionsCount(draft), }); setIsSaving(false); @@ -133,7 +156,7 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal * @param {GalleryDraft} draft * @returns {Promise} */ - const upsert = async (draft: GalleryDraft): Promise => { + const upsert = async (draft: GalleryDraftUnsaved): Promise => { if (isTruthy(draft.id)) { return await update(draft); } @@ -174,8 +197,8 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal * * @returns {Promise} */ - const allDrafts = async (): Promise => { - const allDrafts: GallerySavedDraft[] = await database.getAll(); + const allDrafts = async (): Promise => { + const allDrafts: GalleryDraft[] = await database.getAll(); return allDrafts.filter( (draft) => draft.walletAddress?.toLowerCase() === address.toLowerCase() && !isExpired(draft), ); @@ -187,8 +210,8 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal * @param {number} id * @returns {Promise} */ - const findWalletDraftById = async (id: number | string): Promise => { - const draft: GallerySavedDraft | undefined = await database.getByID(Number(id)); + const findWalletDraftById = async (id: number | string): Promise => { + const draft: GalleryDraft | undefined = await database.getByID(Number(id)); if (draft?.walletAddress?.toLowerCase() !== address.toLowerCase()) { return undefined; @@ -197,24 +220,13 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal return draft; }; - /** - * Determine if gallery is expired. - * - * @param {GalleryDraft} draft - * @returns {boolean} - */ - const isExpired = (draft: GalleryDraft): boolean => { - const thresholdDaysAgo = new Date().getTime() - DRAFT_TTL_DAYS * 86400 * 1000; - return (draft.updatedAt ?? 0) < thresholdDaysAgo; - }; - /** * Find draft or throw. Used internally for add/remove. * * @param {number | string} id * @returns {Promise} */ - const findByIdOrThrow = async (id: number | string): Promise => { + const findByIdOrThrow = async (id: number | string): Promise => { const draft = await findWalletDraftById(id); if (!isTruthy(draft)) { diff --git a/resources/js/Pages/Galleries/hooks/useWalletDraftGallery.ts b/resources/js/Pages/Galleries/hooks/useWalletDraftGallery.ts index d6ff0fc04..1c497895f 100644 --- a/resources/js/Pages/Galleries/hooks/useWalletDraftGallery.ts +++ b/resources/js/Pages/Galleries/hooks/useWalletDraftGallery.ts @@ -1,14 +1,14 @@ import { useEffect, useState } from "react"; -import { type GalleryDraft, useWalletDraftGalleries } from "./useWalletDraftGalleries"; +import { type GalleryDraftUnsaved, useWalletDraftGalleries } from "./useWalletDraftGalleries"; import { isTruthy } from "@/Utils/is-truthy"; interface WalletDraftGalleryState { isSaving: boolean; - draft: GalleryDraft; + draft: GalleryDraftUnsaved; setCover: (image: ArrayBuffer | null, name: string | null, type: string | null) => void; setNfts: (nfts: App.Data.Gallery.GalleryNftData[]) => void; setTitle: (title: string) => void; - reset: (draft?: Partial) => void; + reset: (draft?: Partial) => void; isLoading: boolean; } @@ -36,7 +36,7 @@ export const useWalletDraftGallery = ({ const [isLoading, setIsLoading] = useState(true); const { upsert, findWalletDraftById, isSaving } = useWalletDraftGalleries({ address }); - const [draft, setDraft] = useState(defaultDraft); + const [draft, setDraft] = useState(defaultDraft); useEffect(() => { if (draftId === undefined || isDisabled === true) { @@ -59,7 +59,7 @@ export const useWalletDraftGallery = ({ void getDraft(); }, [draftId, address]); - const saveDraft = async (draft: GalleryDraft): Promise => { + const saveDraft = async (draft: GalleryDraftUnsaved): Promise => { if (isTruthy(isDisabled)) { setIsLoading(false); return; @@ -82,7 +82,7 @@ export const useWalletDraftGallery = ({ void saveDraft({ ...draft, title }); }; - const reset = (draft?: Partial): void => { + const reset = (draft?: Partial): void => { setDraft({ ...defaultDraft, ...draft }); };