diff --git a/backend/src/controllers/newsMedia.controller.js b/backend/src/controllers/newsMedia.controller.js index 3219dc9..1c743dc 100644 --- a/backend/src/controllers/newsMedia.controller.js +++ b/backend/src/controllers/newsMedia.controller.js @@ -4,9 +4,19 @@ import prisma from "../prisma/client.js"; export const getAllNews = async (req, res) => { try { - const news = await prisma.newsMedia.findMany({ - orderBy: { createdAt: "desc" }, - }); + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + + const skip = (page - 1) * limit; + + const [news, total] = await Promise.all([ + prisma.newsMedia.findMany({ + orderBy: { createdAt: "desc" }, + skip, + take: limit, + }), + prisma.newsMedia.count(), + ]); const response = news.map((n) => ({ Id: n.id.toString(), @@ -21,6 +31,12 @@ export const getAllNews = async (req, res) => { return res.status(200).json({ success: true, data: response, + meta: { + total, + page, + limit, + totalPages: Math.ceil(total / limit), + }, }); } catch (error) { console.error(error); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6584407..17b1782 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -19,6 +19,7 @@ import CareerPage from "./pages/Career"; import CandidatePage from "./pages/candidates"; import InquiryPage from "./pages/inquiry"; import AcademicsPage from "./pages/Academics"; +import NewsPage from "./pages/newsMedia"; export default function App() { return ( @@ -42,6 +43,7 @@ export default function App() { } /> } /> } /> + } /> diff --git a/frontend/src/api/newsMedia.ts b/frontend/src/api/newsMedia.ts new file mode 100644 index 0000000..8905b83 --- /dev/null +++ b/frontend/src/api/newsMedia.ts @@ -0,0 +1,23 @@ +import apiClient from "@/api/client"; + +export const getNewsApi = async (page = 1, limit = 10) => { + const res = await apiClient.get( + `/newsMedia/getAll?page=${page}&limit=${limit}`, + ); + return res.data; +}; + +export const createNewsApi = async (data: any) => { + const res = await apiClient.post("/newsMedia", data); + return res.data; +}; + +export const updateNewsApi = async (id: number, data: any) => { + const res = await apiClient.patch(`/newsMedia/${id}`, data); + return res.data; +}; + +export const deleteNewsApi = async (id: number) => { + const res = await apiClient.delete(`/newsMedia/${id}`); + return res.data; +}; diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 95e2762..2610239 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -35,6 +35,10 @@ export default function Sidebar() { name: "Academics & Research", path: "/academics", }, + { + name: "News & Media", + path: "/news", + }, { name: "Email", path: "/email", diff --git a/frontend/src/pages/newsMedia.tsx b/frontend/src/pages/newsMedia.tsx new file mode 100644 index 0000000..fe267c6 --- /dev/null +++ b/frontend/src/pages/newsMedia.tsx @@ -0,0 +1,400 @@ +import { useState, useEffect, useCallback } from "react"; + +import { + getNewsApi, + createNewsApi, + updateNewsApi, + deleteNewsApi, +} from "@/api/newsMedia"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; + +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from "@/components/ui/dialog"; + +import { + Loader2, + Plus, + Pencil, + Trash, + RefreshCw, + Eye, + ChevronLeft, + ChevronRight, +} from "lucide-react"; + +export default function NewsPage() { + const [news, setNews] = useState([]); + const [loading, setLoading] = useState(true); + + const [searchText, setSearchText] = useState(""); + + const [openModal, setOpenModal] = useState(false); + const [editing, setEditing] = useState(null); + + const [viewOpen, setViewOpen] = useState(false); + const [viewData, setViewData] = useState(null); + + const [currentPage, setCurrentPage] = useState(1); + const [itemsPerPage, setItemsPerPage] = useState(10); + + const [form, setForm] = useState({ + headline: "", + content: "", + firstPara: "", + secondPara: "", + date: "", + author: "", + }); + + const fetchAll = useCallback(async () => { + setLoading(true); + try { + const res = await getNewsApi(); + setNews(res?.data || []); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchAll(); + }, [fetchAll]); + + const filteredNews = news.filter((item) => + item.Headline?.toLowerCase().includes(searchText.toLowerCase()), + ); + + const totalPages = Math.ceil(filteredNews.length / itemsPerPage); + const startIndex = (currentPage - 1) * itemsPerPage; + const paginatedData = filteredNews.slice( + startIndex, + startIndex + itemsPerPage, + ); + + function handleChange(e: any) { + setForm({ ...form, [e.target.name]: e.target.value }); + } + + function openAdd() { + setEditing(null); + setForm({ + headline: "", + content: "", + firstPara: "", + secondPara: "", + date: "", + author: "", + }); + setOpenModal(true); + } + + function openEdit(item: any) { + setEditing(item); + + setForm({ + headline: item.Headline || "", + content: item.Content || "", + firstPara: item.FirstPara || "", + secondPara: item.SecondPara || "", + date: item.Date ? item.Date.split("T")[0] : "", + author: item.Author || "", + }); + + setOpenModal(true); + } + + function openView(item: any) { + setViewData(item); + setViewOpen(true); + } + + async function handleSubmit() { + try { + if (editing) { + await updateNewsApi(editing.Id, form); + } else { + await createNewsApi(form); + } + setOpenModal(false); + fetchAll(); + } catch (err) { + console.error(err); + } + } + + async function handleDelete(id: number) { + if (!confirm("Delete news?")) return; + await deleteNewsApi(id); + fetchAll(); + } + + return ( + + + News Media + + + { + setSearchText(e.target.value); + setCurrentPage(1); + }} + className="w-[250px]" + /> + + { + setItemsPerPage(Number(e.target.value)); + setCurrentPage(1); + }} + className="border px-2 py-1 rounded"> + 5 / page + 10 / page + 20 / page + + + + + Refresh + + + + + Add News + + + + + + + News List + + + + + + + + ID + Headline + Author + Date + Content + Actions + + + + + {loading ? ( + + + + + + ) : paginatedData.length === 0 ? ( + + + No news found + + + ) : ( + paginatedData.map((item) => ( + + {item.Id} + + + {item.Headline} + + + {item.Author} + + + {item.Date + ? new Date(item.Date).toLocaleDateString() + : "-"} + + + + {item.Content} + + + + openView(item)}> + + + + openEdit(item)}> + + + + handleDelete(Number(item.Id))}> + + + + + )) + )} + + + + + {/* PAGINATION */} + + + Page {currentPage} of {totalPages} + + + + setCurrentPage((p) => p - 1)}> + + + + setCurrentPage((p) => p + 1)}> + + + + + + + + {/* CREATE / EDIT MODAL */} + + + + {editing ? "Edit News" : "Add News"} + + + + + + + + + + + + + + setOpenModal(false)}> + Cancel + + + {editing ? "Update" : "Create"} + + + + + + {/* VIEW MODAL */} + + + + News Details + + + {viewData && ( + + Headline: + {viewData.Headline} + + Author: + {viewData.Author} + + Date: + + {viewData.Date + ? new Date(viewData.Date).toLocaleDateString() + : "-"} + + + First Para: + {viewData.FirstPara} + + Second Para: + {viewData.SecondPara} + + Content: + {viewData.Content} + + )} + + + setViewOpen(false)}>Close + + + + + ); +}
+ Page {currentPage} of {totalPages} +
{viewData.Headline}
{viewData.Author}
+ {viewData.Date + ? new Date(viewData.Date).toLocaleDateString() + : "-"} +
{viewData.FirstPara}
{viewData.SecondPara}
{viewData.Content}