Dashboard App
Admin dashboard and content management system
Dashboard App
Admin interface for managing Portfolio OS content at apps/dashboard.
Overview
URL: http://localhost:3001 (development)
The dashboard provides a content management system with media library, analytics, and admin tools.
Features
- 📝 Content Management: Create and edit posts
- 🖼️ Media Library: Upload and manage images/files
- 📊 Analytics: View site metrics
- ✍️ Rich Text Editor: TipTap WYSIWYG editor
- 🗄️ Database: Prisma ORM with SQLite/PostgreSQL
- 🔐 Authentication: Session-based admin access
Tech Stack
- Next.js 14 App Router
- Prisma ORM
- SQLite (dev) / PostgreSQL (prod)
- Vercel Blob Storage
- TipTap Editor
- Tailwind CSS
Directory Structure
Database Schema
// apps/dashboard/prisma/schema.prisma
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id String @id @default(cuid())
title String
content String
slug String @unique
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Media {
id String @id @default(cuid())
url String
filename String
size Int
type String
createdAt DateTime @default(now())
}
Key Features
Content Editor
// apps/dashboard/components/Editor.tsx
'use client'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
export function Editor({ content, onChange }: EditorProps) {
const editor = useEditor({
extensions: [StarterKit],
content,
onUpdate: ({ editor }) => {
onChange(editor.getHTML())
},
})
return <EditorContent editor={editor} />
}
Media Upload
// apps/dashboard/components/MediaUploader.tsx
'use client'
import { useState } from 'react'
import { put } from '@vercel/blob'
export function MediaUploader() {
async function handleUpload(file: File) {
const blob = await put(file.name, file, {
access: 'public',
})
// Save to database
await fetch('/api/media', {
method: 'POST',
body: JSON.stringify({
url: blob.url,
filename: file.name,
size: file.size,
type: file.type,
}),
})
}
return <input type="file" onChange={(e) => handleUpload(e.target.files[0])} />
}
API Routes
Posts CRUD
// apps/dashboard/app/api/posts/route.ts
import { prisma } from '@mindware-blog/db'
import { NextResponse } from 'next/server'
export async function GET() {
const posts = await prisma.post.findMany({
orderBy: { createdAt: 'desc' },
})
return NextResponse.json({ posts })
}
export async function POST(request: Request) {
const data = await request.json()
const post = await prisma.post.create({ data })
return NextResponse.json({ post })
}
Development
# Setup database
cd apps/dashboard
pnpm prisma generate
pnpm prisma migrate dev
# Start dev server
pnpm dev
# Open Prisma Studio
pnpm prisma studio
Deployment
# Migrate production database
pnpm prisma migrate deploy
# Deploy to Vercel
vercel --prod
Next Steps
- Site App - Public site
- Database Setup - Database configuration
- Deployment - Production deployment