revisi nih boz
This commit is contained in:
408
app/page.tsx
408
app/page.tsx
@@ -1,114 +1,326 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTheme } from "next-themes";
|
||||
import DashboardStats, { DashboardStatsSkeleton } from "@/components/dashboard/DashboardStats";
|
||||
import DashboardCharts from "@/components/dashboard/DashboardCharts";
|
||||
import { useToast } from "@/components/ui/toast-provider";
|
||||
|
||||
interface MahasiswaTotal {
|
||||
total_mahasiswa: number;
|
||||
mahasiswa_aktif: number;
|
||||
total_lulus: number;
|
||||
pria_lulus: number;
|
||||
wanita_lulus: number;
|
||||
total_berprestasi: number;
|
||||
prestasi_akademik: number;
|
||||
prestasi_non_akademik: number;
|
||||
total_mahasiswa_aktif_lulus: number;
|
||||
total_mahasiswa_beasiswa: number;
|
||||
total_mahasiswa_beasiswa_pemerintah: number;
|
||||
total_mahasiswa_beasiswa_non_pemerintah: number;
|
||||
interface UserData {
|
||||
id_user: number;
|
||||
username?: string;
|
||||
nip?: string;
|
||||
role_user: string;
|
||||
}
|
||||
|
||||
|
||||
export default function DashboardPage() {
|
||||
export default function HomePage() {
|
||||
const { theme } = useTheme();
|
||||
const [mahasiswaData, setMahasiswaData] = useState<MahasiswaTotal>({
|
||||
total_mahasiswa: 0,
|
||||
mahasiswa_aktif: 0,
|
||||
total_lulus: 0,
|
||||
pria_lulus: 0,
|
||||
wanita_lulus: 0,
|
||||
total_berprestasi: 0,
|
||||
prestasi_akademik: 0,
|
||||
prestasi_non_akademik: 0,
|
||||
total_mahasiswa_aktif_lulus: 0,
|
||||
total_mahasiswa_beasiswa: 0,
|
||||
total_mahasiswa_beasiswa_pemerintah: 0,
|
||||
total_mahasiswa_beasiswa_non_pemerintah: 0
|
||||
});
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
const { showSuccess, showError } = useToast();
|
||||
const [user, setUser] = useState<UserData | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showLoginDialog, setShowLoginDialog] = useState(false);
|
||||
|
||||
// Check for existing user session on mount
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
// Menggunakan cache API untuk mempercepat loading
|
||||
const cacheKey = 'mahasiswa-total-data';
|
||||
const cachedData = sessionStorage.getItem(cacheKey);
|
||||
const cachedTimestamp = sessionStorage.getItem(`${cacheKey}-timestamp`);
|
||||
|
||||
// Cek apakah data cache masih valid (kurang dari 60 detik)
|
||||
const isCacheValid = cachedTimestamp &&
|
||||
(Date.now() - parseInt(cachedTimestamp)) < 60000;
|
||||
|
||||
if (cachedData && isCacheValid) {
|
||||
setMahasiswaData(JSON.parse(cachedData));
|
||||
}
|
||||
|
||||
// Fetch data total mahasiswa
|
||||
const totalResponse = await fetch('/api/mahasiswa/total', {
|
||||
cache: 'no-store',
|
||||
});
|
||||
|
||||
if (!totalResponse.ok) {
|
||||
throw new Error('Failed to fetch total data');
|
||||
}
|
||||
|
||||
const totalData = await totalResponse.json();
|
||||
setMahasiswaData(totalData);
|
||||
|
||||
// Menyimpan data dan timestamp ke sessionStorage
|
||||
sessionStorage.setItem(cacheKey, JSON.stringify(totalData));
|
||||
sessionStorage.setItem(`${cacheKey}-timestamp`, Date.now().toString());
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
console.error('Error fetching data:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
checkUserSession();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 space-y-6">
|
||||
<h1 className="text-2xl font-bold mb-4">Visualisasi Data Akademik Mahasiswa Informatika</h1>
|
||||
|
||||
{loading ? (
|
||||
<DashboardStatsSkeleton />
|
||||
) : error ? (
|
||||
<div className="bg-red-50 border-l-4 border-red-500 p-4 mb-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 text-red-500" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-red-700">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<DashboardStats mahasiswaData={mahasiswaData} />
|
||||
<DashboardCharts />
|
||||
</>
|
||||
)
|
||||
const checkUserSession = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/user');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUser(data.user);
|
||||
// Redirect based on user role
|
||||
if (data.user.role_user === 'ketuajurusan') {
|
||||
router.push('/dashboard');
|
||||
} else if (data.user.role_user === 'admin') {
|
||||
router.push('/keloladata/mahasiswa');
|
||||
}
|
||||
} else {
|
||||
// No user session, show login dialog
|
||||
setShowLoginDialog(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking session:', error);
|
||||
setShowLoginDialog(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginSuccess = (userData: any) => {
|
||||
setUser(userData.user);
|
||||
setShowLoginDialog(false);
|
||||
|
||||
// Redirect based on user role
|
||||
if (userData.user.role_user === 'ketuajurusan') {
|
||||
showSuccess("Berhasil!", "Selamat datang, Ketua Jurusan!");
|
||||
router.push('/dashboard');
|
||||
} else if (userData.user.role_user === 'admin') {
|
||||
showSuccess("Berhasil!", "Selamat datang, Admin!");
|
||||
router.push('/keloladata/mahasiswa');
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
|
||||
<p className="text-muted-foreground">Memuat...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-blue-900 to-slate-800 relative overflow-hidden">
|
||||
{/* Background decorative elements */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-blue-500/10 rounded-full blur-3xl"></div>
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-purple-500/10 rounded-full blur-3xl"></div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-sm mx-auto p-4 relative z-10">
|
||||
{/* Header */}
|
||||
<div className="text-center space-y-3 mb-6">
|
||||
<h1 className="text-3xl font-bold text-white">
|
||||
Portal Data Informatika
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{showLoginDialog && (
|
||||
<AutoLoginDialog onLoginSuccess={handleLoginSuccess} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Auto-opening login dialog component
|
||||
interface AutoLoginDialogProps {
|
||||
onLoginSuccess: (userData: any) => void;
|
||||
}
|
||||
|
||||
function AutoLoginDialog({ onLoginSuccess }: AutoLoginDialogProps) {
|
||||
const { showSuccess, showError } = useToast();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState("ketua");
|
||||
|
||||
// Ketua Jurusan form state
|
||||
const [ketuaForm, setKetuaForm] = useState({
|
||||
nip: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
// Admin form state
|
||||
const [adminForm, setAdminForm] = useState({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
|
||||
const handleKetuaLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
nip: ketuaForm.nip,
|
||||
password: ketuaForm.password,
|
||||
role: "ketuajurusan",
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
onLoginSuccess(data);
|
||||
setKetuaForm({ nip: "", password: "" });
|
||||
} else {
|
||||
showError("Gagal!", data.message || "NIP atau password salah");
|
||||
}
|
||||
} catch (error) {
|
||||
showError("Gagal!", "Terjadi kesalahan saat login");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdminLogin = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: adminForm.username,
|
||||
password: adminForm.password,
|
||||
role: "admin",
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
onLoginSuccess(data);
|
||||
setAdminForm({ username: "", password: "" });
|
||||
} else {
|
||||
showError("Gagal!", data.message || "Username atau password salah");
|
||||
}
|
||||
} catch (error) {
|
||||
showError("Gagal!", "Terjadi kesalahan saat login");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-800/95 backdrop-blur-md rounded-xl shadow-xl p-6 w-full border border-slate-600/50 relative">
|
||||
{/* Subtle glow effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500/5 to-purple-500/5 rounded-xl"></div>
|
||||
|
||||
<div className="relative z-10">
|
||||
<div className="text-center mb-6">
|
||||
<h2 className="text-xl font-bold text-white mb-1">
|
||||
Login ke PODIF
|
||||
</h2>
|
||||
<p className="text-slate-300 text-sm">
|
||||
Silakan login untuk melanjutkan
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Tab buttons */}
|
||||
<div className="flex rounded-lg bg-slate-700/50 p-1 border border-slate-600">
|
||||
<button
|
||||
onClick={() => setActiveTab("ketua")}
|
||||
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
activeTab === "ketua"
|
||||
? "bg-blue-600 text-white shadow-md"
|
||||
: "text-slate-300 hover:text-white hover:bg-slate-600/50"
|
||||
}`}
|
||||
>
|
||||
Ketua Jurusan
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("admin")}
|
||||
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
activeTab === "admin"
|
||||
? "bg-blue-600 text-white shadow-md"
|
||||
: "text-slate-300 hover:text-white hover:bg-slate-600/50"
|
||||
}`}
|
||||
>
|
||||
Admin
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Ketua Jurusan form */}
|
||||
{activeTab === "ketua" && (
|
||||
<form onSubmit={handleKetuaLogin} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="nip" className="block text-sm font-medium text-slate-200">
|
||||
NIP
|
||||
</label>
|
||||
<input
|
||||
id="nip"
|
||||
type="text"
|
||||
placeholder="Masukkan NIP"
|
||||
value={ketuaForm.nip}
|
||||
onChange={(e) => setKetuaForm({ ...ketuaForm, nip: e.target.value })}
|
||||
required
|
||||
className="w-full px-3 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="ketua-password" className="block text-sm font-medium text-slate-200">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="ketua-password"
|
||||
type="password"
|
||||
placeholder="Masukkan password"
|
||||
value={ketuaForm.password}
|
||||
onChange={(e) => setKetuaForm({ ...ketuaForm, password: e.target.value })}
|
||||
required
|
||||
className="w-full px-3 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 disabled:from-blue-400 disabled:to-blue-500 text-white font-medium py-2.5 px-4 rounded-lg transition-all duration-200 shadow-md"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Loading...
|
||||
</div>
|
||||
) : (
|
||||
"Login sebagai Ketua Jurusan"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{/* Admin form */}
|
||||
{activeTab === "admin" && (
|
||||
<form onSubmit={handleAdminLogin} className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="username" className="block text-sm font-medium text-slate-200">
|
||||
Username
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Masukkan username"
|
||||
value={adminForm.username}
|
||||
onChange={(e) => setAdminForm({ ...adminForm, username: e.target.value })}
|
||||
required
|
||||
className="w-full px-3 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label htmlFor="admin-password" className="block text-sm font-medium text-slate-200">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="admin-password"
|
||||
type="password"
|
||||
placeholder="Masukkan password"
|
||||
value={adminForm.password}
|
||||
onChange={(e) => setAdminForm({ ...adminForm, password: e.target.value })}
|
||||
required
|
||||
className="w-full px-3 py-2 bg-slate-700/50 border border-slate-600 rounded-lg text-white placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="w-full bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800 disabled:from-blue-400 disabled:to-blue-500 text-white font-medium py-2.5 px-4 rounded-lg transition-all duration-200 shadow-md"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Loading...
|
||||
</div>
|
||||
) : (
|
||||
"Login sebagai Admin"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user