From 24a8bc4113e13f1933f688909a4a2d7711bd3b27 Mon Sep 17 00:00:00 2001 From: ARJUN S THAMPI <61703062+arjun-thampi@users.noreply.github.com> Date: Wed, 25 Mar 2026 17:59:36 +0530 Subject: [PATCH 1/3] feat: add news media api --- .../migration.sql | 15 ++ backend/prisma/schema.prisma | 16 ++ backend/src/app.js | 2 + .../src/controllers/newsMedia.controller.js | 161 ++++++++++++++++++ backend/src/routes/newsMedia.routes.js | 23 +++ 5 files changed, 217 insertions(+) create mode 100644 backend/prisma/migrations/20260325110104_add_news_media/migration.sql create mode 100644 backend/src/controllers/newsMedia.controller.js create mode 100644 backend/src/routes/newsMedia.routes.js diff --git a/backend/prisma/migrations/20260325110104_add_news_media/migration.sql b/backend/prisma/migrations/20260325110104_add_news_media/migration.sql new file mode 100644 index 0000000..08ad7c1 --- /dev/null +++ b/backend/prisma/migrations/20260325110104_add_news_media/migration.sql @@ -0,0 +1,15 @@ +-- CreateTable +CREATE TABLE "NewsMedia" ( + "id" SERIAL NOT NULL, + "headline" TEXT NOT NULL, + "content" TEXT, + "firstPara" TEXT, + "secondPara" TEXT, + "author" TEXT, + "date" TIMESTAMP(3), + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "NewsMedia_pkey" PRIMARY KEY ("id") +); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index e659495..f78df85 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -185,4 +185,20 @@ model EmailConfig { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt +} + +model NewsMedia { + id Int @id @default(autoincrement()) + + headline String + content String? + firstPara String? + secondPara String? + author String? + date DateTime? + + isActive Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } \ No newline at end of file diff --git a/backend/src/app.js b/backend/src/app.js index aa3c088..7efc6a8 100644 --- a/backend/src/app.js +++ b/backend/src/app.js @@ -13,6 +13,7 @@ import appointmentRoutes from "./routes/appointment.routes.js"; import inquiryRoutes from "./routes/inquiry.routes.js"; import academicsResearchRoutes from "./routes/academicsResearch.routes.js"; import emailConfigRoutes from "./routes/emailConfig.routes.js"; +import newsMediaRoutes from "./routes/newsMedia.routes.js"; dotenv.config(); @@ -49,6 +50,7 @@ app.use("/api/appointments", appointmentRoutes); app.use("/api/inquiry", inquiryRoutes); app.use("/api/academics", academicsResearchRoutes); app.use("/api/email", emailConfigRoutes); +app.use("/api/newsMedia", newsMediaRoutes); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { diff --git a/backend/src/controllers/newsMedia.controller.js b/backend/src/controllers/newsMedia.controller.js new file mode 100644 index 0000000..3219dc9 --- /dev/null +++ b/backend/src/controllers/newsMedia.controller.js @@ -0,0 +1,161 @@ +import prisma from "../prisma/client.js"; + +// GET ALL NEWS + +export const getAllNews = async (req, res) => { + try { + const news = await prisma.newsMedia.findMany({ + orderBy: { createdAt: "desc" }, + }); + + const response = news.map((n) => ({ + Id: n.id.toString(), + Headline: n.headline, + Content: n.content, + FirstPara: n.firstPara, + SecondPara: n.secondPara, + Date: n.date, + Author: n.author, + })); + + return res.status(200).json({ + success: true, + data: response, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + success: false, + message: "Failed to fetch news", + }); + } +}; + +// GET NEWS BY ID + +export const getNewsById = async (req, res) => { + try { + const { id } = req.params; + + const n = await prisma.newsMedia.findUnique({ + where: { id: Number(id) }, + }); + + if (!n) { + return res.status(404).json({ + success: false, + message: "News not found", + }); + } + + const response = { + Id: n.id.toString(), + Headline: n.headline, + Content: n.content, + FirstPara: n.firstPara, + SecondPara: n.secondPara, + Date: n.date, + Author: n.author, + }; + + return res.status(200).json({ + success: true, + data: response, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + success: false, + message: "Failed to fetch news", + }); + } +}; + +// CREATE NEWS + +export const createNews = async (req, res) => { + try { + const { headline, content, firstPara, secondPara, date, author } = req.body; + + if (!headline) { + return res.status(400).json({ + success: false, + message: "Headline is required", + }); + } + + const news = await prisma.newsMedia.create({ + data: { + headline, + content, + firstPara, + secondPara, + date: date ? new Date(date) : null, + author, + }, + }); + + return res.status(201).json({ + success: true, + message: "News created successfully", + data: news, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + success: false, + message: "Failed to create news", + }); + } +}; + +// UPDATE NEWS + +export const updateNews = async (req, res) => { + try { + const { id } = req.params; + + const news = await prisma.newsMedia.update({ + where: { id: Number(id) }, + data: { + ...req.body, + date: req.body.date ? new Date(req.body.date) : undefined, + }, + }); + + return res.status(200).json({ + success: true, + message: "News updated successfully", + data: news, + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + success: false, + message: "Failed to update news", + }); + } +}; + +// DELETE NEWS + +export const deleteNews = async (req, res) => { + try { + const { id } = req.params; + + await prisma.newsMedia.delete({ + where: { id: Number(id) }, + }); + + return res.status(200).json({ + success: true, + message: "News deleted successfully", + }); + } catch (error) { + console.error(error); + return res.status(500).json({ + success: false, + message: "Failed to delete news", + }); + } +}; diff --git a/backend/src/routes/newsMedia.routes.js b/backend/src/routes/newsMedia.routes.js new file mode 100644 index 0000000..f7631e4 --- /dev/null +++ b/backend/src/routes/newsMedia.routes.js @@ -0,0 +1,23 @@ +import express from "express"; +import { + createNews, + getAllNews, + getNewsById, + updateNews, + deleteNews, +} from "../controllers/newsMedia.controller.js"; + +import jwtAuthMiddleware from "../middleware/auth.js"; + +const router = express.Router(); + +// PUBLIC ROUTES +router.get("/getAll", getAllNews); +router.get("/:id", getNewsById); + +// PROTECTED ROUTES +router.post("/", jwtAuthMiddleware, createNews); +router.patch("/:id", jwtAuthMiddleware, updateNews); +router.delete("/:id", jwtAuthMiddleware, deleteNews); + +export default router; -- 2.43.0 From 2ed1bee149c3ff63265d6557b1dc9e87f779d95e Mon Sep 17 00:00:00 2001 From: ARJUN S THAMPI <61703062+arjun-thampi@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:20:03 +0530 Subject: [PATCH 2/3] feat: add page newMedia --- .../src/controllers/newsMedia.controller.js | 22 +- frontend/src/App.tsx | 2 + frontend/src/api/newsMedia.ts | 23 + frontend/src/components/layout/Sidebar.tsx | 4 + frontend/src/pages/newsMedia.tsx | 400 ++++++++++++++++++ 5 files changed, 448 insertions(+), 3 deletions(-) create mode 100644 frontend/src/api/newsMedia.ts create mode 100644 frontend/src/pages/newsMedia.tsx 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]" + /> + + + + + + +
+
+ + + + 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} + + + + + + + + + + + )) + )} + +
+
+ + {/* PAGINATION */} +
+

+ Page {currentPage} of {totalPages} +

+ +
+ + + +
+
+
+
+ + {/* CREATE / EDIT MODAL */} + + + + {editing ? "Edit News" : "Add News"} + + +
+ + + + +