Skip to content

Commit

Permalink
show pending state and improve increment progress logic
Browse files Browse the repository at this point in the history
Signed-off-by: Vu Van Dung <[email protected]>
  • Loading branch information
joulev committed Jun 3, 2024
1 parent 189640f commit 345db72
Show file tree
Hide file tree
Showing 12 changed files with 78 additions and 59 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"download-theme": "./scripts/download-theme.sh",
"format": "biome format --write .",
"lint": "biome lint .",
"start": "next start"
"start": "next start",
"type-check": "tsc --pretty --noEmit"
},
"dependencies": {
"@auth/core": "0.32.0",
Expand Down
4 changes: 2 additions & 2 deletions src/app/admin/manage/anime/[[...status]]/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ function BottomPart({ item, status }: { item: AnimeListItem; status: AnimeListIt
const increment = () =>
startTransition(async () => {
optimisticListsAct(["UPDATE_PROGRESS", { status, id: item.id }]);
await incrementProgress(item);
await incrementProgress(item, (item.progress ?? 0) + 1);
});
const cancelRewatch = () =>
startTransition(async () => {
Expand Down Expand Up @@ -259,7 +259,7 @@ function BottomPart({ item, status }: { item: AnimeListItem; status: AnimeListIt
export function Card({ item, status }: { item: AnimeListItem; status: AnimeListItemStatus }) {
return (
<ListItem>
<div className="flex w-full flex-col gap-1.5">
<div className={cn("flex w-full flex-col gap-1.5", item.pending && "opacity-50")}>
<div className="truncate">{getTitle(item.media?.title)}</div>
<BottomPart item={item} status={status} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/admin/manage/anime/add-to-ptw/action-buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { ExternalLink, Plus } from "~/components/icons";
import { Button, LinkButton } from "~/components/ui/button";
import { addToPTW } from "~/lib/anime/actions";
import { useTransitionWithNProgress } from "~/lib/hooks/use-transition-with-nprogress";
import { useTransitionWithNProgress } from "~/lib/hooks/use-nprogress";

export function ActionButtons({ id }: { id: number }) {
const startTransition = useTransitionWithNProgress();
Expand Down
2 changes: 1 addition & 1 deletion src/app/admin/manage/anime/add-to-ptw/search-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useState } from "react";

import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { useTransitionWithNProgress } from "~/lib/hooks/use-transition-with-nprogress";
import { useTransitionWithNProgress } from "~/lib/hooks/use-nprogress";

export function SearchForm({ query }: { query: string | undefined }) {
const router = useRouter();
Expand Down
2 changes: 1 addition & 1 deletion src/app/admin/manage/irasuto/refresh.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { RotateCcw } from "~/components/icons";
import { Button } from "~/components/ui/button";
import { useTransitionWithNProgress } from "~/lib/hooks/use-transition-with-nprogress";
import { useTransitionWithNProgress } from "~/lib/hooks/use-nprogress";

import { refreshIrasuto } from "./refresh-action";

Expand Down
49 changes: 34 additions & 15 deletions src/components/anime/data-context.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"use client";
import { produce } from "immer";
import { useOptimistic } from "react";
import { useMemo, useOptimistic } from "react";
import type { AnimeListItemStatus, AnimeLists } from "~/lib/anime/get-lists";
import { getAccumulatedScore } from "~/lib/anime/utils";
import { generateContext } from "~/lib/hooks/generate-context";
import { useNProgress } from "~/lib/hooks/use-nprogress";

type OptimisticValue = { lists: AnimeLists; pending: boolean };
type ReducerAction =
| ["UPDATE_SCORE", { status: AnimeListItemStatus; id: number; advancedScores: number[] }]
| ["UPDATE_STATUS", { status: AnimeListItemStatus; id: number; newStatus: AnimeListItemStatus }]
Expand All @@ -13,54 +15,71 @@ type ReducerAction =
| ["REMOVE", { status: AnimeListItemStatus; id: number }];

const [Provider, useAnimeData] = generateContext<{
pending: boolean;
optimisticLists: AnimeLists;
optimisticListsAct: (reducerArgument: ReducerAction) => void;
}>("AnimeData");
export { useAnimeData };

function optimisticReducer(current: AnimeLists, action: ReducerAction): AnimeLists {
function optimisticReducer(current: OptimisticValue, action: ReducerAction): OptimisticValue {
return produce(current, draft => {
const [type, data] = action;
draft.pending = true;
switch (type) {
case "UPDATE_SCORE": {
const item = draft[data.status].find(i => i?.id === data.id);
const item = draft.lists[data.status].find(i => i?.id === data.id);
if (!item) return;
item.pending = true;
item.advancedScores = data.advancedScores;
item.score = getAccumulatedScore(data.advancedScores);
break;
}
case "UPDATE_STATUS": {
const item = draft[data.status].find(i => i?.id === data.id);
const item = draft.lists[data.status].find(i => i?.id === data.id);
if (!item) return;
draft[data.newStatus].push(item);
draft[data.status] = draft[data.status].filter(i => i?.id !== data.id);
item.pending = true;
draft.lists[data.newStatus].push(item);
draft.lists[data.status] = draft.lists[data.status].filter(i => i?.id !== data.id);
break;
}
case "UPDATE_PROGRESS": {
const item = draft[data.status].find(i => i?.id === data.id);
const item = draft.lists[data.status].find(i => i?.id === data.id);
if (!item) return;
item.pending = true;
item.progress = (item.progress ?? 0) + 1;
break;
}
case "CANCEL_REWATCH": {
const item = draft.rewatching.find(i => i?.id === data.id);
const item = draft.lists.rewatching.find(i => i?.id === data.id);
if (!item) return;
draft.rewatching = draft.rewatching.filter(i => i?.id !== data.id);
draft[data.newStatus].push(item);
item.pending = true;
draft.lists.rewatching = draft.lists.rewatching.filter(i => i?.id !== data.id);
draft.lists[data.newStatus].push(item);
break;
}
case "REMOVE": {
draft[data.status] = draft[data.status].filter(i => i?.id !== data.id);
draft.lists[data.status] = draft.lists[data.status].filter(i => i?.id !== data.id);
break;
}
}
});
}

export function AnimeDataContextProvider({
lists,
value,
children,
}: { lists: AnimeLists; children: React.ReactNode }) {
const [optimisticLists, optimisticListsAct] = useOptimistic(lists, optimisticReducer);
return <Provider value={{ optimisticLists, optimisticListsAct }}>{children}</Provider>;
}: { value: OptimisticValue; children: React.ReactNode }) {
const [optimisticValue, optimisticListsAct] = useOptimistic(value, optimisticReducer);
useNProgress(optimisticValue.pending);
return (
<Provider
value={{
pending: optimisticValue.pending,
optimisticLists: optimisticValue.lists,
optimisticListsAct,
}}
>
{children}
</Provider>
);
}
2 changes: 1 addition & 1 deletion src/components/anime/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function AnimeLayout({
return (
<main className="container max-w-screen-lg">
<Card className="flex flex-col items-stretch p-0 md:flex-row">
<AnimeDataContextProvider lists={lists}>
<AnimeDataContextProvider value={{ lists, pending: false }}>
<Sidebar isAdmin={isAdmin} />
<div className="mx-auto w-full max-w-lg p-6">{children}</div>
</AnimeDataContextProvider>
Expand Down
2 changes: 1 addition & 1 deletion src/components/anime/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
import { forceRefresh } from "~/lib/anime/actions";
import type { AnimeLists } from "~/lib/anime/get-lists";
import { getListTitleFromStatus } from "~/lib/anime/utils";
import { useTransitionWithNProgress } from "~/lib/hooks/use-transition-with-nprogress";
import { useTransitionWithNProgress } from "~/lib/hooks/use-nprogress";
import { useAnimeData } from "./data-context";

interface Item {
Expand Down
8 changes: 5 additions & 3 deletions src/lib/anime/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ function generateAction<T extends unknown[] = []>(
};
}

export const incrementProgress = generateAction(async (client, item: AnimeListItem) => {
await client.request(UPDATE_PROGRESS, { id: item.id, progress: (item.progress ?? 0) + 1 });
});
export const incrementProgress = generateAction(
async (client, item: AnimeListItem, newProgress: number) => {
await client.request(UPDATE_PROGRESS, { id: item.id, progress: newProgress });
},
);

export const updateStatus = generateAction(
async (client, item: AnimeListItem, status: MediaListStatus) => {
Expand Down
27 changes: 22 additions & 5 deletions src/lib/anime/get-lists.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import { unstable_cache as cache } from "next/cache";
import { cache as dedupe } from "react";

import type { MediaListStatus } from "~/lib/gql/graphql";
import type { GetAnimeQuery, MediaListStatus } from "~/lib/gql/graphql";
import { getClient } from "~/lib/graphql";

import { GET_ANIME } from "./queries";

export type AnimeLists = Awaited<ReturnType<typeof getAllLists>>;
export type AnimeListItemStatus = keyof AnimeLists;
export type AnimeListItem = NonNullable<AnimeLists["watching"][number]>;
export type AnimeListItemStatus =
| "watching"
| "rewatching"
| "paused"
| "dropped"
| "planning"
| "completed/tv"
| "completed/movies"
| "completed/others";

// (𓁹‿𓁹)
type AnimeListItemFromAnilist = NonNullable<
NonNullable<
NonNullable<
NonNullable<NonNullable<GetAnimeQuery["MediaListCollection"]>["lists"]>[number]
>["entries"]
>[number]
>;
export type AnimeListItem = AnimeListItemFromAnilist & { pending?: boolean };
export type AnimeLists = Record<AnimeListItemStatus, (AnimeListItem | null)[]>;

export const getAllLists = dedupe(
cache(
async (status?: MediaListStatus) => {
async (status?: MediaListStatus): Promise<AnimeLists> => {
const client = getClient();
const data = await client.request(GET_ANIME, { status });
const lists = data.MediaListCollection?.lists ?? [];
Expand Down
24 changes: 0 additions & 24 deletions src/lib/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,7 @@
import { GraphQLClient } from "graphql-request";
import { env } from "~/env.mjs";

export function getClient(token?: string) {
return new GraphQLClient("https://graphql.anilist.co", {
fetch: async (url, init) => {
// Temporary "logging"
try {
if (process.env.DEBUG_WEBHOOK) {
const body: { operationName: string; variables?: object | undefined } = JSON.parse(
String(init?.body),
);
await fetch(process.env.DEBUG_WEBHOOK, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
content: `
Fetching from AniList (${body.operationName})
\`\`\`
${JSON.stringify(body.variables)}
\`\`\`
`.trim(),
}),
});
}
} catch {}
return fetch(url, init);
},
headers: token ? { authorization: `Bearer ${token}` } : undefined,
});
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import nProgress from "nprogress";
import { useEffect, useTransition } from "react";

export function useTransitionWithNProgress() {
const [isPending, startTransition] = useTransition();
export function useNProgress(enabled: boolean) {
useEffect(() => {
if (isPending) nProgress.start();
if (enabled) nProgress.start();
else nProgress.done();
}, [isPending]);
}, [enabled]);
}

export function useTransitionWithNProgress() {
const [isPending, startTransition] = useTransition();
useNProgress(isPending);
return startTransition;
}

0 comments on commit 345db72

Please sign in to comment.