Skip to content

Commit dc485fb

Browse files
committed
feat: add restore functionality for scheduled article deletions and improve toast handling
1 parent 8d7abfd commit dc485fb

File tree

3 files changed

+111
-27
lines changed

3 files changed

+111
-27
lines changed

src/app/dashboard/_components/ArticleList.tsx

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,17 @@ import {
1111
} from "@/components/ui/dropdown-menu";
1212
import VisibilitySensor from "@/components/VisibilitySensor";
1313
import { useTranslation } from "@/i18n/use-translation";
14-
import { formattedTime } from "@/lib/utils";
14+
import { actionPromisify, formattedTime } from "@/lib/utils";
1515
import {
1616
CardStackIcon,
1717
DotsHorizontalIcon,
1818
Pencil1Icon,
1919
PlusIcon,
20+
ReloadIcon,
2021
} from "@radix-ui/react-icons";
2122

2223
import { useInfiniteQuery, useMutation } from "@tanstack/react-query";
24+
import { differenceInHours } from "date-fns";
2325
import { TrashIcon } from "lucide-react";
2426
import Link from "next/link";
2527

@@ -37,9 +39,28 @@ const ArticleList = () => {
3739
},
3840
});
3941

40-
// const deleteMutation = useMutation({
41-
// mutationFn: ac
42-
// })
42+
const deleteMutation = useMutation({
43+
mutationFn: (article_id: string) =>
44+
actionPromisify(articleActions.scheduleArticleDelete(article_id), {
45+
enableToast: true,
46+
}),
47+
onSuccess() {
48+
// TODO: optimistic update
49+
feedInfiniteQuery.refetch();
50+
},
51+
});
52+
53+
const restoreMutation = useMutation({
54+
mutationFn: (article_id: string) =>
55+
actionPromisify(
56+
articleActions.restoreShceduleDeletedArticle(article_id),
57+
{ enableToast: true }
58+
),
59+
onSuccess() {
60+
// TODO: optimistic update
61+
feedInfiniteQuery.refetch();
62+
},
63+
});
4364

4465
const appConfirm = useAppConfirm();
4566

@@ -76,6 +97,17 @@ const ArticleList = () => {
7697
>
7798
{article.title}
7899
</Link>
100+
{article?.delete_scheduled_at && (
101+
<p className="text-destructive">
102+
Article will be deleted within{" "}
103+
{differenceInHours(
104+
new Date(article?.delete_scheduled_at!),
105+
new Date()
106+
)}{" "}
107+
hours
108+
</p>
109+
)}
110+
79111
{article.is_published && (
80112
<p className="text-sm text-muted-foreground">
81113
{_t("Published on")} {formattedTime(article.published_at!)}
@@ -170,19 +202,33 @@ const ArticleList = () => {
170202
</span>
171203
</button>
172204
</DropdownMenuItem>
173-
<DropdownMenuItem
174-
onClick={() => {
175-
appConfirm.show({
176-
title: _t("Sure to delete?"),
177-
labels: {
178-
confirm: _t("Delete"),
179-
},
180-
});
181-
}}
182-
>
183-
<TrashIcon />
184-
{_t("Delete")}
185-
</DropdownMenuItem>
205+
{article.delete_scheduled_at ? (
206+
<DropdownMenuItem
207+
onClick={() => {
208+
restoreMutation.mutate(article.id);
209+
}}
210+
>
211+
<ReloadIcon />
212+
{_t("Restore")}
213+
</DropdownMenuItem>
214+
) : (
215+
<DropdownMenuItem
216+
onClick={() => {
217+
appConfirm.show({
218+
title: _t("Sure to delete?"),
219+
labels: {
220+
confirm: _t("Delete"),
221+
},
222+
onConfirm() {
223+
deleteMutation.mutate(article.id);
224+
},
225+
});
226+
}}
227+
>
228+
<TrashIcon />
229+
{_t("Delete")}
230+
</DropdownMenuItem>
231+
)}
186232
</DropdownMenuContent>
187233
</DropdownMenu>
188234
</div>

src/backend/services/article.actions.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,36 @@ export const scheduleArticleDelete = async (
217217
}
218218
};
219219

220+
export const restoreShceduleDeletedArticle = async (
221+
article_id: string
222+
): Promise<ActionResponse<unknown>> => {
223+
try {
224+
const session_userID = await authID();
225+
if (!session_userID) {
226+
throw new ActionException("Unauthorized");
227+
}
228+
229+
const [permissibleArticle] = await persistenceRepository.article.find({
230+
where: and(eq("id", article_id), eq("author_id", session_userID)),
231+
});
232+
233+
if (!permissibleArticle) {
234+
throw new ActionException("Unauthorized");
235+
}
236+
237+
const updated = await persistenceRepository.article.update({
238+
where: and(eq("id", article_id), eq("author_id", session_userID)),
239+
data: { delete_scheduled_at: null },
240+
});
241+
return {
242+
success: true as const,
243+
data: updated.rows[0],
244+
};
245+
} catch (error) {
246+
return handleActionException(error);
247+
}
248+
};
249+
220250
/**
221251
* Deletes an article from the database.
222252
*

src/lib/utils.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { ActionResponse } from "@/backend/models/action-contracts";
22
import { clsx, type ClassValue } from "clsx";
3-
import { toast } from "sonner";
3+
import toast from "react-hot-toast";
44
import { twMerge } from "tailwind-merge";
5-
import { z, ZodAnyDef, ZodObject } from "zod";
5+
import { z } from "zod";
66

77
export function cn(...inputs: ClassValue[]) {
88
return twMerge(clsx(inputs));
@@ -115,7 +115,15 @@ export function filterUndefined<T>(
115115

116116
// Improved with automatic type inference
117117
export const actionPromisify = async <T = any>(
118-
action: Promise<ActionResponse<T>>
118+
action: Promise<ActionResponse<T>>,
119+
options?: {
120+
enableToast?: boolean;
121+
toastOptions?: {
122+
loading?: string;
123+
success?: string;
124+
error?: string;
125+
};
126+
}
119127
): Promise<T> => {
120128
const promise = new Promise<T>(async (resolve, reject) => {
121129
try {
@@ -145,13 +153,13 @@ export const actionPromisify = async <T = any>(
145153
}
146154
});
147155

148-
// if (options?.withToast) {
149-
// toast.promise(promise, {
150-
// loading: options?.messages?.loading ?? "Loading...",
151-
// success: options?.messages?.success ?? "Success!",
152-
// error: (errorMsg: string) => errorMsg || "Operation failed",
153-
// });
154-
// }
156+
if (options?.enableToast) {
157+
toast.promise(promise, {
158+
loading: options?.toastOptions?.loading ?? "Loading...",
159+
success: options?.toastOptions?.success ?? "Success!",
160+
error: (errorMsg: string) => errorMsg || "Operation failed",
161+
});
162+
}
155163

156164
return promise;
157165
};

0 commit comments

Comments
 (0)