Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: count nft collections upon saving draft #464

Merged
merged 7 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ describe("NftGalleryDraftCard", () => {
coverType: null,
coverFileName: null,
walletAddress: "0x22Fd644149ea87ca26237183ad6A66f91dfcFB87",
collectionsCount: 1,
nfts: [],
value: "0",
collectionsCount: 0,
updatedAt: 123,
};

Expand Down
3 changes: 1 addition & 2 deletions resources/js/Components/Drafts/NftGalleryDraftCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ export const NftGalleryDraftCard = ({
return (
<Link
href={route("my-galleries.create", {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
draftId: draft.id!,
draftId: draft.id,
editDraft: 1,
})}
className="group focus-visible:outline-none focus-visible:ring-0"
Expand Down
4 changes: 2 additions & 2 deletions resources/js/Pages/Galleries/MyGalleries/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { usePrevious } from "@/Hooks/usePrevious";
import { useToasts } from "@/Hooks/useToasts";
import { GalleryNameInput } from "@/Pages/Galleries/Components/GalleryNameInput";
import { useGalleryForm } from "@/Pages/Galleries/hooks/useGalleryForm";
import { type GalleryDraft, useWalletDraftGalleries } from "@/Pages/Galleries/hooks/useWalletDraftGalleries";
import { type GalleryDraftUnsaved, useWalletDraftGalleries } from "@/Pages/Galleries/hooks/useWalletDraftGalleries";
import { useWalletDraftGallery } from "@/Pages/Galleries/hooks/useWalletDraftGallery";
import { arrayBufferToFile } from "@/Utils/array-buffer-to-file";
import { assertUser, assertWallet } from "@/Utils/assertions";
Expand Down Expand Up @@ -105,7 +105,7 @@ const Create = ({
return;
}

const redirectToNewDraft = async (existingDraft: GalleryDraft): Promise<void> => {
const redirectToNewDraft = async (existingDraft: GalleryDraftUnsaved): Promise<void> => {
try {
const newDraft = await add({ ...existingDraft, walletAddress: auth.wallet?.address, nfts: [] });
reset(newDraft);
Expand Down
4 changes: 2 additions & 2 deletions resources/js/Pages/Galleries/hooks/useGalleryForm.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
}): {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -12,21 +12,21 @@ 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,
coverFileName: null,
nfts: [],
walletAddress: "mockedAddress",
value: "test",
collectionsCount: 1,
updatedAt: 169901639000,
collectionsCount: 1,
};

interface IndexedDBMockResponse {
Expand All @@ -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<number> => {
add: async (draft: GalleryDraftUnsaved): Promise<number> => {
const id = drafts.length + 1;
drafts.push({ ...draft, id });
return await Promise.resolve(id);
},
getAll: async (): Promise<GalleryDraft[]> => await Promise.resolve(drafts),
getAll: async (): Promise<GalleryDraft[]> => (await Promise.resolve(drafts)) as GalleryDraft[],
update: async (draft: GalleryDraft): Promise<GalleryDraft> => {
const index = drafts.findIndex((savedDraft) => savedDraft.id === draft.id);
drafts.splice(index, 1, draft);
Expand All @@ -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(),
Expand Down Expand Up @@ -98,7 +99,6 @@ describe("useWalletDraftGalleries", () => {
nfts: [],
walletAddress: "mockedAddress",
value: "test",
collectionsCount: 1,
updatedAt: new Date().getTime(),
});
});
Expand Down Expand Up @@ -131,7 +131,6 @@ describe("useWalletDraftGalleries", () => {
nfts: [],
walletAddress: "mockedAddress",
value: "test",
collectionsCount: 1,
updatedAt: new Date().getTime(),
});
});
Expand Down
64 changes: 38 additions & 26 deletions resources/js/Pages/Galleries/hooks/useWalletDraftGalleries.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -16,36 +17,55 @@ export interface DraftNft {
collectionSlug: string;
}

export interface GalleryDraft {
export interface GalleryDraftUnsaved {
title: string;
cover: ArrayBuffer | null;
coverType: string | null;
nfts: DraftNft[];
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<GallerySavedDraft>;
add: (draft: GalleryDraft) => Promise<GallerySavedDraft>;
upsert: (draft: GalleryDraftUnsaved) => Promise<GalleryDraft>;
add: (draft: GalleryDraftUnsaved) => Promise<GalleryDraft>;
remove: (id?: number | null) => Promise<void>;
removeExpired: () => Promise<void>;
drafts: GalleryDraft[];
findWalletDraftById: (id: number | string) => Promise<GallerySavedDraft | undefined>;
findWalletDraftById: (id: number | string) => Promise<GalleryDraft | undefined>;
isLoading: boolean;
isSaving: boolean;
hasReachedLimit: boolean;
allDrafts: () => Promise<GallerySavedDraft[]>;
allDrafts: () => Promise<GalleryDraft[]>;
}

/**
* 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,
Expand Down Expand Up @@ -84,8 +104,9 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal
* @param {GalleryDraft} draft
* @returns {Promise<GalleryDraft>}
*/
const add = async (draft: GalleryDraft): Promise<GallerySavedDraft> => {
const add = async (draft: GalleryDraftUnsaved): Promise<GalleryDraft> => {
const allDraftsCount = await allDrafts();

if (allDraftsCount.length >= MAX_DRAFT_LIMIT_PER_WALLET) {
throw new Error("[useWalletDraftGalleries:upsert] Reached limit");
}
Expand All @@ -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);
Expand All @@ -110,7 +132,7 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal
* @param {GalleryDraft} draft
* @returns {Promise<GalleryDraft>}
*/
const update = async (draft: GalleryDraft): Promise<GallerySavedDraft> => {
const update = async (draft: GalleryDraftUnsaved): Promise<GalleryDraft> => {
if (!isTruthy(draft.id)) {
throw new Error("[useWalletDraftGalleries:update] Missing Id");
}
Expand All @@ -120,6 +142,7 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal
await database.update({
...draft,
updatedAt: new Date().getTime(),
collectionsCount: calculateCollectionsCount(draft),
});

setIsSaving(false);
Expand All @@ -133,7 +156,7 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal
* @param {GalleryDraft} draft
* @returns {Promise<GalleryDraft>}
*/
const upsert = async (draft: GalleryDraft): Promise<GallerySavedDraft> => {
const upsert = async (draft: GalleryDraftUnsaved): Promise<GalleryDraft> => {
if (isTruthy(draft.id)) {
return await update(draft);
}
Expand Down Expand Up @@ -174,8 +197,8 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal
*
* @returns {Promise<GalleryDraft[]>}
*/
const allDrafts = async (): Promise<GallerySavedDraft[]> => {
const allDrafts: GallerySavedDraft[] = await database.getAll();
const allDrafts = async (): Promise<GalleryDraft[]> => {
const allDrafts: GalleryDraft[] = await database.getAll();
return allDrafts.filter(
(draft) => draft.walletAddress?.toLowerCase() === address.toLowerCase() && !isExpired(draft),
);
Expand All @@ -187,8 +210,8 @@ export const useWalletDraftGalleries = ({ address }: Properties): WalletDraftGal
* @param {number} id
* @returns {Promise<GalleryDraft>}
*/
const findWalletDraftById = async (id: number | string): Promise<GallerySavedDraft | undefined> => {
const draft: GallerySavedDraft | undefined = await database.getByID(Number(id));
const findWalletDraftById = async (id: number | string): Promise<GalleryDraft | undefined> => {
const draft: GalleryDraft | undefined = await database.getByID(Number(id));

if (draft?.walletAddress?.toLowerCase() !== address.toLowerCase()) {
return undefined;
Expand All @@ -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<GalleryDraft>}
*/
const findByIdOrThrow = async (id: number | string): Promise<GallerySavedDraft> => {
const findByIdOrThrow = async (id: number | string): Promise<GalleryDraft> => {
const draft = await findWalletDraftById(id);

if (!isTruthy(draft)) {
Expand Down
12 changes: 6 additions & 6 deletions resources/js/Pages/Galleries/hooks/useWalletDraftGallery.ts
Original file line number Diff line number Diff line change
@@ -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<GalleryDraft>) => void;
reset: (draft?: Partial<GalleryDraftUnsaved>) => void;
isLoading: boolean;
}

Expand Down Expand Up @@ -36,7 +36,7 @@ export const useWalletDraftGallery = ({
const [isLoading, setIsLoading] = useState(true);
const { upsert, findWalletDraftById, isSaving } = useWalletDraftGalleries({ address });

const [draft, setDraft] = useState<GalleryDraft>(defaultDraft);
const [draft, setDraft] = useState<GalleryDraftUnsaved>(defaultDraft);

useEffect(() => {
if (draftId === undefined || isDisabled === true) {
Expand All @@ -59,7 +59,7 @@ export const useWalletDraftGallery = ({
void getDraft();
}, [draftId, address]);

const saveDraft = async (draft: GalleryDraft): Promise<void> => {
const saveDraft = async (draft: GalleryDraftUnsaved): Promise<void> => {
if (isTruthy(isDisabled)) {
setIsLoading(false);
return;
Expand All @@ -82,7 +82,7 @@ export const useWalletDraftGallery = ({
void saveDraft({ ...draft, title });
};

const reset = (draft?: Partial<GalleryDraft>): void => {
const reset = (draft?: Partial<GalleryDraftUnsaved>): void => {
setDraft({ ...defaultDraft, ...draft });
};

Expand Down
Loading