mission-control/app/documents/[id]/page.tsx

194 lines
6.0 KiB
TypeScript

import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Link from 'next/link';
import { createClient } from '@supabase/supabase-js';
import { DashboardLayout } from '@/components/layout/sidebar';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ArrowLeft, FileText, FileType, FileIcon, FileCode, Edit3, Trash2, Clock, Folder } from 'lucide-react';
import { cn } from '@/lib/utils';
import { Document, DocumentType, DOCUMENT_TYPE_COLORS } from '@/types/documents';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
const typeIcons: Record<DocumentType, typeof FileText> = {
markdown: FileType,
text: FileText,
pdf: FileIcon,
code: FileCode,
};
function getSupabaseServerClient() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (!supabaseUrl || !supabaseServiceKey) {
throw new Error('Server configuration error: Missing Supabase credentials');
}
return createClient(supabaseUrl, supabaseServiceKey, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
});
}
async function getDocument(id: string): Promise<Document | null> {
try {
const supabase = getSupabaseServerClient();
const { data, error } = await supabase
.from('mission_control_documents')
.select('*')
.eq('id', id)
.single();
if (error || !data) {
console.error('Error fetching document:', error);
return null;
}
return {
id: data.id,
title: data.title,
content: data.content,
type: data.type as DocumentType,
folder: data.folder || 'General',
tags: data.tags || [],
createdAt: data.created_at,
updatedAt: data.updated_at,
size: data.size || 0,
description: data.description,
};
} catch (err) {
console.error('Failed to fetch document:', err);
return null;
}
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
function formatDate(dateString: string): string {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}
interface PageProps {
params: Promise<{ id: string }>;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { id } = await params;
const document = await getDocument(id);
return {
title: document ? `${document.title} - Mission Control` : 'Document - Mission Control',
};
}
export default async function DocumentPage({ params }: PageProps) {
const { id } = await params;
const document = await getDocument(id);
if (!document) {
notFound();
}
const Icon = typeIcons[document.type];
const colorClass = DOCUMENT_TYPE_COLORS[document.type];
return (
<DashboardLayout>
<div className="space-y-6">
{/* Header with back button */}
<div className="flex items-center gap-4">
<Link href="/documents">
<Button variant="outline" size="sm">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Documents
</Button>
</Link>
</div>
{/* Document Header */}
<div className="border rounded-lg p-6 bg-card">
<div className="flex flex-col sm:flex-row sm:items-start justify-between gap-4">
<div className="flex items-start gap-4">
<div className={cn('p-3 rounded-lg shrink-0', colorClass)}>
<Icon className="w-6 h-6" />
</div>
<div className="min-w-0">
<h1 className="text-xl sm:text-2xl font-semibold">{document.title}</h1>
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground mt-2">
<span className="flex items-center gap-1">
<Folder className="w-4 h-4" />
{document.folder}
</span>
<span></span>
<span>{formatFileSize(document.size)}</span>
<span></span>
<span className="flex items-center gap-1">
<Clock className="w-4 h-4" />
Updated {formatDate(document.updatedAt)}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<Link href={`/documents?id=${document.id}&edit=true`}>
<Button variant="outline" size="sm">
<Edit3 className="w-4 h-4 mr-2" />
Edit
</Button>
</Link>
</div>
</div>
{/* Tags */}
{document.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-4 pt-4 border-t">
{document.tags.map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
#{tag}
</Badge>
))}
</div>
)}
</div>
{/* Document Content */}
<div className="border rounded-lg p-6 bg-card">
<div className="prose prose-base dark:prose-invert max-w-none prose-headings:mt-6 prose-headings:mb-4 prose-p:my-3 prose-ul:my-3 prose-ol:my-3">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{document.content || '*No content*'}
</ReactMarkdown>
</div>
</div>
{/* Footer with back button */}
<div className="flex items-center justify-center pt-4">
<Link href="/documents">
<Button variant="outline">
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Documents
</Button>
</Link>
</div>
</div>
</DashboardLayout>
);
}