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' && ( + + Nama + setName(event.target.value)} required /> + + )} + + Email + setEmail(event.target.value)} required /> + + + Password + setPassword(event.target.value)} required /> + + {error && {error}} + + {isSubmitting ? 'Memproses...' : mode === 'login' ? 'Masuk' : 'Daftar'} + + + {googleOAuthEnabled && ( + + Masuk dengan Google + + )} + + + ); +} 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 ( + + ); +} diff --git a/apps/web/src/pages/login-page.tsx b/apps/web/src/pages/login-page.tsx new file mode 100644 index 0000000..4123020 --- /dev/null +++ b/apps/web/src/pages/login-page.tsx @@ -0,0 +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'; + +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 mutation = useMutation({ + mutationFn: apiClient.login, + onSuccess: (response) => { + setUser(response.user); + queryClient.setQueryData(['auth', 'me'], response); + navigate('/dashboard'); + }, + onError: (err) => setError(err instanceof Error ? err.message : 'Login gagal'), + }); + + return ( + + + mutation.mutateAsync({ email, password })} + /> + + Belum punya akun? Daftar + + + + ); +} diff --git a/apps/web/src/pages/register-page.tsx b/apps/web/src/pages/register-page.tsx new file mode 100644 index 0000000..b162b2c --- /dev/null +++ b/apps/web/src/pages/register-page.tsx @@ -0,0 +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'; + +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 mutation = useMutation({ + mutationFn: apiClient.register, + onSuccess: (response) => { + setUser(response.user); + queryClient.setQueryData(['auth', 'me'], response); + navigate('/dashboard'); + }, + onError: (err) => setError(err instanceof Error ? err.message : 'Registrasi gagal'), + }); + + return ( + + + mutation.mutateAsync({ name: name ?? '', email, password })} + /> + + Sudah punya akun? Masuk + + + + ); +}
{error}
+ Belum punya akun? Daftar +
+ Sudah punya akun? Masuk +