Change Dashboard Sidebar

This commit is contained in:
Randa Firman Putra
2025-06-26 13:03:15 +07:00
parent 72dcf452e2
commit e61957b5e9
9 changed files with 676 additions and 686 deletions

View File

@@ -0,0 +1,50 @@
"use client";
import { useState, useEffect } from 'react';
import Navbar from '@/components/ui/Navbar';
import Sidebar from '@/components/ui/Sidebar';
import { ThemeProvider } from '@/components/theme-provider';
import { Toaster } from '@/components/ui/toaster';
interface ClientLayoutProps {
children: React.ReactNode;
}
export default function ClientLayout({ children }: ClientLayoutProps) {
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
// Load sidebar state from localStorage on mount
useEffect(() => {
const savedState = localStorage.getItem('sidebarCollapsed');
if (savedState !== null) {
setIsSidebarCollapsed(JSON.parse(savedState));
}
}, []);
// Save sidebar state to localStorage when it changes
useEffect(() => {
localStorage.setItem('sidebarCollapsed', JSON.stringify(isSidebarCollapsed));
}, [isSidebarCollapsed]);
return (
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<div className="min-h-screen">
<Sidebar isCollapsed={isSidebarCollapsed} />
<div className={`flex flex-col min-h-screen transition-all duration-300 ease-in-out ${
isSidebarCollapsed ? 'md:ml-0' : 'md:ml-64'
}`}>
<Navbar onSidebarToggle={() => setIsSidebarCollapsed(!isSidebarCollapsed)} isSidebarCollapsed={isSidebarCollapsed} />
<main className="flex-1 p-6">
{children}
</main>
</div>
</div>
<Toaster />
</ThemeProvider>
);
}

View File

@@ -1,245 +1,20 @@
'use client';
import { useState, useEffect } from 'react';
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { ThemeToggle } from '@/components/theme-toggle';
import { Menu, User } from 'lucide-react';
import { Menu, PanelLeftClose, PanelLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
import SidebarContent from '@/components/ui/SidebarContent';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useToast } from '@/components/ui/use-toast';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
const Navbar = () => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [dialogOpen, setDialogOpen] = useState(false);
const [loginData, setLoginData] = useState({ nim: '', password: '' });
const [registerData, setRegisterData] = useState({ username: '', nim: '', password: '', confirmPassword: '' });
const [userData, setUserData] = useState<any>(null);
const { toast } = useToast();
const router = useRouter();
// Check login status on component mount and when route changes
useEffect(() => {
const checkAuth = async () => {
try {
const response = await fetch('/api/auth/check');
const data = await response.json();
if (response.ok && data.user) {
setUserData(data.user);
setIsLoggedIn(true);
} else {
setUserData(null);
setIsLoggedIn(false);
}
} catch (error) {
console.error('Auth check failed:', error);
setUserData(null);
setIsLoggedIn(false);
}
};
checkAuth();
}, [router]);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
// Validate input
if (!loginData.nim || !loginData.password) {
toast({
variant: "destructive",
title: "Login gagal",
description: "NIM dan password harus diisi",
});
return;
}
console.log('Login attempt with data:', {
nim: loginData.nim,
password: '***'
});
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nim: loginData.nim.trim(),
password: loginData.password
}),
});
console.log('Login response status:', response.status);
const data = await response.json();
console.log('Login response data:', data);
if (!response.ok) {
throw new Error(data.error || 'Login gagal');
}
toast({
title: "Login berhasil",
description: "Selamat datang kembali!",
});
setUserData(data.user);
setIsLoggedIn(true);
setDialogOpen(false);
router.refresh();
} catch (error) {
console.error('Login error:', error);
toast({
variant: "destructive",
title: "Login gagal",
description: error instanceof Error ? error.message : 'Terjadi kesalahan saat login',
});
}
};
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
// Validate passwords match
if (registerData.password !== registerData.confirmPassword) {
toast({
variant: "destructive",
title: "Registrasi gagal",
description: "Password dan konfirmasi password tidak cocok",
});
return;
}
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: registerData.username,
nim: registerData.nim,
password: registerData.password,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Registrasi gagal');
}
toast({
title: "Registrasi berhasil",
description: "Silakan login dengan akun Anda",
});
// Reset form and switch to login tab
setRegisterData({ username: '', nim: '', password: '', confirmPassword: '' });
const tabsList = document.querySelector('[role="tablist"]');
if (tabsList) {
const loginTab = tabsList.querySelector('[value="login"]');
if (loginTab) {
(loginTab as HTMLElement).click();
}
}
} catch (error) {
toast({
variant: "destructive",
title: "Registrasi gagal",
description: error instanceof Error ? error.message : 'Terjadi kesalahan saat registrasi',
});
}
};
const handleLogout = async () => {
try {
const response = await fetch('/api/auth/logout', {
method: 'POST',
});
if (!response.ok) {
throw new Error('Logout gagal');
}
toast({
title: "Logout berhasil",
description: "Sampai jumpa lagi!",
});
setUserData(null);
setIsLoggedIn(false);
router.push('/');
router.refresh();
} catch (error) {
toast({
variant: "destructive",
title: "Logout gagal",
description: error instanceof Error ? error.message : 'Terjadi kesalahan saat logout',
});
}
};
const handleProfileClick = async () => {
try {
const response = await fetch('/api/auth/check');
const data = await response.json();
if (response.ok && data.user) {
router.push('/mahasiswa/profile');
} else {
toast({
variant: "destructive",
title: "Akses Ditolak",
description: "Silakan login terlebih dahulu untuk mengakses profil",
});
setDialogOpen(true);
router.push('/'); // Redirect to home if not logged in
}
} catch (error) {
console.error('Error checking auth status:', error);
toast({
variant: "destructive",
title: "Error",
description: "Terjadi kesalahan saat memeriksa status login",
});
router.push('/');
}
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
if (name.startsWith('login')) {
const loginField = name.replace('login', '').toLowerCase();
setLoginData(prev => ({
...prev,
[loginField]: value
}));
} else {
setRegisterData(prev => ({
...prev,
[name]: value
}));
}
};
interface NavbarProps {
onSidebarToggle: () => void;
isSidebarCollapsed: boolean;
}
const Navbar = ({ onSidebarToggle, isSidebarCollapsed }: NavbarProps) => {
return (
<div className="bg-background border-b sticky top-0 z-50 py-2 px-5 flex justify-between items-center">
<div className="bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b py-2 px-5 flex justify-between items-center z-30 sticky top-0">
<div className="flex items-center gap-2">
{/* Mobile Menu Button */}
<div className="md:hidden">
@@ -251,11 +26,28 @@ const Navbar = () => {
</Button>
</SheetTrigger>
<SheetContent side="left" className="p-0 w-[250px] overflow-y-auto">
<DialogTitle className="sr-only">Menu Navigasi</DialogTitle>
<SidebarContent />
</SheetContent>
</Sheet>
</div>
{/* Desktop Sidebar Toggle Button */}
<div className="hidden md:block">
<Button
variant="outline"
size="icon"
onClick={onSidebarToggle}
title={isSidebarCollapsed ? "Tampilkan Sidebar" : "Sembunyikan Sidebar"}
>
{isSidebarCollapsed ? (
<PanelLeft className="h-5 w-5" />
) : (
<PanelLeftClose className="h-5 w-5" />
)}
<span className="sr-only">Toggle sidebar</span>
</Button>
</div>
<Link href="/" className="flex items-center text-lg font-semibold hover:text-primary transition-colors">
<img src="/podif-icon.png" alt="PODIF Logo" className="h-6 w-auto mr-2" />
PODIF
@@ -263,108 +55,6 @@ const Navbar = () => {
</div>
<div className="flex items-center gap-4">
{isLoggedIn ? (
<DropdownMenu>
<DropdownMenuTrigger className="focus:outline-none">
<Avatar>
<AvatarImage src="" alt={userData?.username || 'User'} />
<AvatarFallback className="bg-primary/10">
<User className="h-5 w-5" />
</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom" sideOffset={9} alignOffset={0}>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleProfileClick}>
Profile
</DropdownMenuItem>
<DropdownMenuItem onClick={handleLogout}>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="secondary">
Login
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Portal Data Informatika</DialogTitle>
<DialogDescription>Masuk atau daftar untuk mengakses portal</DialogDescription>
</DialogHeader>
<Tabs defaultValue="login" className="w-full mt-4">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="login">Login</TabsTrigger>
<TabsTrigger value="register">Register</TabsTrigger>
</TabsList>
<TabsContent value="login">
<form onSubmit={handleLogin} className="space-y-4">
<Input
type="text"
name="loginNim"
placeholder="NIM"
value={loginData.nim}
onChange={handleInputChange}
required
/>
<Input
type="password"
name="loginPassword"
placeholder="Password"
value={loginData.password}
onChange={handleInputChange}
required
/>
<Button className="w-full" type="submit">
Login
</Button>
</form>
</TabsContent>
<TabsContent value="register">
<form onSubmit={handleRegister} className="space-y-4">
<Input
type="text"
name="username"
placeholder="Nama Lengkap"
value={registerData.username}
onChange={handleInputChange}
required
/>
<Input
type="text"
name="nim"
placeholder="NIM"
value={registerData.nim}
onChange={handleInputChange}
required
/>
<Input
type="password"
name="password"
placeholder="Password"
value={registerData.password}
onChange={handleInputChange}
required
/>
<Input
type="password"
name="confirmPassword"
placeholder="Konfirmasi Password"
value={registerData.confirmPassword}
onChange={handleInputChange}
required
/>
<Button className="w-full" type="submit">
Register
</Button>
</form>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
)}
<ThemeToggle />
</div>
</div>

View File

@@ -2,10 +2,18 @@
import SidebarContent from './SidebarContent';
const Sidebar = () => {
interface SidebarProps {
isCollapsed?: boolean;
}
const Sidebar = ({ isCollapsed = false }: SidebarProps) => {
return (
<div className="hidden md:block h-[calc(100vh-4rem)] w-[250px] fixed">
<SidebarContent />
<div className={`hidden md:flex h-screen w-64 flex-col fixed left-0 top-0 z-40 transition-transform duration-300 ease-in-out ${
isCollapsed ? '-translate-x-full' : 'translate-x-0'
}`}>
<div className="flex-1 overflow-y-auto border-r bg-background">
<SidebarContent />
</div>
</div>
);
};

View File

@@ -24,7 +24,7 @@ import { School, Settings, User, GraduationCap, Award, Users, Clock, BookOpen, H
const SidebarContent = () => {
return (
<Command className="bg-background border-r h-full">
<Command className="bg-background h-full">
<CommandList className="overflow-visible">
<CommandGroup heading="Dashboard PODIF" className="mt-2">
<Link href="/" className="w-full no-underline cursor-pointer">