diff --git a/Channel-Backend/prisma/schema.prisma b/Channel-Backend/prisma/schema.prisma index d84ee8e..6667253 100644 --- a/Channel-Backend/prisma/schema.prisma +++ b/Channel-Backend/prisma/schema.prisma @@ -138,3 +138,17 @@ model AuditLog { createdAt DateTime @default(now()) actor User @relation(fields: [actorId], references: [id]) } + +model BlogPost { + id String @id @default(uuid()) + title String + content String @db.Text + author String + publishDate String + thumbnailUrl String? + readTime String? + tags String[] + status String @default("draft") // draft, published + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/Channel-Backend/seed.ts b/Channel-Backend/seed.ts index 5e50ffc..6e106c8 100644 --- a/Channel-Backend/seed.ts +++ b/Channel-Backend/seed.ts @@ -174,6 +174,36 @@ async function seed() { console.log(`Partner (Approved) already exists: ${activeEmail}`); } + // 7. Seed Blog Posts + const blogCount = await prisma.blogPost.count(); + if (blogCount === 0) { + await prisma.blogPost.createMany({ + data: [ + { + title: "Unlocking Ultra-Low Latency: Synthesis of RISC-V in Edge Devices", + content: "As edge intelligence grows, local compute units require custom processor topologies. In this article, we details the exact synthesis settings and pipelining optimizations that enabled our quad-core RISC-V IP block to achieve 35% better performance per watt compared to baseline architectures. We review cache organization, instruction fetch queue sizing, and how we tackled branch prediction overheads within tightly constrained FPGA silicon boundaries.", + author: "Dr. Marcus Vance", + publishDate: "2026-06-18", + thumbnailUrl: "https://images.unsplash.com/photo-1601524909162-be87252be298?w=500&auto=format&fit=crop&q=60", + readTime: "6 min read", + tags: ["RISC-V", "Hardware-Design", "Edge-AI"], + status: "published" + }, + { + title: "Introduction to CodeNuk: Scalable Microservice Architecture", + content: "Building distributed systems often involves navigating high configuration overhead. CodeNuk solves this by providing a unified, type-safe scaffolding that integrates telemetry, connection pools, and circuit-breakers out of the box. This deep-dive explains how CodeNuk leverages TypeScript decorators to declare service endpoints and automatically generate OpenAPI contracts and React Client hooks during the build phase, saving engineering weeks.", + author: "Yasha Khandelwal", + publishDate: "2026-06-25", + thumbnailUrl: "https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=500&auto=format&fit=crop&q=60", + readTime: "8 min read", + tags: ["CodeNuk", "TypeScript", "Microservices"], + status: "published" + } + ] + }); + console.log('Seeded Blog Posts.'); + } + console.log('Seeding completed successfully.'); } diff --git a/Channel-Backend/src/app.ts b/Channel-Backend/src/app.ts index 7d5da48..3ec89a6 100644 --- a/Channel-Backend/src/app.ts +++ b/Channel-Backend/src/app.ts @@ -15,6 +15,7 @@ import authRoutes from './routes/auth.routes'; import assetRoutes from './routes/asset.routes'; import orgRoutes from './routes/organization.routes'; import legalRoutes from './routes/legal.routes'; +import blogRoutes from './routes/blog.routes'; import { ensureBucketExists } from './utils/s3'; @@ -82,6 +83,7 @@ app.use('/api/v1/auth', authRoutes); app.use('/api/v1/assets', assetRoutes); app.use('/api/v1/organizations', orgRoutes); app.use('/api/v1/legal', legalRoutes); +app.use('/api/v1/blog', blogRoutes); app.get('/api/v1/health', (req: Request, res: Response) => { res.status(200).json({ status: 'success', message: 'API is fully functional and real.' }); diff --git a/Channel-Backend/src/controllers/blog.controller.ts b/Channel-Backend/src/controllers/blog.controller.ts new file mode 100644 index 0000000..cd7677d --- /dev/null +++ b/Channel-Backend/src/controllers/blog.controller.ts @@ -0,0 +1,91 @@ +import { Response, NextFunction } from 'express'; +import { BlogService } from '../services/blog.service'; +import { AuthRequest } from '../middleware/auth.middleware'; +import prisma from '../utils/db'; + +export class BlogController { + private blogService = new BlogService(); + + public listBlogPosts = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { status, tag, search, author } = req.query; + + const filters: any = {}; + if (typeof tag === 'string') filters.tag = tag; + if (typeof search === 'string') filters.search = search; + if (typeof author === 'string') filters.author = author; + + // Restrict status access if user is not admin + if (req.user?.role !== 'ADMIN') { + filters.status = 'published'; + } else if (typeof status === 'string') { + filters.status = status; + } + + const posts = await this.blogService.getBlogPosts(filters); + res.status(200).json(posts); + } catch (err) { + next(err); + } + }; + + public getBlogPost = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const post = await this.blogService.getBlogPostById(req.params.id); + if (!post) { + return res.status(404).json({ error: 'Blog post not found' }); + } + + // Clients cannot view drafts + if (post.status !== 'published' && req.user?.role !== 'ADMIN') { + return res.status(403).json({ error: 'Access forbidden' }); + } + + res.status(200).json(post); + } catch (err) { + next(err); + } + }; + + public createBlogPost = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + let authorVal = req.body.author || 'Technical Architect'; + if (!req.body.author && req.user?.userId) { + const user = await prisma.user.findUnique({ + where: { id: req.user.userId } + }); + if (user) { + authorVal = user.email; + } + } + + const postData = { + ...req.body, + author: authorVal, + }; + + const post = await this.blogService.createBlogPost(postData); + res.status(201).json(post); + } catch (err) { + next(err); + } + }; + + public updateBlogPost = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const post = await this.blogService.updateBlogPost(req.params.id, req.body); + res.status(200).json(post); + } catch (err) { + next(err); + } + }; + + public deleteBlogPost = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + await this.blogService.deleteBlogPost(req.params.id); + res.status(204).send(); + } catch (err) { + next(err); + } + }; +} diff --git a/Channel-Backend/src/routes/blog.routes.ts b/Channel-Backend/src/routes/blog.routes.ts new file mode 100644 index 0000000..9a34d0e --- /dev/null +++ b/Channel-Backend/src/routes/blog.routes.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import { BlogController } from '../controllers/blog.controller'; +import { authenticate, requireRole } from '../middleware/auth.middleware'; + +const router = Router(); +const blogController = new BlogController(); + +router.use(authenticate); + +router.get('/', blogController.listBlogPosts); +router.get('/:id', blogController.getBlogPost); +router.post('/new', requireRole('ADMIN'), blogController.createBlogPost); +router.patch('/:id', requireRole('ADMIN'), blogController.updateBlogPost); +router.delete('/:id', requireRole('ADMIN'), blogController.deleteBlogPost); + +export default router; diff --git a/Channel-Backend/src/services/blog.service.ts b/Channel-Backend/src/services/blog.service.ts new file mode 100644 index 0000000..786c192 --- /dev/null +++ b/Channel-Backend/src/services/blog.service.ts @@ -0,0 +1,124 @@ +import prisma from '../utils/db'; + +export interface BlogPostFilters { + status?: string; + tag?: string; + search?: string; + author?: string; +} + +export class BlogService { + public async getBlogPosts(filters: BlogPostFilters = {}) { + const { status, tag, search, author } = filters; + const where: any = {}; + + if (status) { + where.status = status; + } + + if (tag) { + where.tags = { + has: tag, + }; + } + + if (author) { + where.author = { + contains: author, + mode: 'insensitive', + }; + } + + if (search) { + where.OR = [ + { title: { contains: search, mode: 'insensitive' } }, + { content: { contains: search, mode: 'insensitive' } }, + ]; + } + + return await prisma.blogPost.findMany({ + where, + orderBy: { + publishDate: 'desc', + }, + }); + } + + public async getBlogPostById(id: string) { + return await prisma.blogPost.findUnique({ + where: { id }, + }); + } + + public async createBlogPost(data: any) { + const { tags, content, ...rest } = data; + + // Parse tags + let parsedTags: string[] = []; + if (Array.isArray(tags)) { + parsedTags = tags; + } else if (typeof tags === 'string' && tags.trim()) { + try { + parsedTags = JSON.parse(tags); + } catch { + parsedTags = tags.split(',').map((t: string) => t.trim()).filter(Boolean); + } + } + + // Auto-calculate read time + const wordsPerMinute = 200; + const wordCount = content ? content.trim().split(/\s+/).length : 0; + const readTimeVal = `${Math.max(1, Math.ceil(wordCount / wordsPerMinute))} min read`; + + // Default publishDate if not provided + const publishDateVal = rest.publishDate || new Date().toISOString().split('T')[0]; + + return await prisma.blogPost.create({ + data: { + ...rest, + content: content || '', + tags: parsedTags, + readTime: readTimeVal, + publishDate: publishDateVal, + }, + }); + } + + public async updateBlogPost(id: string, data: any) { + const { tags, content, ...rest } = data; + const updateData: any = { ...rest }; + + if (content !== undefined) { + updateData.content = content; + // Auto-calculate read time + const wordsPerMinute = 200; + const wordCount = content ? content.trim().split(/\s+/).length : 0; + updateData.readTime = `${Math.max(1, Math.ceil(wordCount / wordsPerMinute))} min read`; + } + + if (tags !== undefined) { + let parsedTags: string[] = []; + if (Array.isArray(tags)) { + parsedTags = tags; + } else if (typeof tags === 'string') { + try { + parsedTags = JSON.parse(tags); + } catch { + parsedTags = tags.split(',').map((t: string) => t.trim()).filter(Boolean); + } + } + updateData.tags = parsedTags; + } + + return await prisma.blogPost.update({ + where: { id }, + data: updateData, + }); + } + + public async deleteBlogPost(id: string) { + return await prisma.blogPost.delete({ + where: { id }, + }); + } +} diff --git a/Channel-Frontend/src/components/ui/DocumentPreviewModal.tsx b/Channel-Frontend/src/components/ui/DocumentPreviewModal.tsx index d0645cd..6a28863 100644 --- a/Channel-Frontend/src/components/ui/DocumentPreviewModal.tsx +++ b/Channel-Frontend/src/components/ui/DocumentPreviewModal.tsx @@ -1,12 +1,23 @@ -import React, { useState, useEffect, useRef } from 'react'; -import ReactDOM from 'react-dom'; -import { motion } from 'framer-motion'; -import { - X, ZoomIn, ZoomOut, RotateCcw, Download, Printer, - ShieldCheck, AlertTriangle, CheckCircle, ChevronLeft, - ChevronRight, FileText, Maximize, Minimize -} from 'lucide-react'; -import { Button } from './Button'; +import React, { useState, useEffect, useRef } from "react"; +import ReactDOM from "react-dom"; +import { motion } from "framer-motion"; +import { + X, + ZoomIn, + ZoomOut, + RotateCcw, + Download, + Printer, + ShieldCheck, + AlertTriangle, + CheckCircle, + ChevronLeft, + ChevronRight, + FileText, + Maximize, + Minimize, +} from "lucide-react"; +import { Button } from "./Button"; interface Acceptance { id: string; @@ -31,7 +42,7 @@ interface DocumentPreviewModalProps { partnerCreatedAt: string; acceptances: Acceptance[]; verifiedDocs: { nda: boolean; msa: boolean }; - onVerify: (docType: 'NDA' | 'MSA') => void; + onVerify: (docType: "NDA" | "MSA") => void; onApprovePartner: () => void; isApproving: boolean; } @@ -48,29 +59,29 @@ export const DocumentPreviewModal: React.FC = ({ onApprovePartner, isApproving, }) => { - const [currentTab, setCurrentTab] = useState<'NDA' | 'MSA'>('NDA'); + const [currentTab, setCurrentTab] = useState<"NDA" | "MSA">("NDA"); const [zoom, setZoom] = useState(100); const [fitWidth, setFitWidth] = useState(false); const [currentPage, setCurrentPage] = useState(1); const [isFullScreen, setIsFullScreen] = useState(false); - + const modalRef = useRef(null); const containerRef = useRef(null); - const ndaAcceptance = acceptances.find(a => a.document.type === 'NDA'); - const msaAcceptance = acceptances.find(a => a.document.type === 'MSA'); - const activeAcceptance = currentTab === 'NDA' ? ndaAcceptance : msaAcceptance; + const ndaAcceptance = acceptances.find((a) => a.document.type === "NDA"); + const msaAcceptance = acceptances.find((a) => a.document.type === "MSA"); + const activeAcceptance = currentTab === "NDA" ? ndaAcceptance : msaAcceptance; const totalPages = activeAcceptance?.documentUrl ? 1 : 2; // Prevent background page scrolling when modal is open useEffect(() => { if (isOpen) { - document.body.style.overflow = 'hidden'; + document.body.style.overflow = "hidden"; } else { - document.body.style.overflow = ''; + document.body.style.overflow = ""; } return () => { - document.body.style.overflow = ''; + document.body.style.overflow = ""; }; }, [isOpen]); @@ -108,19 +119,20 @@ export const DocumentPreviewModal: React.FC = ({ const handleFullscreenChange = () => { setIsFullScreen(!!document.fullscreenElement); }; - document.addEventListener('fullscreenchange', handleFullscreenChange); - return () => document.removeEventListener('fullscreenchange', handleFullscreenChange); + document.addEventListener("fullscreenchange", handleFullscreenChange); + return () => + document.removeEventListener("fullscreenchange", handleFullscreenChange); }, []); if (!isOpen) return null; const handleZoomIn = () => { setFitWidth(false); - setZoom(prev => Math.min(200, prev + 25)); + setZoom((prev) => Math.min(200, prev + 25)); }; const handleZoomOut = () => { setFitWidth(false); - setZoom(prev => Math.max(50, prev - 25)); + setZoom((prev) => Math.max(50, prev - 25)); }; const handleZoomReset = () => { setFitWidth(false); @@ -131,12 +143,16 @@ export const DocumentPreviewModal: React.FC = ({ }; const handlePrint = () => { - const printContent = document.getElementById('printable-doc-content'); + const printContent = document.getElementById("printable-doc-content"); if (!printContent) return; - const windowUrl = 'about:blank'; + const windowUrl = "about:blank"; const uniqueName = new Date().getTime(); - const printWindow = window.open(windowUrl, uniqueName.toString(), 'left=50000,top=50000,width=0,height=0'); + const printWindow = window.open( + windowUrl, + uniqueName.toString(), + "left=50000,top=50000,width=0,height=0", + ); if (!printWindow) return; printWindow.document.write(` @@ -153,14 +169,14 @@ export const DocumentPreviewModal: React.FC = ({ -

${currentTab === 'NDA' ? 'Non-Disclosure Agreement (NDA)' : 'Master Services Agreement (MSA)'}

-

${activeAcceptance?.document.content || ''}

+

${currentTab === "NDA" ? "Non-Disclosure Agreement (NDA)" : "Master Services Agreement (MSA)"}

+

${activeAcceptance?.document.content || ""}

Digitally Signed & Verified
Signed By: ${partnerEmail}
-
Verification Hash: ${activeAcceptance?.signatureHash || 'N/A'}
-
IP Address: ${activeAcceptance?.ipAddress || 'N/A'}
-
Date Signed: ${activeAcceptance ? new Date(activeAcceptance.acceptedAt).toLocaleString() : ''}
+
Verification Hash: ${activeAcceptance?.signatureHash || "N/A"}
+
IP Address: ${activeAcceptance?.ipAddress || "N/A"}
+
Date Signed: ${activeAcceptance ? new Date(activeAcceptance.acceptedAt).toLocaleString() : ""}
@@ -174,30 +190,36 @@ export const DocumentPreviewModal: React.FC = ({ const handleDownloadText = () => { if (!activeAcceptance) return; const element = document.createElement("a"); - const file = new Blob([ - `${currentTab === 'NDA' ? 'Non-Disclosure Agreement (NDA)' : 'Master Services Agreement (MSA)'}\n\n`, - activeAcceptance.document.content, - `\n\n=== DIGITAL SIGNATURE ===\n`, - `Signed By: ${partnerEmail}\n`, - `Verification Hash: ${activeAcceptance.signatureHash || 'N/A'}\n`, - `IP Address: ${activeAcceptance.ipAddress}\n`, - `Signed On: ${new Date(activeAcceptance.acceptedAt).toLocaleString()}\n` - ], {type: 'text/plain'}); + const file = new Blob( + [ + `${currentTab === "NDA" ? "Non-Disclosure Agreement (NDA)" : "Master Services Agreement (MSA)"}\n\n`, + activeAcceptance.document.content, + `\n\n=== DIGITAL SIGNATURE ===\n`, + `Signed By: ${partnerEmail}\n`, + `Verification Hash: ${activeAcceptance.signatureHash || "N/A"}\n`, + `IP Address: ${activeAcceptance.ipAddress}\n`, + `Signed On: ${new Date(activeAcceptance.acceptedAt).toLocaleString()}\n`, + ], + { type: "text/plain" }, + ); element.href = URL.createObjectURL(file); - element.download = `${currentTab}_Agreement_${partnerEmail.split('@')[0]}.txt`; + element.download = `${currentTab}_Agreement_${partnerEmail.split("@")[0]}.txt`; document.body.appendChild(element); element.click(); document.body.removeChild(element); }; - const fileHost = (import.meta.env.VITE_API_URL || 'http://localhost:5000/api/v1').replace('/api/v1', ''); + const fileHost = ( + import.meta.env.VITE_API_URL || "http://localhost:5000/api/v1" + ).replace("/api/v1", ""); const isBothVerified = verifiedDocs.nda && verifiedDocs.msa; - const isCurrentVerified = currentTab === 'NDA' ? verifiedDocs.nda : verifiedDocs.msa; + const isCurrentVerified = + currentTab === "NDA" ? verifiedDocs.nda : verifiedDocs.msa; // Responsive classes based on screen size const modalContainerClasses = isFullScreen - ? 'fixed inset-0 w-screen h-screen bg-ink-0 z-[9999] flex flex-col overflow-hidden' - : 'relative w-full h-full sm:w-[90vw] sm:h-[88vh] lg:w-[78vw] lg:h-[86vh] bg-ink-0 border border-ink-200 sm:rounded-2xl shadow-2xl flex flex-col overflow-hidden z-[9999]'; + ? "fixed inset-0 w-screen h-screen bg-ink-0 z-[9999] flex flex-col overflow-hidden" + : "relative w-full h-full sm:w-[90vw] sm:h-[88vh] lg:w-[78vw] lg:h-[86vh] bg-ink-0 border border-ink-200 sm:rounded-2xl shadow-2xl flex flex-col overflow-hidden z-[9999]"; return ReactDOM.createPortal(
@@ -216,7 +238,7 @@ export const DocumentPreviewModal: React.FC = ({ initial={{ opacity: 0, scale: 0.97, y: 15 }} animate={{ opacity: 1, scale: 1, y: 0 }} exit={{ opacity: 0, scale: 0.97, y: 15 }} - transition={{ type: 'spring', damping: 26, stiffness: 340 }} + transition={{ type: "spring", damping: 26, stiffness: 340 }} className={modalContainerClasses} > {/* Sticky Header */} @@ -227,7 +249,9 @@ export const DocumentPreviewModal: React.FC = ({

- {currentTab === 'NDA' ? 'Mutual Non-Disclosure Agreement (NDA)' : 'Master Services Agreement (MSA)'} + {currentTab === "NDA" + ? "Mutual Non-Disclosure Agreement (NDA)" + : "Master Services Agreement (MSA)"}

Partner: {partnerEmail} @@ -239,29 +263,29 @@ export const DocumentPreviewModal: React.FC = ({

@@ -271,7 +295,11 @@ export const DocumentPreviewModal: React.FC = ({ className="p-2 rounded-lg text-ink-500 hover:text-ink-900 hover:bg-ink-50 transition-all cursor-pointer" title={isFullScreen ? "Exit Fullscreen" : "Fullscreen"} > - {isFullScreen ? : } + {isFullScreen ? ( + + ) : ( + + )} - Page {activeAcceptance ? currentPage : 0} of {activeAcceptance ? totalPages : 0} + Page {activeAcceptance ? currentPage : 0} of{" "} + {activeAcceptance ? totalPages : 0}