initial commit

This commit is contained in:
Randa Firman Putra
2025-09-08 22:47:42 +07:00
parent eaaf9f6436
commit a5f2a1f069
5 changed files with 231 additions and 21 deletions

View File

@@ -1,6 +1,6 @@
'use client';
import { useState } from "react";
import { useState, useEffect } from "react";
import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart";
import StatistikPerAngkatanChart from "@/components/charts/StatistikPerAngkatanChart";
import JenisPendaftaranChart from "@/components/charts/JenisPendaftaranChart";
@@ -23,9 +23,76 @@ import ProvinsiMahasiswaChart from "@/components/chartsDashboard/ProvinsiMahasis
import TingkatPrestasiPieChartDash from "@/components/chartsDashboard/TingkatPrestasiPieChartDash";
import LulusTepatWaktuChart from "@/components/charts/LulusTepatWaktuChart";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { ChevronDown, Navigation, ArrowUp } from "lucide-react";
export default function TotalMahasiswaPage() {
const [selectedYear, setSelectedYear] = useState<string>("all");
const [dropdownOpen, setDropdownOpen] = useState(false);
const [showBackToTop, setShowBackToTop] = useState(false);
// Handle scroll event to show/hide back to top button
useEffect(() => {
const handleScroll = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
setShowBackToTop(scrollTop > 300); // Show button after scrolling 300px
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
// Function to scroll to top smoothly
const scrollToTop = () => {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
// Function to scroll to section smoothly
const scrollToSection = (sectionId: string) => {
// Close dropdown first
setDropdownOpen(false);
// Use requestAnimationFrame to ensure smooth operation
requestAnimationFrame(() => {
setTimeout(() => {
const element = document.getElementById(sectionId);
if (element) {
element.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
}
}, 100);
});
};
// Navigation menu items for all data
const allDataNavItems = [
{ id: 'overview', label: 'Jumlah & Status Mahasiswa' },
{ id: 'academic', label: 'IPK & Jenis Pendaftaran' },
{ id: 'study-duration', label: 'Kelulusan Tepat Waktu & Masa Studi' },
{ id: 'expertise', label: 'Kelompok Keahlian' },
{ id: 'scholarship', label: 'Beasiswa & Prestasi' },
{ id: 'demographics', label: 'Asal Kabupaten & Provinsi' }
];
// Navigation menu items for per year data
const perYearNavItems = [
{ id: 'overview-year', label: 'Jumlah & Status per Angkatan' },
{ id: 'status-year', label: 'Jenis Pendaftaran & Kelompok Keahlian' },
{ id: 'achievement-year', label: 'Beasiswa & Prestasi per Angkatan' },
{ id: 'demographics-year', label: 'Asal Kabupaten per Angkatan' }
];
return (
<div className="container mx-auto p-4 space-y-6">
@@ -43,43 +110,67 @@ export default function TotalMahasiswaPage() {
selectedYear={selectedYear}
onYearChange={setSelectedYear}
/>
{/* Quick Navigation Dropdown */}
<DropdownMenu open={dropdownOpen} onOpenChange={setDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="flex items-center gap-2">
Judul Visualisasi
<ChevronDown className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-80">
{(selectedYear === "all" ? allDataNavItems : perYearNavItems).map((item) => (
<DropdownMenuItem
key={item.id}
onClick={(e) => {
e.preventDefault();
scrollToSection(item.id);
}}
className="cursor-pointer"
>
{item.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
</div>
{selectedYear === "all" ? (
<div className="space-y-6">
{/* Overview Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div id="overview" className="grid grid-cols-1 md:grid-cols-2 gap-4 scroll-mt-24">
<StatistikMahasiswaChart />
<StatusMahasiswaChart />
</div>
{/* Academic Performance Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div id="academic" className="grid grid-cols-1 md:grid-cols-2 gap-4 scroll-mt-24">
<IPKChart />
<JenisPendaftaranChart />
</div>
{/* Study Duration Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div id="study-duration" className="grid grid-cols-1 md:grid-cols-2 gap-4 scroll-mt-24">
<LulusTepatWaktuChart selectedYear={selectedYear} />
<MasaStudiLulusChart selectedYear={selectedYear} />
</div>
{/* Expertise & Achievement Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div id="expertise" className="grid grid-cols-1 md:grid-cols-2 gap-4 scroll-mt-24">
<KelompokKeahlianStatusChart selectedYear={selectedYear} />
<KelompokKeahlianLulusTepatPieChart />
</div>
{/* Scholarship & Achievement Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div id="scholarship" className="grid grid-cols-1 md:grid-cols-2 gap-4 scroll-mt-24">
<NamaBeasiswaChart selectedYear={selectedYear} />
<TingkatPrestasiChart selectedYear={selectedYear} />
</div>
{/* Demographics Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div id="demographics" className="grid grid-cols-1 md:grid-cols-2 gap-4 scroll-mt-24">
<div className="col-span-1">
<AsalDaerahChart />
</div>
@@ -91,28 +182,48 @@ export default function TotalMahasiswaPage() {
) : (
<div className="space-y-6">
{/* Overview Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div id="overview-year" className="grid grid-cols-1 md:grid-cols-2 gap-4 scroll-mt-24">
<StatistikPerAngkatanChart tahunAngkatan={selectedYear} />
<StatusMahasiswaPieChartPerangkatan selectedYear={selectedYear} />
</div>
{/* Status Section */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div id="status-year" className="grid grid-cols-1 md:grid-cols-2 gap-4 scroll-mt-24">
<JenisPendaftaranPerAngkatanChart tahunAngkatan={selectedYear} />
<KelompokKeahlianPieChartPerAngkatan selectedYear={selectedYear} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div id="achievement-year" className="grid grid-cols-1 md:grid-cols-2 gap-4 scroll-mt-24">
<NamaBeasiswaDashPieChartPerangkatan selectedYear={selectedYear} />
<TingkatPrestasiPieChartDash selectedYear={selectedYear} />
</div>
{/* Demographics Section */}
<div className="grid grid-cols-1 md:grid-cols-1 gap-4">
<div id="demographics-year" className="grid grid-cols-1 md:grid-cols-1 gap-4 scroll-mt-24">
<AsalDaerahPerAngkatanChart tahunAngkatan={selectedYear} />
</div>
</div>
)}
{/* Back to Top Button */}
{showBackToTop && (
<button
onClick={scrollToTop}
className={`
fixed bottom-6 right-6 z-50
w-12 h-12 rounded-full
bg-primary text-primary-foreground
shadow-lg hover:shadow-xl
transition-all duration-300 ease-in-out
hover:scale-110 hover:bg-primary/90
flex items-center justify-center
group
`}
aria-label="Kembali ke atas"
>
<ArrowUp className="size-5 transition-transform duration-200 group-hover:-translate-y-0.5" />
</button>
)}
</div>
);
}

View File

@@ -0,0 +1,78 @@
'use client';
import { useState } from "react";
import StatistikMahasiswaChart from "@/components/charts/StatistikMahasiswaChart";
import StatistikPerAngkatanChart from "@/components/charts/StatistikPerAngkatanChart";
import FilterTahunAngkatan from "@/components/FilterTahunAngkatan";
export default function JumlahMahasiswaDetailPage() {
const [selectedYear, setSelectedYear] = useState<string>("all");
return (
<div className="min-h-screen bg-gray-50 dark:bg-[var(--background)] p-4">
<div className="container mx-auto max-w-7xl space-y-2">
{/* Header Section */}
{/* Filter Section */}
<FilterTahunAngkatan
selectedYear={selectedYear}
onYearChange={setSelectedYear}
/>
{/* Chart Section - Enhanced Size */}
<div className="grid grid-cols-1 lg:grid-cols-1 gap-6">
{/* Chart untuk semua data atau hanya satu chart ketika tahun tertentu dipilih */}
{selectedYear === "all" ? (
<div className="lg:col-span-2">
<StatistikMahasiswaChart
height="h-[400px] sm:h-[400px] lg:h-[400px]"
showDetailButton={false}
/>
</div>
) : (
<>
<StatistikPerAngkatanChart
tahunAngkatan={selectedYear}
/>
</>
)}
</div>
{/* Information Section */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
Informasi Visualisasi
</h2>
<div className="grid md:grid-cols-2 gap-6">
<div>
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
Grafik Utama (Semua Angkatan)
</h3>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
<li> Menampilkan jumlah mahasiswa per tahun angkatan</li>
<li> Data dibagi berdasarkan jenis kelamin (Laki-laki & Perempuan)</li>
<li> Grafik batang dengan tiga kategori: Laki-laki, Total, dan Perempuan</li>
<li> Data dapat ditampilkan berdasarkan kategori dan di-download</li>
</ul>
</div>
{selectedYear !== "all" && (
<div>
<h3 className="font-medium text-gray-900 dark:text-white mb-2">
Grafik Per Angkatan ({selectedYear})
</h3>
<ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1">
<li> Menampilkan distribusi jenis kelamin untuk angkatan {selectedYear}</li>
<li> Grafik pie chart dengan persentase laki-laki dan perempuan</li>
<li> Data spesifik untuk tahun angkatan yang dipilih</li>
<li> Memberikan insight detail untuk angkatan tertentu</li>
</ul>
</div>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -266,7 +266,7 @@ export default function AsalDaerahChart() {
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Asal Daerah Mahasiswa
Asal Kabupaten Mahasiswa
</CardTitle>
</CardHeader>
<CardContent>

View File

@@ -3,7 +3,10 @@
import { useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { useTheme } from "next-themes";
import { ExternalLink } from "lucide-react";
import Link from "next/link";
// Import ApexCharts secara dinamis untuk menghindari error SSR
const Chart = dynamic(() => import('react-apexcharts'), { ssr: false });
@@ -15,7 +18,15 @@ interface MahasiswaStatistik {
wanita: number;
}
export default function StatistikMahasiswaChart() {
interface StatistikMahasiswaChartProps {
height?: string;
showDetailButton?: boolean;
}
export default function StatistikMahasiswaChart({
height = "h-[300px] sm:h-[350px] md:h-[300px]",
showDetailButton = true
}: StatistikMahasiswaChartProps) {
const { theme } = useTheme();
const [statistikData, setStatistikData] = useState<MahasiswaStatistik[]>([]);
const [loading, setLoading] = useState(true);
@@ -306,12 +317,22 @@ export default function StatistikMahasiswaChart() {
return (
<Card className="bg-white dark:bg-slate-900 shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-bold dark:text-white">
Jumlah Mahasiswa
</CardTitle>
<div className="flex justify-between items-center">
<CardTitle className="text-xl font-bold dark:text-white">
Jumlah Mahasiswa
</CardTitle>
{showDetailButton && (
<Link href="/detail/jumlah-mahasiswa" target="_blank">
<Button variant="outline" size="sm" className="flex items-center gap-1 dark:text-white">
<ExternalLink className="size-3" />
Detail
</Button>
</Link>
)}
</div>
</CardHeader>
<CardContent>
<div className="h-[300px] sm:h-[350px] md:h-[300px] w-full max-w-5xl mx-auto">
<div className={`${height} w-full max-w-5xl mx-auto`}>
<Chart
options={chartOptions}
series={chartSeries}

View File

@@ -2,7 +2,7 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { CheckIcon, ChevronDown, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
@@ -37,14 +37,14 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm font-medium whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
<ChevronDown className="size-4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
@@ -166,7 +166,7 @@ function SelectScrollDownButton({
)}
{...props}
>
<ChevronDownIcon className="size-4" />
<ChevronDown className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}