diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index fcbcd47..d6e50ec 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -4,6 +4,8 @@ import { 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"; @@ -12,59 +14,73 @@ 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: "/", - element: , + path: "/dashboard", + element: ( + + + + + + ), }, { path: "/scan", element: ( - - - + + + + + ), }, { path: "/library", element: ( - - - + + + + + ), }, - - { - path: "/dashboard", - element: ( - - - - ), - }, - { - path: "/diagnoses/:id", - element: , - }, { path: "/diagnoses", element: ( - - - + + + + + + ), + }, + { + path: "/diagnoses/:id", + element: ( + + + + + ), }, { path: "/expert/reviews", - element: , + element: ( + + + + ), }, { path: "/catalog", element: }, { path: "/catalog/:slug", element: }, @@ -73,6 +89,7 @@ const router = createBrowserRouter([ export function App() { return ( + ); 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/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/navbar.tsx b/apps/web/src/components/layout/navbar.tsx index 352d43a..d85e6ba 100644 --- a/apps/web/src/components/layout/navbar.tsx +++ b/apps/web/src/components/layout/navbar.tsx @@ -10,14 +10,14 @@ export function Navbar() { const navigate = useNavigate(); const queryClient = useQueryClient(); const setUser = useAuthStore((state) => state.setUser); - const isDashboard = location.pathname === "/dashboard"; - const isScan = location.pathname === "/scan"; - const isLibrary = location.pathname === "/library"; + const user = useAuthStore((state) => state.user); - const navLinkClassName = (isActive: boolean) => + const isActive = (path: string) => location.pathname === path; + + const navLinkClassName = (active: boolean) => [ - "inline-flex items-center rounded-full px-4 py-2 text-[18px] font-medium transition-colors", - isActive + "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(" "); @@ -29,12 +29,10 @@ export function Navbar() { onSuccess: () => { setUser(null); queryClient.clear(); - navigate("/"); + navigate("/login"); }, }); - const user = useAuthStore((state) => state.user); - return (
@@ -52,27 +50,33 @@ export function Navbar() {
-