fix(frontend): wrap catalog, disease detail, and expert reviews with MainLayout

- Add MainLayout wrapper to /catalog, /catalog/:slug, and /expert/reviews
  routes in app.tsx so they share the same navbar and footer as dashboard
- Remove duplicate header/layout/spacing from CatalogPage,
  DiseaseDetailPage, and ExpertReviewsPage
- Update ExpertReviewsPage to use consistent space-y layout pattern
  matching other pages
- Update CatalogPage to remove outer main wrapper since MainLayout
  provides the container

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Asep Haryana Saputra
2026-06-02 07:47:59 +00:00
co-authored by Claude Opus 4.7
parent 4d6ca4912c
commit 81d083a2b8
4 changed files with 344 additions and 357 deletions
+18 -2
View File
@@ -78,12 +78,28 @@ const router = createBrowserRouter([
path: "/expert/reviews", path: "/expert/reviews",
element: ( element: (
<AuthGuard requireExpert> <AuthGuard requireExpert>
<MainLayout>
<ExpertReviewsPage /> <ExpertReviewsPage />
</MainLayout>
</AuthGuard> </AuthGuard>
), ),
}, },
{ path: "/catalog", element: <CatalogPage /> }, {
{ path: "/catalog/:slug", element: <DiseaseDetailPage /> }, path: "/catalog",
element: (
<MainLayout>
<CatalogPage />
</MainLayout>
),
},
{
path: "/catalog/:slug",
element: (
<MainLayout>
<DiseaseDetailPage />
</MainLayout>
),
},
]); ]);
export function App() { export function App() {
+2 -13
View File
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { Link, useSearchParams } from 'react-router-dom'; import { Link, useSearchParams } from 'react-router-dom';
import { ArrowLeft, Shield, AlertCircle } from 'lucide-react'; import { AlertCircle } from 'lucide-react';
import type { RiskLevel } from '@zeavis/shared'; import type { RiskLevel } from '@zeavis/shared';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
@@ -42,23 +42,13 @@ export function CatalogPage() {
}); });
return ( return (
<main className="min-h-screen px-6 py-8 bg-[#ECF4E8]"> <div className="space-y-6">
<div className="mx-auto max-w-6xl space-y-8">
<header className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button asChild variant="ghost" className="p-2">
<Link to="/dashboard">
<ArrowLeft className="h-5 w-5" />
</Link>
</Button>
<div> <div>
<p className="text-sm font-medium text-primary">Edukasi Penyakit</p> <p className="text-sm font-medium text-primary">Edukasi Penyakit</p>
<h1 className="text-3xl font-bold tracking-tight">Katalog Penyakit</h1> <h1 className="text-3xl font-bold tracking-tight">Katalog Penyakit</h1>
</div> </div>
</div> </div>
</div>
</header>
{/* Filters */} {/* Filters */}
<div className="flex flex-col gap-4 md:flex-row md:items-end"> <div className="flex flex-col gap-4 md:flex-row md:items-end">
@@ -179,6 +169,5 @@ export function CatalogPage() {
</div> </div>
)} )}
</div> </div>
</main>
); );
} }
+10 -12
View File
@@ -1,7 +1,7 @@
import { useParams, Link } from 'react-router-dom'; import { useParams, Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { isDiseaseSlug } from '@zeavis/shared'; import { isDiseaseSlug } from '@zeavis/shared';
import { ArrowLeft, BookOpen, AlertCircle } from 'lucide-react'; import { ArrowLeft, AlertCircle, BookOpen } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { RiskBadge } from '@/components/risk-badge'; import { RiskBadge } from '@/components/risk-badge';
@@ -20,37 +20,37 @@ export function DiseaseDetailPage() {
if (!validatedSlug || error || (!isLoading && !disease)) { if (!validatedSlug || error || (!isLoading && !disease)) {
return ( return (
<main className="min-h-screen px-6 py-8"> <div className="space-y-6">
<div className="mx-auto max-w-4xl"> <div className="flex items-center gap-4">
<Button asChild variant="ghost" className="mb-8"> <Button asChild variant="ghost">
<Link to="/catalog"> <Link to="/catalog">
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
Kembali ke Katalog Kembali ke Katalog
</Link> </Link>
</Button> </Button>
</div>
<Card className="p-8 text-center"> <Card className="p-8 text-center">
<p className="text-muted-foreground">Materi tidak ditemukan</p> <p className="text-muted-foreground">Materi tidak ditemukan</p>
</Card> </Card>
</div> </div>
</main>
); );
} }
if (isLoading) { if (isLoading) {
return ( return (
<main className="min-h-screen px-6 py-8"> <div className="space-y-6">
<div className="mx-auto max-w-4xl"> <div className="flex items-center gap-4">
<Button asChild variant="ghost" className="mb-8"> <Button asChild variant="ghost">
<Link to="/catalog"> <Link to="/catalog">
<ArrowLeft className="mr-2 h-4 w-4" /> <ArrowLeft className="mr-2 h-4 w-4" />
Kembali ke Katalog Kembali ke Katalog
</Link> </Link>
</Button> </Button>
</div>
<Card className="p-8 text-center"> <Card className="p-8 text-center">
<p className="text-muted-foreground">Memuat materi...</p> <p className="text-muted-foreground">Memuat materi...</p>
</Card> </Card>
</div> </div>
</main>
); );
} }
@@ -59,8 +59,7 @@ export function DiseaseDetailPage() {
} }
return ( return (
<main className="min-h-screen px-6 py-8"> <div className="space-y-6">
<div className="mx-auto max-w-4xl space-y-8">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button asChild variant="ghost"> <Button asChild variant="ghost">
<Link to="/catalog"> <Link to="/catalog">
@@ -141,6 +140,5 @@ export function DiseaseDetailPage() {
</Button> </Button>
</div> </div>
</div> </div>
</main>
); );
} }
+14 -30
View File
@@ -1,7 +1,7 @@
import { FormEvent, useMemo, useState } from 'react'; import { FormEvent, useMemo, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useMutation, useQueries, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueries, useQueryClient } from '@tanstack/react-query';
import type { DiseaseSlug, DiagnosisStatus } from '@zeavis/shared'; import type { DiseaseSlug } from '@zeavis/shared';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { DiagnosisStatusBadge } from '@/components/diagnosis-status-badge'; import { DiagnosisStatusBadge } from '@/components/diagnosis-status-badge';
@@ -24,10 +24,14 @@ export function ExpertReviewsPage() {
const reviews = reviewsQuery.data || []; const reviews = reviewsQuery.data || [];
const diseases = diseasesQuery.data || []; const diseases = diseasesQuery.data || [];
const selected = useMemo(() => reviews.find((item) => item.id === selectedId) ?? reviews[0] ?? null, [reviews, selectedId]); const selected = useMemo(
() => reviews.find((item) => item.id === selectedId) ?? reviews[0] ?? null,
[reviews, selectedId],
);
const mutation = useMutation({ const mutation = useMutation({
mutationFn: () => apiClient.reviewDiagnosis(selected!.id, { mutationFn: () =>
apiClient.reviewDiagnosis(selected!.id, {
verdict, verdict,
correctedDiseaseSlug: verdict === 'corrected' ? correctedDiseaseSlug || undefined : undefined, correctedDiseaseSlug: verdict === 'corrected' ? correctedDiseaseSlug || undefined : undefined,
notes, notes,
@@ -47,31 +51,17 @@ export function ExpertReviewsPage() {
await mutation.mutateAsync(); await mutation.mutateAsync();
} }
const statusLabel = (status: DiagnosisStatus) => { const isLoading = reviewsQuery.isPending || diseasesQuery.isPending;
switch (status) {
case 'ai_verified': return 'Terverifikasi AI';
case 'needs_review': return 'Menunggu Review';
case 'expert_verified': return 'Diverifikasi Pakar';
case 'expert_corrected': return 'Dikoreksi Pakar';
case 'failed': return 'Gagal';
}
};
return ( return (
<main className="min-h-screen bg-background"> <div className="space-y-6">
<header className="border-b bg-card"> <div className="flex items-center justify-between">
<div className="mx-auto flex max-w-7xl items-center justify-between p-6">
<div> <div>
<p className="text-sm font-medium text-primary">Review Pakar</p> <p className="text-sm font-medium text-primary">Review Pakar</p>
<h1 className="text-2xl font-bold">Diagnosis Menunggu Review</h1> <h1 className="text-3xl font-bold">Diagnosis Menunggu Review</h1>
</div> </div>
<Link to="/dashboard">
<Button variant="outline">Dashboard</Button>
</Link>
</div> </div>
</header>
<div className="mx-auto max-w-7xl p-6">
<div className="grid gap-6 lg:grid-cols-3"> <div className="grid gap-6 lg:grid-cols-3">
{/* Left: List of reviews */} {/* Left: List of reviews */}
<div className="space-y-3"> <div className="space-y-3">
@@ -81,7 +71,7 @@ export function ExpertReviewsPage() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{reviewsQuery.isPending || diseasesQuery.isPending ? ( {isLoading ? (
<p className="text-sm text-muted-foreground">Memuat diagnosis...</p> <p className="text-sm text-muted-foreground">Memuat diagnosis...</p>
) : reviews.length === 0 ? ( ) : reviews.length === 0 ? (
<p className="text-sm text-muted-foreground">Tidak ada diagnosis yang menunggu review</p> <p className="text-sm text-muted-foreground">Tidak ada diagnosis yang menunggu review</p>
@@ -124,7 +114,7 @@ export function ExpertReviewsPage() {
{/* Right: Detail and form */} {/* Right: Detail and form */}
<div className="lg:col-span-2 space-y-6"> <div className="lg:col-span-2 space-y-6">
{reviewsQuery.isPending || diseasesQuery.isPending ? ( {isLoading ? (
<Card> <Card>
<CardContent className="p-6 text-center text-muted-foreground"> <CardContent className="p-6 text-center text-muted-foreground">
Memuat diagnosis... Memuat diagnosis...
@@ -160,7 +150,6 @@ export function ExpertReviewsPage() {
</div> </div>
</div> </div>
{/* Top predictions */}
{selected.predictions.length > 0 && ( {selected.predictions.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
<h4 className="text-sm font-medium">Prediksi Teratas</h4> <h4 className="text-sm font-medium">Prediksi Teratas</h4>
@@ -184,7 +173,6 @@ export function ExpertReviewsPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
{/* Verdict selection */}
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Keputusan</label> <label className="text-sm font-medium">Keputusan</label>
<div className="flex gap-3"> <div className="flex gap-3">
@@ -218,7 +206,6 @@ export function ExpertReviewsPage() {
</div> </div>
</div> </div>
{/* Disease selection (only if corrected) */}
{verdict === 'corrected' && ( {verdict === 'corrected' && (
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="disease" className="text-sm font-medium"> <label htmlFor="disease" className="text-sm font-medium">
@@ -241,7 +228,6 @@ export function ExpertReviewsPage() {
</div> </div>
)} )}
{/* Notes */}
<div className="space-y-2"> <div className="space-y-2">
<label htmlFor="notes" className="text-sm font-medium"> <label htmlFor="notes" className="text-sm font-medium">
Catatan <span className="text-red-500">*</span> Catatan <span className="text-red-500">*</span>
@@ -257,7 +243,6 @@ export function ExpertReviewsPage() {
/> />
</div> </div>
{/* Submit button */}
<Button <Button
type="submit" type="submit"
disabled={mutation.isPending || (verdict === 'corrected' && !correctedDiseaseSlug) || !notes.trim()} disabled={mutation.isPending || (verdict === 'corrected' && !correctedDiseaseSlug) || !notes.trim()}
@@ -285,6 +270,5 @@ export function ExpertReviewsPage() {
</div> </div>
</div> </div>
</div> </div>
</main>
); );
} }