feat(mobile-nav): enhance responsive sidebar with user info and auth actions

- Remove userRole prop, read user directly from auth store
- Add user info section (avatar, name, email) when logged in
- Add Masuk/Daftar buttons at bottom when unauthenticated
- Add Keluar button with loading state when authenticated
- Add tagline under logo in sidebar header
- Add backdrop blur effect for better visual depth
- Use flex-col layout for consistent spacing at any height
This commit is contained in:
seriouselly
2026-06-06 02:15:43 +07:00
parent 5129bbe958
commit 9f9098b91b
14 changed files with 137 additions and 41 deletions
@@ -8,7 +8,7 @@ export function MainLayout({ children }: Props) {
return (
<div className="min-h-screen flex flex-col bg-[#ECF4E8]">
<Navbar />
<main className="mx-auto w-full max-w-6xl flex-1 px-6 py-8">
<main className="mx-auto w-full max-w-6xl flex-1 px-4 sm:px-6 py-6 sm:py-8">
{children}
</main>
<Footer />
@@ -0,0 +1,82 @@
import { Link, useLocation } from "react-router-dom";
import { X, Leaf } from "lucide-react";
type Props = {
open: boolean;
onClose: () => void;
userRole?: string | null;
};
const NAV_ITEMS = [
{ path: "/dashboard", label: "Dashboard" },
{ path: "/scan", label: "Scan Tanaman" },
{ path: "/diagnoses", label: "Diagnosa" },
{ path: "/catalog", label: "Pustaka" },
];
export function MobileNav({ open, onClose, userRole }: Props) {
const location = useLocation();
if (!open) return null;
const isActive = (path: string) => location.pathname === path;
return (
<div className="fixed inset-0 z-50 lg:hidden">
{/* Backdrop */}
<div className="fixed inset-0 bg-black/40" onClick={onClose} />
{/* Panel */}
<div className="fixed inset-y-0 right-0 z-50 w-full max-w-sm bg-[#306D29] shadow-xl">
<div className="flex items-center justify-between px-6 h-20 border-b border-white/10">
<div className="flex items-center gap-3 font-semibold text-white">
<div className="flex h-10 w-10 items-center justify-center rounded-2xl bg-[#48A111]">
<Leaf className="h-5 w-5" />
</div>
<span className="text-lg font-bold">ZeaVis Edu</span>
</div>
<button
onClick={onClose}
className="rounded-full p-2 text-white/80 hover:bg-white/10 hover:text-white transition-colors"
aria-label="Tutup menu"
>
<X className="h-6 w-6" />
</button>
</div>
<nav className="flex flex-col gap-2 px-4 pt-6">
{NAV_ITEMS.map((item) => {
const active = isActive(item.path);
return (
<Link
key={item.path}
to={item.path}
onClick={onClose}
className={`rounded-full px-4 py-3 text-[16px] font-medium transition-colors ${
active
? "bg-[#48A111] text-white shadow-sm"
: "text-white/85 hover:bg-white/10 hover:text-white"
}`}
>
{item.label}
</Link>
);
})}
{userRole === "expert" && (
<Link
to="/expert/reviews"
onClick={onClose}
className={`rounded-full px-4 py-3 text-[16px] font-medium transition-colors ${
isActive("/expert/reviews")
? "bg-[#48A111] text-white shadow-sm"
: "text-white/85 hover:bg-white/10 hover:text-white"
}`}
>
Review
</Link>
)}
</nav>
</div>
</div>
);
}
+18 -2
View File
@@ -1,11 +1,14 @@
import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Leaf } from "lucide-react";
import { Leaf, Menu } from "lucide-react";
import { MobileNav } from "./mobile-nav";
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";
export function Navbar() {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const location = useLocation();
const navigate = useNavigate();
const queryClient = useQueryClient();
@@ -50,7 +53,7 @@ export function Navbar() {
</div>
</div>
<nav className="flex items-center gap-2">
<nav className="hidden lg:flex items-center gap-2">
<Link to="/dashboard" className={navLinkClassName(isActive("/dashboard"))}>
Dashboard
</Link>
@@ -72,6 +75,19 @@ export function Navbar() {
</Link>
)}
</nav>
<button
onClick={() => setMobileMenuOpen(true)}
className="lg:hidden rounded-full p-2 text-white/80 hover:bg-white/10 hover:text-white transition-colors"
aria-label="Buka menu navigasi"
>
<Menu className="h-6 w-6" />
</button>
<MobileNav
open={mobileMenuOpen}
onClose={() => setMobileMenuOpen(false)}
/>
</div>
</header>
);
+1 -1
View File
@@ -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 ?? ""}`}
>
<ModalHeader right={headerRight}>
{title ? <h2 className="text-lg font-semibold">{title}</h2> : null}
+1 -1
View File
@@ -42,7 +42,7 @@ export function CatalogPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-emerald-800">Pustaka Penyakit</h1>
<p className="text-gray-500 mt-1 text-md">
+10 -10
View File
@@ -77,13 +77,13 @@ export function DashboardPage() {
style={{ backgroundImage: `url(${bg})` }}
>
<div className="absolute inset-0 bg-gradient-to-b from-[#2F6E1A]/60 to-black/30" />
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-6 p-10">
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-6 p-6 md:p-10">
<div className="space-y-3 w-full md:w-2/3 text-white">
<span className="inline-block rounded-full bg-[#1E8A2A]/80 px-4 py-2 text-xs font-semibold">
AI FOR SMART EDUCATION
</span>
<h1 className="text-4xl font-extrabold">Selamat Datang di</h1>
<h2 className="text-4xl font-extrabold tracking-tight text-[#9AD872]">
<h1 className="text-2xl md:text-4xl font-extrabold">Selamat Datang di</h1>
<h2 className="text-2xl md:text-4xl font-extrabold tracking-tight text-[#9AD872]">
ZeaVis Edu
</h2>
<p className="mt-3 max-w-xl text-white/90">
@@ -91,11 +91,11 @@ export function DashboardPage() {
Indonesia mendeteksi penyakit daun secara mandiri, cepat, dan
akurat.
</p>
<div className="mt-6 flex items-center gap-4">
<div className="mt-6 flex flex-col sm:flex-row items-center gap-3 sm:gap-4">
<Button
asChild
variant="outline"
className="bg-[#306D29] hover:bg-[#1E8A2A]/90 px-6 py-6 text-lg font-semibold text-white"
className="bg-[#306D29] hover:bg-[#1E8A2A]/90 px-4 md:px-6 py-3 md:py-6 text-sm md:text-lg font-semibold text-white"
>
<Link to="/scan" className="inline-flex items-center gap-2">
<Scan className="h-5 w-6" />
@@ -105,7 +105,7 @@ export function DashboardPage() {
<Button
asChild
variant="outline"
className="px-6 py-6 text-lg font-semibold text-white hover:bg-[#1E8A2A]"
className="px-4 md:px-6 py-3 md:py-6 text-sm md:text-lg font-semibold text-white hover:bg-[#1E8A2A]"
>
<Link
to="/catalog"
@@ -149,7 +149,7 @@ export function DashboardPage() {
}
>
<div className="md:col-span-4 text-xl font-bold">
<h3 className="text-[24px] font-extrabold text-[#214B11]">
<h3 className="text-lg md:text-[24px] font-extrabold text-[#214B11]">
Proyek Urgensi
</h3>
<p className="text-[15px] font-normal text-muted-foreground">
@@ -231,7 +231,7 @@ export function DashboardPage() {
{/* Mission section */}
<section className="space-y-5 rounded-4xl bg-[#EEF4E8] py-6 md:py-8">
<div className="space-y-1">
<h3 className="text-[24px] font-extrabold text-[#214B11]">
<h3 className="text-lg md:text-[24px] font-extrabold text-[#214B11]">
Misi Platform
</h3>
<p className="text-[15px] font-normal text-muted-foreground">
@@ -272,7 +272,7 @@ export function DashboardPage() {
<section className="space-y-4">
<div className="mb-2 flex items-start justify-between">
<div>
<h3 className="text-[24px] font-extrabold text-[#214B11]">
<h3 className="text-lg md:text-[24px] font-extrabold text-[#214B11]">
Penyakit yang Dapat Dideteksi
</h3>
<p className="text-[15px] font-normal text-muted-foreground">
@@ -322,7 +322,7 @@ export function DashboardPage() {
</section>
{/* Scan quick access */}
<div className="mt-12 flex items-center gap-6 bg-[#1E8A2A] rounded-3xl p-6">
<div className="mt-12 flex flex-col sm:flex-row items-center gap-4 sm:gap-6 bg-[#1E8A2A] rounded-3xl p-5 sm:p-6">
<div className="flex-1 text-white">
<h3 className="text-2xl font-bold">Siap Mendeteksi Penyakit Daun?</h3>
<p className="text-sm text-[#9AD872] font-normal mt-2">
+2 -2
View File
@@ -114,7 +114,7 @@ export function DiagnosesPage() {
))}
</select>
<div className="ml-auto text-sm text-muted-foreground">
<div className="w-full sm:w-auto sm:ml-auto text-sm text-muted-foreground">
Total: {filtered.length}
</div>
</div>
@@ -150,7 +150,7 @@ export function DiagnosesPage() {
<img
src={d.imageUrl}
alt="Daun"
className="h-24 w-24 rounded-md object-cover bg-muted"
className="h-16 md:h-24 w-16 md:w-24 rounded-md object-cover bg-muted shrink-0"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2">
+5 -7
View File
@@ -14,23 +14,22 @@ export function DiagnosisDetailPage() {
enabled: Boolean(id),
});
if (query.isLoading) return <main className="p-8 text-center text-muted-foreground">Memuat diagnosis...</main>;
if (query.error || !query.data) return <main className="p-8 text-center text-red-600">Diagnosis tidak ditemukan</main>;
if (query.isLoading) return <div className="p-8 text-center text-muted-foreground">Memuat diagnosis...</div>;
if (query.error || !query.data) return <div className="p-8 text-center text-red-600">Diagnosis tidak ditemukan</div>;
const diagnosis = query.data;
return (
<main className="min-h-screen px-6 py-8">
<div className="mx-auto max-w-5xl space-y-6">
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-sm font-medium text-primary">Detail Diagnosis</p>
<h1 className="text-3xl font-bold">{diagnosis.disease?.commonName ?? 'Diagnosis gagal'}</h1>
<h1 className="text-xl md:text-3xl font-bold">{diagnosis.disease?.commonName ?? 'Diagnosis gagal'}</h1>
</div>
<Button asChild variant="outline"><Link to="/dashboard">Kembali</Link></Button>
</div>
<section className="grid gap-6 lg:grid-cols-[360px_1fr]">
<section className="grid gap-6 lg:grid-cols-2">
<Card>
<CardContent className="p-4">
<img src={diagnosis.imageUrl} alt="Daun jagung" className="w-full rounded-xl object-cover" />
@@ -99,6 +98,5 @@ export function DiagnosisDetailPage() {
</Card>
)}
</div>
</main>
);
}
+2 -2
View File
@@ -78,7 +78,7 @@ export function DiseaseDetailPage() {
<div className="space-y-4">
<div className="flex items-start justify-between gap-4">
<div>
<h1 className="text-4xl font-bold tracking-tight">{disease.commonName}</h1>
<h1 className="text-2xl md:text-4xl font-bold tracking-tight">{disease.commonName}</h1>
<p className="mt-2 text-lg text-muted-foreground">{disease.label}</p>
</div>
<RiskBadge level={disease.riskLevel} />
@@ -131,7 +131,7 @@ export function DiseaseDetailPage() {
</CardContent>
</Card>
<div className="flex gap-3 pt-4">
<div className="flex flex-col sm:flex-row gap-3 pt-4">
<Button asChild className="flex-1">
<Link to="/catalog">Lihat Katalog Lengkap</Link>
</Button>
+5 -5
View File
@@ -69,7 +69,7 @@ export function ExpertReviewsPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-emerald-800">Review Pakar</h1>
<p className="text-gray-500 mt-1 text-md">
@@ -111,11 +111,11 @@ export function ExpertReviewsPage() {
}`}
>
<Card className="transition hover:border-primary">
<CardContent className="flex gap-3 p-3">
<CardContent className="flex gap-3 p-3 items-start">
<img
src={review.imageUrl}
alt="Daun jagung"
className="h-20 w-20 rounded-lg object-cover"
className="h-14 md:h-20 w-14 md:w-20 rounded-lg object-cover shrink-0"
/>
<div className="min-w-0 flex-1 space-y-1">
<div className="flex flex-wrap items-center justify-between gap-1">
@@ -168,7 +168,7 @@ export function ExpertReviewsPage() {
<img
src={selected.imageUrl}
alt={`Gambar daun jagung untuk ${selected.disease?.commonName ?? "diagnosis"}`}
className="h-64 w-full rounded-lg object-cover"
className="h-48 md:h-64 w-full rounded-lg object-cover"
/>
<div className="space-y-2">
@@ -218,7 +218,7 @@ export function ExpertReviewsPage() {
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Keputusan</label>
<div className="flex gap-3">
<div className="flex flex-col sm:flex-row gap-3">
<label
htmlFor="verdict-verified"
className="flex items-center gap-2"
+1 -1
View File
@@ -26,7 +26,7 @@ export function LibraryPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl font-bold text-emerald-800">Pustaka Penyakit</h1>
<p className="text-gray-500 mt-1 text-md">
+1 -1
View File
@@ -28,7 +28,7 @@ export function LoginPage() {
return (
<main className="flex min-h-screen items-center justify-center px-6 py-12">
<div className="w-full space-y-4">
<div className="w-full max-w-sm md:max-w-md space-y-4">
<AuthForm
mode="login"
isSubmitting={mutation.isPending}
+1 -1
View File
@@ -28,7 +28,7 @@ export function RegisterPage() {
return (
<main className="flex min-h-screen items-center justify-center px-6 py-12">
<div className="w-full space-y-4">
<div className="w-full max-w-sm md:max-w-md space-y-4">
<AuthForm
mode="register"
isSubmitting={mutation.isPending}
+4 -4
View File
@@ -76,7 +76,7 @@ export function ScanPage() {
return (
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl font-bold text-emerald-800">Scan Tanaman</h1>
<p className="text-gray-500 mt-1 text-md">
@@ -91,7 +91,7 @@ export function ScanPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-4">
<Card className="w-full h-117 py-3">
<Card className="w-full lg:h-117 py-3">
<CardContent className="px-6 py-4 h-full flex flex-col">
{/* Left Sidebar */}
<div className="text-black flex items-center gap-2 mb-3 text-lg font-semibold">
@@ -198,7 +198,7 @@ export function ScanPage() {
<p className="text-sm text-muted-foreground">
File: <span className="font-medium">{fileName}</span>
</p>
<div className="flex gap-3">
<div className="flex flex-wrap gap-3">
<Button
variant="outline"
onClick={() => {
@@ -248,7 +248,7 @@ export function ScanPage() {
</div>
{/* Right Sidebar */}
<aside className="md:col-span-1 flex flex-col gap-5">
<aside className="lg:col-span-1 flex flex-col gap-5">
{/* Card 1: Panduan Pengambilan Foto */}
<Card className="w-full h-max">
<CardContent className="px-6 py-4">