fix(frontend): align pages with DB schema, fix auth, and improve UX
- Fix auth system: remove redirect bugs in login/register pages, register routes, wrap protected routes with AuthGuard, add AuthInitializer - Add missing navbar links (Diagnosa, Pustaka, expert Review) - Fix DiagnosesPage status filter to match DB DiagnosisStatus enum, support URL params (?status=, ?risk=) from dashboard links - Replace hardcoded disease data on Dashboard with API-driven data, fix layout bugs (invalid gap-57 class, empty Button) - Rewrite LibraryPage to use API instead of mock-diseases.ts - Indonesian-ize Expert Reviews page labels and text - Wrap DiagnosisDetailPage with MainLayout for consistent layout - Improve ScanPage UX: image preview before upload, lucide icons, proper disease names in result modal, file size validation - Delete dead code: mock-diseases.ts, unused form components, tailwind.config.ts (Tailwind v4 does not read v3 config) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
a28d06cb02
commit
ba24cf1a05
@@ -1,182 +1,95 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { BookOpen } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Modal } from "@/components/ui/modal";
|
||||
import { mockDiseases } from "@/data/mock-diseases";
|
||||
import { useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { DiseaseCard } from "@/components/disease-card";
|
||||
import { apiClient } from "@/lib/api-client";
|
||||
import type { RiskLevel } from "@zeavis/shared";
|
||||
|
||||
export function LibraryPage() {
|
||||
type Disease = (typeof mockDiseases)[number];
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [riskFilter, setRiskFilter] = useState<RiskLevel | "all">("all");
|
||||
|
||||
const [filter, setFilter] = useState<string | null>(null);
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
const [selected, setSelected] = useState<Disease | null>(null);
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const { data: diseases, isLoading, error } = useQuery({
|
||||
queryKey: ["diseases"],
|
||||
queryFn: () => apiClient.getDiseases(),
|
||||
});
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (!filter) return mockDiseases;
|
||||
return mockDiseases.filter((d) =>
|
||||
d.name.toLowerCase().includes(filter.toLowerCase()),
|
||||
);
|
||||
}, [filter]);
|
||||
const filteredDiseases = (diseases || []).filter((disease) => {
|
||||
const matchesSearch =
|
||||
disease.commonName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
disease.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
disease.summary.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
|
||||
const matchesRisk = riskFilter === "all" || disease.riskLevel === riskFilter;
|
||||
|
||||
return matchesSearch && matchesRisk;
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="p-6">
|
||||
<h1 className="text-2xl font-semibold mb-4">Pustaka Penyakit</h1>
|
||||
<div className="mb-4">
|
||||
<input
|
||||
placeholder="Filter penyakit..."
|
||||
value={filter ?? ""}
|
||||
onChange={(e) => setFilter(e.target.value || null)}
|
||||
className="border px-3 py-2 rounded-md w-full max-w-sm"
|
||||
/>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-primary">Edukasi Penyakit</p>
|
||||
<h1 className="text-3xl font-bold">Pustaka Penyakit</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{items.map((d) => (
|
||||
<article key={d.id} className="bg-white p-4 rounded-lg shadow">
|
||||
<div className="flex gap-4">
|
||||
<img
|
||||
src={d.imageUrl}
|
||||
alt={d.name}
|
||||
className="h-28 w-48 rounded-md object-cover"
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h2 className="font-semibold text-lg">
|
||||
{d.name}{" "}
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{d.severity}
|
||||
</span>
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{d.description}
|
||||
</p>
|
||||
{d.pathogen && (
|
||||
<div className="mt-2 text-sm">
|
||||
<strong>Patogen:</strong> {d.pathogen}
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3 flex items-center gap-3">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setSelected(d);
|
||||
setModalOpen(true);
|
||||
}}
|
||||
className="inline-flex items-center gap-2 bg-green-600 text-white px-3 py-1 rounded"
|
||||
>
|
||||
<BookOpen className="h-4 w-4" />
|
||||
<span className="text-sm">Baca lebih lanjut</span>
|
||||
</Button>
|
||||
<Link
|
||||
to={`/catalog/${d.slug}`}
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
Lihat halaman katalog
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="text-sm text-primary underline"
|
||||
onClick={() =>
|
||||
setExpandedId(expandedId === d.id ? null : d.id)
|
||||
}
|
||||
>
|
||||
{expandedId === d.id ? "Tutup" : "Detail"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expandedId === d.id && (
|
||||
<div className="mt-4 grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<h3 className="font-medium">Gejala</h3>
|
||||
<ul className="list-disc list-inside text-sm mt-2">
|
||||
{(d.symptoms || []).length > 0 ? (
|
||||
d.symptoms.map((s: string, i: number) => (
|
||||
<li key={`${d.id}-symptom-${i}`}>{s}</li>
|
||||
))
|
||||
) : (
|
||||
<li>Tidak ada gejala khusus</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">Pencegahan</h3>
|
||||
<p className="text-sm mt-2">{d.prevention}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-end">
|
||||
<div className="flex-1">
|
||||
<label htmlFor="search" className="block text-sm font-medium mb-2">
|
||||
Cari penyakit
|
||||
</label>
|
||||
<input
|
||||
id="search"
|
||||
type="text"
|
||||
placeholder="Cari berdasarkan nama atau gejala..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="risk-filter" className="block text-sm font-medium mb-2">
|
||||
Filter risiko
|
||||
</label>
|
||||
<select
|
||||
id="risk-filter"
|
||||
value={riskFilter}
|
||||
onChange={(e) => setRiskFilter(e.target.value as RiskLevel | "all")}
|
||||
className="rounded-md border border-border bg-background px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="all">Semua Risiko</option>
|
||||
<option value="low">Risiko Rendah</option>
|
||||
<option value="medium">Risiko Sedang</option>
|
||||
<option value="high">Risiko Tinggi</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={modalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
setSelected(null);
|
||||
}}
|
||||
title={selected?.name}
|
||||
footer={
|
||||
selected && (
|
||||
<div className="flex items-center justify-end gap-3">
|
||||
<Link
|
||||
to={`/catalog/${selected.slug}`}
|
||||
className="px-3 py-2 rounded bg-green-600 text-white text-sm"
|
||||
>
|
||||
Buka halaman katalog
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
setSelected(null);
|
||||
}}
|
||||
className="px-3 py-2 rounded bg-gray-200 text-sm"
|
||||
>
|
||||
Tutup
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
{selected ? (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<img
|
||||
src={selected.imageUrl}
|
||||
alt={selected.name}
|
||||
className="w-full rounded-md object-cover"
|
||||
/>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{selected.description}
|
||||
</p>
|
||||
{selected.pathogen && (
|
||||
<p className="mt-2">
|
||||
<strong>Patogen:</strong> {selected.pathogen}
|
||||
</p>
|
||||
)}
|
||||
<h4 className="mt-3 font-medium">Gejala</h4>
|
||||
<ul className="list-disc list-inside text-sm mt-2">
|
||||
{(selected.symptoms || []).length > 0 ? (
|
||||
selected.symptoms.map((s: string, i: number) => (
|
||||
<li key={`${selected.id}-symptom-${i}`}>{s}</li>
|
||||
))
|
||||
) : (
|
||||
<li>Tidak ada gejala khusus</li>
|
||||
)}
|
||||
</ul>
|
||||
<h4 className="mt-3 font-medium">Pencegahan</h4>
|
||||
<p className="text-sm mt-2">{selected.prevention}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Modal>
|
||||
</main>
|
||||
{isLoading && (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
Memuat pustaka penyakit...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="py-12 text-center text-red-600">
|
||||
Gagal memuat pustaka penyakit
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && filteredDiseases.length === 0 && (
|
||||
<div className="py-12 text-center text-muted-foreground">
|
||||
Tidak ada penyakit yang cocok dengan filter ini.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !error && filteredDiseases.length > 0 && (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredDiseases.map((disease) => (
|
||||
<DiseaseCard key={disease.slug} disease={disease} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user