feat: round out rapportage epic
This commit is contained in:
@@ -2,13 +2,16 @@
|
||||
|
||||
/**
|
||||
* Client Header Component
|
||||
* E2.S3: Context-aware header showing client name, status, and last modified
|
||||
* E2.S3: Context-aware header showing client name, status, last modified en acties
|
||||
*/
|
||||
|
||||
import { Mic } from 'lucide-react';
|
||||
import type { FHIRPatient } from '@/lib/fhir';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface ClientHeaderProps {
|
||||
patient: FHIRPatient;
|
||||
onNewReport?: () => void;
|
||||
}
|
||||
|
||||
// Status badge component
|
||||
@@ -35,7 +38,7 @@ function StatusBadge({ status }: { status?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function ClientHeader({ patient }: ClientHeaderProps) {
|
||||
export function ClientHeader({ patient, onNewReport }: ClientHeaderProps) {
|
||||
// Extract name
|
||||
const name = patient.name?.[0];
|
||||
const fullName = [
|
||||
@@ -97,9 +100,15 @@ export function ClientHeader({ patient }: ClientHeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Patient ID (subtle) */}
|
||||
<div className="text-xs text-slate-400">
|
||||
ID: {patient.id}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-xs text-slate-400">
|
||||
ID: {patient.id}
|
||||
</div>
|
||||
{onNewReport && (
|
||||
<Button type="button" size="sm" onClick={onNewReport}>
|
||||
<Mic className="mr-2 h-4 w-4" /> Nieuwe rapportage
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
42
app/epd/patients/[id]/components/patient-layout-client.tsx
Normal file
42
app/epd/patients/[id]/components/patient-layout-client.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { FHIRPatient } from '@/lib/fhir';
|
||||
import { ClientHeader } from './client-header';
|
||||
import { RapportageModal } from '../rapportage/components/rapportage-modal';
|
||||
|
||||
interface PatientLayoutClientProps {
|
||||
patient: FHIRPatient;
|
||||
patientId: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function PatientLayoutClient({ patient, patientId, children }: PatientLayoutClientProps) {
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
|
||||
const patientName = useMemo(() => {
|
||||
const name = patient.name?.[0];
|
||||
if (!name) return 'deze patiënt';
|
||||
return [
|
||||
...(name.prefix || []),
|
||||
...(name.given || []),
|
||||
name.family,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
}, [patient]);
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-1 flex-col bg-white">
|
||||
<ClientHeader patient={patient} onNewReport={() => setModalOpen(true)} />
|
||||
<div className="flex-1 overflow-auto bg-slate-50">{children}</div>
|
||||
<RapportageModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setModalOpen(false)}
|
||||
patientId={patientId}
|
||||
patientName={patientName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { getPatient } from '../actions';
|
||||
import { PatientLayoutClient } from './components/patient-layout-client';
|
||||
|
||||
export default async function PatientDetailLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const patient = await getPatient(id);
|
||||
|
||||
return <>{children}</>;
|
||||
return (
|
||||
<PatientLayoutClient patientId={id} patient={patient}>
|
||||
{children}
|
||||
</PatientLayoutClient>
|
||||
);
|
||||
}
|
||||
|
||||
196
app/epd/patients/[id]/rapportage/components/rapportage-modal.tsx
Normal file
196
app/epd/patients/[id]/rapportage/components/rapportage-modal.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { SpeechRecorder } from '@/components/speech-recorder';
|
||||
import type { ClassificationResult } from '@/lib/types/report';
|
||||
import { createReport } from '../actions';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
|
||||
interface RapportageModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
patientId: string;
|
||||
patientName: string;
|
||||
}
|
||||
|
||||
export function RapportageModal({ isOpen, onClose, patientId, patientName }: RapportageModalProps) {
|
||||
const [content, setContent] = useState('');
|
||||
const [classification, setClassification] = useState<ClassificationResult | null>(null);
|
||||
const [selectedType, setSelectedType] = useState<'behandeladvies' | 'vrije_notitie'>('vrije_notitie');
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setContent('');
|
||||
setClassification(null);
|
||||
setSelectedType('vrije_notitie');
|
||||
setError(null);
|
||||
setIsAnalyzing(false);
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleAnalyze = async () => {
|
||||
if (contentInvalid) return;
|
||||
setIsAnalyzing(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetch('/api/reports/classify', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ content }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('AI-analyse mislukt');
|
||||
}
|
||||
|
||||
const result: ClassificationResult = await response.json();
|
||||
setClassification(result);
|
||||
setSelectedType(result.type);
|
||||
} catch (err) {
|
||||
setClassification(null);
|
||||
setSelectedType('vrije_notitie');
|
||||
const message = err instanceof Error ? err.message : 'AI-analyse mislukt';
|
||||
setError(message);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'AI-analyse mislukt',
|
||||
description: message,
|
||||
});
|
||||
} finally {
|
||||
setIsAnalyzing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
setError(null);
|
||||
try {
|
||||
await createReport(patientId, {
|
||||
type: selectedType,
|
||||
content,
|
||||
ai_confidence: classification?.confidence,
|
||||
ai_reasoning: classification?.reasoning,
|
||||
});
|
||||
toast({
|
||||
title: 'Rapportage opgeslagen',
|
||||
description: `${patientName} heeft nu een nieuwe notitie in de tijdlijn.`,
|
||||
});
|
||||
router.refresh();
|
||||
onClose();
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : 'Opslaan mislukt';
|
||||
setError(message);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Opslaan mislukt',
|
||||
description: message,
|
||||
});
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const characterCount = content.length;
|
||||
const contentInvalid = characterCount < 20 || characterCount > 5000;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Nieuwe rapportage</DialogTitle>
|
||||
<DialogDescription>
|
||||
Leg een rapportage vast voor {patientName}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Beschrijf wat je wilt vastleggen..."
|
||||
className="w-full min-h-[160px] rounded-md border border-slate-200 p-3"
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-slate-500">
|
||||
<span>{characterCount} / 5000 karakters</span>
|
||||
{contentInvalid && <span>Min. 20 karakters</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SpeechRecorder
|
||||
disabled={isSaving || isAnalyzing}
|
||||
onTranscript={(text) =>
|
||||
setContent((prev) => `${prev}${prev ? '\n' : ''}${text}`)
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="rounded-lg border border-slate-200 p-3 text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium text-slate-900">Rapportagetype</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Kies handmatig of gebruik het AI-voorstel.
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">
|
||||
{classification
|
||||
? `AI: ${classification.type} (${Math.round(classification.confidence * 100)}%)`
|
||||
: 'Nog geen AI-analyse'}
|
||||
</span>
|
||||
</div>
|
||||
{classification?.reasoning && (
|
||||
<p className="mt-2 text-xs text-slate-500">{classification.reasoning}</p>
|
||||
)}
|
||||
<select
|
||||
value={selectedType}
|
||||
onChange={(e) => setSelectedType(e.target.value as 'behandeladvies' | 'vrije_notitie')}
|
||||
className="mt-3 w-full rounded-md border border-slate-200 bg-white p-2 text-sm"
|
||||
>
|
||||
<option value="behandeladvies">Behandeladvies</option>
|
||||
<option value="vrije_notitie">Vrije notitie</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="outline" onClick={onClose} disabled={isSaving}>
|
||||
Annuleren
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleAnalyze}
|
||||
disabled={contentInvalid || isAnalyzing || isSaving}
|
||||
>
|
||||
{isAnalyzing ? 'Analyseren…' : 'Analyseer met AI'}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSave}
|
||||
disabled={contentInvalid || isSaving}
|
||||
>
|
||||
{isSaving ? 'Opslaan…' : 'Opslaan'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
109
app/epd/patients/[id]/rapportage/components/report-card.tsx
Normal file
109
app/epd/patients/[id]/rapportage/components/report-card.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import { Loader2, Trash2, FilePenLine, FileText } from 'lucide-react';
|
||||
import { format, formatDistanceToNow } from 'date-fns';
|
||||
import { nl } from 'date-fns/locale';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { Report } from '@/lib/types/report';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const TYPE_META = {
|
||||
behandeladvies: {
|
||||
label: 'Behandeladvies',
|
||||
classes: 'bg-teal-50 text-teal-800 border-teal-200',
|
||||
icon: FilePenLine,
|
||||
},
|
||||
vrije_notitie: {
|
||||
label: 'Vrije notitie',
|
||||
classes: 'bg-slate-100 text-slate-700 border-slate-200',
|
||||
icon: FileText,
|
||||
},
|
||||
} as const;
|
||||
|
||||
interface ReportCardProps {
|
||||
report: Report;
|
||||
onDelete?: () => Promise<void> | void;
|
||||
isDeleting?: boolean;
|
||||
}
|
||||
|
||||
export function ReportCard({ report, onDelete, isDeleting }: ReportCardProps) {
|
||||
const meta = TYPE_META[report.type as keyof typeof TYPE_META] ?? TYPE_META.vrije_notitie;
|
||||
const Icon = meta.icon;
|
||||
const createdAt = report.created_at ? new Date(report.created_at) : null;
|
||||
const relativeTime = createdAt
|
||||
? formatDistanceToNow(createdAt, { addSuffix: true, locale: nl })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<article className="rounded-xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-12 w-12 items-center justify-center rounded-xl border bg-white',
|
||||
meta.classes
|
||||
)}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-semibold text-slate-900">{meta.label}</p>
|
||||
{report.ai_confidence !== null && report.ai_confidence !== undefined && (
|
||||
<span className="text-xs font-medium text-slate-500">
|
||||
AI {Math.round(report.ai_confidence * 100)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{createdAt && (
|
||||
<p className="text-xs text-slate-500">
|
||||
{format(createdAt, "d MMM yyyy 'om' HH:mm", { locale: nl })}
|
||||
{relativeTime && <span className="ml-2">({relativeTime})</span>}
|
||||
</p>
|
||||
)}
|
||||
{report.created_by && (
|
||||
<p className="text-xs text-slate-400">Aangemaakt door {report.created_by}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{onDelete && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-slate-500 hover:text-red-600"
|
||||
disabled={isDeleting}
|
||||
onClick={() => void onDelete()}
|
||||
>
|
||||
{isDeleting ? (
|
||||
<>
|
||||
<Loader2 className="mr-1 h-4 w-4 animate-spin" /> Verwijderen…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Trash2 className="mr-1 h-4 w-4" /> Verwijder
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="mt-4 text-sm leading-relaxed text-slate-700">
|
||||
{truncateContent(report.content)}
|
||||
</p>
|
||||
|
||||
{report.ai_reasoning && (
|
||||
<div className="mt-4 rounded-lg bg-slate-50 p-3 text-xs text-slate-600">
|
||||
<p className="font-semibold text-slate-800">AI toelichting</p>
|
||||
<p className="mt-1 text-slate-600">{report.ai_reasoning}</p>
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
function truncateContent(content: string, maxLength = 320) {
|
||||
if (!content) return '';
|
||||
if (content.length <= maxLength) return content;
|
||||
return `${content.slice(0, maxLength)}…`;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FilePlus2 } from 'lucide-react';
|
||||
import type { Report } from '@/lib/types/report';
|
||||
import { deleteReport } from '../actions';
|
||||
import { ReportCard } from './report-card';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
|
||||
interface ReportTimelineProps {
|
||||
reports: Report[];
|
||||
patientId: string;
|
||||
}
|
||||
|
||||
export function ReportTimeline({ reports, patientId }: ReportTimelineProps) {
|
||||
const [items, setItems] = useState(reports);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setItems(reports);
|
||||
}, [reports]);
|
||||
|
||||
const handleDelete = async (reportId: string) => {
|
||||
setDeletingId(reportId);
|
||||
try {
|
||||
await deleteReport(patientId, reportId);
|
||||
setItems((prev) => prev.filter((report) => report.id !== reportId));
|
||||
toast({
|
||||
title: 'Rapportage verwijderd',
|
||||
description: 'De rapportage is verwijderd uit de tijdlijn.',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to delete report', error);
|
||||
toast({
|
||||
variant: 'destructive',
|
||||
title: 'Verwijderen mislukt',
|
||||
description:
|
||||
error instanceof Error ? error.message : 'Probeer het later opnieuw.',
|
||||
});
|
||||
} finally {
|
||||
setDeletingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-slate-300 bg-slate-50 p-10 text-center">
|
||||
<div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-white text-slate-400">
|
||||
<FilePlus2 className="h-6 w-6" />
|
||||
</div>
|
||||
<p className="text-base font-semibold text-slate-900">Nog geen rapportages</p>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
Klik op "Nieuwe rapportage" in de header om de eerste rapportage vast te leggen.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{items.map((report) => (
|
||||
<ReportCard
|
||||
key={report.id}
|
||||
report={report}
|
||||
onDelete={() => handleDelete(report.id)}
|
||||
isDeleting={deletingId === report.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* Rapportage Page
|
||||
* E2.S3: Placeholder for rapportage functionality (future epic)
|
||||
*/
|
||||
|
||||
import { FileBarChart } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
import { nl } from 'date-fns/locale';
|
||||
import { ClipboardList, Sparkles, Timer } from 'lucide-react';
|
||||
import { getReports } from './actions';
|
||||
import { ReportTimeline } from './components/report-timeline';
|
||||
|
||||
export default async function RapportagePage({
|
||||
params,
|
||||
@@ -11,30 +11,68 @@ export default async function RapportagePage({
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const reports = await getReports(id);
|
||||
const totalReports = reports.length;
|
||||
const latestReport = reports[0];
|
||||
const latestDate = latestReport?.created_at
|
||||
? format(new Date(latestReport.created_at), "d MMM yyyy 'om' HH:mm", { locale: nl })
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{/* Page Header */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Rapportage</h2>
|
||||
<p className="text-sm text-slate-600 mt-1">
|
||||
Overzichten, statistieken en export mogelijkheden
|
||||
<div className="space-y-6 p-6">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-wide text-slate-500">Universele rapportage</p>
|
||||
<h1 className="text-2xl font-semibold text-slate-900">Tijdlijn en notities</h1>
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Alle behandeladviezen en vrije notities voor deze cliënt op één plek.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Placeholder */}
|
||||
<div className="bg-white rounded-lg border border-slate-200 p-12 text-center">
|
||||
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-slate-50 mb-4">
|
||||
<FileBarChart className="h-8 w-8 text-slate-500" />
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-2">
|
||||
Rapportage Module - Coming Soon
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600 max-w-md mx-auto">
|
||||
De rapportage functionaliteit wordt in een latere fase geïmplementeerd.
|
||||
Dit omvat overzichten, statistieken en exportfunctionaliteit.
|
||||
</p>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<StatCard
|
||||
icon={<ClipboardList className="h-5 w-5" />}
|
||||
label="Totaal rapportages"
|
||||
value={totalReports.toString()}
|
||||
helper={totalReports > 0 ? 'Inclusief soft-deletes' : 'Nog geen rapportages'}
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Sparkles className="h-5 w-5" />}
|
||||
label="AI classificaties"
|
||||
value={`${reports.filter((r) => r.ai_confidence !== null).length}`}
|
||||
helper="AI helpt bij type bepaling"
|
||||
/>
|
||||
<StatCard
|
||||
icon={<Timer className="h-5 w-5" />}
|
||||
label="Laatste activiteit"
|
||||
value={latestDate ?? '—'}
|
||||
helper={latestDate ? 'Recentste rapportage' : 'Nog geen activiteit'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ReportTimeline reports={reports} patientId={id} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StatCard({
|
||||
icon,
|
||||
label,
|
||||
value,
|
||||
helper,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
value: string;
|
||||
helper: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
|
||||
<div className="flex items-center justify-between text-slate-500">
|
||||
<span className="text-xs font-medium uppercase tracking-wide">{label}</span>
|
||||
<div className="text-slate-400">{icon}</div>
|
||||
</div>
|
||||
<p className="mt-3 text-2xl font-semibold text-slate-900">{value}</p>
|
||||
<p className="text-xs text-slate-500">{helper}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import localFont from "next/font/local";
|
||||
import "./globals.css";
|
||||
import { Toaster } from '@/components/ui/toaster';
|
||||
|
||||
// Serif font voor long-form content (manifesto) - lokaal geladen om build zonder netwerk te laten slagen
|
||||
const crimsonText = localFont({
|
||||
@@ -129,6 +130,7 @@ export default function RootLayout({
|
||||
className={`${crimsonText.variable} ${inter.variable} ${jetBrainsMono.variable} antialiased`}
|
||||
>
|
||||
{children}
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user