diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index 4a4f7b3..93b7301 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -5,7 +5,7 @@ import { Navigate, } from "react-router-dom"; import { AuthInitializer } from "@/components/auth-initializer"; -import { AuthGuard } from "@/components/auth-guard"; +// import { AuthGuard } from "@/components/auth-guard"; import { DashboardPage } from "@/pages/dashboard-page"; import { ScanPage } from "@/pages/scan-page"; import { LibraryPage } from "@/pages/library-page"; @@ -14,74 +14,62 @@ import { DiseaseDetailPage } from "@/pages/disease-detail-page"; import { DiagnosisDetailPage } from "@/pages/diagnosis-detail-page"; import { ExpertReviewsPage } from "@/pages/expert-reviews-page"; import { DiagnosesPage } from "@/pages/diagnoses-page"; -import { LoginPage } from "@/pages/login-page"; -import { RegisterPage } from "@/pages/register-page"; +// import { LoginPage } from "@/pages/login-page"; +// import { RegisterPage } from "@/pages/register-page"; import { MainLayout } from "@/components/layout/main-layout"; const queryClient = new QueryClient(); const router = createBrowserRouter([ - { path: "/", element: }, - { path: "/login", element: }, - { path: "/register", element: }, + { path: "/", element: }, + // { path: "/login", element: }, + // { path: "/register", element: }, { path: "/dashboard", element: ( - - - - - + + + ), }, { path: "/scan", element: ( - - - - - + + + ), }, { path: "/library", element: ( - - - - - + + + ), }, { path: "/diagnoses", element: ( - - - - - + + + ), }, { path: "/diagnoses/:id", element: ( - - - - - + + + ), }, { path: "/expert/reviews", element: ( - - - - - + + + ), }, { diff --git a/apps/web/src/assets/images/scan-guide-bg.png b/apps/web/src/assets/images/scan-guide-bg.png new file mode 100644 index 0000000..7a3d888 Binary files /dev/null and b/apps/web/src/assets/images/scan-guide-bg.png differ diff --git a/apps/web/src/components/layout/main-layout.tsx b/apps/web/src/components/layout/main-layout.tsx index 193181c..c4cfb03 100644 --- a/apps/web/src/components/layout/main-layout.tsx +++ b/apps/web/src/components/layout/main-layout.tsx @@ -8,7 +8,7 @@ export function MainLayout({ children }: Props) { return (
-
+
{children}
diff --git a/apps/web/src/components/layout/mobile-nav.tsx b/apps/web/src/components/layout/mobile-nav.tsx new file mode 100644 index 0000000..6403345 --- /dev/null +++ b/apps/web/src/components/layout/mobile-nav.tsx @@ -0,0 +1,94 @@ +import { createPortal } from "react-dom"; +import { Link, useLocation } from "react-router-dom"; +import { X, Leaf } from "lucide-react"; + +type Props = { + open: boolean; + onClose: () => void; +}; + +const NAV_ITEMS = [ + { path: "/dashboard", label: "Dashboard" }, + { path: "/scan", label: "Scan Tanaman" }, + { path: "/diagnoses", label: "Diagnosa" }, + { path: "/catalog", label: "Pustaka", altPath: "/library" }, +]; + +export function MobileNav({ open, onClose }: Props) { + const location = useLocation(); + + const isActive = (path: string) => location.pathname === path; + + const navContent = ( +
+ {/* Backdrop */} +
+ + {/* Panel */} +
+ {/* Header */} +
+
+
+ + + +
+
+
ZeaVis Edu
+
+ Smart AI for Corn Disease Detection +
+
+
+ +
+ + {/* Nav items */} + +
+
+ ); + + return typeof document !== "undefined" + ? createPortal(navContent, document.body) + : navContent; +} diff --git a/apps/web/src/components/layout/navbar.tsx b/apps/web/src/components/layout/navbar.tsx index d85e6ba..20993e1 100644 --- a/apps/web/src/components/layout/navbar.tsx +++ b/apps/web/src/components/layout/navbar.tsx @@ -1,16 +1,12 @@ -import { Link, useLocation, useNavigate } from "react-router-dom"; -import { Leaf } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useAuthStore } from "@/store/auth-store"; -import { apiClient } from "@/lib/api-client"; +import { useState } from "react"; +import { Link, useLocation } from "react-router-dom"; +import { Leaf, Menu } from "lucide-react"; +import { MobileNav } from "./mobile-nav"; export function Navbar() { + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const location = useLocation(); - const navigate = useNavigate(); - const queryClient = useQueryClient(); - const setUser = useAuthStore((state) => state.setUser); - const user = useAuthStore((state) => state.user); + // const isActive = (path: string) => location.pathname === path; @@ -22,17 +18,6 @@ export function Navbar() { : "text-white/85 hover:bg-white/10 hover:text-white", ].join(" "); - const logoutMutation = useMutation({ - mutationFn: async () => { - await apiClient.logout(); - }, - onSuccess: () => { - setUser(null); - queryClient.clear(); - navigate("/login"); - }, - }); - return (
@@ -50,38 +35,50 @@ export function Navbar() {
-
); diff --git a/apps/web/src/components/ui/modal.tsx b/apps/web/src/components/ui/modal.tsx index e536f04..97d5b6b 100644 --- a/apps/web/src/components/ui/modal.tsx +++ b/apps/web/src/components/ui/modal.tsx @@ -63,7 +63,7 @@ export function Modal({ aria-modal="true" tabIndex={-1} ref={containerRef} - className={`relative z-10 w-full ${sizeClass[size]} rounded-lg bg-white p-6 shadow-lg ${className ?? ""}`} + className={`relative z-10 w-full ${sizeClass[size]} mx-4 max-h-[90vh] overflow-y-auto rounded-lg bg-white p-6 shadow-lg ${className ?? ""}`} > {title ?

{title}

: null} diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index cd93ede..e24c6c7 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -15,6 +15,11 @@ import type { const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? ''; +export interface ApiError extends Error { + status: number; + source?: 'uploader' | 'model-service' | 'unknown'; +} + async function fetchApi(endpoint: string, options?: RequestInit): Promise { const url = `${apiBaseUrl}${endpoint}`; const response = await fetch(url, { @@ -25,14 +30,21 @@ async function fetchApi(endpoint: string, options?: RequestInit): Promise if (!response.ok) { let errorMessage = `HTTP ${response.status}`; + let source: 'uploader' | 'model-service' | 'unknown' | undefined; try { const errorData = await response.json(); if (errorData.error) { errorMessage = errorData.error; } + if (errorData.source) { + source = errorData.source; + } } catch { } - throw new Error(errorMessage); + const error = new Error(errorMessage) as ApiError; + error.status = response.status; + error.source = source; + throw error; } return response.json(); diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx index 81819a4..b2bc486 100644 --- a/apps/web/src/main.tsx +++ b/apps/web/src/main.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import { App } from './app'; +import { App } from './App'; import './index.css'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/apps/web/src/pages/catalog-page.tsx b/apps/web/src/pages/catalog-page.tsx index 82f6b4c..11620b1 100644 --- a/apps/web/src/pages/catalog-page.tsx +++ b/apps/web/src/pages/catalog-page.tsx @@ -3,7 +3,6 @@ import { useQuery } from '@tanstack/react-query'; import { Link, useSearchParams } from 'react-router-dom'; import { AlertCircle } from 'lucide-react'; import type { RiskLevel } from '@zeavis/shared'; -import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { RiskBadge } from '@/components/risk-badge'; import { apiClient } from '@/lib/api-client'; @@ -43,10 +42,12 @@ export function CatalogPage() { return (
-
+
-

Edukasi Penyakit

-

Katalog Penyakit

+

Pustaka Penyakit

+

+ Referensi lengkap penyakit dan kondisi daun jagung yang dapat dideteksi oleh sistem AI ZeaVis Edu +

diff --git a/apps/web/src/pages/dashboard-page.tsx b/apps/web/src/pages/dashboard-page.tsx index 88d8913..a5cdbc3 100644 --- a/apps/web/src/pages/dashboard-page.tsx +++ b/apps/web/src/pages/dashboard-page.tsx @@ -77,13 +77,13 @@ export function DashboardPage() { style={{ backgroundImage: `url(${bg})` }} >
-
+
AI FOR SMART EDUCATION -

Selamat Datang di

-

+

Selamat Datang di

+

ZeaVis Edu

@@ -91,11 +91,11 @@ export function DashboardPage() { Indonesia mendeteksi penyakit daun secara mandiri, cepat, dan akurat.

-
+
- - -
- - +
+ + - - + + -
- Total: {filtered.length} -
-
+
+ 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) => ( - - + {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) => ( + + Daun
@@ -192,8 +190,6 @@ export function 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'}

-
+
Daun jagung @@ -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 c0407eb..2bfe5a2 100644 --- a/apps/web/src/pages/disease-detail-page.tsx +++ b/apps/web/src/pages/disease-detail-page.tsx @@ -78,7 +78,7 @@ export function DiseaseDetailPage() {
-

{disease.commonName}

+

{disease.commonName}

{disease.label}

@@ -131,7 +131,7 @@ export function DiseaseDetailPage() { -
+
diff --git a/apps/web/src/pages/expert-reviews-page.tsx b/apps/web/src/pages/expert-reviews-page.tsx index a2f8117..cd8d0fe 100644 --- a/apps/web/src/pages/expert-reviews-page.tsx +++ b/apps/web/src/pages/expert-reviews-page.tsx @@ -1,24 +1,35 @@ -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'; +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() }, ], }); @@ -33,15 +44,18 @@ export function ExpertReviewsPage() { mutationFn: () => apiClient.reviewDiagnosis(selected!.id, { verdict, - correctedDiseaseSlug: verdict === 'corrected' ? correctedDiseaseSlug || undefined : undefined, + 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"] }); }, }); @@ -55,10 +69,13 @@ export function ExpertReviewsPage() { return (
-
+
-

Review Pakar

-

Diagnosis Menunggu Review

+

Review Pakar

+

+ Tinjau hasil diagnosa dari scan yang telah dilakukan dan berikan + feedback untuk meningkatkan akurasi sistem AI ZeaVis Edu +

@@ -67,14 +84,20 @@ export function ExpertReviewsPage() {

Daftar Diagnosis

- {reviews.length} + + {reviews.length} +
{isLoading ? ( -

Memuat diagnosis...

+

+ Memuat diagnosis... +

) : reviews.length === 0 ? ( -

Tidak ada diagnosis yang menunggu review

+

+ Tidak ada diagnosis yang menunggu review +

) : ( reviews.map((review) => (