From 84fb808b5381ccb446a6ce45539a61cfb4a33885 Mon Sep 17 00:00:00 2001 From: Asep Haryana Saputra <90584806+MythEclipse@users.noreply.github.com> Date: Fri, 22 May 2026 16:51:01 +0000 Subject: [PATCH] feat: add frontend authentication flow --- apps/web/src/app.tsx | 26 +++++------ apps/web/src/components/auth-form.tsx | 64 ++++++++++++++++++++++++++ apps/web/src/components/auth-guard.tsx | 38 +++++++++++++++ apps/web/src/components/ui/input.tsx | 14 ++++++ apps/web/src/components/ui/label.tsx | 11 +++++ apps/web/src/pages/login-page.tsx | 41 +++++++++++++++++ apps/web/src/pages/register-page.tsx | 41 +++++++++++++++++ 7 files changed, 222 insertions(+), 13 deletions(-) create mode 100644 apps/web/src/components/auth-form.tsx create mode 100644 apps/web/src/components/auth-guard.tsx create mode 100644 apps/web/src/components/ui/input.tsx create mode 100644 apps/web/src/components/ui/label.tsx create mode 100644 apps/web/src/pages/login-page.tsx create mode 100644 apps/web/src/pages/register-page.tsx diff --git a/apps/web/src/app.tsx b/apps/web/src/app.tsx index b667f42..9458d1d 100644 --- a/apps/web/src/app.tsx +++ b/apps/web/src/app.tsx @@ -1,29 +1,29 @@ 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 { LoginPage } from '@/pages/login-page'; +import { RegisterPage } from '@/pages/register-page'; const queryClient = new QueryClient(); const router = createBrowserRouter([ - { - path: '/', - element: , - }, + { path: '/', element: }, + { path: '/login', element: }, + { path: '/register', element: }, { path: '/dashboard', - element: , - }, - { - path: '/catalog', - element: , - }, - { - path: '/catalog/:slug', - element: , + element: ( + + + + ), }, + { path: '/catalog', element: }, + { path: '/catalog/:slug', element: }, ]); export function App() { diff --git a/apps/web/src/components/auth-form.tsx b/apps/web/src/components/auth-form.tsx new file mode 100644 index 0000000..b04f02f --- /dev/null +++ b/apps/web/src/components/auth-form.tsx @@ -0,0 +1,64 @@ +import { FormEvent, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; + +type AuthFormProps = { + mode: 'login' | 'register'; + isSubmitting: boolean; + error: string | null; + googleOAuthEnabled: boolean; + onSubmit: (payload: { name?: string; email: string; password: string }) => Promise; +}; + +export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubmit }: AuthFormProps) { + const [name, setName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + async function handleSubmit(event: FormEvent) { + event.preventDefault(); + await onSubmit({ name, email, password }); + } + + return ( + + + {mode === 'login' ? 'Masuk ke ZeaVis Edu' : 'Buat akun ZeaVis Edu'} + + {mode === 'login' + ? 'Masuk untuk melihat riwayat diagnosis daun jagung Anda.' + : 'Daftar untuk menyimpan diagnosis dan mengikuti review pakar.'} + + + +
+ {mode === 'register' && ( +
+ + setName(event.target.value)} required /> +
+ )} +
+ + setEmail(event.target.value)} required /> +
+
+ + setPassword(event.target.value)} required /> +
+ {error &&

{error}

} + +
+ {googleOAuthEnabled && ( + + )} +
+
+ ); +} diff --git a/apps/web/src/components/auth-guard.tsx b/apps/web/src/components/auth-guard.tsx new file mode 100644 index 0000000..ed4ac06 --- /dev/null +++ b/apps/web/src/components/auth-guard.tsx @@ -0,0 +1,38 @@ +import { ReactNode, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { Navigate } from 'react-router-dom'; +import { apiClient } from '@/lib/api-client'; +import { useAuthStore } from '@/store/auth-store'; + +type AuthGuardProps = { + children: ReactNode; + requireExpert?: boolean; +}; + +export function AuthGuard({ children, requireExpert = false }: AuthGuardProps) { + const setUser = useAuthStore((state) => state.setUser); + const query = useQuery({ + queryKey: ['auth', 'me'], + queryFn: () => apiClient.getMe(), + }); + + useEffect(() => { + if (query.data) { + setUser(query.data.user); + } + }, [query.data, setUser]); + + if (query.isLoading) { + return
Memeriksa sesi...
; + } + + if (!query.data?.user) { + return ; + } + + if (requireExpert && query.data.user.role !== 'expert') { + return ; + } + + return <>{children}; +} diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx new file mode 100644 index 0000000..f2fe482 --- /dev/null +++ b/apps/web/src/components/ui/input.tsx @@ -0,0 +1,14 @@ +import type { InputHTMLAttributes } from 'react'; +import { cn } from '@/lib/utils'; + +export function Input({ className, ...props }: InputHTMLAttributes) { + return ( + + ); +} diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx new file mode 100644 index 0000000..da28348 --- /dev/null +++ b/apps/web/src/components/ui/label.tsx @@ -0,0 +1,11 @@ +import type { LabelHTMLAttributes } from 'react'; +import { cn } from '@/lib/utils'; + +export function Label({ className, ...props }: LabelHTMLAttributes) { + return ( +