Skip to content

Commit 72adbfd

Browse files
authored
Merge pull request #230 from OpenPecha/verse_of_day_POST_cms
Verse of day post cms
2 parents 71c3eec + 006650d commit 72adbfd

10 files changed

Lines changed: 929 additions & 0 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { useState } from "react";
2+
import { IoMdAdd, IoMdSearch } from "react-icons/io";
3+
import { useDebounce } from "use-debounce";
4+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5+
import { toast } from "sonner";
6+
import { Pecha } from "@/components/ui/shadimport";
7+
import { Button } from "@/components/ui/atoms/button";
8+
import AuthButton from "@/components/ui/molecules/auth-button/AuthButton";
9+
import { Pagination } from "@/components/ui/molecules/pagination/Pagination";
10+
import { getApiErrorMessage } from "@/lib/apiErrors";
11+
import {
12+
fetchVerseOfDayList,
13+
deleteVerseOfDay,
14+
type VerseOfDayItem,
15+
} from "./api/verseOfDayApi";
16+
import VerseOfDayList from "./VerseOfDayList";
17+
import VerseOfDayFormDialog from "./VerseOfDayFormDialog";
18+
19+
const PAGE_SIZE = 10;
20+
21+
const VerseOfDay = () => {
22+
const [search, setSearch] = useState("");
23+
const [currentPage, setCurrentPage] = useState(1);
24+
const [debouncedSearch] = useDebounce(search, 500);
25+
const [formOpen, setFormOpen] = useState(false);
26+
const [editingItem, setEditingItem] = useState<VerseOfDayItem | null>(null);
27+
const [deleteTarget, setDeleteTarget] = useState<VerseOfDayItem | null>(null);
28+
29+
const queryClient = useQueryClient();
30+
31+
const {
32+
data,
33+
isLoading,
34+
error,
35+
} = useQuery({
36+
queryKey: ["verse-of-day-list", currentPage, debouncedSearch],
37+
queryFn: fetchVerseOfDayList,
38+
refetchOnWindowFocus: false,
39+
retry: false,
40+
});
41+
42+
const invalidateVerses = () => {
43+
queryClient.invalidateQueries({ queryKey: ["verse-of-day-list"] });
44+
};
45+
46+
const deleteMutation = useMutation({
47+
mutationFn: deleteVerseOfDay,
48+
onSuccess: () => {
49+
toast.success("Verse of Day deleted successfully!");
50+
setDeleteTarget(null);
51+
invalidateVerses();
52+
},
53+
onError: (err) => {
54+
toast.error(getApiErrorMessage(err));
55+
setDeleteTarget(null);
56+
},
57+
});
58+
59+
const handleOpenCreate = () => {
60+
setEditingItem(null);
61+
setFormOpen(true);
62+
};
63+
64+
const handleOpenEdit = (item: VerseOfDayItem) => {
65+
setEditingItem(item);
66+
setFormOpen(true);
67+
};
68+
69+
const handleFormSuccess = () => {
70+
setFormOpen(false);
71+
setEditingItem(null);
72+
invalidateVerses();
73+
};
74+
75+
const sortedVerses = data?.verses
76+
? [...data.verses].sort(
77+
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
78+
)
79+
: [];
80+
81+
const totalPages = sortedVerses.length > 0 ? Math.ceil(sortedVerses.length / PAGE_SIZE) : 1;
82+
83+
return (
84+
<div className="flex flex-col h-screen overflow-hidden bg-[#F5F5F5] dark:bg-[#181818] font-dynamic">
85+
<div className="px-6 py-4 flex items-center justify-between border-b border-gray-200 dark:border-[#313132] bg-white dark:bg-[#1E1E1E]">
86+
<div className="flex items-center space-x-2">
87+
<div className="border w-fit px-2 bg-white dark:bg-input/30 rounded-md border-gray-200 dark:border-[#313132] flex items-center">
88+
<IoMdSearch className="w-4 h-4" />
89+
<Pecha.Input
90+
placeholder="Search verses..."
91+
className="rounded-md border-none dark:bg-transparent px-4 shadow-none py-2"
92+
value={search}
93+
onChange={(e) => {
94+
setSearch(e.target.value);
95+
if (currentPage !== 1) setCurrentPage(1);
96+
}}
97+
/>
98+
</div>
99+
<Button
100+
variant="outline"
101+
className="bg-gray-100 hover:bg-gray-200"
102+
onClick={handleOpenCreate}
103+
>
104+
<IoMdAdd /> Add Verse
105+
</Button>
106+
</div>
107+
<AuthButton />
108+
</div>
109+
110+
<div className="flex-1 overflow-hidden px-6 py-4">
111+
{error ? (
112+
<p className="text-sm text-red-500 py-8">
113+
Failed to load verses. {getApiErrorMessage(error)}
114+
</p>
115+
) : sortedVerses.length === 0 && !isLoading ? (
116+
<div className="flex flex-col h-full items-center justify-center">
117+
<p className="text-base text-muted-foreground">No verses found</p>
118+
<Button
119+
variant="outline"
120+
className="mt-2"
121+
onClick={handleOpenCreate}
122+
>
123+
<IoMdAdd /> Add Verse
124+
</Button>
125+
</div>
126+
) : (
127+
<div className="h-full overflow-auto">
128+
<VerseOfDayList
129+
onEdit={handleOpenEdit}
130+
onDelete={setDeleteTarget}
131+
/>
132+
</div>
133+
)}
134+
</div>
135+
136+
{sortedVerses.length > 0 && (
137+
<div className="border-t border-gray-200 dark:border-[#313132] px-6 py-4 bg-white dark:bg-[#1E1E1E]">
138+
<Pagination
139+
currentPage={currentPage}
140+
totalPages={totalPages}
141+
onPageChange={setCurrentPage}
142+
/>
143+
</div>
144+
)}
145+
146+
<VerseOfDayFormDialog
147+
open={formOpen}
148+
onOpenChange={(open: boolean) => {
149+
setFormOpen(open);
150+
if (!open) setEditingItem(null);
151+
}}
152+
editingItem={editingItem}
153+
onSuccess={handleFormSuccess}
154+
existingVerses={sortedVerses}
155+
/>
156+
157+
<Pecha.AlertDialog
158+
open={!!deleteTarget}
159+
onOpenChange={(open) => !open && setDeleteTarget(null)}
160+
>
161+
<Pecha.AlertDialogContent>
162+
<Pecha.AlertDialogHeader>
163+
<Pecha.AlertDialogTitle>Delete Verse of Day</Pecha.AlertDialogTitle>
164+
<Pecha.AlertDialogDescription>
165+
Are you sure you want to delete this verse? This action cannot be
166+
undone.
167+
</Pecha.AlertDialogDescription>
168+
</Pecha.AlertDialogHeader>
169+
<Pecha.AlertDialogFooter>
170+
<Pecha.AlertDialogCancel disabled={deleteMutation.isPending}>
171+
Cancel
172+
</Pecha.AlertDialogCancel>
173+
<Pecha.AlertDialogAction
174+
className="bg-[#AD1B21] dark:text-white hover:bg-[#AD1B21]/90"
175+
disabled={deleteMutation.isPending}
176+
onClick={() =>
177+
deleteTarget && deleteMutation.mutate(deleteTarget.id)
178+
}
179+
>
180+
{deleteMutation.isPending ? "Deleting..." : "Delete"}
181+
</Pecha.AlertDialogAction>
182+
</Pecha.AlertDialogFooter>
183+
</Pecha.AlertDialogContent>
184+
</Pecha.AlertDialog>
185+
</div>
186+
);
187+
};
188+
189+
export default VerseOfDay;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { IoBookOutline } from "react-icons/io5";
2+
import { Link } from "react-router-dom";
3+
import {
4+
Tooltip,
5+
TooltipContent,
6+
TooltipTrigger,
7+
} from "@/components/ui/atoms/tooltip";
8+
import { ROUTES } from "@/routes/paths";
9+
10+
const VerseOfDayButton = () => {
11+
return (
12+
<Tooltip>
13+
<TooltipTrigger asChild>
14+
<Link
15+
to={ROUTES.verseOfDay}
16+
className="border p-2 rounded-md text-zinc-400 dark:text-zinc-600 hover:text-black dark:hover:text-white transition-colors"
17+
aria-label="Verse of Day"
18+
>
19+
<IoBookOutline className="w-4 h-4" />
20+
</Link>
21+
</TooltipTrigger>
22+
<TooltipContent side="right">Verse of Day</TooltipContent>
23+
</Tooltip>
24+
);
25+
};
26+
27+
export default VerseOfDayButton;

0 commit comments

Comments
 (0)