diff --git a/Channel-Backend/prisma/schema.prisma b/Channel-Backend/prisma/schema.prisma index 03fdde0..e4764d6 100644 --- a/Channel-Backend/prisma/schema.prisma +++ b/Channel-Backend/prisma/schema.prisma @@ -40,6 +40,11 @@ model User { partnerGroup String? assignedNdaId String? assignedMsaId String? + website String? + sector String? + companySize String? + defaultTheme String? @default("dark") + companyName String? assignedNda LegalDocument? @relation("AssignedNda", fields: [assignedNdaId], references: [id], onDelete: SetNull) assignedMsa LegalDocument? @relation("AssignedMsa", fields: [assignedMsaId], references: [id], onDelete: SetNull) organization Organization? @relation(fields: [organizationId], references: [id]) @@ -150,20 +155,6 @@ model AuditLog { 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 -} - model AssetGroup { id String @id @default(uuid()) name String @unique diff --git a/Channel-Backend/seed.ts b/Channel-Backend/seed.ts index b390eb9..6491305 100644 --- a/Channel-Backend/seed.ts +++ b/Channel-Backend/seed.ts @@ -113,37 +113,6 @@ async function seed() { } else { 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 35db76c..8396494 100644 --- a/Channel-Backend/src/app.ts +++ b/Channel-Backend/src/app.ts @@ -15,7 +15,6 @@ 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'; import { originStorage } from './utils/origin-storage'; @@ -108,7 +107,6 @@ 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/auth.controller.ts b/Channel-Backend/src/controllers/auth.controller.ts index 21eb797..4a5eb9d 100644 --- a/Channel-Backend/src/controllers/auth.controller.ts +++ b/Channel-Backend/src/controllers/auth.controller.ts @@ -60,8 +60,8 @@ export class AuthController { const result = await this.authService.updatePartner(partnerId, { partnerGroup: (partnerGroup === null || partnerGroup === '') ? null : partnerGroup, - assignedNdaId: assignedNdaId === null ? undefined : assignedNdaId, - assignedMsaId: assignedMsaId === null ? undefined : assignedMsaId, + assignedNdaId: assignedNdaId === undefined ? undefined : assignedNdaId, + assignedMsaId: assignedMsaId === undefined ? undefined : assignedMsaId, sharedAssetIds, mfaEnabled, }); @@ -153,5 +153,24 @@ export class AuthController { res.status(200).json(user); } catch(err) { next(err); } }; + + public updateProfile = async (req: Request, res: Response, next: NextFunction) => { + try { + const userId = (req as any).user?.userId; + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + const { password, companyName, website, sector, companySize, defaultTheme } = req.body; + const updatedUser = await this.authService.updateProfile(userId, { + password, + companyName, + website, + sector, + companySize, + defaultTheme + }); + res.status(200).json(updatedUser); + } catch (err) { next(err); } + }; } diff --git a/Channel-Backend/src/controllers/blog.controller.ts b/Channel-Backend/src/controllers/blog.controller.ts deleted file mode 100644 index cd7677d..0000000 --- a/Channel-Backend/src/controllers/blog.controller.ts +++ /dev/null @@ -1,91 +0,0 @@ -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/auth.routes.ts b/Channel-Backend/src/routes/auth.routes.ts index 622bcdb..80bed41 100644 --- a/Channel-Backend/src/routes/auth.routes.ts +++ b/Channel-Backend/src/routes/auth.routes.ts @@ -12,6 +12,7 @@ router.post('/refresh', authController.refresh); // Invite Flow router.get('/me', authenticate, authController.getCurrentUser); +router.put('/profile', authenticate, authController.updateProfile); router.post('/invite', authenticate, requireRole('ADMIN'), authController.invitePartner); router.get('/invite/:token', authController.validateInvite); router.post('/invite/accept', authController.acceptInvite); diff --git a/Channel-Backend/src/routes/blog.routes.ts b/Channel-Backend/src/routes/blog.routes.ts deleted file mode 100644 index 9a34d0e..0000000 --- a/Channel-Backend/src/routes/blog.routes.ts +++ /dev/null @@ -1,16 +0,0 @@ -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/auth.service.ts b/Channel-Backend/src/services/auth.service.ts index 898ed71..41625a2 100644 --- a/Channel-Backend/src/services/auth.service.ts +++ b/Channel-Backend/src/services/auth.service.ts @@ -442,6 +442,11 @@ export class AuthService { organizationId: true, onboardingStatus: true, partnerGroup: true, + website: true, + sector: true, + companySize: true, + defaultTheme: true, + companyName: true, createdAt: true, updatedAt: true, assignedNdaId: true, @@ -474,6 +479,11 @@ export class AuthService { organizationId: true, onboardingStatus: true, partnerGroup: true, + website: true, + sector: true, + companySize: true, + defaultTheme: true, + companyName: true, createdAt: true, updatedAt: true, assignedNdaId: true, @@ -490,6 +500,102 @@ export class AuthService { } } + if (user && user.role === 'PARTNER_USER' && user.organizationId) { + const sharedAssets = await prisma.sharedAsset.findMany({ + where: { + organizationId: user.organizationId, + OR: [ + { userId: null }, + { userId: user.id } + ] + }, + select: { + assetId: true, + asset: { + select: { + id: true, + title: true, + type: true, + categoryId: true, + subcategory: true, + url: true + } + } + } + }); + return { + ...user, + sharedAssets + }; + } + return user; } + + public async updateProfile(userId: string, data: { + password?: string; + companyName?: string; + website?: string; + sector?: string; + companySize?: string; + defaultTheme?: string; + }) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { organization: true } + }); + + if (!user) throw new AppError('User not found', 404); + + const updateData: any = {}; + + if (data.password) { + updateData.passwordHash = await bcrypt.hash(data.password, 10); + } + if (data.website !== undefined) updateData.website = data.website; + if (data.sector !== undefined) updateData.sector = data.sector; + if (data.companySize !== undefined) updateData.companySize = data.companySize; + if (data.defaultTheme !== undefined) updateData.defaultTheme = data.defaultTheme; + if (data.companyName !== undefined) { + updateData.companyName = data.companyName; + if (user.organizationId) { + await prisma.organization.update({ + where: { id: user.organizationId }, + data: { name: data.companyName } + }); + } + } + + const updated = await prisma.user.update({ + where: { id: userId }, + data: updateData, + select: { + id: true, + email: true, + role: true, + mfaEnabled: true, + organizationId: true, + onboardingStatus: true, + partnerGroup: true, + website: true, + sector: true, + companySize: true, + defaultTheme: true, + companyName: true, + createdAt: true, + updatedAt: true, + assignedNdaId: true, + assignedMsaId: true, + organization: { + select: { + id: true, + name: true, + status: true, + } + } + } + }); + + return this.getUserById(userId); + } } diff --git a/Channel-Backend/src/services/blog.service.ts b/Channel-Backend/src/services/blog.service.ts deleted file mode 100644 index 786c192..0000000 --- a/Channel-Backend/src/services/blog.service.ts +++ /dev/null @@ -1,124 +0,0 @@ -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/app/layouts/AdminLayout.tsx b/Channel-Frontend/src/app/layouts/AdminLayout.tsx index b687f2b..0b919c7 100644 --- a/Channel-Frontend/src/app/layouts/AdminLayout.tsx +++ b/Channel-Frontend/src/app/layouts/AdminLayout.tsx @@ -6,7 +6,6 @@ import { ShieldCheck, ClipboardCheck, FolderGit2, - BookCopy, Users, LogOut, Menu, @@ -36,7 +35,6 @@ export const AdminLayout: React.FC = () => { { name: "Approvals Queue", path: "/admin/approvals", icon: ClipboardCheck }, { name: "Legal Templates", path: "/admin/legal", icon: ShieldCheck }, { name: "Manage Catalog", path: "/admin/assets", icon: FolderGit2 }, - { name: "Blog CMS", path: "/admin/blog", icon: BookCopy }, ]; return ( diff --git a/Channel-Frontend/src/app/layouts/ClientLayout.tsx b/Channel-Frontend/src/app/layouts/ClientLayout.tsx index 33bb118..65256a7 100644 --- a/Channel-Frontend/src/app/layouts/ClientLayout.tsx +++ b/Channel-Frontend/src/app/layouts/ClientLayout.tsx @@ -2,12 +2,11 @@ import React, { useState } from 'react'; import { Link, Outlet, useLocation, useNavigate } from 'react-router-dom'; import { useThemeStore } from '../../hooks/use-theme'; import { useAuthStore } from '../../hooks/use-auth'; -import { Cpu, BookOpen, LogOut, CheckCircle, Clock, Menu, X, Sun, Moon, ChevronRight, ChevronLeft } from 'lucide-react'; +import { Cpu, LogOut, CheckCircle, Clock, Menu, X, Sun, Moon, ChevronRight, ChevronLeft, Settings } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; const navItems = [ { name: 'Assets', path: '/client', icon: Cpu, label: 'Asset Explorer' }, - { name: 'Blog', path: '/client/blog', icon: BookOpen, label: 'Insights Blog' }, ]; export const ClientLayout: React.FC = () => { @@ -18,6 +17,66 @@ export const ClientLayout: React.FC = () => { const [mobileOpen, setMobileOpen] = useState(false); const [isCollapsed, setIsCollapsed] = useState(false); + // Settings Modal State + const [settingsOpen, setSettingsOpen] = useState(false); + const [defaultTheme, setDefaultTheme] = useState(user?.defaultTheme || 'dark'); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isSaving, setIsSaving] = useState(false); + const [settingsError, setSettingsError] = useState(''); + const [settingsSuccess, setSettingsSuccess] = useState(''); + + const openSettings = () => { + setDefaultTheme(user?.defaultTheme || 'dark'); + setPassword(''); + setConfirmPassword(''); + setSettingsError(''); + setSettingsSuccess(''); + setSettingsOpen(true); + }; + + const handleSaveSettings = async (e: React.FormEvent) => { + e.preventDefault(); + setSettingsError(''); + setSettingsSuccess(''); + + if (password && password !== confirmPassword) { + setSettingsError("Passwords do not match"); + return; + } + + setIsSaving(true); + try { + const { updateProfile } = await import('../../services/auth-api'); + const updatedUser = await updateProfile({ + defaultTheme: defaultTheme || undefined, + password: password || undefined + }); + + // Update auth store + useAuthStore.getState().setAuth({ + user: updatedUser, + accessToken: useAuthStore.getState().accessToken! + }); + + if (defaultTheme) { + useThemeStore.getState().setTheme(defaultTheme as any); + } + + setSettingsSuccess("Profile settings updated successfully!"); + setPassword(''); + setConfirmPassword(''); + + setTimeout(() => { + setSettingsOpen(false); + }, 1500); + } catch (err: any) { + setSettingsError(err.response?.data?.error || err.message || "Failed to update profile settings"); + } finally { + setIsSaving(false); + } + }; + const handleLogout = () => { logout(); navigate('/login'); @@ -78,6 +137,17 @@ export const ClientLayout: React.FC = () => { ); })} + + {/* Footer */} @@ -110,6 +180,8 @@ export const ClientLayout: React.FC = () => { )} + {/* Profile Settings button removed from here */} + - + {/* Mobile Menu Drawer */} {mobileOpen && ( @@ -162,6 +234,13 @@ export const ClientLayout: React.FC = () => { {item.label} ))} +
+
+ + {settingsError && ( +
+ {settingsError} +
+ )} + {settingsSuccess && ( +
+ {settingsSuccess} +
+ )} + +
+ {/* Read-only Email Field */} +
+ + +

Email address cannot be changed.

+
+ + {/* Company Profile Fields Removed */} + + {/* Theme settings */} +
+ + +
+ + {/* Security Fields */} +
+

Change Password

+
+
+ + setPassword(e.target.value)} + className="w-full px-4 py-2.5 rounded-xl bg-ink-50 border border-ink-200 focus:border-ink-400 focus:bg-ink-0 text-sm font-bold transition-all outline-none" + placeholder="••••••••" + /> +
+
+ + setConfirmPassword(e.target.value)} + className="w-full px-4 py-2.5 rounded-xl bg-ink-50 border border-ink-200 focus:border-ink-400 focus:bg-ink-0 text-sm font-bold transition-all outline-none" + placeholder="••••••••" + /> +
+
+
+ + {/* Action Buttons */} +
+ + +
+
+ + + )} +
); }; diff --git a/Channel-Frontend/src/app/router/index.tsx b/Channel-Frontend/src/app/router/index.tsx index 351c576..8aaa532 100644 --- a/Channel-Frontend/src/app/router/index.tsx +++ b/Channel-Frontend/src/app/router/index.tsx @@ -41,11 +41,7 @@ const LegalTemplatesPage = React.lazy(() => default: m.LegalTemplatesPage, })), ); -const BlogCatalog = React.lazy(() => - import("../../features/blog/components/BlogCatalog").then((m) => ({ - default: m.BlogCatalog, - })), -); + const ClientAgreementsPage = React.lazy(() => import("../../pages/ClientAgreementsPage").then((m) => ({ default: m.ClientAgreementsPage, @@ -135,14 +131,6 @@ export const router = createBrowserRouter([ ), }, - { - path: "blog", - element: ( - }> - - - ), - }, ], }, { @@ -177,14 +165,6 @@ export const router = createBrowserRouter([ ), }, - { - path: "blog", - element: ( - }> - - - ), - }, { path: "approvals", element: ( diff --git a/Channel-Frontend/src/features/assets/components/AssetCard.tsx b/Channel-Frontend/src/features/assets/components/AssetCard.tsx index a8db2b1..d55e5b5 100644 --- a/Channel-Frontend/src/features/assets/components/AssetCard.tsx +++ b/Channel-Frontend/src/features/assets/components/AssetCard.tsx @@ -156,7 +156,9 @@ export const AssetCard: React.FC = ({ className={`group bg-ink-0 border rounded-xl p-4 transition-all duration-300 flex flex-col justify-between cursor-pointer min-h-[410px] ${ isExpanded ? 'absolute z-20 top-0 left-0 right-0 h-auto shadow-2xl border-ink-300 bg-ink-0' - : 'relative w-full h-full border-ink-200 hover:border-ink-300 hover:shadow-md hover:-translate-y-0.5' + : isRecommended + ? 'relative w-full h-full border-amber-500/35 bg-gradient-to-br from-amber-500/[0.02] via-ink-0 to-ink-0 hover:border-amber-500 hover:shadow-lg hover:shadow-amber-500/10 hover:-translate-y-0.5' + : 'relative w-full h-full border-ink-200 hover:border-ink-300 hover:shadow-md hover:-translate-y-0.5' } ${isSelected ? 'border-ink-900 ring-1 ring-ink-900 bg-ink-50/30' : ''}`} >
@@ -175,7 +177,7 @@ export const AssetCard: React.FC = ({ {isRecommended && (
- Recommended + Recommended for You
)}
diff --git a/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx b/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx index 7d64b04..ad7f785 100644 --- a/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx +++ b/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx @@ -274,7 +274,10 @@ export const AssetViewerModal: React.FC = ({ } size="full" - className={isMaximized ? '!max-w-[96vw] !max-h-[92vh] !h-[92vh] !mt-4' : '!max-w-[85vw] md:!max-w-[80vw] xl:!max-w-[75vw] !h-[75vh] !max-h-[75vh] !w-full'} + className={isMaximized + ? '!fixed !inset-0 !z-[10001] !max-w-none !max-h-none !w-screen !h-screen !rounded-none !border-none !m-0' + : '!max-w-[85vw] md:!max-w-[80vw] xl:!max-w-[75vw] !h-[75vh] !max-h-[75vh] !w-full' + } footer={ <> - - ) : undefined; - - return ( - -
- {loading ? ( -
- {[1, 2].map((n) => ( -
-
-
-
-
- ))} -
- ) : posts.length === 0 ? ( -
- -

No blog posts published yet.

-
- ) : ( -
- {posts.map((post) => ( -
-
- {post.title} - {isAdmin && ( - - {post.status} - - )} -
-
-
-
- - - {post.author} - - - - {post.publishDate} - - - - {post.readTime} - -
-

- {post.title} -

-

- {post.content} -

-
-
- {post.tags.map((t) => ( - - #{t} - - ))} -
-
-
- ))} -
- )} -
- - {/* Post Creator Modal */} - setIsOpen(false)} - title={ - - - Write Blog Article - - } - subtitle="Draft or publish a technical write-up for the developer channel." - size="md" - > - {formError && ( -
- {formError} -
- )} - -
-
- - setTitle(e.target.value)} - placeholder="e.g. Optimizing Pipeline Hazards in RV64GC Core Designs" - className="w-full rounded-lg border border-ink-200 px-3.5 py-2 text-sm focus:border-ink-900/50 focus:outline-none bg-ink-50 text-ink-900 placeholder-ink-400" - required - /> -
- -
- -