194 lines
6.0 KiB
TypeScript
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>
|
|
);
|
|
}
|