diff --git a/apps/web/package.json b/apps/web/package.json index 03f9120..51a996d 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -29,6 +29,7 @@ "@types/react-dom": "^19.2.3", "postcss": "^8.5.15", "typescript": "^6.0.3", - "vite": "^8.0.14" + "vite": "^8.0.14", + "vite-tsconfig-paths": "6.1.1" } } diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index 29a38cc..93b7301 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -1,52 +1,99 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { createBrowserRouter, RouterProvider } from 'react-router-dom'; -import { AuthGuard } from '@/components/auth-guard'; -import { DashboardPage } from '@/pages/dashboard-page'; -import { LandingPage } from '@/pages/landing-page'; -import { CatalogPage } from '@/pages/catalog-page'; -import { DiseaseDetailPage } from '@/pages/disease-detail-page'; -import { DiagnosisDetailPage } from '@/pages/diagnosis-detail-page'; -import { ExpertReviewsPage } from '@/pages/expert-reviews-page'; -import { LoginPage } from '@/pages/login-page'; -import { RegisterPage } from '@/pages/register-page'; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { + createBrowserRouter, + RouterProvider, + Navigate, +} from "react-router-dom"; +import { AuthInitializer } from "@/components/auth-initializer"; +// import { AuthGuard } from "@/components/auth-guard"; +import { DashboardPage } from "@/pages/dashboard-page"; +import { ScanPage } from "@/pages/scan-page"; +import { LibraryPage } from "@/pages/library-page"; +import { CatalogPage } from "@/pages/catalog-page"; +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 { 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', + path: "/dashboard", element: ( - + - + ), }, { - path: '/diagnoses/:id', + path: "/scan", element: ( - + + + + ), + }, + { + path: "/library", + element: ( + + + + ), + }, + { + path: "/diagnoses", + element: ( + + + + ), + }, + { + path: "/diagnoses/:id", + element: ( + - + ), }, { - path: '/expert/reviews', + path: "/expert/reviews", element: ( - + - + + ), + }, + { + path: "/catalog", + element: ( + + + + ), + }, + { + path: "/catalog/:slug", + element: ( + + + ), }, - { path: '/catalog', element: }, - { path: '/catalog/:slug', element: }, ]); export function App() { return ( + ); diff --git a/apps/web/src/assets/images/dashboard-bg.png b/apps/web/src/assets/images/dashboard-bg.png new file mode 100644 index 0000000..613383d Binary files /dev/null and b/apps/web/src/assets/images/dashboard-bg.png differ 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/auth-initializer.tsx b/apps/web/src/components/auth-initializer.tsx new file mode 100644 index 0000000..765408a --- /dev/null +++ b/apps/web/src/components/auth-initializer.tsx @@ -0,0 +1,21 @@ +import { useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { apiClient } from '@/lib/api-client'; +import { useAuthStore } from '@/store/auth-store'; + +export function AuthInitializer() { + const setUser = useAuthStore((state) => state.setUser); + const query = useQuery({ + queryKey: ['auth', 'me'], + queryFn: () => apiClient.getMe(), + retry: false, + }); + + useEffect(() => { + if (query.data) { + setUser(query.data.user); + } + }, [query.data, setUser]); + + return null; +} diff --git a/apps/web/src/components/disease-card.tsx b/apps/web/src/components/disease-card.tsx index 9a74b2f..2b8fcac 100644 --- a/apps/web/src/components/disease-card.tsx +++ b/apps/web/src/components/disease-card.tsx @@ -14,8 +14,8 @@ export function DiseaseCard({ disease }: DiseaseCardProps) { -
-
+
+
{disease.commonName} {disease.label}
diff --git a/apps/web/src/components/image-classification-form.tsx b/apps/web/src/components/image-classification-form.tsx deleted file mode 100644 index 87f259f..0000000 --- a/apps/web/src/components/image-classification-form.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { ChangeEvent, FormEvent, useRef, useState, useEffect } from 'react'; -import type { DiagnosisRecord } from '@zeavis/shared'; -import { Upload } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { DiagnosisStatusBadge } from '@/components/diagnosis-status-badge'; - -type ImageClassificationFormProps = { - onSubmit: (file: File) => Promise; - isSubmitting: boolean; - latestResult: DiagnosisRecord | null; -}; - -export function ImageClassificationForm({ - onSubmit, - isSubmitting, - latestResult, -}: ImageClassificationFormProps) { - const fileInputRef = useRef(null); - const previewUrlRef = useRef(null); - const [file, setFile] = useState(null); - const [previewUrl, setPreviewUrl] = useState(null); - const [error, setError] = useState(null); - - // Revoke object URL on component unmount - useEffect(() => { - return () => { - if (previewUrlRef.current) { - URL.revokeObjectURL(previewUrlRef.current); - previewUrlRef.current = null; - } - }; - }, []); - - function handleFileChange(event: ChangeEvent) { - const selectedFile = event.target.files?.[0] ?? null; - setError(null); - setFile(selectedFile); - - // Revoke previous preview URL before replacing it - if (previewUrlRef.current) { - URL.revokeObjectURL(previewUrlRef.current); - previewUrlRef.current = null; - } - - const newUrl = selectedFile ? URL.createObjectURL(selectedFile) : null; - previewUrlRef.current = newUrl; - setPreviewUrl(newUrl); - } - - async function handleSubmit(event: FormEvent) { - event.preventDefault(); - if (!file) { - setError('Pilih gambar terlebih dahulu'); - return; - } - - try { - await onSubmit(file); - - // Revoke current preview URL after successful submit before setting null - if (previewUrlRef.current) { - URL.revokeObjectURL(previewUrlRef.current); - previewUrlRef.current = null; - } - - setFile(null); - setPreviewUrl(null); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } - } catch (err) { - setError(err instanceof Error ? err.message : 'Gagal mengirim gambar'); - } - } - - return ( - - - - Diagnosis Gambar - - - Upload gambar daun jagung untuk klasifikasi AI dan review pakar jika confidence rendah. - - - -
-
- - - {file && ( -

- File dipilih: {file.name} -

- )} -
- - {previewUrl && ( - Pratinjau gambar daun jagung untuk diagnosis - )} - - {error &&

{error}

} - - -
- - {latestResult && ( -
-
-

- {latestResult.disease?.commonName ?? 'Diagnosis gagal'} -

- -
-

- {latestResult.confidence === null - ? 'Tidak ada confidence' - : `Confidence ${(latestResult.confidence * 100).toFixed(1)}%`} -

-
- )} -
-
- ); -} diff --git a/apps/web/src/components/layout/footer.tsx b/apps/web/src/components/layout/footer.tsx new file mode 100644 index 0000000..6995856 --- /dev/null +++ b/apps/web/src/components/layout/footer.tsx @@ -0,0 +1,9 @@ +export function Footer() { + return ( +
+
+ © 2026 ZeaVis Edu - AI for Smart Education +
+
+ ); +} diff --git a/apps/web/src/components/layout/main-layout.tsx b/apps/web/src/components/layout/main-layout.tsx new file mode 100644 index 0000000..c4cfb03 --- /dev/null +++ b/apps/web/src/components/layout/main-layout.tsx @@ -0,0 +1,17 @@ +import { ReactNode } from "react"; +import { Navbar } from "./navbar"; +import { Footer } from "./footer"; + +type Props = { children: ReactNode }; + +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 new file mode 100644 index 0000000..20993e1 --- /dev/null +++ b/apps/web/src/components/layout/navbar.tsx @@ -0,0 +1,85 @@ +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 isActive = (path: string) => location.pathname === path; + + const navLinkClassName = (active: boolean) => + [ + "inline-flex items-center rounded-full px-4 py-2 text-[16px] font-medium transition-colors", + active + ? "bg-[#48A111] text-white shadow-sm" + : "text-white/85 hover:bg-white/10 hover:text-white", + ].join(" "); + + return ( +
+
+
+
+ + + +
+
+
ZeaVis Edu
+
+ Smart AI for Corn Disease Detection +
+
+
+ + + + + + setMobileMenuOpen(false)} + /> +
+
+ ); +} diff --git a/apps/web/src/components/manual-classification-form.tsx b/apps/web/src/components/manual-classification-form.tsx deleted file mode 100644 index 96d47b9..0000000 --- a/apps/web/src/components/manual-classification-form.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useEffect, useState } from 'react'; -import type { DiseaseSlug, DiseaseCatalogItem, ManualClassificationRequest } from '@zeavis/shared'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; - -export interface ManualClassificationFormProps { - diseases: DiseaseCatalogItem[]; - onSubmit: (payload: ManualClassificationRequest) => Promise; - isSubmitting: boolean; -} - -export function ManualClassificationForm({ - diseases, - onSubmit, - isSubmitting, -}: ManualClassificationFormProps) { - const [selectedSlug, setSelectedSlug] = useState(''); - const [observation, setObservation] = useState(''); - const [location, setLocation] = useState(''); - const [error, setError] = useState(null); - - useEffect(() => { - if (diseases.length > 0 && !selectedSlug) { - setSelectedSlug(diseases[0].slug); - } - }, [diseases, selectedSlug]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - - if (!selectedSlug || !observation.trim() || !location.trim()) { - setError('Semua field harus diisi'); - return; - } - - try { - await onSubmit({ - diseaseSlug: selectedSlug, - observation: observation.trim(), - location: location.trim(), - }); - - setObservation(''); - setLocation(''); - if (diseases.length > 0) { - setSelectedSlug(diseases[0].slug); - } - } catch (err) { - setError(err instanceof Error ? err.message : 'Terjadi kesalahan saat mengirim data'); - } - }; - - const isFormValid = selectedSlug && observation.trim() && location.trim(); - - return ( - - - Klasifikasi Manual - Laporkan penyakit daun jagung yang Anda temukan - - -
- {error && ( -
{error}
- )} - -
- - -
- -
- -