diff --git a/backend/package-lock.json b/backend/package-lock.json index fe3ad00..3967c09 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -22,7 +22,8 @@ "jsonwebtoken": "^9.0.3", "multer": "^2.1.1", "postmark": "^4.0.7", - "prisma": "^6.19.2" + "prisma": "^6.19.2", + "slugify": "^1.6.9" }, "devDependencies": { "nodemon": "^3.1.11" @@ -1727,7 +1728,6 @@ "integrity": "sha512-XTKeKxtQElcq3U9/jHyxSPgiRgeYDKxWTPOf6NkXA0dNj5j40MfEsZkMbyNpwDWCUv7YBFUl7I2VK/6ALbmhEg==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.19.2", "@prisma/engines": "6.19.2" @@ -2064,6 +2064,15 @@ "node": ">=10" } }, + "node_modules/slugify": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz", + "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 4f05cfd..b18dc4c 100644 --- a/backend/package.json +++ b/backend/package.json @@ -28,7 +28,8 @@ "jsonwebtoken": "^9.0.3", "multer": "^2.1.1", "postmark": "^4.0.7", - "prisma": "^6.19.2" + "prisma": "^6.19.2", + "slugify": "^1.6.9" }, "devDependencies": { "nodemon": "^3.1.11" diff --git a/backend/prisma/migrations/20260414083619_added_slug/migration.sql b/backend/prisma/migrations/20260414083619_added_slug/migration.sql new file mode 100644 index 0000000..1b64c8f --- /dev/null +++ b/backend/prisma/migrations/20260414083619_added_slug/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `slug` to the `Blog` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Blog" ADD COLUMN "slug" TEXT NOT NULL; diff --git a/backend/prisma/migrations/20260414091055/migration.sql b/backend/prisma/migrations/20260414091055/migration.sql new file mode 100644 index 0000000..aaf6685 --- /dev/null +++ b/backend/prisma/migrations/20260414091055/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[slug]` on the table `Blog` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Blog_slug_key" ON "Blog"("slug"); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f78df85..9cc2df9 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -93,6 +93,7 @@ model Blog { image String? content Json isActive Boolean @default(true) + slug String @unique createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -149,20 +150,20 @@ model Appointment { } model Inquiry { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) - fullName String - number String - emailId String? - subject String? - message String? + fullName String + number String + emailId String? + subject String? + message String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model AcademicsResearch { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) fullName String number String @@ -171,24 +172,23 @@ model AcademicsResearch { courseName String? message String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } - model EmailConfig { - id Int @id @default(autoincrement()) - name String - email String - type String - isActive Boolean @default(true) + id Int @id @default(autoincrement()) + name String + email String + type String + isActive Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } model NewsMedia { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) headline String content String? @@ -197,8 +197,8 @@ model NewsMedia { author String? date DateTime? - isActive Boolean @default(true) + isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} \ No newline at end of file + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/backend/src/controllers/blog.controller.js b/backend/src/controllers/blog.controller.js index bf58a09..f4cfd9f 100644 --- a/backend/src/controllers/blog.controller.js +++ b/backend/src/controllers/blog.controller.js @@ -1,4 +1,5 @@ import prisma from "../prisma/client.js"; +import slugify from "slugify"; /* CREATE BLOG */ @@ -13,6 +14,7 @@ export async function createBlog(req, res) { image, content, isActive, + slug: slugify(title), }, }); @@ -54,6 +56,26 @@ export async function getAllBlogs(req, res) { /* GET SINGLE BLOG */ export async function getBlog(req, res) { + try { + const slug = req.params.slug; + + const blog = await prisma.blog.findUnique({ + where: {slug}, + }); + + if (!blog) { + return res.status(404).json({error: "Blog not found"}); + } + + res.json(blog); + } catch (error) { + res.status(500).json({error: error.message}); + } +} + +/* GET SINGLE BLOG (ADMIN)*/ + +export async function getBlogForAdmin(req, res) { try { const id = Number(req.params.id); diff --git a/backend/src/routes/blog.routes.js b/backend/src/routes/blog.routes.js index 7cf9ad2..26a825b 100644 --- a/backend/src/routes/blog.routes.js +++ b/backend/src/routes/blog.routes.js @@ -6,6 +6,7 @@ import { updateBlog, deleteBlog, getAllBlogs, + getBlogForAdmin, } from "../controllers/blog.controller.js"; import jwtAuthMiddleware from "../middleware/auth.js"; @@ -15,11 +16,14 @@ const router = express.Router(); /* PUBLIC */ router.get("/", getBlogs); -router.get("/:id", getBlog); +router.get("/:slug", getBlog); // Protected router.get("/admin/all", jwtAuthMiddleware, getAllBlogs); + +router.get("/admin/:id", jwtAuthMiddleware, getBlogForAdmin); + router.post("/", jwtAuthMiddleware, createBlog); router.put("/:id", jwtAuthMiddleware, updateBlog); router.delete("/:id", jwtAuthMiddleware, deleteBlog); diff --git a/backend/uploads/blog/1776156111743.png b/backend/uploads/blog/1776156111743.png new file mode 100644 index 0000000..0781df1 Binary files /dev/null and b/backend/uploads/blog/1776156111743.png differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e1bb47c..46ed472 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -115,7 +115,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1760,7 +1759,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -4093,7 +4091,6 @@ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4104,7 +4101,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -4115,7 +4111,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -4177,7 +4172,6 @@ "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", @@ -4468,7 +4462,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4728,7 +4721,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5558,7 +5550,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5811,7 +5802,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6466,7 +6456,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.7.tgz", "integrity": "sha512-jq9l1DM0zVIvsm3lv9Nw9nlJnMNPOcAtsbsgiUhWcFzPE99Gvo6yRTlszSLLYacMeQ6quHD6hMfId8crVHvexw==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -8131,7 +8120,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -8141,7 +8129,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8905,8 +8892,7 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -9113,7 +9099,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9314,7 +9299,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9659,7 +9643,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 17b1782..1838c21 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,6 +20,7 @@ import CandidatePage from "./pages/candidates"; import InquiryPage from "./pages/inquiry"; import AcademicsPage from "./pages/Academics"; import NewsPage from "./pages/newsMedia"; +import BlogDetail from "./pages/BlogDetails"; export default function App() { return ( @@ -35,6 +36,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/api/blog.ts b/frontend/src/api/blog.ts index 7e46c5c..ceee3d3 100644 --- a/frontend/src/api/blog.ts +++ b/frontend/src/api/blog.ts @@ -14,7 +14,7 @@ export const getAllBlogsApi = async () => { }; export const getBlogByIdApi = async (id: number) => { - const res = await apiClient.get(`/blogs/${id}`); + const res = await apiClient.get(`/blogs/admin/${id}`); return res.data; }; diff --git a/frontend/src/pages/Blog.tsx b/frontend/src/pages/Blog.tsx index d1fc088..444c881 100644 --- a/frontend/src/pages/Blog.tsx +++ b/frontend/src/pages/Blog.tsx @@ -17,7 +17,7 @@ import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; import {Button} from "@/components/ui/button"; import {Input} from "@/components/ui/input"; -import {Loader2, RefreshCw, Plus, Pencil, Trash} from "lucide-react"; +import {Loader2, RefreshCw, Plus, Pencil, Trash, Eye} from "lucide-react"; interface Blog { id: number; @@ -161,6 +161,13 @@ export default function BlogPage() { + navigate(`/blog/${blog.id}`)} + > + + { + const {id} = useParams(); + const navigate = useNavigate(); + const [blog, setBlog] = useState(null); + + const fetchBlogById = async () => { + try { + const response = await getBlogByIdApi(Number(id)); + + setBlog(response); + } catch (error) { + console.error("Error fetching blog", error); + } + }; + + useEffect(() => { + fetchBlogById(); + }, [id]); // ✅ FIXED dependency + + if (!blog) { + return Loading...; + } + + return ( + + + + {/* Back */} + navigate(-1)} + > + ← Back + + + {/* Title */} + + {blog.title} + + + {/* Meta */} + + {blog.writer} • {new Date(blog.createdAt).toLocaleDateString()} + + + {/* Image */} + + + {/* Content */} + + {blog.content?.blocks?.map((block: any, index: number) => ( + {block.data?.text} + ))} + + + + + ); +}; + +export default BlogDetail;
+ {blog.writer} • {new Date(blog.createdAt).toLocaleDateString()} +
{block.data?.text}