-
-
- Cari penyakit
-
+ {/* Filters */}
+
+
+
+ Cari penyakit
+
+
-
-
- Filter risiko
-
- setRiskFilter(e.target.value as RiskLevel | 'all')}
- className="rounded-md border border-border bg-background px-3 py-2 text-sm"
- >
- Semua Risiko
- Risiko Rendah
- Risiko Sedang
- Risiko Tinggi
-
-
+
+
+
+ Filter risiko
+
+ {
+ const v = e.target.value as RiskLevel | 'all';
+ setRiskFilter(v);
+ const next = new URLSearchParams(searchParams);
+ if (v === 'all') next.delete('risk');
+ else next.set('risk', v);
+ setSearchParams(next);
+ }}
+ className="rounded-md border border-border bg-background px-3 py-2 text-sm"
+ >
+ Semua Risiko
+ Risiko Rendah
+ Risiko Sedang
+ Risiko Tinggi
+
@@ -97,13 +113,62 @@ export function CatalogPage() {
)}
{!isLoading && !error && filteredDiseases.length > 0 && (
-
+
{filteredDiseases.map((disease) => (
-
+
+
+
+
+
+
+ {disease.commonName}
+
+
+ {disease.label}
+
+
+
+
+
+
+ {disease.summary}
+
+
+ {disease.symptoms.length > 0 && (
+
+
+ Gejala:
+
+
+ {disease.symptoms.slice(0, 2).map((symptom, index) => (
+
+ •
+ {symptom}
+
+ ))}
+
+
+ )}
+
+
+
+
+
))}
)}
-
-
+
);
}
diff --git a/apps/web/src/pages/dashboard-page.tsx b/apps/web/src/pages/dashboard-page.tsx
index 68748ab..a5cdbc3 100644
--- a/apps/web/src/pages/dashboard-page.tsx
+++ b/apps/web/src/pages/dashboard-page.tsx
@@ -1,272 +1,348 @@
-import { useQueries, useMutation, useQueryClient } from '@tanstack/react-query';
-import { Link, useNavigate } from 'react-router-dom';
-import { BookOpen, History, LayoutDashboard, TrendingUp, LogOut, CheckCircle2 } from 'lucide-react';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { ManualClassificationForm } from '@/components/manual-classification-form';
-import { ImageClassificationForm } from '@/components/image-classification-form';
-import { DiagnosisCard } from '@/components/diagnosis-card';
-import { useAuthStore } from '@/store/auth-store';
-import { useUiStore } from '@/store/ui-store';
-import { apiClient } from '@/lib/api-client';
+import { useMemo } from "react";
+import { Link } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+import { BookOpen, ChevronRight, Pill, Shield, Scan } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { useUiStore } from "@/store/ui-store";
+import { apiClient } from "@/lib/api-client";
+import bg from "@/assets/images/dashboard-bg.png";
export function DashboardPage() {
- const { user } = useAuthStore();
- const setUser = useAuthStore((state) => state.setUser);
- const { dashboardCompact, toggleDashboardCompact } = useUiStore();
- const queryClient = useQueryClient();
- const navigate = useNavigate();
-
- const [diseasesQuery, summaryQuery, diagnosesQuery, classificationsQuery] = useQueries({
- queries: [
- {
- queryKey: ['diseases'],
- queryFn: () => apiClient.getDiseases(),
- },
- {
- queryKey: ['dashboard-summary'],
- queryFn: () => apiClient.getDashboardSummary(),
- },
- {
- queryKey: ['diagnoses'],
- queryFn: () => apiClient.getDiagnoses(),
- },
- {
- queryKey: ['manual-classifications'],
- queryFn: () => apiClient.getManualClassifications(),
- },
- ],
+ const { dashboardCompact } = useUiStore();
+ const summaryQuery = useQuery({
+ queryKey: ["dashboard-summary"],
+ queryFn: () => apiClient.getDashboardSummary(),
});
- const createDiagnosisMutation = useMutation({
- mutationFn: async (file: File) => {
- return await apiClient.createDiagnosis(file);
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['diagnoses'] });
- queryClient.invalidateQueries({ queryKey: ['dashboard-summary'] });
- },
+ const diseasesQuery = useQuery({
+ queryKey: ["diseases"],
+ queryFn: () => apiClient.getDiseases(),
});
- const createClassificationMutation = useMutation({
- mutationFn: async (payload: Parameters
[0]) => {
- await apiClient.createManualClassification(payload);
- },
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['dashboard-summary'] });
- queryClient.invalidateQueries({ queryKey: ['manual-classifications'] });
- },
- });
-
- const logoutMutation = useMutation({
- mutationFn: async () => {
- await apiClient.logout();
- },
- onSuccess: () => {
- setUser(null);
- queryClient.clear();
- navigate('/login');
- },
- });
-
- const diseases = diseasesQuery.data || [];
const summary = summaryQuery.data;
- const diagnoses = diagnosesQuery.data || [];
- const classifications = classificationsQuery.data || [];
+ const diseases = diseasesQuery.data ?? [];
- const isLoadingData = diseasesQuery.isLoading || summaryQuery.isLoading;
- const hasError = diseasesQuery.error || summaryQuery.error;
+ const diseasesQuick = useMemo(() =>
+ diseases
+ .sort((a, b) => a.displayOrder - b.displayOrder)
+ .map((d) => ({
+ name: d.commonName,
+ sci: d.label,
+ color: d.accentColor,
+ slug: d.slug,
+ })),
+ [diseases]
+ );
+
+ const missionCards = [
+ {
+ icon: Scan,
+ title: "Deteksi Otomatis",
+ description:
+ "Upload foto daun jagung dan AI kami akan mengidentifikasi penyakit secara instan.",
+ accent: "text-lime-600",
+ },
+ {
+ icon: BookOpen,
+ title: "Modul Edukasi",
+ description:
+ "Informasi detail tentang gejala, penyebab, dan dampak setiap penyakit daun jagung.",
+ accent: "text-amber-600",
+ },
+ {
+ icon: Shield,
+ title: "Panduan Pencegahan",
+ description:
+ "Strategi pencegahan berbasis sains untuk melindungi tanaman Anda dari infeksi.",
+ accent: "text-blue-600",
+ },
+ {
+ icon: Pill,
+ title: "Rekomendasi Obat",
+ description:
+ "Saran fungisida dan perawatan mandiri yang tepat sesuai jenis penyakit.",
+ accent: "text-violet-600",
+ },
+ ];
+
+ const isLoadingData = summaryQuery.isLoading;
+ const hasError = Boolean(summaryQuery.error);
return (
-
-
-
-
-
- Dashboard
-
-
ZeaVis Edu Workspace
-
- {user?.name ? `Selamat datang, ${user.name}` : 'Pantau penyakit daun jagung dan laporkan pengamatan Anda'}
+
+ {/* Hero header */}
+
+
+
+
+
+ AI FOR SMART EDUCATION
+
+
Selamat Datang di
+
+ ZeaVis Edu
+
+
+ Platform edukasi berbasis AI untuk membantu petani jagung
+ Indonesia mendeteksi penyakit daun secara mandiri, cepat, dan
+ akurat.
-
-
- {user?.role === 'expert' && (
-
-
-
- Review Pakar
+
+
+
+
+ Scan Daun Jagung
- )}
-
- {dashboardCompact ? 'Mode Nyaman' : 'Mode Ringkas'}
-
- logoutMutation.mutate()}
- disabled={logoutMutation.isPending}
- >
-
- {logoutMutation.isPending ? 'Keluar...' : 'Keluar'}
-
+
+
+ Pustaka Penyakit
+
+
+
+
-
+
+
+
- {isLoadingData && (
-
- Memuat data dashboard...
-
- )}
+ {/* Loading / Error states */}
+ {isLoadingData && (
+
+ Memuat data dashboard...
+
+ )}
- {hasError && (
-
- Gagal memuat data dashboard
-
- )}
+ {hasError && (
+
+ Gagal memuat data dashboard
+
+ {String(summaryQuery.error?.message)}
+
+
+ )}
- {!isLoadingData && !hasError && (
- <>
- {summary && (
-
-
-
-
- Total Penyakit
-
-
-
- {summary.diseaseCount}
-
-
+ {/* Main dashboard content */}
+ {!isLoadingData && !hasError && (
+ <>
+ {summary && (
+
+
+
+ Proyek Urgensi
+
+
+ Data ringkasan terbaru dari proyek Anda untuk memantau
+ perkembangan dan hasil deteksi penyakit daun jagung
+
+
-
-
-
- Total Diagnosis
-
-
-
- {summary.imageClassificationCount}
-
-
-
-
-
-
- Menunggu Review
-
-
-
-
- {summary.needsReviewCount}
-
-
-
-
-
-
-
- Risiko Tinggi
-
-
-
-
- {summary.riskDistribution.high}
-
-
-
-
- )}
-
-
+ {/* Total Diagnoses — the actual count from diagnoses table */}
-
-
-
- Katalog Penyakit
+
+
+ Total Diagnosa
-
- Pelajari tentang {diseases.length} penyakit daun jagung
-
-
-
- Buka Katalog
-
+
+
+ {summary.imageClassificationCount}
+
+
+ Lihat daftar
+
+ {/* Menunggu Review */}
-
-
-
- Distribusi Risiko
+
+
+ Menunggu Review
-
- Penyakit berdasarkan tingkat risiko
-
-
- {summary && (
-
-
- Risiko Tinggi
- {summary.riskDistribution.high}
-
-
- Risiko Sedang
- {summary.riskDistribution.medium}
-
-
- Risiko Rendah
- {summary.riskDistribution.low}
-
-
- )}
+
+
+ {summary.needsReviewCount}
+
+
+ Lihat daftar
+
+
+
+
+ {/* Diagnosa Gagal */}
+
+
+
+ Gagal
+
+
+
+
+ —
+
+
+ Lihat daftar
+
+
+
+
+ {/* Risiko Tinggi — catalog count, link to catalog filtered */}
+
+
+
+ Kategori Risiko Tinggi
+
+
+
+
+ {summary.riskDistribution.high}
+
+
+ Lihat pustaka
+
+ )}
- {
- await createDiagnosisMutation.mutateAsync(file);
- }}
- isSubmitting={createDiagnosisMutation.isPending}
- latestResult={diagnoses[0] ?? null}
- />
+ {/* Mission section */}
+
+
+
+ Misi Platform
+
+
+ Fitur inti yang kami sediakan untuk mendukung petani jagung
+ Indonesia
+
+
- {
- await createClassificationMutation.mutateAsync(payload);
- }}
- isSubmitting={createClassificationMutation.isPending}
- />
+
+ {missionCards.map((card) => {
+ const Icon = card.icon;
- {diagnoses.length > 0 && (
-
-
-
-
- Riwayat Diagnosis
-
-
- {diagnoses.length} diagnosis yang telah dibuat
-
-
-
-
- {diagnoses.slice(0, 6).map((diagnosis) => (
-
- ))}
-
-
-
+ return (
+
+
+
+
+
+
+
+ {card.title}
+
+
+ {card.description}
+
+
+
+
+ );
+ })}
+
+
+
+ {/* Diseases quick access - now clickable, link to catalog/:slug */}
+
+
+
+
+ Penyakit yang Dapat Dideteksi
+
+
+ {diseases.length} kelas penyakit dan kondisi daun jagung dalam sistem kami
+
+
+
+ Lihat Pustaka
+
+
+
+ {diseasesQuery.isLoading ? (
+
+ Memuat data penyakit...
+
+ ) : (
+
+ {diseasesQuick.map((d) => (
+
+
+
+
+
+
+
+ {d.name}
+
+
+ {d.sci}
+
+
+
+
+
+
+ ))}
+
)}
- >
- )}
-
-
+
+
+ {/* Scan quick access */}
+
+
+
Siap Mendeteksi Penyakit Daun?
+
+ Unggah foto daun jagung Anda dan dapatkan hasil analisis AI
+ dalam hitungan detik.
+
+
+
+
+
+ Mulai Scan Sekarang
+
+
+
+ >
+ )}
+
);
}
diff --git a/apps/web/src/pages/diagnoses-page.tsx b/apps/web/src/pages/diagnoses-page.tsx
new file mode 100644
index 0000000..9f33d63
--- /dev/null
+++ b/apps/web/src/pages/diagnoses-page.tsx
@@ -0,0 +1,197 @@
+import { useMemo, useState } from "react";
+import { Link, useSearchParams } from "react-router-dom";
+import { useQuery } from "@tanstack/react-query";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { DiagnosisStatusBadge } from "@/components/diagnosis-status-badge";
+import { RiskBadge } from "@/components/risk-badge";
+import { apiClient } from "@/lib/api-client";
+import type { DiagnosisStatus, RiskLevel } from "@zeavis/shared";
+
+const STATUS_OPTIONS: { value: "all" | DiagnosisStatus; label: string }[] = [
+ { value: "all", label: "Semua" },
+ { value: "ai_verified", label: "Terverifikasi AI" },
+ { value: "needs_review", label: "Menunggu Review" },
+ { value: "expert_verified", label: "Diverifikasi Pakar" },
+ { value: "expert_corrected", label: "Dikoreksi Pakar" },
+ { value: "failed", label: "Gagal" },
+];
+
+const RISK_OPTIONS: { value: "all" | RiskLevel; label: string }[] = [
+ { value: "all", label: "Semua Risiko" },
+ { value: "high", label: "Risiko Tinggi" },
+ { value: "medium", label: "Risiko Sedang" },
+ { value: "low", label: "Risiko Rendah" },
+];
+
+export function DiagnosesPage() {
+ const [searchParams, setSearchParams] = useSearchParams();
+ const initialStatus = searchParams.get("status") as DiagnosisStatus | null;
+ const initialRisk = searchParams.get("risk") as RiskLevel | null;
+
+ const [statusFilter, setStatusFilter] = useState<
+ "all" | DiagnosisStatus
+ >(initialStatus ?? "all");
+ const [riskFilter, setRiskFilter] = useState<
+ "all" | RiskLevel
+ >(initialRisk ?? "all");
+
+ const query = useQuery({
+ queryKey: ["diagnoses"],
+ queryFn: () => apiClient.getDiagnoses(),
+ });
+ const diagnoses = query.data ?? [];
+
+ const filtered = useMemo(() => {
+ return diagnoses.filter((d) => {
+ if (statusFilter !== "all" && d.status !== statusFilter) return false;
+ if (riskFilter !== "all") {
+ const level = d.disease?.riskLevel ?? "low";
+ if (level !== riskFilter) return false;
+ }
+ return true;
+ });
+ }, [diagnoses, statusFilter, riskFilter]);
+
+ const handleStatusChange = (value: string) => {
+ const v = value as "all" | DiagnosisStatus;
+ setStatusFilter(v);
+ const next = new URLSearchParams(searchParams);
+ if (v === "all") next.delete("status");
+ else next.set("status", v);
+ setSearchParams(next);
+ };
+
+ const handleRiskChange = (value: string) => {
+ const v = value as "all" | RiskLevel;
+ setRiskFilter(v);
+ const next = new URLSearchParams(searchParams);
+ if (v === "all") next.delete("risk");
+ else next.set("risk", v);
+ setSearchParams(next);
+ };
+
+ return (
+
+
+
+
Diagnosa Tanaman
+
+ Lihat hasil diagnosa dari scan yang telah dilakukan
+
+
+
+
+ Scan Baru
+
+
+
+
+
+
Status
+
handleStatusChange(e.target.value)}
+ className="rounded-md border border-border bg-background px-3 py-2 text-sm"
+ >
+ {STATUS_OPTIONS.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+
Risiko
+
handleRiskChange(e.target.value)}
+ className="rounded-md border border-border bg-background px-3 py-2 text-sm"
+ >
+ {RISK_OPTIONS.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+
+ Total: {filtered.length}
+
+
+
+ {query.isLoading ? (
+
+ Memuat diagnosis...
+
+ ) : query.isError ? (
+
+ Gagal memuat diagnosis
+
+ ) : filtered.length === 0 ? (
+
+
+ Tidak ada diagnosis sesuai filter
+
+ {diagnoses.length === 0 && (
+
+ Mulai dengan{" "}
+
+ melakukan scan daun
+
+ .
+
+ )}
+
+ ) : (
+
+ {filtered.map((d) => (
+
+
+
+
+
+
+
+ {d.disease?.commonName ?? "Diagnosis gagal"}
+
+
+ {d.disease?.label ?? d.predictedDiseaseSlug}
+
+
+
+
+
+
+
+
+
+
+ {new Date(d.createdAt).toLocaleString("id-ID")}
+ {d.confidence !== null && (
+
+ • {(d.confidence * 100).toFixed(0)}%
+
+ )}
+
+
+ Lihat detail
+
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+export default DiagnosesPage;
diff --git a/apps/web/src/pages/diagnosis-detail-page.tsx b/apps/web/src/pages/diagnosis-detail-page.tsx
index c9f45e6..c2c56ff 100644
--- a/apps/web/src/pages/diagnosis-detail-page.tsx
+++ b/apps/web/src/pages/diagnosis-detail-page.tsx
@@ -14,23 +14,22 @@ export function DiagnosisDetailPage() {
enabled: Boolean(id),
});
- if (query.isLoading) return Memuat diagnosis... ;
- if (query.error || !query.data) return Diagnosis tidak ditemukan ;
+ if (query.isLoading) return Memuat diagnosis...
;
+ if (query.error || !query.data) return Diagnosis tidak ditemukan
;
const diagnosis = query.data;
return (
-
-
-
+
+
Detail Diagnosis
-
{diagnosis.disease?.commonName ?? 'Diagnosis gagal'}
+
{diagnosis.disease?.commonName ?? 'Diagnosis gagal'}
Kembali
-
+
@@ -99,6 +98,5 @@ export function DiagnosisDetailPage() {
)}
-
- );
-}
+ );
+ }
diff --git a/apps/web/src/pages/disease-detail-page.tsx b/apps/web/src/pages/disease-detail-page.tsx
index 3bae73f..2bfe5a2 100644
--- a/apps/web/src/pages/disease-detail-page.tsx
+++ b/apps/web/src/pages/disease-detail-page.tsx
@@ -1,7 +1,7 @@
import { useParams, Link } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { RiskBadge } from '@/components/risk-badge';
@@ -20,37 +20,37 @@ export function DiseaseDetailPage() {
if (!validatedSlug || error || (!isLoading && !disease)) {
return (
-
-
-
+
+
+
Kembali ke Katalog
-
- Materi tidak ditemukan
-
-
+
+ Materi tidak ditemukan
+
+
);
}
if (isLoading) {
return (
-
-
-
+
+
+
Kembali ke Katalog
-
- Memuat materi...
-
-
+
+ Memuat materi...
+
+
);
}
@@ -59,88 +59,86 @@ export function DiseaseDetailPage() {
}
return (
-
-
-
-
-
-
- Kembali ke Katalog
-
-
-
-
-
- Dashboard
-
-
-
-
-
-
-
-
{disease.commonName}
-
{disease.label}
-
-
-
-
{disease.summary}
-
-
-
-
- Deskripsi
-
-
- {disease.description}
-
-
-
-
-
- Gejala
- Tanda-tanda yang perlu diperhatikan
-
-
-
- {disease.symptoms.map((symptom, index) => (
-
-
- {symptom}
-
- ))}
-
-
-
-
-
-
- Rekomendasi Penanganan
- Langkah-langkah untuk mengendalikan penyakit
-
-
-
- {disease.recommendations.map((recommendation, index) => (
-
-
- {index + 1}
-
- {recommendation}
-
- ))}
-
-
-
-
-
-
- Lihat Katalog Lengkap
-
-
- Kembali ke Dashboard
-
-
+
+
+
+
+
+ Kembali ke Katalog
+
+
+
+
+
+ Dashboard
+
+
-
+
+
+
+
+
{disease.commonName}
+
{disease.label}
+
+
+
+
{disease.summary}
+
+
+
+
+ Deskripsi
+
+
+ {disease.description}
+
+
+
+
+
+ Gejala
+ Tanda-tanda yang perlu diperhatikan
+
+
+
+ {disease.symptoms.map((symptom, index) => (
+
+
+ {symptom}
+
+ ))}
+
+
+
+
+
+
+ Rekomendasi Penanganan
+ Langkah-langkah untuk mengendalikan penyakit
+
+
+
+ {disease.recommendations.map((recommendation, index) => (
+
+
+ {index + 1}
+
+ {recommendation}
+
+ ))}
+
+
+
+
+
+
+ Lihat Katalog Lengkap
+
+
+ Kembali ke Dashboard
+
+
+
);
}
diff --git a/apps/web/src/pages/expert-reviews-page.tsx b/apps/web/src/pages/expert-reviews-page.tsx
index 98af3ce..cd8d0fe 100644
--- a/apps/web/src/pages/expert-reviews-page.tsx
+++ b/apps/web/src/pages/expert-reviews-page.tsx
@@ -1,43 +1,61 @@
-import { FormEvent, useMemo, useState } from 'react';
-import { Link, useSearchParams } from 'react-router-dom';
-import { useMutation, useQueries, useQueryClient } from '@tanstack/react-query';
-import type { DiseaseSlug } from '@zeavis/shared';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { DiagnosisStatusBadge } from '@/components/diagnosis-status-badge';
-import { apiClient } from '@/lib/api-client';
+import { FormEvent, useMemo, useState } from "react";
+import { useSearchParams } from "react-router-dom";
+import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query";
+import type { DiseaseSlug } from "@zeavis/shared";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { DiagnosisStatusBadge } from "@/components/diagnosis-status-badge";
+import { apiClient } from "@/lib/api-client";
export function ExpertReviewsPage() {
const [searchParams, setSearchParams] = useSearchParams();
- const selectedId = searchParams.get('diagnosis');
- const [verdict, setVerdict] = useState<'verified' | 'corrected'>('verified');
- const [correctedDiseaseSlug, setCorrectedDiseaseSlug] = useState
('');
- const [notes, setNotes] = useState('');
+ const selectedId = searchParams.get("diagnosis");
+ const [verdict, setVerdict] = useState<"verified" | "corrected">("verified");
+ const [correctedDiseaseSlug, setCorrectedDiseaseSlug] = useState<
+ DiseaseSlug | ""
+ >("");
+ const [notes, setNotes] = useState("");
const queryClient = useQueryClient();
const [reviewsQuery, diseasesQuery] = useQueries({
queries: [
- { queryKey: ['expert-reviews'], queryFn: () => apiClient.getExpertReviews() },
- { queryKey: ['diseases'], queryFn: () => apiClient.getDiseases() },
+ {
+ queryKey: ["expert-reviews"],
+ queryFn: () => apiClient.getExpertReviews(),
+ },
+ { queryKey: ["diseases"], queryFn: () => apiClient.getDiseases() },
],
});
const reviews = reviewsQuery.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({
- mutationFn: () => apiClient.reviewDiagnosis(selected!.id, {
- verdict,
- correctedDiseaseSlug: verdict === 'corrected' ? correctedDiseaseSlug || undefined : undefined,
- notes,
- }),
+ mutationFn: () =>
+ apiClient.reviewDiagnosis(selected!.id, {
+ verdict,
+ correctedDiseaseSlug:
+ verdict === "corrected"
+ ? correctedDiseaseSlug || undefined
+ : undefined,
+ notes,
+ }),
onSuccess: () => {
- setNotes('');
- setVerdict('verified');
- setCorrectedDiseaseSlug('');
- queryClient.invalidateQueries({ queryKey: ['expert-reviews'] });
- queryClient.invalidateQueries({ queryKey: ['dashboard-summary'] });
+ setNotes("");
+ setVerdict("verified");
+ setCorrectedDiseaseSlug("");
+ queryClient.invalidateQueries({ queryKey: ["expert-reviews"] });
+ queryClient.invalidateQueries({ queryKey: ["dashboard-summary"] });
},
});
@@ -47,231 +65,279 @@ export function ExpertReviewsPage() {
await mutation.mutateAsync();
}
+ const isLoading = reviewsQuery.isPending || diseasesQuery.isPending;
+
return (
-
-
-
-
Expert Reviews
-
-
Dashboard
-
+
+
+
+
Review Pakar
+
+ Tinjau hasil diagnosa dari scan yang telah dilakukan dan berikan
+ feedback untuk meningkatkan akurasi sistem AI ZeaVis Edu
+
-
+
-
-
- {/* Left: List of reviews */}
-
-
-
Diagnoses to Review
- {reviews.length}
-
-
-
- {reviewsQuery.isPending || diseasesQuery.isPending ? (
-
Loading reviews and diseases...
- ) : reviews.length === 0 ? (
-
No diagnoses pending review
- ) : (
- reviews.map((review) => (
-
setSearchParams({ diagnosis: review.id })}
- className={`w-full text-left transition ${
- selected?.id === review.id
- ? 'ring-2 ring-primary'
- : 'hover:border-primary'
- }`}
- >
-
-
-
-
-
-
{review.disease?.commonName ?? 'Diagnosis gagal'}
-
-
-
- {review.confidence === null ? 'Tidak ada confidence' : `${(review.confidence * 100).toFixed(1)}%`}
-
-
{new Date(review.createdAt).toLocaleString('id-ID')}
-
-
-
-
- ))
- )}
-
+
+ {/* Left: List of reviews */}
+
+
+
Daftar Diagnosis
+
+ {reviews.length}
+
- {/* Right: Detail and form */}
-
- {reviewsQuery.isPending || diseasesQuery.isPending ? (
-
-
- Loading reviews and diseases...
-
-
- ) : selected ? (
- <>
- {/* Detail card */}
-
-
- {selected.disease?.commonName ?? 'Diagnosis gagal'}
-
- {new Date(selected.createdAt).toLocaleString('id-ID')}
-
-
-
-
-
-
-
- Status
-
-
-
- Confidence
-
- {selected.confidence === null ? 'N/A' : `${(selected.confidence * 100).toFixed(1)}%`}
-
-
-
-
- {/* Top predictions */}
- {selected.predictions.length > 0 && (
-
-
Top Predictions
-
- {selected.predictions.map((pred) => (
-
- {pred.modelLabel}
- {(pred.confidence * 100).toFixed(1)}%
-
- ))}
-
-
- )}
-
-
-
- {/* Review form */}
-
-
- Expert Review
-
-
-
-
-
- >
+
+ {isLoading ? (
+
+ Memuat diagnosis...
+
+ ) : reviews.length === 0 ? (
+
+ Tidak ada diagnosis yang menunggu review
+
) : (
-
-
- No diagnoses available for review
-
-
+ reviews.map((review) => (
+
setSearchParams({ diagnosis: review.id })}
+ className={`w-full text-left transition ${
+ selected?.id === review.id
+ ? "ring-2 ring-primary"
+ : "hover:border-primary"
+ }`}
+ >
+
+
+
+
+
+
+ {review.disease?.commonName ?? "Diagnosis gagal"}
+
+
+
+
+ {review.confidence === null
+ ? "Tidak ada confidence"
+ : `${(review.confidence * 100).toFixed(1)}%`}
+
+
+ {new Date(review.createdAt).toLocaleString("id-ID")}
+
+
+
+
+
+ ))
)}
+
+ {/* Right: Detail and form */}
+
+ {isLoading ? (
+
+
+ Memuat diagnosis...
+
+
+ ) : selected ? (
+ <>
+ {/* Detail card */}
+
+
+
+ {selected.disease?.commonName ?? "Diagnosis gagal"}
+
+
+ {new Date(selected.createdAt).toLocaleString("id-ID")}
+
+
+
+
+
+
+
+ Status
+
+
+
+ Confidence
+
+ {selected.confidence === null
+ ? "N/A"
+ : `${(selected.confidence * 100).toFixed(1)}%`}
+
+
+
+
+ {selected.predictions.length > 0 && (
+
+
Prediksi Teratas
+
+ {selected.predictions.map((pred) => (
+
+
+ {pred.modelLabel}
+
+
+ {(pred.confidence * 100).toFixed(1)}%
+
+
+ ))}
+
+
+ )}
+
+
+
+ {/* Review form */}
+
+
+ Review Pakar
+
+
+
+
+
+ {verdict === "corrected" && (
+
+
+ Penyakit yang Benar
+
+
+ setCorrectedDiseaseSlug(
+ e.target.value as DiseaseSlug | "",
+ )
+ }
+ required={verdict === "corrected"}
+ className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
+ >
+ Pilih penyakit...
+ {diseases.map((disease) => (
+
+ {disease.commonName}
+
+ ))}
+
+
+ )}
+
+
+
+ Catatan *
+
+ setNotes(e.target.value)}
+ placeholder="Tulis catatan review Anda..."
+ required
+ className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary"
+ rows={4}
+ />
+
+
+
+ {mutation.isPending ? "Mengirim..." : "Kirim Review"}
+
+
+ {mutation.isError && (
+
+ Error:{" "}
+ {mutation.error instanceof Error
+ ? mutation.error.message
+ : "Terjadi kesalahan"}
+
+ )}
+
+
+
+ >
+ ) : (
+
+
+ Tidak ada diagnosis yang tersedia untuk review
+
+
+ )}
+
-
+
);
}
diff --git a/apps/web/src/pages/landing-page.tsx b/apps/web/src/pages/landing-page.tsx
index 58a0913..d6eea0a 100644
--- a/apps/web/src/pages/landing-page.tsx
+++ b/apps/web/src/pages/landing-page.tsx
@@ -1,23 +1,31 @@
-import { ArrowRight, Leaf, ShieldCheck, Sprout } from 'lucide-react';
-import { Link } from 'react-router-dom';
-import { Button } from '@/components/ui/button';
-import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { ArrowRight, Leaf, ShieldCheck, Sprout } from "lucide-react";
+import { Link } from "react-router-dom";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
const features = [
{
icon: Leaf,
- title: 'Katalog Penyakit Lengkap',
- description: 'Pelajari tentang empat penyakit daun jagung utama dengan gejala dan rekomendasi penanganan.',
+ title: "Katalog Penyakit Lengkap",
+ description:
+ "Pelajari tentang empat penyakit daun jagung utama dengan gejala dan rekomendasi penanganan.",
},
{
icon: ShieldCheck,
- title: 'Pantau Risiko Penyakit',
- description: 'Lacak distribusi risiko penyakit dan lihat laporan pengamatan terbaru di dashboard.',
+ title: "Pantau Risiko Penyakit",
+ description:
+ "Lacak distribusi risiko penyakit dan lihat laporan pengamatan terbaru di dashboard.",
},
{
icon: Sprout,
- title: 'Laporkan Pengamatan',
- description: 'Kirimkan laporan penyakit yang Anda temukan untuk membantu penelitian dan edukasi.',
+ title: "Laporkan Pengamatan",
+ description:
+ "Kirimkan laporan penyakit yang Anda temukan untuk membantu penelitian dan edukasi.",
},
];
@@ -44,23 +52,26 @@ export function LandingPage() {
- Belajar mengenali penyakit daun jagung dan pantau pengamatan Anda.
+ Belajar mengenali penyakit daun jagung dan pantau pengamatan
+ Anda.
- Jelajahi katalog penyakit daun jagung, pelajari gejala dan cara penanganannya,
- serta laporkan pengamatan Anda untuk membantu penelitian dan edukasi.
+ Jelajahi katalog penyakit daun jagung, pelajari gejala dan cara
+ penanganannya, serta laporkan pengamatan Anda untuk membantu
+ penelitian dan edukasi.
-
- Buka Dashboard
+
+ Mulai Scan Sekarang
-
- Lihat Katalog Penyakit
-
+ Pustaka Penyakit
+
+
+ Dashboard
@@ -74,7 +85,9 @@ export function LandingPage() {
{feature.title}
- {feature.description}
+
+ {feature.description}
+
diff --git a/apps/web/src/pages/library-page.tsx b/apps/web/src/pages/library-page.tsx
new file mode 100644
index 0000000..0050cbc
--- /dev/null
+++ b/apps/web/src/pages/library-page.tsx
@@ -0,0 +1,97 @@
+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() {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [riskFilter, setRiskFilter] = useState
("all");
+
+ const { data: diseases, isLoading, error } = useQuery({
+ queryKey: ["diseases"],
+ queryFn: () => apiClient.getDiseases(),
+ });
+
+ 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 (
+
+
+
+
Pustaka Penyakit
+
+ Referensi lengkap penyakit daun jagung yang dapat dideteksi oleh sistem AI
+
+
+
+
+
+
+
+ Cari penyakit
+
+ setSearchQuery(e.target.value)}
+ className="w-full rounded-md border border-border bg-background px-3 py-2 text-sm"
+ />
+
+
+
+ Filter risiko
+
+ setRiskFilter(e.target.value as RiskLevel | "all")}
+ className="rounded-md border border-border bg-background px-3 py-2 text-sm"
+ >
+ Semua Risiko
+ Risiko Rendah
+ Risiko Sedang
+ Risiko Tinggi
+
+
+
+
+ {isLoading && (
+
+ Memuat pustaka penyakit...
+
+ )}
+
+ {error && (
+
+ Gagal memuat pustaka penyakit
+
+ )}
+
+ {!isLoading && !error && filteredDiseases.length === 0 && (
+
+ Tidak ada penyakit yang cocok dengan filter ini.
+
+ )}
+
+ {!isLoading && !error && filteredDiseases.length > 0 && (
+
+ {filteredDiseases.map((disease) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/pages/login-page.tsx b/apps/web/src/pages/login-page.tsx
index 94aef67..995af45 100644
--- a/apps/web/src/pages/login-page.tsx
+++ b/apps/web/src/pages/login-page.tsx
@@ -1,35 +1,41 @@
-import { useState } from 'react';
-import { Link, useNavigate } from 'react-router-dom';
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { AuthForm } from '@/components/auth-form';
-import { apiClient } from '@/lib/api-client';
-import { useAuthStore } from '@/store/auth-store';
+import { useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { AuthForm } from "@/components/auth-form";
+import { apiClient } from "@/lib/api-client";
+import { useAuthStore } from "@/store/auth-store";
export function LoginPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const setUser = useAuthStore((state) => state.setUser);
const [error, setError] = useState(null);
- const meQuery = useQuery({ queryKey: ['auth', 'me'], queryFn: () => apiClient.getMe() });
+ const meQuery = useQuery({
+ queryKey: ["auth", "me"],
+ queryFn: () => apiClient.getMe(),
+ });
const mutation = useMutation({
mutationFn: apiClient.login,
onSuccess: (response) => {
setUser(response.user);
- queryClient.setQueryData(['auth', 'me'], response);
- navigate('/dashboard');
+ queryClient.setQueryData(["auth", "me"], response);
+ navigate("/dashboard");
},
- onError: (err) => setError(err instanceof Error ? err.message : 'Login gagal'),
+ onError: (err) =>
+ setError(err instanceof Error ? err.message : "Login gagal"),
});
return (
-
+
{
setError(null);
return mutation.mutateAsync({ email, password });
@@ -37,7 +43,10 @@ export function LoginPage() {
onFieldChange={() => setError(null)}
/>
- Belum punya akun? Daftar
+ Belum punya akun?{" "}
+
+ Daftar
+
diff --git a/apps/web/src/pages/register-page.tsx b/apps/web/src/pages/register-page.tsx
index ddf2670..f6d418e 100644
--- a/apps/web/src/pages/register-page.tsx
+++ b/apps/web/src/pages/register-page.tsx
@@ -1,43 +1,52 @@
-import { useState } from 'react';
-import { Link, useNavigate } from 'react-router-dom';
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { AuthForm } from '@/components/auth-form';
-import { apiClient } from '@/lib/api-client';
-import { useAuthStore } from '@/store/auth-store';
+import { useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { AuthForm } from "@/components/auth-form";
+import { apiClient } from "@/lib/api-client";
+import { useAuthStore } from "@/store/auth-store";
export function RegisterPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const setUser = useAuthStore((state) => state.setUser);
const [error, setError] = useState
(null);
- const meQuery = useQuery({ queryKey: ['auth', 'me'], queryFn: () => apiClient.getMe() });
+ const meQuery = useQuery({
+ queryKey: ["auth", "me"],
+ queryFn: () => apiClient.getMe(),
+ });
const mutation = useMutation({
mutationFn: apiClient.register,
onSuccess: (response) => {
setUser(response.user);
- queryClient.setQueryData(['auth', 'me'], response);
- navigate('/dashboard');
+ queryClient.setQueryData(["auth", "me"], response);
+ navigate("/dashboard");
},
- onError: (err) => setError(err instanceof Error ? err.message : 'Registrasi gagal'),
+ onError: (err) =>
+ setError(err instanceof Error ? err.message : "Registrasi gagal"),
});
return (
-
+
{
setError(null);
- return mutation.mutateAsync({ name: name ?? '', email, password });
+ return mutation.mutateAsync({ name: name ?? "", email, password });
}}
onFieldChange={() => setError(null)}
/>
- Sudah punya akun? Masuk
+ Sudah punya akun?{" "}
+
+ Masuk
+
diff --git a/apps/web/src/pages/scan-page.tsx b/apps/web/src/pages/scan-page.tsx
new file mode 100644
index 0000000..33c4edc
--- /dev/null
+++ b/apps/web/src/pages/scan-page.tsx
@@ -0,0 +1,488 @@
+import { useRef, useState } from "react";
+import { useNavigate, Link } from "react-router-dom";
+import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
+import {
+ Camera,
+ Upload,
+ X,
+ ImageOff,
+ CircleAlert,
+ ZoomIn,
+ Sun,
+ AlignCenter,
+ Check,
+} from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Modal } from "@/components/ui/modal";
+import { DiagnosisStatusBadge } from "@/components/diagnosis-status-badge";
+import type { DiagnosisRecord } from "@zeavis/shared";
+import { apiClient } from "@/lib/api-client";
+
+export function ScanPage() {
+ const [fileName, setFileName] = useState
(null);
+ const [previewUrl, setPreviewUrl] = useState(null);
+ const [imageDimensions, setImageDimensions] = useState<{ width: number; height: number } | null>(null);
+ const inputRef = useRef(null);
+ const imageRef = useRef(null);
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+
+ const mutation = useMutation({
+ mutationFn: (file: File) => apiClient.createDiagnosis(file),
+ onSuccess: (diagnosis) => {
+ queryClient.invalidateQueries({ queryKey: ["diagnoses"] });
+ setDiagnosisPreview(diagnosis);
+ setPreviewOpen(true);
+ setFileName(null);
+ setPreviewUrl(null);
+ if (inputRef.current) inputRef.current.value = "";
+ },
+ });
+
+ const handleFile = (f?: File) => {
+ if (!f) return;
+ if (f.size > 5 * 1024 * 1024) {
+ alert("Ukuran file melebihi batas 5MB");
+ return;
+ }
+ setFileName(f.name);
+ const url = URL.createObjectURL(f);
+ setPreviewUrl(url);
+
+ // Detect image dimensions
+ const img = new Image();
+ img.onload = () => {
+ setImageDimensions({ width: img.width, height: img.height });
+ };
+ img.src = url;
+ };
+
+ const handleUpload = () => {
+ const file = inputRef.current?.files?.[0];
+ if (!file) return;
+ mutation.mutate(file);
+ };
+
+ const [previewOpen, setPreviewOpen] = useState(false);
+ const [diagnosisPreview, setDiagnosisPreview] =
+ useState(null);
+
+ const diagnosesQuery = useQuery({
+ queryKey: ["diagnoses"],
+ queryFn: () => apiClient.getDiagnoses(),
+ enabled: previewOpen,
+ });
+
+ return (
+
+
+
+
Scan Tanaman
+
+ Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara
+ real-time.
+
+
+
+ Riwayat Diagnosis
+
+
+
+
+
+
+
+ {/* Left Sidebar */}
+
+
+ Area Unggah Gambar
+
+
+ {/* Upload area */}
+ {!previewUrl ? (
+
+
inputRef.current?.click()}
+ >
+
+
+ Seret & Lepas Foto Daun
+
+
+ atau klik untuk memilih file berkas dari perangkat Anda
+
+
+
+
+ PNG, JPG, JPEG
+
+
+
+ Maks. 5 MB
+
+
+
+
+
inputRef.current?.click()}
+ className="w-full bg-green-600 hover:bg-green-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors flex items-center justify-center gap-2"
+ >
+
+ Pilih Berkas
+
+
+ ) : (
+
+
+
{
+ const img = e.currentTarget;
+ setImageDimensions({
+ width: img.naturalWidth,
+ height: img.naturalHeight,
+ });
+ }}
+ />
+ {/* Scanner area detection overlay */}
+ {imageDimensions && (
+
+ {/* Outer dark overlay */}
+
+
+ {/* Scan area frame - responsive to image */}
+
+ {/* Corner markers */}
+
+
+
+
+
+ {/* Center text */}
+
+ AREA SCAN
+
+ {imageDimensions.width} × {imageDimensions.height}
+
+
+
+
+ )}
+
+
{
+ setPreviewUrl(null);
+ setFileName(null);
+ setImageDimensions(null);
+ if (inputRef.current) inputRef.current.value = "";
+ }}
+ >
+
+
+
+
+
+ File: {fileName}
+
+
+ {
+ setPreviewUrl(null);
+ setFileName(null);
+ setImageDimensions(null);
+ if (inputRef.current) inputRef.current.value = "";
+ }}
+ >
+
+ Ganti Foto
+
+
+
+ {mutation.isPending ? "Memproses..." : "Analisis"}
+
+
+
+
+ )}
+
+ handleFile(e.target.files?.[0])}
+ />
+
+ {/* Error state */}
+ {mutation.isError && (
+
+
Upload gagal
+
+ {mutation.error instanceof Error
+ ? mutation.error.message
+ : "Terjadi kesalahan saat memproses gambar"}
+
+
+ )}
+
+
+
+
+ {/* Right Sidebar */}
+
+ {/* Card 1: Panduan Pengambilan Foto */}
+
+
+
+
+
+
+
+ Panduan Pengambilan Foto
+
+
+
+
+
+
+
+
+
+ Jarak 15–30 cm dari daun
+
+
+
+
+
+
+
+ Pencahayaan cukup merata
+
+
+
+
+
+ Posisi daun mengisi bingkai dengan jelas
+
+
+
+
+
+
+
+ Hindari bayangan atau pantulan pada daun & blur pada gambar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AREA SCAN
+
+
+
+ Posisi ideal: daun memenuhi bingkai
+
+
+
+
+
+
+
+
+ {/* Card 2: Persyaratan Berkas */}
+
+
+
+ Persyaratan Berkas
+
+
+ {[
+ { label: "Format", value: "PNG, JPG, JPEG" },
+ { label: "Ukuran Maks.", value: "5 MB" },
+ { label: "Resolusi Min.", value: "512 × 512 px" },
+ { label: "Objek Foto", value: "Daun jagung tunggal" },
+ ].map((r, i) => (
+
+
+ {r.label}
+
+
+ {r.value}
+
+
+ ))}
+
+
+
+
+
+
+ {/* Result Modal */}
+
{
+ setPreviewOpen(false);
+ setDiagnosisPreview(null);
+ }}
+ title={diagnosisPreview?.disease?.commonName ?? "Hasil Diagnosis"}
+ size="md"
+ footer={
+ diagnosisPreview && (
+
+ {
+ setPreviewOpen(false);
+ setDiagnosisPreview(null);
+ }}
+ >
+ Tutup
+
+ navigate(`/diagnoses/${diagnosisPreview.id}`)}
+ className="bg-green-600 hover:bg-green-700"
+ >
+ Lihat Detail Lengkap
+
+
+ )
+ }
+ >
+ {diagnosisPreview ? (
+
+
+
+ {diagnosisPreview.confidence !== null && (
+
+ Confidence: {(diagnosisPreview.confidence * 100).toFixed(1)}%
+
+ )}
+
+
+ {diagnosisPreview.disease && (
+
+
+ {diagnosisPreview.disease.commonName}
+
+
+ {diagnosisPreview.disease.summary}
+
+
+ )}
+
+ {diagnosisPreview.failureReason && (
+
+ {diagnosisPreview.failureReason}
+
+ )}
+
+ {diagnosisPreview.predictions.length > 0 && (
+
+
Semua Prediksi
+
+ {diagnosisPreview.predictions
+ .sort((a, b) => a.rank - b.rank)
+ .map((pred) => (
+
+ {pred.modelLabel}
+
+ {(pred.confidence * 100).toFixed(1)}%
+
+
+ ))}
+
+
+ )}
+
+
+
+ Riwayat Diagnosis Terbaru
+
+ {diagnosesQuery.isLoading && (
+
+ Memuat riwayat...
+
+ )}
+ {!diagnosesQuery.isLoading && !diagnosesQuery.isError && (
+
+ {(diagnosesQuery.data ?? []).slice(0, 5).map((d) => (
+
+
+
+
+
+ {d.disease?.commonName ?? "Unknown"}
+
+
+ {new Date(d.createdAt).toLocaleString("id-ID")}
+
+
+
+
+ Lihat
+
+
+ ))}
+
+ )}
+
+
+ ) : null}
+
+
+ );
+}
diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts
deleted file mode 100644
index 4a9e6c7..0000000
--- a/apps/web/tailwind.config.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-export default {
- darkMode: ['class'],
- content: ['./index.html', './src/**/*.{ts,tsx}'],
- theme: {
- extend: {
- colors: {
- border: 'hsl(var(--border) / )',
- background: 'hsl(var(--background) / )',
- foreground: 'hsl(var(--foreground) / )',
- primary: {
- DEFAULT: 'hsl(var(--primary) / )',
- foreground: 'hsl(var(--primary-foreground) / )',
- },
- muted: {
- DEFAULT: 'hsl(var(--muted) / )',
- foreground: 'hsl(var(--muted-foreground) / )',
- },
- card: {
- DEFAULT: 'hsl(var(--card) / )',
- foreground: 'hsl(var(--card-foreground) / )',
- },
- },
- borderRadius: {
- lg: 'var(--radius)',
- md: 'calc(var(--radius) - 2px)',
- sm: 'calc(var(--radius) - 4px)',
- },
- },
- },
- plugins: [],
-};
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index eeb89ec..08b0a15 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -1,4 +1,5 @@
import react from '@vitejs/plugin-react';
+import tsconfigPaths from 'vite-tsconfig-paths';
import path from 'node:path';
import { defineConfig, loadEnv } from 'vite';
@@ -7,7 +8,7 @@ export default defineConfig(({ mode }) => {
const apiProxyTarget = env.VITE_API_PROXY_TARGET || 'http://localhost:3000';
return {
- plugins: [react()],
+ plugins: [react(), tsconfigPaths()],
server: {
proxy: {
'/api': apiProxyTarget,
diff --git a/bun.lock b/bun.lock
index 00ebf72..b432085 100644
--- a/bun.lock
+++ b/bun.lock
@@ -50,6 +50,7 @@
"postcss": "^8.5.15",
"typescript": "^6.0.3",
"vite": "^8.0.14",
+ "vite-tsconfig-paths": "6.1.1",
},
},
"packages/shared": {
@@ -297,6 +298,8 @@
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="],
+ "globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
+
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
@@ -381,6 +384,8 @@
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
+ "tsconfck": ["tsconfck@3.1.6", "", { "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"], "bin": { "tsconfck": "bin/tsconfck.js" } }, "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w=="],
+
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsx": ["tsx@4.22.3", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg=="],
@@ -393,6 +398,8 @@
"vite": ["vite@8.0.14", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="],
+ "vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
+
"zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],