diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1f4c4bb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "printWidth": 100 +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5a586b3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "css.lint.unknownAtRules": "ignore" +} diff --git a/app/api/auth/check/route.ts b/app/api/auth/check/route.ts new file mode 100644 index 0000000..966aff1 --- /dev/null +++ b/app/api/auth/check/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; +import { jwtVerify } from 'jose'; +import pool from '@/lib/db'; + +export async function GET() { + let connection; + try { + const token = (await (await cookies()).get('token'))?.value; + + if (!token) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Verify JWT token + const { payload } = await jwtVerify( + token, + new TextEncoder().encode(process.env.JWT_SECRET || 'your-secret-key') + ); + + // Get connection from pool + connection = await pool.getConnection(); + + // Get user data + const [users]: any = await connection.execute( + 'SELECT id_user, nim, username, role FROM user WHERE id_user = ?', + [payload.id] + ); + + if (users.length === 0) { + connection.release(); + return NextResponse.json( + { error: 'User not found' }, + { status: 404 } + ); + } + + const user = users[0]; + connection.release(); + + return NextResponse.json({ + user: { + id: user.id_user, + nim: user.nim, + username: user.username, + role: user.role + } + }); + } catch (error) { + if (connection) { + connection.release(); + } + + console.error('Auth check error:', error); + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } +} \ No newline at end of file diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts new file mode 100644 index 0000000..f90fce0 --- /dev/null +++ b/app/api/auth/login/route.ts @@ -0,0 +1,174 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import bcrypt from 'bcryptjs'; +import { SignJWT } from 'jose'; +import { RowDataPacket } from 'mysql2'; + +interface User extends RowDataPacket { + id_user: number; + nim: string; + username: string; + password: string; + role: string; +} + +export async function POST(request: Request) { + let connection; + try { + console.log('Login request received'); + + // Test database connection first + try { + connection = await pool.getConnection(); + console.log('Database connection successful'); + } catch (dbError) { + console.error('Database connection error:', dbError); + return NextResponse.json( + { error: 'Tidak dapat terhubung ke database' }, + { status: 500 } + ); + } + + const body = await request.json(); + console.log('Request body:', body); + + const { nim, password } = body; + console.log('Extracted credentials:', { nim, password: '***' }); + + // Validate input + if (!nim || !password) { + console.log('Missing credentials:', { nim: !!nim, password: !!password }); + return NextResponse.json( + { error: 'NIM dan password harus diisi' }, + { status: 400 } + ); + } + + // Get user by NIM + console.log('Querying user with NIM:', nim); + let users: User[]; + try { + const [rows] = await connection.execute( + 'SELECT * FROM user WHERE nim = ?', + [nim] + ); + users = rows; + console.log('Query result:', users.length > 0 ? 'User found' : 'User not found'); + } catch (queryError) { + console.error('Database query error:', queryError); + return NextResponse.json( + { error: 'Terjadi kesalahan saat memeriksa data pengguna' }, + { status: 500 } + ); + } + + if (users.length === 0) { + console.log('No user found with NIM:', nim); + connection.release(); + return NextResponse.json( + { error: 'NIM atau password salah' }, + { status: 401 } + ); + } + + const user = users[0]; + console.log('User found:', { + id: user.id_user, + nim: user.nim, + username: user.username, + role: user.role + }); + + // Verify password + console.log('Verifying password...'); + let isPasswordValid; + try { + isPasswordValid = await bcrypt.compare(password, user.password); + console.log('Password verification result:', isPasswordValid ? 'Valid' : 'Invalid'); + } catch (bcryptError) { + console.error('Password verification error:', bcryptError); + return NextResponse.json( + { error: 'Terjadi kesalahan saat memverifikasi password' }, + { status: 500 } + ); + } + + if (!isPasswordValid) { + console.log('Invalid password for user:', nim); + connection.release(); + return NextResponse.json( + { error: 'NIM atau password salah' }, + { status: 401 } + ); + } + + // Create JWT token + console.log('Creating JWT token...'); + let token; + try { + token = await new SignJWT({ + id: user.id_user, + nim: user.nim, + role: user.role + }) + .setProtectedHeader({ alg: 'HS256' }) + .setExpirationTime('24h') + .sign(new TextEncoder().encode(process.env.JWT_SECRET || 'your-secret-key')); + console.log('JWT token created'); + } catch (jwtError) { + console.error('JWT creation error:', jwtError); + return NextResponse.json( + { error: 'Terjadi kesalahan saat membuat token' }, + { status: 500 } + ); + } + + // Set cookie + console.log('Setting response...'); + const response = NextResponse.json({ + user: { + id: user.id_user, + nim: user.nim, + username: user.username, + role: user.role + } + }); + + response.cookies.set('token', token, { + httpOnly: true, + secure: false, + sameSite: 'lax', + maxAge: 60 * 60 * 24 // 24 hours + }); + console.log('Cookie set'); + + connection.release(); + console.log('Login process completed successfully'); + return response; + } catch (error) { + if (connection) { + connection.release(); + } + + console.error('Login error details:', error); + if (error instanceof Error) { + console.error('Error message:', error.message); + console.error('Error stack:', error.stack); + } + return NextResponse.json( + { error: 'Terjadi kesalahan saat login' }, + { status: 500 } + ); + } +} + +// Handle OPTIONS request for CORS +export async function OPTIONS() { + return NextResponse.json({}, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + } + }); +} \ No newline at end of file diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..679a9cf --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from 'next/server'; +import { cookies } from 'next/headers'; + +export async function POST() { + try { + const response = NextResponse.json( + { message: 'Logout berhasil' }, + { status: 200 } + ); + + // Clear the token cookie with additional security options + response.cookies.set('token', '', { + expires: new Date(0), + path: '/', + httpOnly: true, + secure: false, + sameSite: 'lax' + }); + + return response; + } catch (error) { + console.error('Logout error:', error); + return NextResponse.json( + { error: 'Terjadi kesalahan saat logout' }, + { status: 500 } + ); + } +} diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts new file mode 100644 index 0000000..667125f --- /dev/null +++ b/app/api/auth/register/route.ts @@ -0,0 +1,83 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import bcrypt from 'bcryptjs'; + +export async function POST(request: Request) { + let connection; + try { + const { username, nim, password } = await request.json(); + + // Validate input + if (!username || !nim || !password) { + return NextResponse.json( + { error: 'Semua field harus diisi' }, + { status: 400 } + ); + } + + // Validate NIM format (11 characters) + if (nim.length !== 11) { + return NextResponse.json( + { error: 'NIM harus 11 karakter' }, + { status: 400 } + ); + } + + // Get connection from pool + connection = await pool.getConnection(); + + // Check if NIM exists in mahasiswa table + const [mahasiswa]: any = await connection.execute( + 'SELECT * FROM mahasiswa WHERE nim = ?', + [nim] + ); + + if (mahasiswa.length === 0) { + connection.release(); + return NextResponse.json( + { error: 'NIM tidak terdaftar sebagai mahasiswa' }, + { status: 400 } + ); + } + + // Check if NIM already exists in user table + const [existingUsers]: any = await connection.execute( + 'SELECT * FROM user WHERE nim = ?', + [nim] + ); + + if (existingUsers.length > 0) { + connection.release(); + return NextResponse.json( + { error: 'NIM sudah terdaftar sebagai pengguna' }, + { status: 400 } + ); + } + + // Hash password + const hashedPassword = await bcrypt.hash(password, 10); + + // Insert new user + await connection.execute( + 'INSERT INTO user (nim, username, password, role, created_at, updated_at) VALUES (?, ?, ?, ?, NOW(), NOW())', + [nim, username, hashedPassword, 'mahasiswa'] + ); + + connection.release(); + + return NextResponse.json( + { message: 'Registrasi berhasil' }, + { status: 201 } + ); + } catch (error) { + if (connection) { + connection.release(); + } + + console.error('Registration error:', error); + return NextResponse.json( + { error: 'Terjadi kesalahan saat registrasi' }, + { status: 500 } + ); + } +} diff --git a/app/api/mahasiswa/asal-daerah-angkatan/route.ts b/app/api/mahasiswa/asal-daerah-angkatan/route.ts new file mode 100644 index 0000000..bad666e --- /dev/null +++ b/app/api/mahasiswa/asal-daerah-angkatan/route.ts @@ -0,0 +1,58 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface AsalDaerah extends RowDataPacket { + kabupaten: string; + jumlah: number; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + if (!tahunAngkatan) { + return NextResponse.json( + { error: 'Tahun angkatan diperlukan' }, + { status: 400 } + ); + } + + const connection = await pool.getConnection(); + + try { + const query = ` + SELECT kabupaten, COUNT(*) AS jumlah + FROM mahasiswa + WHERE tahun_angkatan = ? + GROUP BY kabupaten + ORDER BY jumlah DESC, kabupaten ASC + `; + + const [results] = await connection.query(query, [tahunAngkatan]); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching asal daerah per angkatan:', error); + return NextResponse.json( + { error: 'Failed to fetch asal daerah data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/asal-daerah-beasiswa/route.ts b/app/api/mahasiswa/asal-daerah-beasiswa/route.ts new file mode 100644 index 0000000..aa31de3 --- /dev/null +++ b/app/api/mahasiswa/asal-daerah-beasiswa/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahunAngkatan'); + const jenisBeasiswa = searchParams.get('jenisBeasiswa'); + + let query = ` + SELECT + m.tahun_angkatan, + m.kabupaten, + COUNT(m.nim) AS jumlah_mahasiswa + FROM + mahasiswa m + JOIN + beasiswa_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_beasiswa = ? + `; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + } + + query += ` + GROUP BY + m.kabupaten, m.tahun_angkatan + ORDER BY + m.tahun_angkatan ASC, m.kabupaten + `; + + const params = [jenisBeasiswa]; + if (tahunAngkatan && tahunAngkatan !== 'all') { + params.push(tahunAngkatan); + } + + const [rows] = await pool.query(query, params); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/asal-daerah-lulus/route.ts b/app/api/mahasiswa/asal-daerah-lulus/route.ts new file mode 100644 index 0000000..aec3c96 --- /dev/null +++ b/app/api/mahasiswa/asal-daerah-lulus/route.ts @@ -0,0 +1,43 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahunAngkatan'); + + let query = ` + SELECT + m.tahun_angkatan, + m.kabupaten, + COUNT(m.nim) AS jumlah_lulus_tepat_waktu + FROM + mahasiswa m + JOIN + status_mahasiswa s ON m.nim = s.nim + WHERE + s.status_kuliah = 'Lulus' + AND s.semester <= 8 + `; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = '${tahunAngkatan}'`; + } + + query += ` + GROUP BY + m.tahun_angkatan, m.kabupaten + ORDER BY + m.tahun_angkatan DESC, jumlah_lulus_tepat_waktu DESC + `; + + const [rows] = await pool.query(query); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/asal-daerah-prestasi/route.ts b/app/api/mahasiswa/asal-daerah-prestasi/route.ts new file mode 100644 index 0000000..11ddb75 --- /dev/null +++ b/app/api/mahasiswa/asal-daerah-prestasi/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahunAngkatan'); + const jenisPrestasi = searchParams.get('jenisPrestasi'); + + let query = ` + SELECT + m.tahun_angkatan, + m.kabupaten, + COUNT(m.nim) AS asal_daerah_mahasiswa_prestasi + FROM + mahasiswa m + JOIN + prestasi_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_prestasi = ? + `; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + } + + query += ` + GROUP BY + m.tahun_angkatan, m.kabupaten + ORDER BY + m.tahun_angkatan DESC, m.kabupaten + `; + + const params = [jenisPrestasi]; + if (tahunAngkatan && tahunAngkatan !== 'all') { + params.push(tahunAngkatan); + } + + const [rows] = await pool.query(query, params); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/asal-daerah-status/route.ts b/app/api/mahasiswa/asal-daerah-status/route.ts new file mode 100644 index 0000000..6c938e4 --- /dev/null +++ b/app/api/mahasiswa/asal-daerah-status/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface AsalDaerahStatus extends RowDataPacket { + kabupaten: string; + tahun_angkatan?: number; + status_kuliah: string; + total_mahasiswa: number; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + const statusKuliah = searchParams.get('status_kuliah'); + + const connection = await pool.getConnection(); + + try { + let query = ` + SELECT + m.kabupaten, + ${tahunAngkatan && tahunAngkatan !== 'all' ? 'm.tahun_angkatan,' : ''} + s.status_kuliah, + COUNT(m.nim) AS total_mahasiswa + FROM + mahasiswa m + JOIN + status_mahasiswa s ON m.nim = s.nim + WHERE + s.status_kuliah = ? + `; + + const params: any[] = [statusKuliah]; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + params.push(tahunAngkatan); + } + + query += ` + GROUP BY + m.kabupaten${tahunAngkatan && tahunAngkatan !== 'all' ? ', m.tahun_angkatan' : ''}, s.status_kuliah + ORDER BY + ${tahunAngkatan && tahunAngkatan !== 'all' ? 'm.tahun_angkatan ASC,' : ''} m.kabupaten, s.status_kuliah + `; + + const [results] = await connection.query(query, params); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching asal daerah status:', error); + return NextResponse.json( + { error: 'Failed to fetch asal daerah status data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/asal-daerah/route.ts b/app/api/mahasiswa/asal-daerah/route.ts new file mode 100644 index 0000000..0639fd4 --- /dev/null +++ b/app/api/mahasiswa/asal-daerah/route.ts @@ -0,0 +1,45 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface AsalDaerah extends RowDataPacket { + kabupaten: string; + jumlah: number; +} + +export async function GET() { + const connection = await pool.getConnection(); + + try { + const [results] = await connection.query(` + SELECT kabupaten, COUNT(*) AS jumlah + FROM mahasiswa + GROUP BY kabupaten + ORDER BY kabupaten ASC + `); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching asal daerah:', error); + return NextResponse.json( + { error: 'Failed to fetch asal daerah data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/gender-per-angkatan/route.ts b/app/api/mahasiswa/gender-per-angkatan/route.ts new file mode 100644 index 0000000..9e55444 --- /dev/null +++ b/app/api/mahasiswa/gender-per-angkatan/route.ts @@ -0,0 +1,49 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahun = searchParams.get('tahun'); + + if (!tahun) { + return NextResponse.json( + { error: 'Tahun angkatan is required' }, + { status: 400 } + ); + } + + const connection = await pool.getConnection(); + + try { + const [results] = await connection.query(` + SELECT tahun_angkatan, jk, COUNT(*) AS jumlah + FROM mahasiswa + WHERE tahun_angkatan = ? + GROUP BY jk + `, [tahun]); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching gender per angkatan:', error); + return NextResponse.json( + { error: 'Failed to fetch gender per angkatan data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/ipk-beasiswa/route.ts b/app/api/mahasiswa/ipk-beasiswa/route.ts new file mode 100644 index 0000000..d462ba6 --- /dev/null +++ b/app/api/mahasiswa/ipk-beasiswa/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const jenisBeasiswa = searchParams.get('jenisBeasiswa'); + + const query = ` + SELECT + m.tahun_angkatan, + COUNT(m.nim) AS total_mahasiswa_beasiswa, + ROUND(AVG(m.ipk), 2) AS rata_rata_ipk + FROM + mahasiswa m + JOIN + beasiswa_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_beasiswa = ? + GROUP BY + m.tahun_angkatan + ORDER BY + m.tahun_angkatan ASC + `; + + const [rows] = await pool.query(query, [jenisBeasiswa]); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/ipk-jenis-kelamin/route.ts b/app/api/mahasiswa/ipk-jenis-kelamin/route.ts new file mode 100644 index 0000000..633f101 --- /dev/null +++ b/app/api/mahasiswa/ipk-jenis-kelamin/route.ts @@ -0,0 +1,55 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + if (!tahunAngkatan) { + return NextResponse.json( + { error: 'Tahun angkatan diperlukan' }, + { status: 400 } + ); + } + + const connection = await pool.getConnection(); + + try { + const query = ` + SELECT + jk, + ROUND(AVG(ipk), 2) as rata_rata_ipk + FROM mahasiswa + WHERE tahun_angkatan = ? + GROUP BY jk + ORDER BY jk ASC + `; + + const [results] = await connection.query(query, [tahunAngkatan]); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching IPK data:', error); + return NextResponse.json( + { error: 'Failed to fetch IPK data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/ipk-lulus-tepat/route.ts b/app/api/mahasiswa/ipk-lulus-tepat/route.ts new file mode 100644 index 0000000..6d46724 --- /dev/null +++ b/app/api/mahasiswa/ipk-lulus-tepat/route.ts @@ -0,0 +1,42 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahunAngkatan'); + + let query = ` + SELECT + m.tahun_angkatan, + ROUND(AVG(m.ipk), 2) AS rata_rata_ipk + FROM + mahasiswa m + JOIN + status_mahasiswa s ON m.nim = s.nim + WHERE + s.status_kuliah = 'Lulus' + AND s.semester <= 8 + `; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = '${tahunAngkatan}'`; + } + + query += ` + GROUP BY + m.tahun_angkatan + ORDER BY + m.tahun_angkatan ASC + `; + + const [rows] = await pool.query(query); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/ipk-prestasi/route.ts b/app/api/mahasiswa/ipk-prestasi/route.ts new file mode 100644 index 0000000..d5809d9 --- /dev/null +++ b/app/api/mahasiswa/ipk-prestasi/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const jenisPrestasi = searchParams.get('jenisPrestasi'); + + const query = ` + SELECT + m.tahun_angkatan, + COUNT(m.nim) AS total_mahasiswa_prestasi, + ROUND(AVG(m.ipk), 2) AS rata_rata_ipk + FROM + mahasiswa m + JOIN + prestasi_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_prestasi = ? + GROUP BY + m.tahun_angkatan + ORDER BY + m.tahun_angkatan ASC + `; + + const [rows] = await pool.query(query, [jenisPrestasi]); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/ipk-status/route.ts b/app/api/mahasiswa/ipk-status/route.ts new file mode 100644 index 0000000..1254066 --- /dev/null +++ b/app/api/mahasiswa/ipk-status/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +interface IpkStatus { + tahun_angkatan: number; + status_kuliah: string; + total_mahasiswa: number; + rata_rata_ipk: number; +} + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + const statusKuliah = searchParams.get('status_kuliah'); + + if (!statusKuliah) { + console.error('Missing required parameter: status_kuliah'); + return NextResponse.json( + { error: 'Missing required parameter: status_kuliah' }, + { status: 400 } + ); + } + + let query = ` + SELECT + m.tahun_angkatan, + s.status_kuliah, + COUNT(m.nim) AS total_mahasiswa, + ROUND(AVG(m.ipk), 2) AS rata_rata_ipk + FROM + mahasiswa m + JOIN + status_mahasiswa s ON m.nim = s.nim + WHERE + s.status_kuliah = ? + `; + + const params: any[] = [statusKuliah]; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ' AND m.tahun_angkatan = ?'; + params.push(tahunAngkatan); + } + + query += ` + GROUP BY + m.tahun_angkatan, s.status_kuliah + ORDER BY + m.tahun_angkatan DESC, s.status_kuliah + `; + + const [rows] = await pool.query(query, params); + + return NextResponse.json(rows); + } catch (error) { + console.error('Error in ipk-status route:', error); + return NextResponse.json( + { error: 'Internal Server Error', details: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/ipk/route.ts b/app/api/mahasiswa/ipk/route.ts new file mode 100644 index 0000000..c2a977e --- /dev/null +++ b/app/api/mahasiswa/ipk/route.ts @@ -0,0 +1,44 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface IPKData extends RowDataPacket { + tahun_angkatan: number; + rata_rata_ipk: number; +} + +export async function GET() { + const connection = await pool.getConnection(); + + try { + const [results] = await connection.query(` + SELECT tahun_angkatan, ROUND(AVG(ipk), 2) AS rata_rata_ipk + FROM mahasiswa + GROUP BY tahun_angkatan + `); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching IPK data:', error); + return NextResponse.json( + { error: 'Failed to fetch IPK data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/jenis-beasiswa/route.ts b/app/api/mahasiswa/jenis-beasiswa/route.ts new file mode 100644 index 0000000..9899235 --- /dev/null +++ b/app/api/mahasiswa/jenis-beasiswa/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET() { + try { + const [rows] = await pool.query(` + SELECT DISTINCT jenis_beasiswa + FROM beasiswa_mahasiswa + ORDER BY jenis_beasiswa ASC + `); + + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/jenis-pendaftaran-beasiswa/route.ts b/app/api/mahasiswa/jenis-pendaftaran-beasiswa/route.ts new file mode 100644 index 0000000..468278b --- /dev/null +++ b/app/api/mahasiswa/jenis-pendaftaran-beasiswa/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahunAngkatan'); + const jenisBeasiswa = searchParams.get('jenisBeasiswa'); + + let query = ` + SELECT + m.tahun_angkatan, + m.jenis_pendaftaran, + COUNT(m.nim) AS jumlah_mahasiswa_beasiswa + FROM + mahasiswa m + JOIN + beasiswa_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_beasiswa = ? + `; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + } + + query += ` + GROUP BY + m.tahun_angkatan, m.jenis_pendaftaran + ORDER BY + m.tahun_angkatan DESC, m.jenis_pendaftaran + `; + + const params = [jenisBeasiswa]; + if (tahunAngkatan && tahunAngkatan !== 'all') { + params.push(tahunAngkatan); + } + + const [rows] = await pool.query(query, params); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/jenis-pendaftaran-lulus/route.ts b/app/api/mahasiswa/jenis-pendaftaran-lulus/route.ts new file mode 100644 index 0000000..b127923 --- /dev/null +++ b/app/api/mahasiswa/jenis-pendaftaran-lulus/route.ts @@ -0,0 +1,53 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +interface JenisPendaftaranLulus { + tahun_angkatan: number; + jenis_pendaftaran: string; + jumlah_lulus_tepat_waktu: number; +} + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = ` + SELECT + m.tahun_angkatan, + m.jenis_pendaftaran, + COUNT(m.nim) AS jumlah_lulus_tepat_waktu + FROM + mahasiswa m + JOIN + status_mahasiswa s ON m.nim = s.nim + WHERE + s.status_kuliah = 'Lulus' + AND s.semester <= 8 + `; + + const queryParams: any[] = []; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + queryParams.push(parseInt(tahunAngkatan)); + } + + query += ` + GROUP BY + m.tahun_angkatan, m.jenis_pendaftaran + ORDER BY + m.tahun_angkatan DESC, m.jenis_pendaftaran + `; + + const [rows] = await pool.query(query, queryParams); + + return NextResponse.json(rows); + } catch (error) { + console.error('Detailed error in GET /api/mahasiswa/jenis-pendaftaran-lulus:', error); + return NextResponse.json( + { error: 'Internal Server Error', details: error instanceof Error ? error.message : 'Unknown error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/jenis-pendaftaran-prestasi/route.ts b/app/api/mahasiswa/jenis-pendaftaran-prestasi/route.ts new file mode 100644 index 0000000..a88e739 --- /dev/null +++ b/app/api/mahasiswa/jenis-pendaftaran-prestasi/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const jenisPrestasi = searchParams.get('jenisPrestasi'); + + const query = ` + SELECT + m.tahun_angkatan, + m.jenis_pendaftaran, + COUNT(m.nim) AS jenis_pendaftaran_mahasiswa_prestasi + FROM + mahasiswa m + JOIN + prestasi_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_prestasi = ? + GROUP BY + m.tahun_angkatan, m.jenis_pendaftaran + ORDER BY + m.tahun_angkatan DESC, m.jenis_pendaftaran + `; + + const [rows] = await pool.query(query, [jenisPrestasi]); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/jenis-pendaftaran-status/route.ts b/app/api/mahasiswa/jenis-pendaftaran-status/route.ts new file mode 100644 index 0000000..f452304 --- /dev/null +++ b/app/api/mahasiswa/jenis-pendaftaran-status/route.ts @@ -0,0 +1,74 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface JenisPendaftaranStatus extends RowDataPacket { + jenis_pendaftaran: string; + tahun_angkatan: number; + status_kuliah: string; + total_mahasiswa: number; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + const statusKuliah = searchParams.get('status_kuliah'); + + const connection = await pool.getConnection(); + + try { + let query = ` + SELECT + m.jenis_pendaftaran, + m.tahun_angkatan, + s.status_kuliah, + COUNT(m.nim) AS total_mahasiswa + FROM + mahasiswa m + JOIN + status_mahasiswa s ON m.nim = s.nim + WHERE + s.status_kuliah = ? + `; + + const params: any[] = [statusKuliah]; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + params.push(tahunAngkatan); + } + + query += ` + GROUP BY + m.jenis_pendaftaran, m.tahun_angkatan, s.status_kuliah + ORDER BY + m.tahun_angkatan DESC, m.jenis_pendaftaran, s.status_kuliah + `; + + const [results] = await connection.query(query, params); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching jenis pendaftaran status:', error); + return NextResponse.json( + { error: 'Failed to fetch jenis pendaftaran status data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/jenis-pendaftaran/route.ts b/app/api/mahasiswa/jenis-pendaftaran/route.ts new file mode 100644 index 0000000..8ea8d30 --- /dev/null +++ b/app/api/mahasiswa/jenis-pendaftaran/route.ts @@ -0,0 +1,61 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface JenisPendaftaran extends RowDataPacket { + tahun_angkatan: number; + jenis_pendaftaran: string; + jumlah: number; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + const connection = await pool.getConnection(); + + try { + let query = ` + SELECT tahun_angkatan, jenis_pendaftaran, COUNT(*) AS jumlah + FROM mahasiswa + `; + + const params: any[] = []; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` WHERE tahun_angkatan = ?`; + params.push(tahunAngkatan); + } + + query += ` + GROUP BY tahun_angkatan, jenis_pendaftaran + ORDER BY tahun_angkatan DESC, jenis_pendaftaran + `; + + const [results] = await connection.query(query, params); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching jenis pendaftaran:', error); + return NextResponse.json( + { error: 'Failed to fetch jenis pendaftaran data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/jenis-prestasi/route.ts b/app/api/mahasiswa/jenis-prestasi/route.ts new file mode 100644 index 0000000..26eacf9 --- /dev/null +++ b/app/api/mahasiswa/jenis-prestasi/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET() { + try { + const [rows] = await pool.query(` + SELECT jenis_prestasi + FROM prestasi_mahasiswa + WHERE jenis_prestasi = 'Akademik' OR jenis_prestasi = 'Non-Akademik' + GROUP BY jenis_prestasi + ORDER BY jenis_prestasi ASC + `); + + return NextResponse.json(rows); + } catch (error) { + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/lulus-tepat-waktu/route.ts b/app/api/mahasiswa/lulus-tepat-waktu/route.ts new file mode 100644 index 0000000..749f345 --- /dev/null +++ b/app/api/mahasiswa/lulus-tepat-waktu/route.ts @@ -0,0 +1,51 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +interface LulusTepatWaktu { + tahun_angkatan: number; + jk: string; + jumlah_lulus_tepat_waktu: number; +} + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + let query = ` + SELECT + m.tahun_angkatan, + m.jk, + COUNT(m.nim) AS jumlah_lulus_tepat_waktu + FROM + mahasiswa m + JOIN + status_mahasiswa s ON m.nim = s.nim + WHERE + s.status_kuliah = 'Lulus' + AND s.semester <= 8 + `; + + const params: any[] = []; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ' AND m.tahun_angkatan = ?'; + params.push(tahunAngkatan); + } + + query += ` + GROUP BY + m.tahun_angkatan, m.jk + ORDER BY + m.tahun_angkatan DESC, m.jk + `; + + const [rows] = await pool.query(query, params); + return NextResponse.json(rows); + } catch (error) { + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/nama-beasiswa/route.ts b/app/api/mahasiswa/nama-beasiswa/route.ts new file mode 100644 index 0000000..e1b8d07 --- /dev/null +++ b/app/api/mahasiswa/nama-beasiswa/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahunAngkatan'); + const jenisBeasiswa = searchParams.get('jenisBeasiswa'); + + let query = ` + SELECT + m.tahun_angkatan, + s.nama_beasiswa, + COUNT(m.nim) AS jumlah_nama_beasiswa + FROM + mahasiswa m + JOIN + beasiswa_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_beasiswa = ? + `; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + } + + query += ` + GROUP BY + m.tahun_angkatan, s.nama_beasiswa, s.jenis_beasiswa + ORDER BY + m.tahun_angkatan DESC, s.nama_beasiswa, s.jenis_beasiswa + `; + + const params = [jenisBeasiswa]; + if (tahunAngkatan && tahunAngkatan !== 'all') { + params.push(tahunAngkatan); + } + + const [rows] = await pool.query(query, params); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/profile/route.ts b/app/api/mahasiswa/profile/route.ts new file mode 100644 index 0000000..9fcf2f3 --- /dev/null +++ b/app/api/mahasiswa/profile/route.ts @@ -0,0 +1,91 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; +import { cookies } from 'next/headers'; +import { jwtVerify } from 'jose'; + +interface MahasiswaProfile extends RowDataPacket { + nim: string; + nama: string; + jk: 'Pria' | 'Wanita'; + agama: string; + kabupaten: string; + provinsi: string; + jenis_pendaftaran: string; + status_beasiswa: 'YA' | 'TIDAK'; + tahun_angkatan: string; + ipk: number | null; + prestasi: 'YA' | 'TIDAK'; + status_kuliah: string; +} + +export async function GET(request: Request) { + let connection; + try { + // Get token from cookies + const cookieStore = await cookies(); + const token = cookieStore.get('token')?.value; + + if (!token) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Verify JWT token + const { payload } = await jwtVerify( + token, + new TextEncoder().encode(process.env.JWT_SECRET || 'your-secret-key') + ); + + const nim = payload.nim as string; + + // Get connection from pool + connection = await pool.getConnection(); + + const query = ` + SELECT + m.nim, + m.nama, + m.jk, + m.agama, + m.kabupaten, + m.provinsi, + m.jenis_pendaftaran, + m.status_beasiswa, + m.tahun_angkatan, + m.ipk, + m.prestasi, + s.status_kuliah + FROM + mahasiswa m + LEFT JOIN + status_mahasiswa s ON m.nim = s.nim + WHERE + m.nim = ? + `; + + const [rows] = await connection.query(query, [nim]); + + if (rows.length === 0) { + connection.release(); + return NextResponse.json( + { error: 'Data mahasiswa tidak ditemukan' }, + { status: 404 } + ); + } + + connection.release(); + return NextResponse.json(rows[0]); + } catch (error) { + if (connection) { + connection.release(); + } + console.error('Error fetching profile data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/statistik/route.ts b/app/api/mahasiswa/statistik/route.ts new file mode 100644 index 0000000..4cfe637 --- /dev/null +++ b/app/api/mahasiswa/statistik/route.ts @@ -0,0 +1,65 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface MahasiswaStatistik extends RowDataPacket { + tahun_angkatan: number; + total_mahasiswa: number; + pria: number; + wanita: number; +} + +// Fungsi untuk menangani preflight request (OPTIONS) +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400', // 24 jam + }, + }); +} + +export async function GET() { + const connection = await pool.getConnection(); + + try { + // Query untuk mendapatkan statistik mahasiswa per tahun angkatan + const [results] = await connection.query(` + SELECT + tahun_angkatan, + COUNT(*) as total_mahasiswa, + SUM(CASE WHEN jk = 'Pria' THEN 1 ELSE 0 END) as pria, + SUM(CASE WHEN jk = 'Wanita' THEN 1 ELSE 0 END) as wanita + FROM mahasiswa + GROUP BY tahun_angkatan + `); + + // Menambahkan header cache dan CORS + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching mahasiswa statistik:', error); + return NextResponse.json( + { error: 'Failed to fetch mahasiswa statistik' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/status-kuliah/route.ts b/app/api/mahasiswa/status-kuliah/route.ts new file mode 100644 index 0000000..04057ad --- /dev/null +++ b/app/api/mahasiswa/status-kuliah/route.ts @@ -0,0 +1,63 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface StatusKuliah extends RowDataPacket { + tahun_angkatan: number; + status_kuliah: string; + jumlah: number; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + + const connection = await pool.getConnection(); + + try { + let query = ` + SELECT m.tahun_angkatan, s.status_kuliah, COUNT(*) AS jumlah + FROM mahasiswa m + JOIN status_mahasiswa s ON m.nim = s.nim + WHERE s.status_kuliah IN ('Lulus', 'Cuti', 'Aktif', 'DO') + `; + + const params: any[] = []; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + params.push(tahunAngkatan); + } + + query += ` + GROUP BY m.tahun_angkatan, s.status_kuliah + ORDER BY m.tahun_angkatan, s.status_kuliah + `; + + const [results] = await connection.query(query, params); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching status kuliah:', error); + return NextResponse.json( + { error: 'Failed to fetch status kuliah data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/status-mahasiswa/route.ts b/app/api/mahasiswa/status-mahasiswa/route.ts new file mode 100644 index 0000000..0be3382 --- /dev/null +++ b/app/api/mahasiswa/status-mahasiswa/route.ts @@ -0,0 +1,76 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface StatusMahasiswa extends RowDataPacket { + tahun_angkatan: number; + jk: string; + total_mahasiswa: number; +} + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahun_angkatan'); + const statusKuliah = searchParams.get('status_kuliah'); + + const connection = await pool.getConnection(); + + try { + let query = ` + SELECT + m.tahun_angkatan, + CASE + WHEN m.jk = 'Pria' THEN 'L' + WHEN m.jk = 'Wanita' THEN 'P' + ELSE m.jk + END as jk, + COUNT(m.nim) AS total_mahasiswa + FROM + mahasiswa m + JOIN + status_mahasiswa s ON m.nim = s.nim + WHERE + s.status_kuliah = ? + `; + + const params: any[] = [statusKuliah]; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + params.push(tahunAngkatan); + } + + query += ` + GROUP BY + m.tahun_angkatan, m.jk + ORDER BY + m.tahun_angkatan DESC, m.jk + `; + + const [results] = await connection.query(query, params); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching status mahasiswa:', error); + return NextResponse.json( + { error: 'Failed to fetch status mahasiswa data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/status/route.ts b/app/api/mahasiswa/status/route.ts new file mode 100644 index 0000000..3a1b07d --- /dev/null +++ b/app/api/mahasiswa/status/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from 'next/server'; +import db from '@/lib/db'; + +export async function GET() { + try { + const query = ` + SELECT m.tahun_angkatan, s.status_kuliah, COUNT(*) AS jumlah + FROM mahasiswa m + JOIN status_mahasiswa s ON m.nim = s.nim + WHERE s.status_kuliah IN ('Lulus', 'Cuti', 'Aktif', 'DO') + GROUP BY m.tahun_angkatan, s.status_kuliah; + `; + + const [rows] = await db.query(query); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching status data:', error); + return NextResponse.json( + { error: 'Failed to fetch status data' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/tahun-angkatan/route.ts b/app/api/mahasiswa/tahun-angkatan/route.ts new file mode 100644 index 0000000..0396dfb --- /dev/null +++ b/app/api/mahasiswa/tahun-angkatan/route.ts @@ -0,0 +1,41 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET() { + const connection = await pool.getConnection(); + + try { + const currentYear = new Date().getFullYear(); + const [results] = await connection.query(` + SELECT DISTINCT tahun_angkatan + FROM mahasiswa + WHERE tahun_angkatan >= ? + ORDER BY tahun_angkatan DESC + LIMIT 7 + `, [currentYear - 6]); + + return NextResponse.json(results, { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching tahun angkatan:', error); + return NextResponse.json( + { error: 'Failed to fetch tahun angkatan data' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/tingkat-prestasi/route.ts b/app/api/mahasiswa/tingkat-prestasi/route.ts new file mode 100644 index 0000000..f8437ce --- /dev/null +++ b/app/api/mahasiswa/tingkat-prestasi/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const jenisPrestasi = searchParams.get('jenisPrestasi'); + + const query = ` + SELECT + m.tahun_angkatan, + s.tingkat, + COUNT(m.nim) AS tingkat_mahasiswa_prestasi + FROM + mahasiswa m + JOIN + prestasi_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_prestasi = ? + GROUP BY + m.tahun_angkatan, s.tingkat + ORDER BY + m.tahun_angkatan DESC, s.tingkat + `; + + const [rows] = await pool.query(query, [jenisPrestasi]); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/total-beasiswa/route.ts b/app/api/mahasiswa/total-beasiswa/route.ts new file mode 100644 index 0000000..07e20e7 --- /dev/null +++ b/app/api/mahasiswa/total-beasiswa/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const tahunAngkatan = searchParams.get('tahunAngkatan'); + const jenisBeasiswa = searchParams.get('jenisBeasiswa'); + + let query = ` + SELECT + m.tahun_angkatan, + m.jk, + COUNT(m.nim) AS jumlah_mahasiswa_beasiswa + FROM + mahasiswa m + JOIN + beasiswa_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_beasiswa = ? + `; + + if (tahunAngkatan && tahunAngkatan !== 'all') { + query += ` AND m.tahun_angkatan = ?`; + } + + query += ` + GROUP BY + m.tahun_angkatan, m.jk + ORDER BY + m.tahun_angkatan DESC, m.jk + `; + + const params = [jenisBeasiswa]; + if (tahunAngkatan && tahunAngkatan !== 'all') { + params.push(tahunAngkatan); + } + + const [rows] = await pool.query(query, params); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/total-prestasi/route.ts b/app/api/mahasiswa/total-prestasi/route.ts new file mode 100644 index 0000000..bf822cd --- /dev/null +++ b/app/api/mahasiswa/total-prestasi/route.ts @@ -0,0 +1,35 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const jenisPrestasi = searchParams.get('jenisPrestasi'); + + const query = ` + SELECT + m.tahun_angkatan, + m.jk, + COUNT(m.nim) AS jumlah_mahasiswa_prestasi + FROM + mahasiswa m + JOIN + prestasi_mahasiswa s ON m.nim = s.nim + WHERE + s.jenis_prestasi = ? + GROUP BY + m.tahun_angkatan, m.jk + ORDER BY + m.tahun_angkatan DESC, m.jk + `; + + const [rows] = await pool.query(query, [jenisPrestasi]); + return NextResponse.json(rows); + } catch (error) { + console.error('Error fetching data:', error); + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/api/mahasiswa/total/route.ts b/app/api/mahasiswa/total/route.ts new file mode 100644 index 0000000..18272b3 --- /dev/null +++ b/app/api/mahasiswa/total/route.ts @@ -0,0 +1,91 @@ +import { NextResponse } from 'next/server'; +import pool from '@/lib/db'; +import { RowDataPacket } from 'mysql2'; + +interface MahasiswaTotal extends RowDataPacket { + 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; + ipk_rata_rata_aktif: number; + ipk_rata_rata_lulus: number; + total_mahasiswa_aktif_lulus: number; +} + +// Fungsi untuk menangani preflight request (OPTIONS) +export async function OPTIONS() { + return new NextResponse(null, { + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + 'Access-Control-Max-Age': '86400', // 24 jam + }, + }); +} + +export async function GET() { + const connection = await pool.getConnection(); + + try { + // Query gabungan untuk semua data + const [results] = await connection.query(` + SELECT + (SELECT COUNT(*) FROM mahasiswa) AS total_mahasiswa, + (SELECT COUNT(*) FROM mahasiswa m + JOIN status_mahasiswa s ON m.nim = s.nim + WHERE s.status_kuliah = 'Aktif') AS mahasiswa_aktif, + (SELECT COUNT(*) FROM mahasiswa m + JOIN status_mahasiswa s ON m.nim = s.nim + WHERE s.status_kuliah = 'Lulus') AS total_lulus, + (SELECT COUNT(*) FROM mahasiswa m + JOIN status_mahasiswa s ON m.nim = s.nim + WHERE s.status_kuliah = 'Lulus' AND m.jk = 'Pria') AS pria_lulus, + (SELECT COUNT(*) FROM mahasiswa m + JOIN status_mahasiswa s ON m.nim = s.nim + WHERE s.status_kuliah = 'Lulus' AND m.jk = 'Wanita') AS wanita_lulus, + (SELECT COUNT(*) FROM prestasi_mahasiswa) AS total_berprestasi, + (SELECT COUNT(*) FROM prestasi_mahasiswa WHERE jenis_prestasi = 'Akademik') AS prestasi_akademik, + (SELECT COUNT(*) FROM prestasi_mahasiswa WHERE jenis_prestasi = 'Non-Akademik') AS prestasi_non_akademik, + (SELECT COUNT(*) FROM mahasiswa m + JOIN status_mahasiswa s ON m.nim = s.nim + WHERE s.status_kuliah IN ('Aktif', 'Lulus')) AS total_mahasiswa_aktif_lulus, + (SELECT ROUND(AVG(m.ipk), 2) FROM mahasiswa m + JOIN status_mahasiswa s ON m.nim = s.nim + WHERE s.status_kuliah = 'Aktif') AS ipk_rata_rata_aktif, + (SELECT ROUND(AVG(m.ipk), 2) FROM mahasiswa m + JOIN status_mahasiswa s ON m.nim = s.nim + WHERE s.status_kuliah = 'Lulus') AS ipk_rata_rata_lulus + `); + + // Menambahkan header cache dan CORS + return NextResponse.json(results[0], { + headers: { + 'Cache-Control': 'public, max-age=60, stale-while-revalidate=30', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + }); + } catch (error) { + console.error('Error fetching total mahasiswa:', error); + return NextResponse.json( + { error: 'Failed to fetch total mahasiswa' }, + { + status: 500, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', + }, + } + ); + } finally { + connection.release(); + } +} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index a2dc41e..0fe601c 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,26 +1,122 @@ -@import "tailwindcss"; +@import 'tailwindcss'; +@import 'tw-animate-css'; -:root { - --background: #ffffff; - --foreground: #171717; -} +@custom-variant dark (&:is(.dark *)); @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: rgb(0, 0, 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: rgb(16, 39, 73); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.205 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(1 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: rgb(12, 28, 52); + --muted-foreground: oklch(0.708 0 0); + --accent: rgb(22, 50, 91); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; } } - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..8e3df99 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,33 +1,60 @@ -import type { Metadata } from "next"; -import { Geist, Geist_Mono } from "next/font/google"; -import "./globals.css"; +import type { Metadata } from 'next'; +import { Geist, Geist_Mono } from 'next/font/google'; +import './globals.css'; +import Navbar from '@/components/ui/Navbar'; +import Sidebar from '@/components/ui/Sidebar'; +import { ThemeProvider } from '@/components/theme-provider'; +import { Toaster } from '@/components/ui/toaster'; const geistSans = Geist({ - variable: "--font-geist-sans", - subsets: ["latin"], + variable: '--font-geist-sans', + subsets: ['latin'], }); const geistMono = Geist_Mono({ - variable: "--font-geist-mono", - subsets: ["latin"], + variable: '--font-geist-mono', + subsets: ['latin'], }); export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", + title: 'Portal Data Informatika', + description: 'Admin Dashboard', }; +function ClientLayout({ children }: { children: React.ReactNode }) { + return ( + +
+ +
+ +
+ {children} +
+
+
+ +
+ ); +} + export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { return ( - - - {children} + + + + + + {children} ); diff --git a/app/mahasiswa/beasiswa/page.tsx b/app/mahasiswa/beasiswa/page.tsx new file mode 100644 index 0000000..37e85be --- /dev/null +++ b/app/mahasiswa/beasiswa/page.tsx @@ -0,0 +1,94 @@ +'use client'; + +import { useState } from "react"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import FilterJenisBeasiswa from "@/components/FilterJenisBeasiswa"; +import TotalBeasiswaChart from "@/components/TotalBeasiswaChart"; +import TotalBeasiswaPieChart from "@/components/TotalBeasiswaPieChart"; +import NamaBeasiswaChart from "@/components/NamaBeasiswaChart"; +import NamaBeasiswaPieChart from "@/components/NamaBeasiswaPieChart"; +import JenisPendaftaranBeasiswaChart from "@/components/JenisPendaftaranBeasiswaChart"; +import JenisPendaftaranBeasiswaPieChart from "@/components/JenisPendaftaranBeasiswaPieChart"; +import AsalDaerahBeasiswaChart from "@/components/AsalDaerahBeasiswaChart"; +import IPKBeasiswaChart from "@/components/IPKBeasiswaChart"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +export default function BeasiswaMahasiswaPage() { + const [selectedYear, setSelectedYear] = useState("all"); + const [selectedJenisBeasiswa, setSelectedJenisBeasiswa] = useState("Pemerintah"); + + return ( +
+

Mahasiswa Beasiswa

+ +
+

+ Mahasiswa yang mendapatkan beasiswa di program studi Informatika Fakultas Teknik Universitas Tanjungpura. +

+
+ + + + + Filter Data + + + +
+ + +
+
+
+ + {selectedYear === "all" ? ( + <> + + + + + ) : ( + <> + + + + + )} + + + + {selectedYear === "all" && ( + + )} +
+ ); + } \ No newline at end of file diff --git a/app/mahasiswa/berprestasi/page.tsx b/app/mahasiswa/berprestasi/page.tsx new file mode 100644 index 0000000..8707374 --- /dev/null +++ b/app/mahasiswa/berprestasi/page.tsx @@ -0,0 +1,80 @@ +'use client'; + +import { useState } from "react"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import FilterJenisPrestasi from "@/components/FilterJenisPrestasi"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import TotalPrestasiChart from "@/components/TotalPrestasiChart"; +import TotalPrestasiPieChart from "@/components/TotalPrestasiPieChart"; +import TingkatPrestasiChart from "@/components/TingkatPrestasiChart"; +import TingkatPrestasiPieChart from "@/components/TingkatPrestasiPieChart"; +import JenisPendaftaranPrestasiChart from "@/components/JenisPendaftaranPrestasiChart"; +import JenisPendaftaranPrestasiPieChart from "@/components/JenisPendaftaranPrestasiPieChart"; +import AsalDaerahPrestasiChart from "@/components/AsalDaerahPrestasiChart"; +import IPKPrestasiChart from "@/components/IPKPrestasiChart"; + +export default function BerprestasiMahasiswaPage() { + const [selectedYear, setSelectedYear] = useState("all"); + const [selectedJenisPrestasi, setSelectedJenisPrestasi] = useState("Akademik"); + + return ( +
+

Mahasiswa Berprestasi

+ +
+

+ Mahasiswa yang mendapatkan prestasi akademik dan non akademik di program studi Informatika Fakultas Teknik Universitas Tanjungpura. +

+
+ + + + + Filter Data + + + +
+ + +
+
+
+ + {selectedYear === "all" ? ( + <> + + + + + ) : ( + <> + + + + + )} + + + + {selectedYear === "all" && ( + + )} +
+ ); + } \ No newline at end of file diff --git a/app/mahasiswa/lulustepatwaktu/page.tsx b/app/mahasiswa/lulustepatwaktu/page.tsx new file mode 100644 index 0000000..0735a93 --- /dev/null +++ b/app/mahasiswa/lulustepatwaktu/page.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { useState } from "react"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import LulusTepatWaktuChart from "@/components/LulusTepatWaktuChart"; +import LulusTepatWaktuPieChart from "@/components/LulusTepatWaktuPieChart"; +import JenisPendaftaranLulusChart from "@/components/JenisPendaftaranLulusChart"; +import JenisPendaftaranLulusPieChart from "@/components/JenisPendaftaranLulusPieChart"; +import AsalDaerahLulusChart from "@/components/AsalDaerahLulusChart"; +import IPKLulusTepatChart from "@/components/IPKLulusTepatChart"; + +export default function LulusTepatWaktuPage() { + const [selectedYear, setSelectedYear] = useState("all"); + + return ( +
+

Mahasiswa Lulus Tepat Waktu

+ +
+

+ Mahasiswa yang lulus tepat waktu sesuai dengan masa studi ≤ 4 tahun program studi Informatika Fakultas Teknik Universitas Tanjungpura. +

+
+ + + + + Filter Data + + + +
+ +
+
+
+ + {selectedYear === "all" ? ( + <> + + + + ) : ( + <> + + + + )} + + + {selectedYear === "all" && ( + + )} + +
+ ); + } \ No newline at end of file diff --git a/app/mahasiswa/profile/page.tsx b/app/mahasiswa/profile/page.tsx new file mode 100644 index 0000000..1eeca37 --- /dev/null +++ b/app/mahasiswa/profile/page.tsx @@ -0,0 +1,171 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { useToast } from '@/components/ui/use-toast'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Skeleton } from '@/components/ui/skeleton'; +import { User } from 'lucide-react'; +import { useTheme } from 'next-themes'; + +interface MahasiswaProfile { + nim: string; + nama: string; + jk: 'Pria' | 'Wanita'; + agama: string; + kabupaten: string; + provinsi: string; + jenis_pendaftaran: string; + status_beasiswa: 'YA' | 'TIDAK'; + tahun_angkatan: string; + ipk: number | null; + prestasi: 'YA' | 'TIDAK'; + status_kuliah: string; +} + +export default function ProfilePage() { + const [profile, setProfile] = useState(null); + const [loading, setLoading] = useState(true); + const { toast } = useToast(); + const router = useRouter(); + const { theme } = useTheme(); + + useEffect(() => { + const fetchProfile = async () => { + try { + console.log('Fetching profile data...'); + const response = await fetch('/api/mahasiswa/profile'); + console.log('Profile response status:', response.status); + + if (response.status === 401) { + toast({ + variant: "destructive", + title: "Akses Ditolak", + description: "Silakan login terlebih dahulu untuk mengakses halaman ini.", + }); + router.push('/'); + return; + } + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Failed to fetch profile data'); + } + + const data = await response.json(); + console.log('Profile data received:', data); + setProfile(data); + } catch (error) { + console.error('Error fetching profile:', error); + toast({ + variant: "destructive", + title: "Error", + description: error instanceof Error ? error.message : "Gagal memuat data profil. Silakan coba lagi nanti.", + }); + } finally { + setLoading(false); + } + }; + + fetchProfile(); + }, [toast, router]); + + if (loading) { + return ( +
+ + + Profil Mahasiswa + + +
+ {[...Array(8)].map((_, i) => ( +
+ + +
+ ))} +
+
+
+
+ ); + } + + if (!profile) { + return ( +
+ + + Profil Mahasiswa + + +
+ Data profil tidak tersedia +
+
+
+
+ ); + } + + // Format IPK value + const formatIPK = (ipk: number | null): string => { + if (ipk === null || ipk === undefined) return '-'; + return Number(ipk).toFixed(2); + }; + + return ( +
+ + +
+
+ +
+
+ {profile.nama} +

{profile.nim}

+
+
+
+ +
+
+
Jenis Kelamin
+
{profile.jk}
+
+
+
Agama
+
{profile.agama}
+
+
+
Kabupaten
+
{profile.kabupaten}
+
+
+
Provinsi
+
{profile.provinsi}
+
+
+
Jenis Pendaftaran
+
{profile.jenis_pendaftaran}
+
+
+
Tahun Angkatan
+
{profile.tahun_angkatan}
+
+
+
IPK
+
{formatIPK(profile.ipk)}
+
+
+
Status Kuliah
+
{profile.status_kuliah || '-'}
+
+
+
+
+
+ ); +} diff --git a/app/mahasiswa/status/page.tsx b/app/mahasiswa/status/page.tsx new file mode 100644 index 0000000..b3f9ccb --- /dev/null +++ b/app/mahasiswa/status/page.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { useState } from "react"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import FilterStatusKuliah from "@/components/FilterStatusKuliah"; +import StatusMahasiswaFilterChart from "@/components/StatusMahasiswaFilterChart"; +import StatusMahasiswaFilterPieChart from "@/components/StatusMahasiswaFilterPieChart"; +import JenisPendaftaranStatusChart from "@/components/JenisPendaftaranStatusChart"; +import JenisPendaftaranStatusPieChart from "@/components/JenisPendaftaranStatusPieChart"; +import AsalDaerahStatusChart from '@/components/AsalDaerahStatusChart'; +import IpkStatusChart from '@/components/IpkStatusChart'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +export default function StatusMahasiswaPage() { + const [selectedYear, setSelectedYear] = useState("all"); + const [selectedStatus, setSelectedStatus] = useState("Aktif"); + + return ( +
+

Status Mahasiswa

+ +
+

+ Mahasiswa status adalah status kuliah mahasiswa program studi Informatika Fakultas Teknik Universitas Tanjungpura. +

+
+ + + + + Filter Data + + + +
+ + +
+
+
+ + {selectedYear === "all" ? ( + <> + + + + ) : ( + <> + + + + )} + + + + {selectedYear === "all" && ( + + )} + +
+ ); + } \ No newline at end of file diff --git a/app/mahasiswa/total/page.tsx b/app/mahasiswa/total/page.tsx new file mode 100644 index 0000000..d64745d --- /dev/null +++ b/app/mahasiswa/total/page.tsx @@ -0,0 +1,60 @@ +'use client'; + +import { useState } from "react"; +import StatistikMahasiswaChart from "@/components/StatistikMahasiswaChart"; +import StatistikPerAngkatanChart from "@/components/StatistikPerAngkatanChart"; +import JenisPendaftaranChart from "@/components/JenisPendaftaranChart"; +import AsalDaerahChart from "@/components/AsalDaerahChart"; +import IPKChart from "@/components/IPKChart"; +import FilterTahunAngkatan from "@/components/FilterTahunAngkatan"; +import JenisPendaftaranPerAngkatanChart from "@/components/JenisPendaftaranPerAngkatanChart"; +import AsalDaerahPerAngkatanChart from "@/components/AsalDaerahPerAngkatanChart"; +import IPKJenisKelaminChart from "@/components/IPKJenisKelaminChart"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +export default function TotalMahasiswaPage() { + const [selectedYear, setSelectedYear] = useState("all"); + + return ( +
+

Total Mahasiswa

+ +
+

+ Mahasiswa total adalah jumlah mahasiswa program studi Informatika Fakultas Teknik Universitas Tanjungpura. +

+
+ + + + + Filter Data + + + +
+ +
+
+
+ + {selectedYear === "all" ? ( + <> + + + + + + ) : ( + <> + + + + + )} +
+ ); +} diff --git a/app/page.tsx b/app/page.tsx index 88f0cc9..14cd478 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,103 +1,218 @@ -import Image from "next/image"; +'use client'; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Users, GraduationCap, Trophy, BookOpen } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useTheme } from "next-themes"; +import StatusMahasiswaChart from "@/components/StatusMahasiswaChart"; +import StatistikMahasiswaChart from "@/components/StatistikMahasiswaChart"; +import JenisPendaftaranChart from "@/components/JenisPendaftaranChart"; +import AsalDaerahChart from "@/components/AsalDaerahChart"; +import IPKChart from '@/components/IPKChart'; + +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; + ipk_rata_rata_aktif: number; + ipk_rata_rata_lulus: number; + total_mahasiswa_aktif_lulus: number; +} + +// Skeleton loading component +const CardSkeleton = () => ( + + +
+
+
+ +
+
+
+
+
+
+
+); + +export default function HomePage() { + const { theme } = useTheme(); + const [mahasiswaData, setMahasiswaData] = useState({ + 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, + ipk_rata_rata_aktif: 0, + ipk_rata_rata_lulus: 0, + total_mahasiswa_aktif_lulus: 0 + }); + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + 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(); + }, []); -export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- -
- - Vercel logomark - Deploy now - - - Read our docs - +
+

Dashboard Portal Data Informatika

+ + {loading ? ( +
+ + + +
-
- + ) : error ? ( +
+
+
+ + + +
+
+

{error}

+
+
+
+ ) : ( + <> +
+ {/* Kartu Total Mahasiswa */} + + + + Total Mahasiswa + + + + +
{mahasiswaData.total_mahasiswa}
+
+ Aktif: {mahasiswaData.mahasiswa_aktif} +
+
+
+ + {/* Kartu Total Kelulusan */} + + + + Total Kelulusan + + + + +
{mahasiswaData.total_lulus}
+
+ Laki-laki: {mahasiswaData.pria_lulus} + Perempuan: {mahasiswaData.wanita_lulus} +
+
+
+ + {/* Kartu Total Prestasi */} + + + + Mahasiswa Berprestasi + + + + +
{mahasiswaData.total_berprestasi}
+
+ Akademik: {mahasiswaData.prestasi_akademik} + Non-Akademik: {mahasiswaData.prestasi_non_akademik} +
+
+
+ + {/* Kartu Rata-rata IPK */} + + + + Rata-rata IPK + + + + +
{mahasiswaData.total_mahasiswa_aktif_lulus}
+
+ Aktif: {mahasiswaData.ipk_rata_rata_aktif} + Lulus: {mahasiswaData.ipk_rata_rata_lulus} +
+
+
+
+ + {/* Diagram Statistik Mahasiswa */} + + + {/* Diagram Status Mahasiswa */} + + + {/* Diagram Jenis Pendaftaran */} + + + {/* Diagram Asal Daerah */} + + + {/* Diagram IPK */} + + + )}
); } diff --git a/components.json b/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/AsalDaerahBeasiswaChart.tsx b/components/AsalDaerahBeasiswaChart.tsx new file mode 100644 index 0000000..39a0657 --- /dev/null +++ b/components/AsalDaerahBeasiswaChart.tsx @@ -0,0 +1,237 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface AsalDaerahBeasiswaData { + tahun_angkatan: number; + kabupaten: string; + jumlah_mahasiswa: number; +} + +interface Props { + selectedYear: string; + selectedJenisBeasiswa: string; +} + +export default function AsalDaerahBeasiswaChart({ selectedYear, selectedJenisBeasiswa }: Props) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/asal-daerah-beasiswa?tahunAngkatan=${selectedYear}&jenisBeasiswa=${selectedJenisBeasiswa}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisBeasiswa]); + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: true, + columnWidth: '85%', + distributed: false, + barHeight: '90%', + dataLabels: { + position: 'top' + } + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '14px', + colors: [theme === 'dark' ? '#fff' : '#000'] + }, + offsetX: 10, + }, + stroke: { + show: true, + width: 1, + colors: [theme === 'dark' ? '#374151' : '#E5E7EB'], + }, + xaxis: { + categories: [...new Set(data.map(item => item.kabupaten))].sort(), + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Kabupaten', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '14px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + maxWidth: 200, + }, + tickAmount: undefined, + }, + grid: { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 10 + } + }, + fill: { + opacity: 1, + }, + colors: ['#3B82F6'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }; + + const series = [{ + name: 'Jumlah Mahasiswa', + data: [...new Set(data.map(item => item.kabupaten))].sort().map(kabupaten => { + const item = data.find(d => d.kabupaten === kabupaten); + return item ? item.jumlah_mahasiswa : 0; + }) + }]; + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Asal Daerah Mahasiswa Beasiswa {selectedJenisBeasiswa} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/AsalDaerahChart.tsx b/components/AsalDaerahChart.tsx new file mode 100644 index 0000000..417cb88 --- /dev/null +++ b/components/AsalDaerahChart.tsx @@ -0,0 +1,286 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface AsalDaerahData { + kabupaten: string; + jumlah: number; +} + +export default function AsalDaerahChart() { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([{ + name: 'Jumlah Mahasiswa', + data: [] + }]); + const [options, setOptions] = useState({ + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + }, + }, + plotOptions: { + bar: { + horizontal: true, + columnWidth: '85%', + distributed: false, + barHeight: '90%', + dataLabels: { + position: 'top', + }, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '14px', + colors: [theme === 'dark' ? '#fff' : '#000'] + }, + offsetX: 10, + }, + stroke: { + show: true, + width: 1, + colors: [theme === 'dark' ? '#374151' : '#E5E7EB'], + }, + xaxis: { + categories: [], + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Kabupaten', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '14px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + maxWidth: 200, + }, + tickAmount: undefined, + }, + grid: { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + }, + fill: { + opacity: 1, + }, + colors: ['#3B82F6'], // Blue color for bars + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style), + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style), + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch('/api/mahasiswa/asal-daerah'); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + + // Process data for chart + const kabupaten = result.map(item => item.kabupaten); + const jumlah = result.map(item => item.jumlah); + + setSeries([{ + name: 'Jumlah Mahasiswa', + data: jumlah + }]); + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: kabupaten, + }, + })); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Asal Daerah Mahasiswa + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/AsalDaerahLulusChart.tsx b/components/AsalDaerahLulusChart.tsx new file mode 100644 index 0000000..3dcdfd3 --- /dev/null +++ b/components/AsalDaerahLulusChart.tsx @@ -0,0 +1,279 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface AsalDaerahLulusChartProps { + selectedYear: string; +} + +interface ChartData { + tahun_angkatan: string; + kabupaten: string; + jumlah_lulus_tepat_waktu: number; +} + +export default function AsalDaerahLulusChart({ selectedYear }: AsalDaerahLulusChartProps) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [chartData, setChartData] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + const [series, setSeries] = useState([{ + name: 'Jumlah Lulus Tepat Waktu', + data: [] + }]); + + const [options, setOptions] = useState({ + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: true, + columnWidth: '55%', + dataLabels: { + position: 'top' + } + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '14px', + colors: [theme === 'dark' ? '#fff' : '#000'] + }, + offsetX: 10, + }, + stroke: { + show: true, + width: 1, + colors: [theme === 'dark' ? '#374151' : '#E5E7EB'], + }, + xaxis: { + categories: [], + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Kabupaten', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '14px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + maxWidth: 200, + }, + tickAmount: undefined, + }, + grid: { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + }, + fill: { + opacity: 1, + }, + colors: ['#3B82F6'], // Blue color for bars + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style), + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style), + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setIsLoading(true); + setError(null); + const response = await fetch(`/api/mahasiswa/asal-daerah-lulus?tahunAngkatan=${selectedYear}`); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const data = await response.json(); + setChartData(data); + + // Process data for chart + const kabupaten = data.map((item: ChartData) => item.kabupaten); + const jumlah = data.map((item: ChartData) => item.jumlah_lulus_tepat_waktu); + + setSeries([{ + name: 'Jumlah Lulus Tepat Waktu', + data: jumlah + }]); + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: kabupaten, + }, + })); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + console.error('Error fetching data:', err); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + if (!mounted) { + return null; + } + + if (isLoading) { + return ( + + + Loading... + + + ); + } + + if (error) { + return ( + + + Error: {error} + + + ); + } + + if (chartData.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Asal Daerah Mahasiswa Lulus Tepat Waktu + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/AsalDaerahPerAngkatanChart.tsx b/components/AsalDaerahPerAngkatanChart.tsx new file mode 100644 index 0000000..9896a8a --- /dev/null +++ b/components/AsalDaerahPerAngkatanChart.tsx @@ -0,0 +1,323 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface AsalDaerahData { + kabupaten: string; + jumlah: number; +} + +interface Props { + tahunAngkatan: string; +} + +export default function AsalDaerahPerAngkatanChart({ tahunAngkatan }: Props) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([{ + name: 'Jumlah Mahasiswa', + data: [] + }]); + const [options, setOptions] = useState({ + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true, + customIcons: [] + }, + export: { + csv: { + filename: `asal-daerah-angkatan`, + columnDelimiter: ',', + headerCategory: 'Asal Daerah', + headerValue: 'Jumlah Mahasiswa' + } + }, + }, + }, + plotOptions: { + bar: { + horizontal: true, + columnWidth: '85%', + distributed: false, + barHeight: '90%', + dataLabels: { + position: 'top' + } + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '14px', + colors: [theme === 'dark' ? '#fff' : '#000'] + }, + offsetX: 10, + }, + stroke: { + show: true, + width: 1, + colors: [theme === 'dark' ? '#374151' : '#E5E7EB'], + }, + xaxis: { + categories: [], + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Kabupaten', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '14px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + maxWidth: 200, + }, + tickAmount: undefined, + }, + grid: { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 0 + } + }, + fill: { + opacity: 1, + }, + colors: ['#3B82F6'], // Blue color for bars + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style), + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style), + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(`/api/mahasiswa/asal-daerah-angkatan?tahun_angkatan=${tahunAngkatan}`); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + + // Process data for chart + const kabupaten = result.map(item => item.kabupaten); + const jumlah = result.map(item => item.jumlah); + + setSeries([{ + name: 'Jumlah Mahasiswa', + data: jumlah + }]); + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: kabupaten, + }, + chart: { + ...prev.chart, + toolbar: { + ...prev.chart?.toolbar, + export: { + ...prev.chart?.toolbar?.export, + csv: { + ...prev.chart?.toolbar?.export?.csv, + filename: `asal-daerah-angkatan-${tahunAngkatan}`, + } + } + } + } + })); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + if (tahunAngkatan) { + fetchData(); + } + }, [tahunAngkatan]); + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Asal Daerah Mahasiswa Angkatan {tahunAngkatan} + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/AsalDaerahPrestasiChart.tsx b/components/AsalDaerahPrestasiChart.tsx new file mode 100644 index 0000000..0558acb --- /dev/null +++ b/components/AsalDaerahPrestasiChart.tsx @@ -0,0 +1,237 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface AsalDaerahPrestasiData { + tahun_angkatan: number; + kabupaten: string; + asal_daerah_mahasiswa_prestasi: number; +} + +interface Props { + selectedYear: string; + selectedJenisPrestasi: string; +} + +export default function AsalDaerahPrestasiChart({ selectedYear, selectedJenisPrestasi }: Props) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/asal-daerah-prestasi?tahunAngkatan=${selectedYear}&jenisPrestasi=${selectedJenisPrestasi}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisPrestasi]); + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: true, + columnWidth: '85%', + distributed: false, + barHeight: '90%', + dataLabels: { + position: 'top' + } + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '14px', + colors: [theme === 'dark' ? '#fff' : '#000'] + }, + offsetX: 10, + }, + stroke: { + show: true, + width: 1, + colors: [theme === 'dark' ? '#374151' : '#E5E7EB'], + }, + xaxis: { + categories: [...new Set(data.map(item => item.kabupaten))].sort(), + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Kabupaten', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '14px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + maxWidth: 200, + }, + tickAmount: undefined, + }, + grid: { + padding: { + top: 0, + right: 0, + bottom: 0, + left: 10 + } + }, + fill: { + opacity: 1, + }, + colors: ['#3B82F6'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }; + + const series = [{ + name: 'Jumlah Mahasiswa', + data: [...new Set(data.map(item => item.kabupaten))].sort().map(kabupaten => { + const item = data.find(d => d.kabupaten === kabupaten); + return item ? item.asal_daerah_mahasiswa_prestasi : 0; + }) + }]; + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Asal Daerah Mahasiswa Prestasi {selectedJenisPrestasi} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/AsalDaerahStatusChart.tsx b/components/AsalDaerahStatusChart.tsx new file mode 100644 index 0000000..db65aee --- /dev/null +++ b/components/AsalDaerahStatusChart.tsx @@ -0,0 +1,242 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface AsalDaerahStatusData { + kabupaten: string; + tahun_angkatan?: number; + status_kuliah: string; + total_mahasiswa: number; +} + +interface Props { + selectedYear: string; + selectedStatus: string; +} + +export default function AsalDaerahStatusChart({ selectedYear, selectedStatus }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + console.log('Fetching data with params:', { selectedYear, selectedStatus }); + + const response = await fetch( + `/api/mahasiswa/asal-daerah-status?tahun_angkatan=${selectedYear}&status_kuliah=${selectedStatus}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const result = await response.json(); + console.log('Received data:', result); + + // Sort data by kabupaten + const sortedData = result.sort((a: AsalDaerahStatusData, b: AsalDaerahStatusData) => + a.kabupaten.localeCompare(b.kabupaten) + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedStatus]); + + // Log data changes + useEffect(() => { + console.log('Current data state:', data); + }, [data]); + + // Get unique kabupaten + const kabupaten = [...new Set(data.map(item => item.kabupaten))].sort(); + console.log('Kabupaten:', kabupaten); + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: true, + columnWidth: '55%', + dataLabels: { + position: 'top' + } + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + }, + offsetX: 10, + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'], + }, + xaxis: { + categories: kabupaten, + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Kabupaten', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + fill: { + opacity: 1, + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + colors: ['#008FFB'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const seriesData = kabupaten.map(kab => { + const item = data.find(d => d.kabupaten === kab); + return item ? item.total_mahasiswa : 0; + }); + + return [{ + name: 'Jumlah Mahasiswa', + data: seriesData + }]; + }; + + const series = processSeriesData(); + console.log('Processed series data:', series); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Asal Daerah Mahasiswa {selectedStatus} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/FilterJenisBeasiswa.tsx b/components/FilterJenisBeasiswa.tsx new file mode 100644 index 0000000..00d3601 --- /dev/null +++ b/components/FilterJenisBeasiswa.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +interface Props { + selectedJenisBeasiswa: string; + onJenisBeasiswaChange: (jenisBeasiswa: string) => void; +} + +export default function FilterJenisBeasiswa({ selectedJenisBeasiswa, onJenisBeasiswaChange }: Props) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/components/FilterJenisPrestasi.tsx b/components/FilterJenisPrestasi.tsx new file mode 100644 index 0000000..2fcf8ee --- /dev/null +++ b/components/FilterJenisPrestasi.tsx @@ -0,0 +1,27 @@ +'use client'; + +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +interface Props { + selectedJenisPrestasi: string; + onJenisPrestasiChange: (jenisPrestasi: string) => void; +} + +export default function FilterJenisPrestasi({ selectedJenisPrestasi, onJenisPrestasiChange }: Props) { + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/components/FilterStatusKuliah.tsx b/components/FilterStatusKuliah.tsx new file mode 100644 index 0000000..aac9641 --- /dev/null +++ b/components/FilterStatusKuliah.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +interface Props { + selectedStatus: string; + onStatusChange: (status: string) => void; +} + +export default function FilterStatusKuliah({ selectedStatus, onStatusChange }: Props) { + const statusOptions = [ + { value: 'Aktif', label: 'Aktif' }, + { value: 'Lulus', label: 'Lulus' }, + { value: 'Cuti', label: 'Cuti' }, + { value: 'DO', label: 'DO' } + ]; + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/components/FilterTahunAngkatan.tsx b/components/FilterTahunAngkatan.tsx new file mode 100644 index 0000000..22dd4c8 --- /dev/null +++ b/components/FilterTahunAngkatan.tsx @@ -0,0 +1,58 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; + +interface TahunAngkatan { + tahun_angkatan: number; +} + +interface Props { + selectedYear: string; + onYearChange: (year: string) => void; + showAllOption?: boolean; +} + +export default function FilterTahunAngkatan({ selectedYear, onYearChange, showAllOption = true }: Props) { + const [tahunAngkatan, setTahunAngkatan] = useState([]); + + useEffect(() => { + const fetchTahunAngkatan = async () => { + try { + const response = await fetch('/api/mahasiswa/tahun-angkatan'); + if (!response.ok) { + throw new Error('Failed to fetch tahun angkatan'); + } + const data = await response.json(); + setTahunAngkatan(data); + } catch (error) { + console.error('Error fetching tahun angkatan:', error); + } + }; + + fetchTahunAngkatan(); + }, []); + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/components/IPKBeasiswaChart.tsx b/components/IPKBeasiswaChart.tsx new file mode 100644 index 0000000..88ae48e --- /dev/null +++ b/components/IPKBeasiswaChart.tsx @@ -0,0 +1,344 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface IPKBeasiswaData { + tahun_angkatan: number; + total_mahasiswa_beasiswa: number; + rata_rata_ipk: number; +} + +interface Props { + selectedJenisBeasiswa: string; +} + +export default function IPKBeasiswaChart({ selectedJenisBeasiswa }: Props) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([{ + name: 'Rata-rata IPK', + data: [] + }]); + const [options, setOptions] = useState({ + chart: { + type: 'line', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + zoom: { + enabled: true, + type: 'x', + autoScaleYaxis: true + } + }, + stroke: { + curve: 'smooth', + width: 3, + lineCap: 'round' + }, + markers: { + size: 5, + strokeWidth: 2, + strokeColors: theme === 'dark' ? ['#fff'] : ['#3B82F6'], + colors: ['#3B82F6'], + hover: { + size: 7 + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toFixed(2); + }, + style: { + fontSize: '14px', + fontWeight: 'bold' + }, + background: { + enabled: false + }, + offsetY: -10 + }, + xaxis: { + categories: [], + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Rata-rata IPK', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + formatter: function (val: number) { + return val.toFixed(2); + } + }, + min: 0, + max: 4, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + }, + colors: ['#3B82F6'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val.toFixed(2); + } + }, + marker: { + show: true + } + }, + legend: { + show: true, + position: 'top', + horizontalAlign: 'right', + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + }, + background: { + ...prev.dataLabels?.background, + foreColor: currentTheme === 'dark' ? '#fff' : '#000', + borderColor: currentTheme === 'dark' ? '#374151' : '#3B82F6' + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + ...prev.xaxis?.axisBorder, + color: currentTheme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + ...prev.xaxis?.axisTicks, + color: currentTheme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style), + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style), + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(`/api/mahasiswa/ipk-beasiswa?jenisBeasiswa=${selectedJenisBeasiswa}`); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + + // Process data for chart + const tahunAngkatan = result.map(item => item.tahun_angkatan); + const rataRataIPK = result.map(item => item.rata_rata_ipk); + + setSeries([{ + name: 'Rata-rata IPK', + data: rataRataIPK + }]); + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: tahunAngkatan, + }, + })); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedJenisBeasiswa]); + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Rata-rata IPK Mahasiswa Beasiswa {selectedJenisBeasiswa} + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/IPKChart.tsx b/components/IPKChart.tsx new file mode 100644 index 0000000..9376e6a --- /dev/null +++ b/components/IPKChart.tsx @@ -0,0 +1,339 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface IPKData { + tahun_angkatan: number; + rata_rata_ipk: number; +} + +export default function IPKChart() { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([{ + name: 'Rata-rata IPK', + data: [] + }]); + const [options, setOptions] = useState({ + chart: { + type: 'line', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + zoom: { + enabled: true, + type: 'x', + autoScaleYaxis: true + } + }, + stroke: { + curve: 'smooth', + width: 3, + lineCap: 'round' + }, + markers: { + size: 5, + strokeWidth: 2, + strokeColors: theme === 'dark' ? ['#fff'] : ['#3B82F6'], + colors: ['#3B82F6'], + hover: { + size: 7 + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toFixed(2); + }, + style: { + fontSize: '14px', + fontWeight: 'bold' + }, + background: { + enabled: false + }, + offsetY: -10 + }, + xaxis: { + categories: [], + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Rata-rata IPK', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + formatter: function (val: number) { + return val.toFixed(2); + } + }, + min: 0, + max: 4, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + }, + colors: ['#3B82F6'], // Blue color for line + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val.toFixed(2); + } + }, + marker: { + show: true + } + }, + legend: { + show: true, + position: 'top', + horizontalAlign: 'right', + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + }, + background: { + ...prev.dataLabels?.background, + foreColor: currentTheme === 'dark' ? '#fff' : '#000', + borderColor: currentTheme === 'dark' ? '#374151' : '#3B82F6' + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + ...prev.xaxis?.axisBorder, + color: currentTheme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + ...prev.xaxis?.axisTicks, + color: currentTheme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style), + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style), + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch('/api/mahasiswa/ipk'); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + + // Process data for chart + const tahunAngkatan = result.map(item => item.tahun_angkatan); + const rataRataIPK = result.map(item => item.rata_rata_ipk); + + setSeries([{ + name: 'Rata-rata IPK', + data: rataRataIPK + }]); + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: tahunAngkatan, + }, + })); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Rata-rata IPK Mahasiswa + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/IPKJenisKelaminChart.tsx b/components/IPKJenisKelaminChart.tsx new file mode 100644 index 0000000..67b9d16 --- /dev/null +++ b/components/IPKJenisKelaminChart.tsx @@ -0,0 +1,295 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface Props { + tahunAngkatan: string; +} + +export default function IPKJenisKelaminChart({ tahunAngkatan }: Props) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [series, setSeries] = useState([]); + const [categories, setCategories] = useState([]); + + const [options, setOptions] = useState({ + chart: { + type: 'bar', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true, + customIcons: [] + }, + export: { + csv: { + filename: `ipk-jenis-kelamin-angkatan`, + columnDelimiter: ',', + headerCategory: 'Jenis Kelamin', + headerValue: 'Rata-rata IPK' + } + }, + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + colors: ['#3B82F6', '#EC4899'], + plotOptions: { + bar: { + horizontal: false, + columnWidth: '30%', + borderRadius: 2, + distributed: true + }, + }, + states: { + hover: { + filter: { + type: 'none' + } + }, + active: { + filter: { + type: 'none' + } + } + }, + fill: { + opacity: 1 + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toFixed(2); + }, + offsetY: -20, + style: { + fontSize: '14px', + fontWeight: 'bold', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + xaxis: { + categories: [], + title: { + text: 'Jenis Kelamin', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + min: 0, + max: 4.0, + tickAmount: 4, + title: { + text: 'Rata-rata IPK', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val.toFixed(2) + " IPK"; + } + }, + }, + legend: { + show: false + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(prev.yaxis as any)?.title, + style: { + ...(prev.yaxis as any)?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(prev.yaxis as any)?.labels, + style: { + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + if (!tahunAngkatan || tahunAngkatan === 'all') { + console.log('Tahun angkatan tidak tersedia atau "all"'); + return; + } + + console.log('Fetching data for tahun angkatan:', tahunAngkatan); + const url = `/api/mahasiswa/ipk-jenis-kelamin?tahun_angkatan=${tahunAngkatan}`; + console.log('API URL:', url); + + const response = await fetch(url); + + if (!response.ok) { + console.error('Error response:', response.status, response.statusText); + return; + } + + const data = await response.json(); + console.log('Data received from API:', data); + + if (!data || data.length === 0) { + console.log('No data received from API'); + return; + } + + // Process data for chart + const labels = data.map((item: any) => { + console.log('Processing item:', item); + return item.jk === 'Pria' ? 'Laki-laki' : 'Perempuan'; + }); + + const values = data.map((item: any) => { + const ipk = parseFloat(item.rata_rata_ipk); + console.log(`IPK for ${item.jk}:`, ipk); + return ipk; + }); + + console.log('Processed labels:', labels); + console.log('Processed values:', values); + + if (values.length === 0) { + console.log('No values to display'); + return; + } + + setCategories(labels); + // Untuk bar chart, kita memerlukan array dari objects + setSeries([{ + name: 'Rata-rata IPK', + data: values + }]); + + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: labels + }, + chart: { + ...prev.chart, + toolbar: { + ...prev.chart?.toolbar, + export: { + ...prev.chart?.toolbar?.export, + csv: { + ...prev.chart?.toolbar?.export?.csv, + filename: `ipk-jenis-kelamin-angkatan-${tahunAngkatan}` + } + } + } + } + })); + }; + + if (mounted) { + fetchData(); + } + }, [tahunAngkatan, mounted]); + + if (!mounted) { + return null; + } + + return ( + + + + Rata-rata IPK Berdasarkan Jenis Kelamin Angkatan {tahunAngkatan} + + + +
+ {series.length > 0 && series[0].data?.length > 0 ? ( + + ) : ( +
+

Tidak ada data

+
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/IPKLulusTepatChart.tsx b/components/IPKLulusTepatChart.tsx new file mode 100644 index 0000000..3853b58 --- /dev/null +++ b/components/IPKLulusTepatChart.tsx @@ -0,0 +1,337 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface IPKLulusTepatData { + tahun_angkatan: number; + rata_rata_ipk: number; +} + +interface IPKLulusTepatChartProps { + selectedYear: string; +} + +export default function IPKLulusTepatChart({ selectedYear }: IPKLulusTepatChartProps) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([{ + name: 'Rata-rata IPK', + data: [] + }]); + const [options, setOptions] = useState({ + chart: { + type: 'line', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + zoom: { + enabled: true, + type: 'x', + autoScaleYaxis: true + } + }, + stroke: { + curve: 'smooth', + width: 3, + lineCap: 'round' + }, + markers: { + size: 5, + strokeWidth: 2, + strokeColors: theme === 'dark' ? ['#fff'] : ['#3B82F6'], + colors: ['#3B82F6'], + hover: { + size: 7 + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toFixed(2); + }, + style: { + fontSize: '14px', + fontWeight: 'bold' + }, + background: { + enabled: false + }, + offsetY: -10 + }, + xaxis: { + categories: [], + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Rata-rata IPK', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + formatter: function (val: number) { + return val.toFixed(2); + } + }, + min: 0, + max: 4, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + }, + colors: ['#3B82F6'], // Blue color for line + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val.toFixed(2); + } + }, + marker: { + show: true + } + }, + legend: { + show: true, + position: 'top', + horizontalAlign: 'right', + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + }, + background: { + ...prev.dataLabels?.background, + foreColor: currentTheme === 'dark' ? '#fff' : '#000', + borderColor: currentTheme === 'dark' ? '#374151' : '#3B82F6' + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + ...prev.xaxis?.axisBorder, + color: currentTheme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + ...prev.xaxis?.axisTicks, + color: currentTheme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style), + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style), + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + const response = await fetch(`/api/mahasiswa/ipk-lulus-tepat?tahunAngkatan=${selectedYear}`); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const fetchedData: IPKLulusTepatData[] = await response.json(); + setData(fetchedData); + + // Process data for chart + const tahunAngkatan = fetchedData.map(item => item.tahun_angkatan); + const rataRataIPK = fetchedData.map(item => item.rata_rata_ipk); + + setSeries([{ + name: 'Rata-rata IPK', + data: rataRataIPK + }]); + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: tahunAngkatan, + }, + })); + } catch (error) { + setError(error instanceof Error ? error.message : 'An error occurred'); + console.error('Error fetching data:', error); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Rata-rata IPK Mahasiswa Lulus Tepat Waktu + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/IPKPrestasiChart.tsx b/components/IPKPrestasiChart.tsx new file mode 100644 index 0000000..c24ef59 --- /dev/null +++ b/components/IPKPrestasiChart.tsx @@ -0,0 +1,347 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface IPKPrestasiData { + tahun_angkatan: number; + total_mahasiswa_prestasi: number; + rata_rata_ipk: number; +} + +interface Props { + selectedJenisPrestasi: string; +} + +export default function IPKPrestasiChart({ selectedJenisPrestasi }: Props) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([{ + name: 'Rata-rata IPK', + data: [] + }]); + const [options, setOptions] = useState({ + chart: { + type: 'line', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + zoom: { + enabled: true, + type: 'x', + autoScaleYaxis: true + } + }, + stroke: { + curve: 'smooth', + width: 3, + lineCap: 'round' + }, + markers: { + size: 5, + strokeWidth: 2, + strokeColors: theme === 'dark' ? ['#fff'] : ['#3B82F6'], + colors: ['#3B82F6'], + hover: { + size: 7 + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toFixed(2); + }, + style: { + fontSize: '14px', + fontWeight: 'bold' + }, + background: { + enabled: false + }, + offsetY: -10 + }, + xaxis: { + categories: [], + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Rata-rata IPK', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + formatter: function (val: number) { + return val.toFixed(2); + } + }, + min: 0, + max: 4, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + }, + colors: ['#3B82F6'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val.toFixed(2); + } + }, + marker: { + show: true + } + }, + legend: { + show: true, + position: 'top', + horizontalAlign: 'right', + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + }, + background: { + ...prev.dataLabels?.background, + foreColor: currentTheme === 'dark' ? '#fff' : '#000', + borderColor: currentTheme === 'dark' ? '#374151' : '#3B82F6' + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + ...prev.xaxis?.axisBorder, + color: currentTheme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + ...prev.xaxis?.axisTicks, + color: currentTheme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style), + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style), + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(`/api/mahasiswa/ipk-prestasi?jenisPrestasi=${selectedJenisPrestasi}`); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + + // Sort data by tahun_angkatan in ascending order + const sortedData = [...result].sort((a, b) => a.tahun_angkatan - b.tahun_angkatan); + + // Process data for chart + const tahunAngkatan = sortedData.map(item => item.tahun_angkatan); + const rataRataIPK = sortedData.map(item => item.rata_rata_ipk); + + setSeries([{ + name: 'Rata-rata IPK', + data: rataRataIPK + }]); + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: tahunAngkatan, + }, + })); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedJenisPrestasi]); + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Rata-rata IPK Mahasiswa Prestasi {selectedJenisPrestasi} + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/IpkStatusChart.tsx b/components/IpkStatusChart.tsx new file mode 100644 index 0000000..3bc3e70 --- /dev/null +++ b/components/IpkStatusChart.tsx @@ -0,0 +1,295 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface IpkStatusData { + tahun_angkatan: number; + status_kuliah: string; + total_mahasiswa: number; + rata_rata_ipk: number; +} + +interface Props { + selectedYear: string; + selectedStatus: string; +} + +export default function IpkStatusChart({ selectedYear, selectedStatus }: Props) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/ipk-status?tahun_angkatan=${selectedYear}&status_kuliah=${selectedStatus}`; + + const response = await fetch(url); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error response:', errorText); + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + console.log('Received data:', result); + + if (!Array.isArray(result)) { + console.error('Invalid data format:', result); + throw new Error('Invalid data format received from server'); + } + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: IpkStatusData, b: IpkStatusData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + console.log('Sorted data:', sortedData); + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedStatus]); + + // Log data changes + useEffect(() => { + console.log('Current data state:', data); + }, [data]); + + const chartOptions: ApexOptions = { + chart: { + type: selectedYear === 'all' ? 'line' : 'bar', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + zoom: { + enabled: true, + type: 'x', + autoScaleYaxis: true + } + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 4, + }, + }, + stroke: { + curve: 'smooth', + width: 3, + lineCap: 'round' + }, + markers: { + size: 5, + strokeWidth: 2, + strokeColors: theme === 'dark' ? ['#fff'] : ['#3B82F6'], + colors: ['#3B82F6'], + hover: { + size: 7 + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toFixed(2); + }, + style: { + fontSize: '14px', + fontWeight: 'bold', + colors: [theme === 'dark' ? '#fff' : '#000'] + }, + background: { + enabled: false + }, + offsetY: -10 + }, + xaxis: { + categories: data.map(item => item.tahun_angkatan.toString()), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Rata-rata IPK', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + min: 0, + max: 4, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + }, + formatter: function (val: number) { + return val.toFixed(2); + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + }, + colors: ['#3B82F6'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val.toFixed(2); + } + }, + marker: { + show: true + } + }, + legend: { + show: true, + position: 'top', + horizontalAlign: 'right', + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }; + + // Process data for series + const processSeriesData = () => { + return [{ + name: 'Rata-rata IPK', + data: data.map(item => item.rata_rata_ipk) + }]; + }; + + const series = processSeriesData(); + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Rata-rata IPK Mahasiswa {selectedStatus} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranBeasiswaChart.tsx b/components/JenisPendaftaranBeasiswaChart.tsx new file mode 100644 index 0000000..7cd400f --- /dev/null +++ b/components/JenisPendaftaranBeasiswaChart.tsx @@ -0,0 +1,265 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranBeasiswaData { + tahun_angkatan: number; + jenis_pendaftaran: string; + jumlah_mahasiswa_beasiswa: number; +} + +interface Props { + selectedYear: string; + selectedJenisBeasiswa: string; +} + +export default function JenisPendaftaranBeasiswaChart({ selectedYear, selectedJenisBeasiswa }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/jenis-pendaftaran-beasiswa?tahunAngkatan=${selectedYear}&jenisBeasiswa=${selectedJenisBeasiswa}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: JenisPendaftaranBeasiswaData, b: JenisPendaftaranBeasiswaData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisBeasiswa]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) return []; + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort(); + const pendaftaranTypes = [...new Set(data.map(item => item.jenis_pendaftaran))].sort(); + + return pendaftaranTypes.map(pendaftaran => ({ + name: pendaftaran, + data: years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.jenis_pendaftaran === pendaftaran); + return item ? item.jumlah_mahasiswa_beasiswa : 0; + }) + })); + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#EC4899', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Jenis Pendaftaran Mahasiswa Beasiswa {selectedJenisBeasiswa} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranBeasiswaPieChart.tsx b/components/JenisPendaftaranBeasiswaPieChart.tsx new file mode 100644 index 0000000..af33373 --- /dev/null +++ b/components/JenisPendaftaranBeasiswaPieChart.tsx @@ -0,0 +1,192 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranBeasiswaData { + tahun_angkatan: number; + jenis_pendaftaran: string; + jumlah_mahasiswa_beasiswa: number; +} + +interface Props { + selectedYear: string; + selectedJenisBeasiswa: string; +} + +export default function JenisPendaftaranBeasiswaPieChart({ selectedYear, selectedJenisBeasiswa }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/jenis-pendaftaran-beasiswa?tahunAngkatan=${selectedYear}&jenisBeasiswa=${selectedJenisBeasiswa}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisBeasiswa]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + background: theme === 'dark' ? '#0F172B' : '#fff', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + } + }, + labels: [], + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const jenisPendaftaran = [...new Set(data.map(item => item.jenis_pendaftaran))].sort(); + const jumlahData = jenisPendaftaran.map(jenis => { + const item = data.find(d => d.jenis_pendaftaran === jenis); + return item ? item.jumlah_mahasiswa_beasiswa : 0; + }); + + return { + series: jumlahData, + labels: jenisPendaftaran + }; + }; + + const { series, labels } = processSeriesData(); + const totalMahasiswa = data.reduce((sum, item) => sum + item.jumlah_mahasiswa_beasiswa, 0); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Jenis Pendaftaran Mahasiswa Beasiswa {selectedJenisBeasiswa} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranChart.tsx b/components/JenisPendaftaranChart.tsx new file mode 100644 index 0000000..3ed30a9 --- /dev/null +++ b/components/JenisPendaftaranChart.tsx @@ -0,0 +1,293 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranData { + tahun_angkatan: number; + jenis_pendaftaran: string; + jumlah: number; +} + +export default function JenisPendaftaranChart() { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([]); + const [options, setOptions] = useState({ + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: true, + columnWidth: '55%', + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'], + }, + xaxis: { + categories: [], + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + fill: { + opacity: 1, + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [currentTheme === 'dark' ? '#fff' : '#000'] + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + ...prev.yaxis, + title: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title : prev.yaxis?.title), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.title?.style : prev.yaxis?.title?.style), + color: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels : prev.yaxis?.labels), + style: { + ...(Array.isArray(prev.yaxis) ? prev.yaxis[0]?.labels?.style : prev.yaxis?.labels?.style), + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + } + }, + legend: { + ...prev.legend, + labels: { + ...prev.legend?.labels, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch('/api/mahasiswa/jenis-pendaftaran'); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + + const tahunAngkatan = [...new Set(result.map(item => item.tahun_angkatan))] + .sort((a, b) => b - a); + const jenisPendaftaran = [...new Set(result.map(item => item.jenis_pendaftaran))].sort(); + + const seriesData = jenisPendaftaran.map(jenis => ({ + name: jenis, + data: tahunAngkatan.map(tahun => { + const item = result.find(d => d.tahun_angkatan === tahun && d.jenis_pendaftaran === jenis); + return item ? item.jumlah : 0; + }), + })); + + setSeries(seriesData); + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: tahunAngkatan, + }, + })); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Jenis Pendaftaran Mahasiswa + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranLulusChart.tsx b/components/JenisPendaftaranLulusChart.tsx new file mode 100644 index 0000000..f0f97ce --- /dev/null +++ b/components/JenisPendaftaranLulusChart.tsx @@ -0,0 +1,262 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranLulusData { + tahun_angkatan: number; + jenis_pendaftaran: string; + jumlah_lulus_tepat_waktu: number; +} + +interface Props { + selectedYear: string; +} + +export default function JenisPendaftaranLulusChart({ selectedYear }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/jenis-pendaftaran-lulus?tahun_angkatan=${selectedYear}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: JenisPendaftaranLulusData, b: JenisPendaftaranLulusData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + setData(sortedData); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) return []; + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort(); + const pendaftaranTypes = [...new Set(data.map(item => item.jenis_pendaftaran))]; + + return pendaftaranTypes.map(type => ({ + name: type, + data: years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.jenis_pendaftaran === type); + return item ? item.jumlah_lulus_tepat_waktu : 0; + }) + })); + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + fill: { + opacity: 1 + }, + colors: ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Jenis Pendaftaran Mahasiswa Lulus Tepat Waktu + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranLulusPieChart.tsx b/components/JenisPendaftaranLulusPieChart.tsx new file mode 100644 index 0000000..135185b --- /dev/null +++ b/components/JenisPendaftaranLulusPieChart.tsx @@ -0,0 +1,191 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranLulusData { + tahun_angkatan: number; + jenis_pendaftaran: string; + jumlah_lulus_tepat_waktu: number; +} + +interface Props { + selectedYear: string; +} + +export default function JenisPendaftaranLulusPieChart({ selectedYear }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/jenis-pendaftaran-lulus?tahun_angkatan=${selectedYear}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + labels: [], + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000', + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const jenisPendaftaran = [...new Set(data.map(item => item.jenis_pendaftaran))].sort(); + const jumlahData = jenisPendaftaran.map(jenis => { + const item = data.find(d => d.jenis_pendaftaran === jenis); + return item ? item.jumlah_lulus_tepat_waktu : 0; + }); + + return { + series: jumlahData, + labels: jenisPendaftaran + }; + }; + + const { series, labels } = processSeriesData(); + const totalMahasiswa = data.reduce((sum, item) => sum + item.jumlah_lulus_tepat_waktu, 0); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Jenis Pendaftaran Mahasiswa Lulus Tepat Waktu + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranPerAngkatanChart.tsx b/components/JenisPendaftaranPerAngkatanChart.tsx new file mode 100644 index 0000000..bb8a618 --- /dev/null +++ b/components/JenisPendaftaranPerAngkatanChart.tsx @@ -0,0 +1,250 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranData { + tahun_angkatan: number; + jenis_pendaftaran: string; + jumlah: number; +} + +interface Props { + tahunAngkatan: string; +} + +export default function JenisPendaftaranPerAngkatanChart({ tahunAngkatan }: Props) { + const { theme, systemTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([]); + const [options, setOptions] = useState({ + chart: { + type: 'pie', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + } + }, + labels: [], + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number, opts: any) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + legend: { + ...prev.legend, + labels: { + ...prev.legend?.labels, + colors: currentTheme === 'dark' ? '#fff' : '#000' + } + }, + tooltip: { + ...prev.tooltip, + theme: currentTheme === 'dark' ? 'dark' : 'light' + } + })); + }, [theme, systemTheme]); + + // Update dataLabels formatter when data changes + useEffect(() => { + if (data.length > 0) { + setOptions(prev => ({ + ...prev, + dataLabels: { + ...prev.dataLabels, + formatter: function(val: number, opts: any) { + // Calculate the percentage based on the current data + const total = data.reduce((sum, item) => sum + item.jumlah, 0); + const seriesIndex = opts.seriesIndex; + const jenisPendaftaran = prev.labels?.[seriesIndex]; + + if (jenisPendaftaran) { + const item = data.find(d => d.jenis_pendaftaran === jenisPendaftaran); + const jumlah = item ? item.jumlah : 0; + const percentage = (jumlah / total) * 100; + return `${percentage.toFixed(0)}%`; + } + return `${val.toFixed(0)}%`; + } + } + })); + } + }, [data]); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(`/api/mahasiswa/jenis-pendaftaran?tahun_angkatan=${tahunAngkatan}`); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + // Process data for pie chart + const jenisPendaftaran = [...new Set(result.map(item => item.jenis_pendaftaran))].sort(); + const jumlahData = jenisPendaftaran.map(jenis => { + const item = result.find(d => d.jenis_pendaftaran === jenis); + return item ? item.jumlah : 0; + }); + + setSeries(jumlahData); + setOptions(prev => ({ + ...prev, + labels: jenisPendaftaran, + })); + + // Store processed data + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + if (tahunAngkatan) { + fetchData(); + } + }, [tahunAngkatan]); + + if (!mounted) { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + const totalMahasiswa = data.reduce((sum, item) => sum + item.jumlah, 0); + + return ( + + + + Jenis Pendaftaran Angkatan {tahunAngkatan} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranPrestasiChart.tsx b/components/JenisPendaftaranPrestasiChart.tsx new file mode 100644 index 0000000..8c470a3 --- /dev/null +++ b/components/JenisPendaftaranPrestasiChart.tsx @@ -0,0 +1,263 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranPrestasiData { + tahun_angkatan: number; + jenis_pendaftaran: string; + jenis_pendaftaran_mahasiswa_prestasi: number; +} + +interface Props { + selectedJenisPrestasi: string; +} + +export default function JenisPendaftaranPrestasiChart({ selectedJenisPrestasi }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/jenis-pendaftaran-prestasi?jenisPrestasi=${selectedJenisPrestasi}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: JenisPendaftaranPrestasiData, b: JenisPendaftaranPrestasiData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedJenisPrestasi]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) return []; + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort(); + const pendaftaranTypes = [...new Set(data.map(item => item.jenis_pendaftaran))].sort(); + + return pendaftaranTypes.map(pendaftaran => ({ + name: pendaftaran, + data: years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.jenis_pendaftaran === pendaftaran); + return item?.jenis_pendaftaran_mahasiswa_prestasi || 0; + }) + })); + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val?.toString() || '0'; + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Jenis Pendaftaran Mahasiswa Berprestasi {selectedJenisPrestasi} + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranPrestasiPieChart.tsx b/components/JenisPendaftaranPrestasiPieChart.tsx new file mode 100644 index 0000000..934b6f8 --- /dev/null +++ b/components/JenisPendaftaranPrestasiPieChart.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranPrestasiData { + tahun_angkatan: number; + jenis_pendaftaran: string; + jenis_pendaftaran_mahasiswa_prestasi: number; +} + +interface Props { + selectedYear: string; + selectedJenisPrestasi: string; +} + +export default function JenisPendaftaranPrestasiPieChart({ selectedYear, selectedJenisPrestasi }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/jenis-pendaftaran-prestasi?tahunAngkatan=${selectedYear}&jenisPrestasi=${selectedJenisPrestasi}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisPrestasi]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + background: theme === 'dark' ? '#0F172B' : '#fff', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + } + }, + labels: [], + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const filtered = data.filter(item => String(item.tahun_angkatan) === String(selectedYear)); + const jenisPendaftaran = [...new Set(filtered.map(item => item.jenis_pendaftaran))].sort(); + const jumlahData = jenisPendaftaran.map(jenis => { + const item = filtered.find(d => d.jenis_pendaftaran === jenis); + return item ? item.jenis_pendaftaran_mahasiswa_prestasi : 0; + }); + + return { + series: jumlahData, + labels: jenisPendaftaran + }; + }; + + const { series, labels } = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Jenis Pendaftaran Mahasiswa Berprestasi {selectedJenisPrestasi} Angkatan {selectedYear} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranStatusChart.tsx b/components/JenisPendaftaranStatusChart.tsx new file mode 100644 index 0000000..1c975bb --- /dev/null +++ b/components/JenisPendaftaranStatusChart.tsx @@ -0,0 +1,250 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranStatusData { + jenis_pendaftaran: string; + tahun_angkatan: number; + status_kuliah: string; + total_mahasiswa: number; +} + +interface Props { + selectedYear: string; + selectedStatus: string; +} + +export default function JenisPendaftaranStatusChart({ selectedYear, selectedStatus }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + console.log('Fetching data with params:', { selectedYear, selectedStatus }); + + const response = await fetch( + `/api/mahasiswa/jenis-pendaftaran-status?tahun_angkatan=${selectedYear}&status_kuliah=${selectedStatus}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const result = await response.json(); + console.log('Received data:', result); + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: JenisPendaftaranStatusData, b: JenisPendaftaranStatusData) => + Number(a.tahun_angkatan) - Number(b.tahun_angkatan) + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedStatus]); + + // Log data changes + useEffect(() => { + console.log('Current data state:', data); + }, [data]); + + // Get unique years and sort them + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort((a, b) => Number(a) - Number(b)); + console.log('Sorted years:', years); + + // Get unique jenis pendaftaran + const jenisPendaftaran = [...new Set(data.map(item => item.jenis_pendaftaran))]; + console.log('Jenis pendaftaran:', jenisPendaftaran); + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'], + }, + xaxis: { + categories: years, + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + fill: { + opacity: 1, + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + colors: ['#008FFB', '#00E396', '#FEB019', '#FF4560', '#775DD0'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + return jenisPendaftaran.map(jenis => { + const seriesData = new Array(years.length).fill(0); + + data.forEach(item => { + if (item.jenis_pendaftaran === jenis) { + const yearIndex = years.indexOf(item.tahun_angkatan); + if (yearIndex !== -1) { + seriesData[yearIndex] = item.total_mahasiswa; + } + } + }); + + return { + name: jenis, + data: seriesData + }; + }); + }; + + const series = processSeriesData(); + console.log('Processed series data:', series); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Jenis Pendaftaran Mahasiswa {selectedStatus} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/JenisPendaftaranStatusPieChart.tsx b/components/JenisPendaftaranStatusPieChart.tsx new file mode 100644 index 0000000..db4167d --- /dev/null +++ b/components/JenisPendaftaranStatusPieChart.tsx @@ -0,0 +1,193 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface JenisPendaftaranStatusData { + jenis_pendaftaran: string; + tahun_angkatan: number; + status_kuliah: string; + total_mahasiswa: number; +} + +interface Props { + selectedYear: string; + selectedStatus: string; +} + +export default function JenisPendaftaranStatusPieChart({ selectedYear, selectedStatus }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + + const response = await fetch( + `/api/mahasiswa/jenis-pendaftaran-status?tahun_angkatan=${selectedYear}&status_kuliah=${selectedStatus}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedStatus]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + labels: [], + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000', + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const jenisPendaftaran = [...new Set(data.map(item => item.jenis_pendaftaran))].sort(); + const jumlahData = jenisPendaftaran.map(jenis => { + const item = data.find(d => d.jenis_pendaftaran === jenis); + return item ? item.total_mahasiswa : 0; + }); + + return { + series: jumlahData, + labels: jenisPendaftaran + }; + }; + + const { series, labels } = processSeriesData(); + const totalMahasiswa = data.reduce((sum, item) => sum + item.total_mahasiswa, 0); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Jenis Pendaftaran Mahasiswa {selectedStatus} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/LulusTepatWaktuChart.tsx b/components/LulusTepatWaktuChart.tsx new file mode 100644 index 0000000..e56f858 --- /dev/null +++ b/components/LulusTepatWaktuChart.tsx @@ -0,0 +1,280 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface LulusTepatWaktuData { + tahun_angkatan: number; + jk: string; + jumlah_lulus_tepat_waktu: number; +} + +interface Props { + selectedYear: string; +} + +export default function LulusTepatWaktuChart({ selectedYear }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/lulus-tepat-waktu?tahun_angkatan=${selectedYear}`; + + const response = await fetch(url); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error response:', errorText); + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + console.error('Invalid data format:', result); + throw new Error('Invalid data format received from server'); + } + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: LulusTepatWaktuData, b: LulusTepatWaktuData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + console.log('Sorted data:', sortedData); + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) return []; + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort(); + + const pria = years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.jk === 'Pria'); + return item ? item.jumlah_lulus_tepat_waktu : 0; + }); + + const wanita = years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.jk === 'Wanita'); + return item ? item.jumlah_lulus_tepat_waktu : 0; + }); + + return [ + { + name: 'Laki-laki', + data: pria + }, + { + name: 'Perempuan', + data: wanita + } + ]; + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#EC4899'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Mahasiswa Lulus Tepat Waktu + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/LulusTepatWaktuPieChart.tsx b/components/LulusTepatWaktuPieChart.tsx new file mode 100644 index 0000000..13e232e --- /dev/null +++ b/components/LulusTepatWaktuPieChart.tsx @@ -0,0 +1,184 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface LulusTepatWaktuData { + tahun_angkatan: number; + jk: string; + jumlah_lulus_tepat_waktu: number; +} + +interface Props { + selectedYear: string; +} + +export default function LulusTepatWaktuPieChart({ selectedYear }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/lulus-tepat-waktu?tahun_angkatan=${selectedYear}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + labels: ['Laki-laki', 'Perempuan'], + colors: ['#3B82F6', '#EC4899'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000', + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const maleData = data.find(item => item.jk === 'Pria')?.jumlah_lulus_tepat_waktu || 0; + const femaleData = data.find(item => item.jk === 'Wanita')?.jumlah_lulus_tepat_waktu || 0; + return [maleData, femaleData]; + }; + + const series = processSeriesData(); + const totalMahasiswa = data.reduce((sum, item) => sum + item.jumlah_lulus_tepat_waktu, 0); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Mahasiswa Lulus Tepat Waktu + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/NamaBeasiswaChart.tsx b/components/NamaBeasiswaChart.tsx new file mode 100644 index 0000000..bce9b11 --- /dev/null +++ b/components/NamaBeasiswaChart.tsx @@ -0,0 +1,265 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface NamaBeasiswaData { + tahun_angkatan: number; + nama_beasiswa: string; + jumlah_nama_beasiswa: number; +} + +interface Props { + selectedYear: string; + selectedJenisBeasiswa: string; +} + +export default function NamaBeasiswaChart({ selectedYear, selectedJenisBeasiswa }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/nama-beasiswa?tahunAngkatan=${selectedYear}&jenisBeasiswa=${selectedJenisBeasiswa}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: NamaBeasiswaData, b: NamaBeasiswaData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisBeasiswa]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) return []; + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort(); + const beasiswaTypes = [...new Set(data.map(item => item.nama_beasiswa))].sort(); + + return beasiswaTypes.map(beasiswa => ({ + name: beasiswa, + data: years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.nama_beasiswa === beasiswa); + return item ? item.jumlah_nama_beasiswa : 0; + }) + })); + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#EC4899', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#06B6D4'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Nama Beasiswa {selectedJenisBeasiswa} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/NamaBeasiswaPieChart.tsx b/components/NamaBeasiswaPieChart.tsx new file mode 100644 index 0000000..189e94a --- /dev/null +++ b/components/NamaBeasiswaPieChart.tsx @@ -0,0 +1,192 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface NamaBeasiswaData { + tahun_angkatan: number; + nama_beasiswa: string; + jumlah_nama_beasiswa: number; +} + +interface Props { + selectedYear: string; + selectedJenisBeasiswa: string; +} + +export default function NamaBeasiswaPieChart({ selectedYear, selectedJenisBeasiswa }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/nama-beasiswa?tahunAngkatan=${selectedYear}&jenisBeasiswa=${selectedJenisBeasiswa}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisBeasiswa]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + background: theme === 'dark' ? '#0F172B' : '#fff', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + } + }, + labels: [], + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000', + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const namaBeasiswa = [...new Set(data.map(item => item.nama_beasiswa))].sort(); + const jumlahData = namaBeasiswa.map(nama => { + const item = data.find(d => d.nama_beasiswa === nama); + return item ? item.jumlah_nama_beasiswa : 0; + }); + + return { + series: jumlahData, + labels: namaBeasiswa + }; + }; + + const { series, labels } = processSeriesData(); + const totalMahasiswa = data.reduce((sum, item) => sum + item.jumlah_nama_beasiswa, 0); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Nama Beasiswa {selectedJenisBeasiswa} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/StatistikMahasiswaChart.tsx b/components/StatistikMahasiswaChart.tsx new file mode 100644 index 0000000..82c2760 --- /dev/null +++ b/components/StatistikMahasiswaChart.tsx @@ -0,0 +1,318 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useTheme } from "next-themes"; + +// Import ApexCharts secara dinamis untuk menghindari error SSR +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface MahasiswaStatistik { + tahun_angkatan: number; + total_mahasiswa: number; + pria: number; + wanita: number; +} + +export default function StatistikMahasiswaChart() { + const { theme, systemTheme } = useTheme(); + const [statistikData, setStatistikData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [chartOptions, setChartOptions] = useState({ + chart: { + type: 'bar' as const, + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + }, + }, + zoom: { + enabled: true, + type: 'x' as const, + autoScaleYaxis: true + }, + }, + markers: { + size: 10, + shape: 'circle' as const, + strokeWidth: 0, + hover: { + size: 10 + } + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString() + }, + position: 'top', + style: { + fontSize: '12px', + colors: ['#000'] + }, + }, + stroke: { + show: true, + width: [0, 0, 2], + colors: ['transparent', 'transparent', '#10B981'], + curve: 'straight' as const + }, + xaxis: { + categories: [] as number[], + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: '#000' + } + } + }, + yaxis: [ + { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: '#000' + } + } + } + ], + fill: { + opacity: 1 + }, + legend: { + position: 'top' as const, + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: '#000' + } + }, + colors: ['#3B82F6', '#EC4899', '#10B981'], + tooltip: { + theme: 'light', + y: [ + { + formatter: function (val: number) { + return val + " mahasiswa" + } + }, + { + formatter: function (val: number) { + return val + " mahasiswa" + } + }, + { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + ] + } + }); + + useEffect(() => { + const fetchData = async () => { + try { + const statistikResponse = await fetch('/api/mahasiswa/statistik', { + cache: 'no-store', + }); + + if (!statistikResponse.ok) { + throw new Error('Failed to fetch statistik data'); + } + + const statistikData = await statistikResponse.json(); + setStatistikData(statistikData); + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + console.error('Error fetching data:', err); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + const textColor = currentTheme === 'dark' ? '#fff' : '#000'; + const tooltipTheme = currentTheme === 'dark' ? 'dark' : 'light'; + + setChartOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels.style, + colors: [textColor] + }, + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis.title, + style: { + ...prev.xaxis.title.style, + color: textColor + } + }, + labels: { + ...prev.xaxis.labels, + style: { + ...prev.xaxis.labels.style, + colors: textColor + } + } + }, + yaxis: [ + { + ...prev.yaxis[0], + title: { + ...prev.yaxis[0].title, + style: { + ...prev.yaxis[0].title.style, + color: textColor + } + }, + labels: { + ...prev.yaxis[0].labels, + style: { + ...prev.yaxis[0].labels.style, + colors: textColor + } + } + } + ], + legend: { + ...prev.legend, + labels: { + ...prev.legend.labels, + colors: textColor + } + }, + tooltip: { + ...prev.tooltip, + theme: tooltipTheme + } + })); + }, [theme, systemTheme]); + + // Update categories when data changes + useEffect(() => { + if (statistikData.length > 0) { + setChartOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: statistikData.map(item => item.tahun_angkatan) + } + })); + } + }, [statistikData]); + + const chartSeries = [ + { + name: 'Laki-laki', + type: 'bar' as const, + data: statistikData.map(item => item.pria) + }, + { + name: 'Perempuan', + type: 'bar' as const, + data: statistikData.map(item => item.wanita) + }, + { + name: 'Total', + type: 'line' as const, + data: statistikData.map(item => item.total_mahasiswa) + } + ]; + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + return ( + + + + Total Mahasiswa + + + +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/StatistikPerAngkatanChart.tsx b/components/StatistikPerAngkatanChart.tsx new file mode 100644 index 0000000..3f9eb8d --- /dev/null +++ b/components/StatistikPerAngkatanChart.tsx @@ -0,0 +1,238 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { useTheme } from "next-themes"; + +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface GenderStatistik { + tahun_angkatan: number; + jk: string; + jumlah: number; +} + +interface Props { + tahunAngkatan: string; +} + +export default function StatistikPerAngkatanChart({ tahunAngkatan }: Props) { + const { theme, systemTheme } = useTheme(); + const [statistikData, setStatistikData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch(`/api/mahasiswa/gender-per-angkatan?tahun=${tahunAngkatan}`); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setStatistikData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + if (tahunAngkatan !== "all") { + fetchData(); + } + }, [tahunAngkatan]); + + const [chartOptions, setChartOptions] = useState({ + chart: { + type: 'pie' as const, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: 'transparent', + }, + labels: ['Laki-laki', 'Perempuan'], + colors: ['#3B82F6', '#EC4899'], + legend: { + position: 'bottom' as const, + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: '#000', + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + } + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number, opts: any) { + return val.toString(); + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + }, + position: 'top' + }, + tooltip: { + theme: 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + const textColor = currentTheme === 'dark' ? '#fff' : '#000'; + const tooltipTheme = currentTheme === 'dark' ? 'dark' : 'light'; + + setChartOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + legend: { + ...prev.legend, + labels: { + ...prev.legend.labels, + colors: textColor, + } + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels.style, + color: textColor + } + }, + tooltip: { + ...prev.tooltip, + theme: tooltipTheme + } + })); + }, [theme, systemTheme]); + + // Update dataLabels formatter when data changes + useEffect(() => { + if (statistikData.length > 0) { + setChartOptions(prev => ({ + ...prev, + dataLabels: { + ...prev.dataLabels, + formatter: function (val: number, opts: any) { + const total = statistikData.reduce((sum, item) => sum + item.jumlah, 0); + const percentage = ((statistikData[opts.seriesIndex]?.jumlah / total) * 100) || 0; + return `${percentage.toFixed(0)}%`; + } + } + })); + } + }, [statistikData]); + + const chartSeries = statistikData.map(item => item.jumlah); + const totalMahasiswa = statistikData.reduce((sum, item) => sum + item.jumlah, 0); + + if (tahunAngkatan === "all") { + return null; + } + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (statistikData.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Total Mahasiswa Angkatan {tahunAngkatan} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/components/StatusMahasiswaChart.tsx b/components/StatusMahasiswaChart.tsx new file mode 100644 index 0000000..ad73b24 --- /dev/null +++ b/components/StatusMahasiswaChart.tsx @@ -0,0 +1,289 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface StatusData { + tahun_angkatan: string; + status_kuliah: string; + jumlah: number; +} + +export default function StatusMahasiswaChart() { + const { theme, systemTheme } = useTheme(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [data, setData] = useState([]); + const [series, setSeries] = useState([]); + const [options, setOptions] = useState({ + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + }, + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: ['#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'], + }, + xaxis: { + categories: [], + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: '#000' + } + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: '#000' + } + } + }, + fill: { + opacity: 1, + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: '#000' + } + }, + colors: ['#008FFB', '#00E396', '#FEB019', '#EF4444'], + tooltip: { + theme: 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const response = await fetch('/api/mahasiswa/status'); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + setData(result); + + // Process data to create series + const tahunAngkatan = [...new Set(result.map(item => item.tahun_angkatan))].sort(); + const statuses = ['Aktif', 'Lulus', 'Cuti', 'DO']; + + const seriesData = statuses.map(status => ({ + name: status, + data: tahunAngkatan.map(tahun => { + const item = result.find(d => d.tahun_angkatan === tahun && d.status_kuliah === status); + return item ? item.jumlah : 0; + }), + })); + + setSeries(seriesData); + setOptions(prev => ({ + ...prev, + xaxis: { + ...prev.xaxis, + categories: tahunAngkatan, + }, + })); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, []); + + // Update theme when it changes + useEffect(() => { + const currentTheme = theme === 'system' ? systemTheme : theme; + const textColor = currentTheme === 'dark' ? '#fff' : '#000'; + const tooltipTheme = currentTheme === 'dark' ? 'dark' : 'light'; + + setOptions(prev => ({ + ...prev, + chart: { + ...prev.chart, + background: currentTheme === 'dark' ? '#0F172B' : '#fff', + }, + dataLabels: { + ...prev.dataLabels, + style: { + ...prev.dataLabels?.style, + colors: [textColor] + } + }, + xaxis: { + ...prev.xaxis, + title: { + ...prev.xaxis?.title, + style: { + ...prev.xaxis?.title?.style, + color: textColor + } + }, + labels: { + ...prev.xaxis?.labels, + style: { + ...prev.xaxis?.labels?.style, + colors: textColor + } + } + }, + yaxis: { + ...(prev.yaxis as ApexYAxis), + title: { + ...(prev.yaxis as ApexYAxis)?.title, + style: { + ...(prev.yaxis as ApexYAxis)?.title?.style, + color: textColor + } + }, + labels: { + ...(prev.yaxis as ApexYAxis)?.labels, + style: { + ...(prev.yaxis as ApexYAxis)?.labels?.style, + colors: textColor + } + } + }, + legend: { + ...prev.legend, + labels: { + ...prev.legend?.labels, + colors: textColor + } + }, + tooltip: { + ...prev.tooltip, + theme: tooltipTheme + } + })); + }, [theme, systemTheme]); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Status Mahasiswa + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/StatusMahasiswaFilterChart.tsx b/components/StatusMahasiswaFilterChart.tsx new file mode 100644 index 0000000..db7c24c --- /dev/null +++ b/components/StatusMahasiswaFilterChart.tsx @@ -0,0 +1,252 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface StatusMahasiswaData { + tahun_angkatan: number; + jk: string; + total_mahasiswa: number; +} + +interface Props { + selectedYear: string; + selectedStatus: string; +} + +export default function StatusMahasiswaFilterChart({ selectedYear, selectedStatus }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + console.log('Fetching data with params:', { selectedYear, selectedStatus }); + + const response = await fetch( + `/api/mahasiswa/status-mahasiswa?tahun_angkatan=${selectedYear}&status_kuliah=${selectedStatus}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const result = await response.json(); + console.log('Received data:', result); + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: StatusMahasiswaData, b: StatusMahasiswaData) => + Number(a.tahun_angkatan) - Number(b.tahun_angkatan) + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedStatus]); + + // Log data changes + useEffect(() => { + console.log('Current data state:', data); + }, [data]); + + // Get unique years and sort them + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort((a, b) => Number(a) - Number(b)); + console.log('Sorted years:', years); + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'], + }, + xaxis: { + categories: years, + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + } + }, + fill: { + opacity: 1, + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + colors: ['#3B82F6', '#EC4899'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const maleData = new Array(years.length).fill(0); + const femaleData = new Array(years.length).fill(0); + + data.forEach(item => { + const yearIndex = years.indexOf(item.tahun_angkatan); + if (yearIndex !== -1) { + if (item.jk === 'L') { + maleData[yearIndex] = item.total_mahasiswa; + } else if (item.jk === 'P') { + femaleData[yearIndex] = item.total_mahasiswa; + } + } + }); + + return [ + { + name: 'Laki-laki', + data: maleData + }, + { + name: 'Perempuan', + data: femaleData + } + ]; + }; + + const series = processSeriesData(); + console.log('Processed series data:', series); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Status Mahasiswa {selectedStatus} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/StatusMahasiswaFilterPieChart.tsx b/components/StatusMahasiswaFilterPieChart.tsx new file mode 100644 index 0000000..eb36b2d --- /dev/null +++ b/components/StatusMahasiswaFilterPieChart.tsx @@ -0,0 +1,185 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface StatusMahasiswaData { + tahun_angkatan: number; + jk: string; + total_mahasiswa: number; +} + +interface Props { + selectedYear: string; + selectedStatus: string; +} + +export default function StatusMahasiswaFilterPieChart({ selectedYear, selectedStatus }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + + const response = await fetch( + `/api/mahasiswa/status-mahasiswa?tahun_angkatan=${selectedYear}&status_kuliah=${selectedStatus}` + ); + + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedStatus]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + labels: ['Laki-laki', 'Perempuan'], + colors: ['#3B82F6', '#EC4899'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000', + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const maleData = data.find(item => item.jk === 'L')?.total_mahasiswa || 0; + const femaleData = data.find(item => item.jk === 'P')?.total_mahasiswa || 0; + return [maleData, femaleData]; + }; + + const series = processSeriesData(); + const totalMahasiswa = data.reduce((sum, item) => sum + item.total_mahasiswa, 0); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Status Mahasiswa {selectedStatus} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/TingkatPrestasiChart.tsx b/components/TingkatPrestasiChart.tsx new file mode 100644 index 0000000..07544df --- /dev/null +++ b/components/TingkatPrestasiChart.tsx @@ -0,0 +1,263 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface TingkatPrestasiData { + tahun_angkatan: number; + tingkat: string; + tingkat_mahasiswa_prestasi: number; +} + +interface Props { + selectedJenisPrestasi: string; +} + +export default function TingkatPrestasiChart({ selectedJenisPrestasi }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/tingkat-prestasi?jenisPrestasi=${selectedJenisPrestasi}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: TingkatPrestasiData, b: TingkatPrestasiData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedJenisPrestasi]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) return []; + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort(); + const tingkatTypes = [...new Set(data.map(item => item.tingkat))].sort(); + + return tingkatTypes.map(tingkat => ({ + name: tingkat, + data: years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.tingkat === tingkat); + return item?.tingkat_mahasiswa_prestasi || 0; + }) + })); + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val?.toString() || '0'; + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Tingkat Prestasi {selectedJenisPrestasi} + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/TingkatPrestasiPieChart.tsx b/components/TingkatPrestasiPieChart.tsx new file mode 100644 index 0000000..7bdd514 --- /dev/null +++ b/components/TingkatPrestasiPieChart.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface TingkatPrestasiData { + tahun_angkatan: number; + tingkat: string; + tingkat_mahasiswa_prestasi: number; +} + +interface Props { + selectedYear: string; + selectedJenisPrestasi: string; +} + +export default function TingkatPrestasiPieChart({ selectedYear, selectedJenisPrestasi }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/tingkat-prestasi?tahunAngkatan=${selectedYear}&jenisPrestasi=${selectedJenisPrestasi}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisPrestasi]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + background: theme === 'dark' ? '#0F172B' : '#fff', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + } + }, + labels: [], + colors: ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#06B6D4'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + // Filter data sesuai tahun angkatan + const filtered = data.filter(item => String(item.tahun_angkatan) === String(selectedYear)); + const tingkat = [...new Set(filtered.map(item => item.tingkat))].sort(); + const jumlahData = tingkat.map(t => { + const item = filtered.find(d => d.tingkat === t); + return item ? item.tingkat_mahasiswa_prestasi : 0; + }); + return { + series: jumlahData, + labels: tingkat + }; + }; + + const { series, labels } = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Tingkat Prestasi {selectedJenisPrestasi} Angkatan {selectedYear} + + + +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/TotalBeasiswaChart.tsx b/components/TotalBeasiswaChart.tsx new file mode 100644 index 0000000..1c788d4 --- /dev/null +++ b/components/TotalBeasiswaChart.tsx @@ -0,0 +1,277 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface TotalBeasiswaData { + tahun_angkatan: number; + jk: string; + jumlah_mahasiswa_beasiswa: number; +} + +interface Props { + selectedYear: string; + selectedJenisBeasiswa: string; +} + +export default function TotalBeasiswaChart({ selectedYear, selectedJenisBeasiswa }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/total-beasiswa?tahunAngkatan=${selectedYear}&jenisBeasiswa=${selectedJenisBeasiswa}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: TotalBeasiswaData, b: TotalBeasiswaData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisBeasiswa]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) return []; + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort(); + + const pria = years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.jk === 'Pria'); + return item ? item.jumlah_mahasiswa_beasiswa : 0; + }); + + const wanita = years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.jk === 'Wanita'); + return item ? item.jumlah_mahasiswa_beasiswa : 0; + }); + + return [ + { + name: 'Laki-laki', + data: pria + }, + { + name: 'Perempuan', + data: wanita + } + ]; + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#EC4899'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Total Mahasiswa Beasiswa {selectedJenisBeasiswa} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/TotalBeasiswaPieChart.tsx b/components/TotalBeasiswaPieChart.tsx new file mode 100644 index 0000000..ae26200 --- /dev/null +++ b/components/TotalBeasiswaPieChart.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface TotalBeasiswaData { + tahun_angkatan: number; + jk: string; + jumlah_mahasiswa_beasiswa: number; +} + +interface Props { + selectedYear: string; + selectedJenisBeasiswa: string; +} + +export default function TotalBeasiswaPieChart({ selectedYear, selectedJenisBeasiswa }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/total-beasiswa?tahunAngkatan=${selectedYear}&jenisBeasiswa=${selectedJenisBeasiswa}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisBeasiswa]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + background: theme === 'dark' ? '#0F172B' : '#fff', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + } + }, + labels: ['Laki-laki', 'Perempuan'], + colors: ['#3B82F6', '#EC4899'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + formatter: function(legendName: string, opts: any) { + return legendName; + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + const maleData = data.find(item => item.jk === 'Pria')?.jumlah_mahasiswa_beasiswa || 0; + const femaleData = data.find(item => item.jk === 'Wanita')?.jumlah_mahasiswa_beasiswa || 0; + return [maleData, femaleData]; + }; + + const series = processSeriesData(); + const totalMahasiswa = data.reduce((sum, item) => sum + item.jumlah_mahasiswa_beasiswa, 0); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Total Mahasiswa Beasiswa {selectedJenisBeasiswa} + {selectedYear !== 'all' ? ` Angkatan ${selectedYear}` : ''} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/TotalPrestasiChart.tsx b/components/TotalPrestasiChart.tsx new file mode 100644 index 0000000..44db36c --- /dev/null +++ b/components/TotalPrestasiChart.tsx @@ -0,0 +1,275 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface TotalPrestasiData { + tahun_angkatan: number; + jk: string; + jumlah_mahasiswa_prestasi: number; +} + +interface Props { + selectedJenisPrestasi: string; +} + +export default function TotalPrestasiChart({ selectedJenisPrestasi }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/total-prestasi?jenisPrestasi=${selectedJenisPrestasi}`; + + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (!Array.isArray(result)) { + throw new Error('Invalid data format received from server'); + } + + // Sort data by tahun_angkatan + const sortedData = result.sort((a: TotalPrestasiData, b: TotalPrestasiData) => + a.tahun_angkatan - b.tahun_angkatan + ); + + setData(sortedData); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedJenisPrestasi]); + + // Process data for series + const processSeriesData = () => { + if (!data.length) return []; + + const years = [...new Set(data.map(item => item.tahun_angkatan))].sort(); + + const pria = years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.jk === 'Pria'); + return item ? item.jumlah_mahasiswa_prestasi : 0; + }); + + const wanita = years.map(year => { + const item = data.find(d => d.tahun_angkatan === year && d.jk === 'Wanita'); + return item ? item.jumlah_mahasiswa_prestasi : 0; + }); + + return [ + { + name: 'Laki-laki', + data: pria + }, + { + name: 'Perempuan', + data: wanita + } + ]; + }; + + const chartOptions: ApexOptions = { + chart: { + type: 'bar', + stacked: false, + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + }, + background: theme === 'dark' ? '#0F172B' : '#fff', + }, + plotOptions: { + bar: { + horizontal: false, + columnWidth: '55%', + borderRadius: 1, + }, + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return val.toString(); + }, + style: { + fontSize: '12px', + colors: [theme === 'dark' ? '#fff' : '#000'] + } + }, + stroke: { + show: true, + width: 2, + colors: ['transparent'] + }, + xaxis: { + categories: [...new Set(data.map(item => item.tahun_angkatan))].sort(), + title: { + text: 'Tahun Angkatan', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + }, + axisTicks: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + yaxis: { + title: { + text: 'Jumlah Mahasiswa', + style: { + fontSize: '14px', + fontWeight: 'bold', + color: theme === 'dark' ? '#fff' : '#000' + } + }, + labels: { + style: { + fontSize: '12px', + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + axisBorder: { + show: true, + color: theme === 'dark' ? '#374151' : '#E5E7EB' + } + }, + fill: { + opacity: 1 + }, + colors: ['#3B82F6', '#EC4899'], + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa"; + } + } + }, + legend: { + position: 'top', + fontSize: '14px', + markers: { + size: 12, + }, + itemMargin: { + horizontal: 10, + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + grid: { + borderColor: theme === 'dark' ? '#374151' : '#E5E7EB', + strokeDashArray: 4, + padding: { + top: 20, + right: 0, + bottom: 0, + left: 0 + } + } + }; + + const series = processSeriesData(); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Total Mahasiswa Berprestasi {selectedJenisPrestasi} + + + +
+ {typeof window !== 'undefined' && series.length > 0 && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/TotalPrestasiPieChart.tsx b/components/TotalPrestasiPieChart.tsx new file mode 100644 index 0000000..2773e9b --- /dev/null +++ b/components/TotalPrestasiPieChart.tsx @@ -0,0 +1,188 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { ApexOptions } from 'apexcharts'; +import { useTheme } from 'next-themes'; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +// Dynamically import ApexCharts to avoid SSR issues +const Chart = dynamic(() => import('react-apexcharts'), { ssr: false }); + +interface TotalPrestasiData { + tahun_angkatan: number; + jk: string; + jumlah_mahasiswa_prestasi: number; +} + +interface Props { + selectedYear: string; + selectedJenisPrestasi: string; +} + +export default function TotalPrestasiPieChart({ selectedYear, selectedJenisPrestasi }: Props) { + const { theme } = useTheme(); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + setLoading(true); + setError(null); + + const url = `/api/mahasiswa/total-prestasi?tahunAngkatan=${selectedYear}&jenisPrestasi=${selectedJenisPrestasi}`; + const response = await fetch(url); + + if (!response.ok) { + throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + setData(result); + } catch (err) { + console.error('Error in fetchData:', err); + setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); + } finally { + setLoading(false); + } + }; + + fetchData(); + }, [selectedYear, selectedJenisPrestasi]); + + const chartOptions: ApexOptions = { + chart: { + type: 'pie', + background: theme === 'dark' ? '#0F172B' : '#fff', + toolbar: { + show: true, + tools: { + download: true, + selection: true, + zoom: true, + zoomin: true, + zoomout: true, + pan: true, + reset: true + } + } + }, + labels: ['Laki-laki', 'Perempuan'], + colors: ['#3B82F6', '#EC4899'], + legend: { + position: 'bottom', + fontSize: '14px', + markers: { + size: 12, + strokeWidth: 0 + }, + itemMargin: { + horizontal: 10 + }, + labels: { + colors: theme === 'dark' ? '#fff' : '#000' + } + }, + dataLabels: { + enabled: true, + formatter: function (val: number) { + return `${val.toFixed(0)}%`; + }, + style: { + fontSize: '14px', + fontFamily: 'Inter, sans-serif', + fontWeight: '500' + }, + offsetY: 0, + dropShadow: { + enabled: false + } + }, + tooltip: { + theme: theme === 'dark' ? 'dark' : 'light', + y: { + formatter: function (val: number) { + return val + " mahasiswa" + } + } + } + }; + + // Process data for series + const processSeriesData = () => { + // Filter data sesuai tahun angkatan + const filtered = data.filter(item => String(item.tahun_angkatan) === String(selectedYear)); + const pria = filtered.find(item => item.jk === 'Pria')?.jumlah_mahasiswa_prestasi || 0; + const wanita = filtered.find(item => item.jk === 'Wanita')?.jumlah_mahasiswa_prestasi || 0; + return [pria, wanita]; + }; + + const series = processSeriesData(); + const totalMahasiswa = data + .filter(item => String(item.tahun_angkatan) === String(selectedYear)) + .reduce((sum, item) => sum + item.jumlah_mahasiswa_prestasi, 0); + + if (loading) { + return ( + + + + Loading... + + + + ); + } + + if (error) { + return ( + + + + Error: {error} + + + + ); + } + + if (data.length === 0) { + return ( + + + + Tidak ada data yang tersedia + + + + ); + } + + return ( + + + + Total Mahasiswa Berprestasi {selectedJenisPrestasi} Angkatan {selectedYear} + +
+ Total Mahasiswa: {totalMahasiswa} +
+
+ +
+ {typeof window !== 'undefined' && ( + + )} +
+
+
+ ); +} \ No newline at end of file diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..1c5ff26 --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,18 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" + +export function ThemeProvider({ children, ...props }: React.ComponentProps) { + const [mounted, setMounted] = React.useState(false) + + React.useEffect(() => { + setMounted(true) + }, []) + + if (!mounted) { + return <>{children} + } + + return {children} +} \ No newline at end of file diff --git a/components/theme-toggle.tsx b/components/theme-toggle.tsx new file mode 100644 index 0000000..97ccf1f --- /dev/null +++ b/components/theme-toggle.tsx @@ -0,0 +1,37 @@ +"use client" + +import * as React from "react" +import { Moon, Sun } from "lucide-react" +import { useTheme } from "next-themes" + +import { Button } from "@/components/ui/button" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export function ThemeToggle() { + const { setTheme } = useTheme() + + return ( + + + + + + setTheme("light")}> + Light + + setTheme("dark")}> + Dark + + + + ) +} \ No newline at end of file diff --git a/components/ui/Navbar.tsx b/components/ui/Navbar.tsx new file mode 100644 index 0000000..fecd190 --- /dev/null +++ b/components/ui/Navbar.tsx @@ -0,0 +1,374 @@ +'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 { 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(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) => { + 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 + })); + } + }; + + return ( +
+
+ {/* Mobile Menu Button */} +
+ + + + + + Menu Navigasi + + + +
+ + PODIF Logo + PODIF + +
+ +
+ {isLoggedIn ? ( + + + + + + + + + + + My Account + + + Profile + + Logout + + + ) : ( + + + + + + + Portal Data Informatika + Masuk atau daftar untuk mengakses portal + + + + Login + Register + + +
+ + + +
+
+ +
+ + + + + +
+
+
+
+
+ )} + +
+
+ ); +}; + +export default Navbar; diff --git a/components/ui/ProfileMenuItem.tsx b/components/ui/ProfileMenuItem.tsx new file mode 100644 index 0000000..0152c83 --- /dev/null +++ b/components/ui/ProfileMenuItem.tsx @@ -0,0 +1,32 @@ +import { DropdownMenuItem } from '@/components/ui/dropdown-menu'; +import { useToast } from '@/components/ui/use-toast'; +import { useRouter } from 'next/navigation'; + +interface ProfileMenuItemProps { + isLoggedIn: boolean; + setDialogOpen: (open: boolean) => void; +} + +export const ProfileMenuItem = ({ isLoggedIn, setDialogOpen }: ProfileMenuItemProps) => { + const { toast } = useToast(); + const router = useRouter(); + + const handleClick = () => { + if (!isLoggedIn) { + toast({ + variant: "destructive", + title: "Akses Ditolak", + description: "Silakan login terlebih dahulu untuk mengakses profil", + }); + setDialogOpen(true); + } else { + router.push('/mahasiswa/profile'); + } + }; + + return ( + + Profile + + ); +}; \ No newline at end of file diff --git a/components/ui/Sidebar.tsx b/components/ui/Sidebar.tsx new file mode 100644 index 0000000..2dac4ad --- /dev/null +++ b/components/ui/Sidebar.tsx @@ -0,0 +1,13 @@ +"use client"; + +import SidebarContent from './SidebarContent'; + +const Sidebar = () => { + return ( +
+ +
+ ); +}; + +export default Sidebar; \ No newline at end of file diff --git a/components/ui/SidebarContent.tsx b/components/ui/SidebarContent.tsx new file mode 100644 index 0000000..f903c3b --- /dev/null +++ b/components/ui/SidebarContent.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} from "@/components/ui/command"; + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; + +import Link from "next/link"; +import { School, Settings, User, GraduationCap, Award, Users, Clock, BookOpen, Home } from "lucide-react"; + +const SidebarContent = () => { + return ( + + + + + + + Dashboard + + + + + + + + +
+ + Data Mahasiswa +
+
+ +
+ + + Mahasiswa Total + + + + Mahasiswa Status + + + + Mahasiswa Lulus Tepat Waktu + + + + Mahasiswa Beasiswa + + + + Mahasiswa Berprestasi + +
+
+
+
+
+
+ + + + + + Profile + + + +
+
+ ); +}; + +export default SidebarContent; \ No newline at end of file diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..134bb63 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,72 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDownIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
+ {children} +
+
+ ) +} + + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } \ No newline at end of file diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..1e79f31 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,41 @@ +'use client'; + +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from '@/lib/utils'; + +function Avatar({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ className, ...props }: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..0660af6 --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; + +import { cn } from '@/lib/utils'; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', + destructive: + 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', + secondary: 'bg-secondary text-card shadow-xs hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', + }, + size: { + default: 'h-9 px-4 py-2 has-[>svg]:px-3', + sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<'button'> & + VariantProps & { + asChild?: boolean; + }) { + const Comp = asChild ? Slot : 'button'; + + return ( + + ); +} + +export { Button, buttonVariants }; diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/components/ui/command.tsx b/components/ui/command.tsx new file mode 100644 index 0000000..16d6e0b --- /dev/null +++ b/components/ui/command.tsx @@ -0,0 +1,177 @@ +"use client" + +import * as React from "react" +import { Command as CommandPrimitive } from "cmdk" +import { SearchIcon } from "lucide-react" + +import { cn } from "@/lib/utils" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" + +function Command({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + ...props +}: React.ComponentProps & { + title?: string + description?: string +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ) +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ) +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx new file mode 100644 index 0000000..7d7a9d3 --- /dev/null +++ b/components/ui/dialog.tsx @@ -0,0 +1,135 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { XIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function Dialog({ + ...props +}: React.ComponentProps) { + return +} + +function DialogTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DialogPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DialogClose({ + ...props +}: React.ComponentProps) { + return +} + +function DialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + {children} + + + Close + + + + ) +} + +function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogOverlay, + DialogPortal, + DialogTitle, + DialogTrigger, +} diff --git a/components/ui/drawer.tsx b/components/ui/drawer.tsx new file mode 100644 index 0000000..199023b --- /dev/null +++ b/components/ui/drawer.tsx @@ -0,0 +1,132 @@ +"use client" + +import * as React from "react" +import { Drawer as DrawerPrimitive } from "vaul" + +import { cn } from "@/lib/utils" + +function Drawer({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerPortal({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerClose({ + ...props +}: React.ComponentProps) { + return +} + +function DrawerOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + +
+ {children} + + + ) +} + +function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function DrawerTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DrawerDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + Drawer, + DrawerPortal, + DrawerOverlay, + DrawerTrigger, + DrawerClose, + DrawerContent, + DrawerHeader, + DrawerFooter, + DrawerTitle, + DrawerDescription, +} diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..0c1fffc --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,228 @@ +'use client'; + +import * as React from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react'; + +import { cn } from '@/lib/utils'; + +function DropdownMenu({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function DropdownMenuGroup({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuItem({ + className, + inset, + variant = 'default', + ...props +}: React.ComponentProps & { + inset?: boolean; + variant?: 'default' | 'destructive'; +}) { + return ( + + ); +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) { + return ( + + ); +} + +function DropdownMenuSub({ ...props }: React.ComponentProps) { + return ; +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +}; diff --git a/components/ui/input.tsx b/components/ui/input.tsx new file mode 100644 index 0000000..03295ca --- /dev/null +++ b/components/ui/input.tsx @@ -0,0 +1,21 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Input({ className, type, ...props }: React.ComponentProps<"input">) { + return ( + + ) +} + +export { Input } diff --git a/components/ui/label.tsx b/components/ui/label.tsx new file mode 100644 index 0000000..fb5fbc3 --- /dev/null +++ b/components/ui/label.tsx @@ -0,0 +1,24 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" + +import { cn } from "@/lib/utils" + +function Label({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Label } diff --git a/components/ui/select.tsx b/components/ui/select.tsx new file mode 100644 index 0000000..e626e85 --- /dev/null +++ b/components/ui/select.tsx @@ -0,0 +1,121 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + {children} + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, +} \ No newline at end of file diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx new file mode 100644 index 0000000..db9193f --- /dev/null +++ b/components/ui/sheet.tsx @@ -0,0 +1,140 @@ +"use client" + +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Sheet = SheetPrimitive.Root + +const SheetTrigger = SheetPrimitive.Trigger + +const SheetClose = SheetPrimitive.Close + +const SheetPortal = SheetPrimitive.Portal + +const SheetOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName + +const sheetVariants = cva( + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open:animate-in:data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-300", + { + variants: { + side: { + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", + bottom: + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", + right: + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", + }, + }, + defaultVariants: { + side: "right", + }, + } +) + +interface SheetContentProps + extends React.ComponentPropsWithoutRef, + VariantProps {} + +const SheetContent = React.forwardRef< + React.ElementRef, + SheetContentProps +>(({ side = "right", className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +SheetContent.displayName = SheetPrimitive.Content.displayName + +const SheetHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetHeader.displayName = "SheetHeader" + +const SheetFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +SheetFooter.displayName = "SheetFooter" + +const SheetTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName + +const SheetDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName + +export { + Sheet, + SheetPortal, + SheetOverlay, + SheetTrigger, + SheetClose, + SheetContent, + SheetHeader, + SheetFooter, + SheetTitle, + SheetDescription, +} \ No newline at end of file diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx new file mode 100644 index 0000000..f7de1db --- /dev/null +++ b/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } \ No newline at end of file diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx new file mode 100644 index 0000000..497ba5e --- /dev/null +++ b/components/ui/tabs.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/components/ui/toast.tsx b/components/ui/toast.tsx new file mode 100644 index 0000000..243896a --- /dev/null +++ b/components/ui/toast.tsx @@ -0,0 +1,127 @@ +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[swipe=end]:slide-out-to-right-full sm:data-[state=open]:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} \ No newline at end of file diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx new file mode 100644 index 0000000..62bb68a --- /dev/null +++ b/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} \ No newline at end of file diff --git a/components/ui/use-toast.ts b/components/ui/use-toast.ts new file mode 100644 index 0000000..604ecf8 --- /dev/null +++ b/components/ui/use-toast.ts @@ -0,0 +1,192 @@ +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_VALUE + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } \ No newline at end of file diff --git a/lib/db.ts b/lib/db.ts new file mode 100644 index 0000000..bb34577 --- /dev/null +++ b/lib/db.ts @@ -0,0 +1,28 @@ +import mysql from 'mysql2/promise'; + +const pool = mysql.createPool({ + host: '127.0.0.1', + port: 3306, + user: 'root', + password: 'semogabisayok321', + database: 'mhsdb', + waitForConnections: true, + connectionLimit: 10, + queueLimit: 0 +}); + +// Test the connection +const testConnection = async () => { + try { + const connection = await pool.getConnection(); + console.log('Database connection successful'); + connection.release(); + return true; + } catch (error) { + console.error('Database connection failed:', error); + return false; + } +}; + +// Export both the pool and the test function +export { pool as default, testConnection }; \ No newline at end of file diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..2819a83 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..1956510 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,65 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; +import { jwtVerify } from 'jose'; + +export async function middleware(request: NextRequest) { + const token = request.cookies.get('token')?.value; + const { pathname } = request.nextUrl; + + // Define public paths that don't require authentication + const publicPaths = ['/', '/login', '/register']; + const isPublicPath = publicPaths.includes(pathname); + + // Check if the path is an API route or static file + const isApiRoute = pathname.startsWith('/api/'); + const isStaticFile = pathname.match(/\.(jpg|jpeg|png|gif|ico|css|js)$/); + + // Skip middleware for API routes and static files + if (isApiRoute || isStaticFile) { + return NextResponse.next(); + } + + // If trying to access public route with token + if (token && isPublicPath) { + return NextResponse.next(); + } + + // If the path is protected and user is logged in, verify token + if (!isPublicPath && token) { + try { + // Verify the token + await jwtVerify( + token, + new TextEncoder().encode(process.env.JWT_SECRET || 'your-secret-key') + ); + return NextResponse.next(); + } catch (error) { + // If token is invalid, redirect to home + const response = NextResponse.redirect(new URL('/', request.url)); + response.cookies.set('token', '', { + expires: new Date(0), + path: '/', + httpOnly: true, + secure: false, + sameSite: 'lax' + }); + return response; + } + } + + return NextResponse.next(); +} + +// Configure which paths the middleware should run on +export const config = { + matcher: [ + /* + * Match all request paths except for the ones starting with: + * - api (API routes) + * - _next/static (static files) + * - _next/image (image optimization files) + * - favicon.ico (favicon file) + */ + '/((?!api|_next/static|_next/image|favicon.ico).*)', + ], +}; \ No newline at end of file diff --git a/next.config.ts b/next.config.ts index e9ffa30..0cbca92 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ -import type { NextConfig } from "next"; +import type { NextConfig } from 'next'; const nextConfig: NextConfig = { - /* config options here */ + devIndicators: false }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index 4b7cb66..d0bc0d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,50 @@ { - "name": "traversypress-ui", + "name": "podif", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "traversypress-ui", + "name": "podif", "version": "0.1.0", "dependencies": { + "@radix-ui/react-accordion": "^1.2.4", + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dropdown-menu": "^2.1.7", + "@radix-ui/react-label": "^2.1.4", + "@radix-ui/react-select": "^2.2.2", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-tabs": "^1.1.7", + "@radix-ui/react-toast": "^1.2.10", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.9", + "apexcharts": "^4.5.0", + "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "jose": "^6.0.11", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.488.0", + "mysql2": "^3.14.0", "next": "15.3.0", + "next-auth": "^4.24.11", + "next-themes": "^0.4.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-apexcharts": "^1.7.0", + "react-dom": "^19.0.0", + "recharts": "^2.15.2", + "tailwind-merge": "^3.2.0", + "tw-animate-css": "^1.2.5", + "vaul": "^1.1.2" }, "devDependencies": { "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "prettier": "^3.5.3", "tailwindcss": "^4", "typescript": "^5" } @@ -34,6 +62,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.1.tgz", @@ -44,6 +84,44 @@ "tslib": "^2.4.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.34.1", "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", @@ -555,6 +633,1363 @@ "node": ">= 10" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.4.tgz", + "integrity": "sha512-SGCxlSBaMvEzDROzyZjsVNzu9XY5E28B3k8jOENyrz6csOv/pG1eHyYfLJai1n9tRjwG61coXDhfpgtxKxUv5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collapsible": "1.1.4", + "@radix-ui/react-collection": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-controllable-state": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.3.tgz", + "integrity": "sha512-2dvVU4jva0qkNZH6HHWuSz5FN5GeU5tymvCgutF8WaXz9WnD1NgUhy73cqzkjkN4Zkn8lfTPv5JIfrC221W+Nw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.4.tgz", + "integrity": "sha512-+kBesLBzwqyDiYCtYFK+6Ktf+N7+Y6QOTUueLGLIbLZ/YeyFW6bsBGDsN+5HxHpM55C90u5fxsg0ErxzXTcwKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.4.tgz", + "integrity": "sha512-u7LCw1EYInQtBNLGjm9nZ89S/4GcvX1UR5XbekEgnQae2Hkpq39ycJ1OhdeN1/JDfVNG91kWaWoest127TaEKQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-controllable-state": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.3.tgz", + "integrity": "sha512-mM2pxoQw5HJ49rkzwOs7Y6J4oYH22wS8BfK2/bBxROlI4xuR0c4jEenQP63LlTlDkO6Buj2Vt+QYAYcOgqtrXA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.7.tgz", + "integrity": "sha512-EIdma8C0C/I6kL6sO02avaCRqi3fmWJpxH6mqbVScorW6nNktzKJT/le7VPho3o/7wCsyRg3z0+Q+Obr0Gy/VQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.6", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.3", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.5", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-controllable-state": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.6.tgz", + "integrity": "sha512-7gpgMT2gyKym9Jz2ZhlRXSg2y6cNQIK8d/cqBZ0RBCaps8pFryCWXiUKI+uHGFrhMrbGUP7U6PWgiXzIxoyF3Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.7.tgz", + "integrity": "sha512-7/1LiuNZuCQE3IzdicGoHdQOHkS2Q08+7p8w6TXZ6ZjgAULaCI85ZY15yPl4o4FVgoKLRT43/rsfNVN8osClQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.7", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-controllable-state": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.3.tgz", + "integrity": "sha512-4XaDlq0bPt7oJwR+0k0clCiCO/7lO7NKZTAaJBYxDNQT/vj4ig0/UvctrRscZaFREpRvUTkpKR96ov1e6jptQg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.4.tgz", + "integrity": "sha512-wy3dqizZnZVV4ja0FNnUhIWNwWdoldXrneEyUcVtLYDAt8ovGS4ridtMAOGgXBBIfggL4BOveVWsjXDORdGEQg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.7.tgz", + "integrity": "sha512-tBODsrk68rOi1/iQzbM54toFF+gSw/y+eQgttFflqlGekuSebNqvFNHjJgjqPhiMb4Fw9A0zNFly1QT6ZFdQ+Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.6", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.3", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.3", + "@radix-ui/react-portal": "1.1.5", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-roving-focus": "1.1.3", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.3.tgz", + "integrity": "sha512-iNb9LYUMkne9zIahukgQmHlSBp9XWGeQQ7FvUGNk45ywzOb6kQa+Ca38OphXlWDiKvyneo9S+KSJsLfLt8812A==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.5.tgz", + "integrity": "sha512-ps/67ZqsFm+Mb6lSPJpfhRLrVL2i2fntgCmGMqqth4eaGUf+knAuuRtWVJrNjUhExgmdRqftSgzpf0DF0n6yXA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz", + "integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.3.tgz", + "integrity": "sha512-Pf/t/GkndH7CQ8wE2hbkXA+WyZ83fhQQn5DDmwDiDo6AwN/fhaH8oqZ0jRjMrO2iaMhDi6P1HRx6AZwyMinY1g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.3.tgz", + "integrity": "sha512-ufbpLUjZiOg4iYgb2hQrWXEPYX6jOLBbR27bDyAff5GYMRrCzcze8lukjuXVUQvJ6HZe8+oL+hhswDcjmcgVyg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.0.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.2.tgz", + "integrity": "sha512-HjkVHtBkuq+r3zUAZ/CvNWUGKPfuicGDbgtZgiQuFmNcV5F+Tgy24ep2nsAW2nFgvhGPJVqeBZa6KyVN0EyrBA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.4", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.4", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz", + "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", + "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", + "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.4.tgz", + "integrity": "sha512-r2annK27lIW5w9Ho5NyQgqs0MmgZSTIKXWpVCJaLC1q2kZrZkcqnmHkCHMEmv8XLvsLlurKMPT+kbKkRkm/xVA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz", + "integrity": "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-portal": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.6.tgz", + "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz", + "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.7.tgz", + "integrity": "sha512-sawt4HkD+6haVGjYOC3BMIiCumBpqTK6o407n6zN/6yReed2EN7bXyykNrpqg+xCfudpBUZg7Y2cJBd/x/iybA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-roving-focus": "1.1.6", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", + "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.6.tgz", + "integrity": "sha512-D2ReXCuIueKf5L2f1ks/wTj3bWck1SvK1pjLmEHPbwksS1nOHBsvgY0b9Hypt81FczqBqSyLHQxn/vbsQ0gDHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.10.tgz", + "integrity": "sha512-lVe1mQL8Di8KPQp62CDaLgttqyUGTchPuwDiCnaZz40HGxngJKB/fOJCHYxHZh2p1BtcuiPOYOKrxTVEmrnV5A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.4", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.7", + "@radix-ui/react-portal": "1.1.6", + "@radix-ui/react-presence": "1.1.3", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-collection": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.4.tgz", + "integrity": "sha512-cv4vSf7HttqXilDnAnvINd53OTl1/bjUYVZrkFnA7nwmY9Ob2POUy0WY0sfqBAe1s5FyKsyceQlqiEGPYNTadg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz", + "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.6.tgz", + "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.1.tgz", + "integrity": "sha512-YnEXIy8/ga01Y1PN0VfaNH//MhA91JlEGVBDxDzROqwrAtG5Yr2QGEPz8A/rJA3C7ZAHryOYGaUv8fLSW2H/mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.0.tgz", + "integrity": "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz", + "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@svgdotjs/svg.draggable.js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", + "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==", + "license": "MIT", + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4" + } + }, + "node_modules/@svgdotjs/svg.filter.js": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz", + "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==", + "license": "MIT", + "dependencies": { + "@svgdotjs/svg.js": "^3.2.4" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@svgdotjs/svg.js": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz", + "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/@svgdotjs/svg.resize.js": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz", + "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==", + "license": "MIT", + "engines": { + "node": ">= 14.18" + }, + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4", + "@svgdotjs/svg.select.js": "^4.0.1" + } + }, + "node_modules/@svgdotjs/svg.select.js": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.2.tgz", + "integrity": "sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g==", + "license": "MIT", + "engines": { + "node": ">= 14.18" + }, + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4" + } + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -838,11 +2273,95 @@ "tailwindcss": "4.1.4" } }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "license": "MIT" + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.17.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.30.tgz", "integrity": "sha512-7zf4YyHA+jvBNfVrk2Gtvs6x7E8V+YDW05bNfG2XkWDJfYRXrTiP/DsB2zSYTaHX0bGIujTBQdMVAhb+j7mwpg==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.19.2" @@ -852,7 +2371,7 @@ "version": "19.1.2", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz", "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.0.2" @@ -862,12 +2381,68 @@ "version": "19.1.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz", "integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" } }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "license": "MIT" + }, + "node_modules/apexcharts": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.5.0.tgz", + "integrity": "sha512-E7ZkrVqPNBUWy/Rmg8DEIqHNBmElzICE/oxOX5Ekvs2ICQUOK/VkEkMH09JGJu+O/EA0NL31hxlmF+wrwrSLaQ==", + "license": "MIT", + "dependencies": { + "@svgdotjs/svg.draggable.js": "^3.0.4", + "@svgdotjs/svg.filter.js": "^3.0.8", + "@svgdotjs/svg.js": "^3.2.4", + "@svgdotjs/svg.resize.js": "^2.0.2", + "@svgdotjs/svg.select.js": "^4.0.1", + "@yr/monotone-cubic-spline": "^1.0.3" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.2.tgz", + "integrity": "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog==", + "license": "BSD-3-Clause", + "bin": { + "bcrypt": "bin/bcrypt" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -899,12 +2474,49 @@ ], "license": "CC-BY-4.0" }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -950,13 +2562,157 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -967,6 +2723,31 @@ "node": ">=8" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -981,6 +2762,39 @@ "node": ">=10.13.0" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -988,6 +2802,27 @@ "dev": true, "license": "ISC" }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -995,6 +2830,12 @@ "license": "MIT", "optional": true }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -1005,6 +2846,64 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/lightningcss": { "version": "1.29.2", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz", @@ -1244,6 +3143,143 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/lru.min": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.2.tgz", + "integrity": "sha512-Nv9KddBcQSlQopmBHXSsZVY5xsdlZkdH/Iey0BlcBYggMd4two7cZnKOK9vmy3nY0O5RGH99z1PCeTpPqszUYg==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/lucide-react": { + "version": "0.488.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.488.0.tgz", + "integrity": "sha512-ronlL0MyKut4CEzBY/ai2ZpKPxyWO4jUqdAkm2GNK5Zn3Rj+swDz+3lvyAUXN0PNqPKIX6XM9Xadwz/skLs/pQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.14.0.tgz", + "integrity": "sha512-8eMhmG6gt/hRkU1G+8KlGOdQi2w+CgtNoD1ksXZq9gQfkfDsX4LHaBwTe1SY0Imx//t2iZA03DFnyYKPinxSRw==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "license": "MIT", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -1316,6 +3352,57 @@ } } }, + "node_modules/next-auth": { + "version": "4.24.11", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-4.24.11.tgz", + "integrity": "sha512-pCFXzIDQX7xmHFs4KVH4luCjaCbuPRtZ9oBUjUhOk84mZ9WVPf94n87TxYI4rSRf9HmfHEF8Yep3JrYDVOo3Cw==", + "license": "ISC", + "dependencies": { + "@babel/runtime": "^7.20.13", + "@panva/hkdf": "^1.0.2", + "cookie": "^0.7.0", + "jose": "^4.15.5", + "oauth": "^0.9.15", + "openid-client": "^5.4.0", + "preact": "^10.6.3", + "preact-render-to-string": "^5.1.19", + "uuid": "^8.3.2" + }, + "peerDependencies": { + "@auth/core": "0.34.2", + "next": "^12.2.5 || ^13 || ^14 || ^15", + "nodemailer": "^6.6.5", + "react": "^17.0.2 || ^18 || ^19", + "react-dom": "^17.0.2 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@auth/core": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-auth/node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -1344,6 +3431,75 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/oauth": { + "version": "0.9.15", + "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", + "integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", + "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/oidc-token-hash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", + "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", + "license": "MIT", + "engines": { + "node": "^10.13.0 || >=12.0.0" + } + }, + "node_modules/openid-client": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", + "dependencies": { + "jose": "^4.15.9", + "lru-cache": "^6.0.0", + "object-hash": "^2.2.0", + "oidc-token-hash": "^5.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/openid-client/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1379,6 +3535,67 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/preact": { + "version": "10.26.5", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.5.tgz", + "integrity": "sha512-fmpDkgfGU6JYux9teDWLhj9mKN55tyepwYbxHgQuIxbWQzgFg5vk7Mrrtfx7xRxq798ynkY4DDDxZr235Kk+4w==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz", + "integrity": "sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz", + "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", + "license": "MIT" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", @@ -1388,6 +3605,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-apexcharts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.7.0.tgz", + "integrity": "sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "apexcharts": ">=4.0.0", + "react": ">=0.13" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", @@ -1400,6 +3630,176 @@ "react": "^19.1.0" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.15.2", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.2.tgz", + "integrity": "sha512-xv9lVztv3ingk7V3Jf05wfAZbM9Q2umJzu5t/cfnAK7LUslNrGT7LPBr74G+ok8kSCeFMaePmWMg0rcYOnczTw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -1411,7 +3811,6 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "license": "ISC", - "optional": true, "bin": { "semver": "bin/semver.js" }, @@ -1419,6 +3818,11 @@ "node": ">=10" } }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/sharp": { "version": "0.34.1", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", @@ -1479,6 +3883,15 @@ "node": ">=0.10.0" } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -1510,6 +3923,16 @@ } } }, + "node_modules/tailwind-merge": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", + "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.4.tgz", @@ -1527,12 +3950,27 @@ "node": ">=6" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tw-animate-css": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.2.5.tgz", + "integrity": "sha512-ABzjfgVo+fDbhRREGL4KQZUqqdPgvc5zVrLyeW9/6mVqvaDepXc7EvedA+pYmMnIOsUAQMwcWzNvom26J2qYvQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -1551,8 +3989,100 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, "license": "MIT" + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vaul": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", + "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" } } } diff --git a/package.json b/package.json index 1fd9b33..7f92c91 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { - "name": "traversypress-ui", + "$schema": "https://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/package.json", + "name": "podif", "version": "0.1.0", "private": true, "scripts": { @@ -9,16 +10,44 @@ "lint": "next lint" }, "dependencies": { + "@radix-ui/react-accordion": "^1.2.4", + "@radix-ui/react-avatar": "^1.1.4", + "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dropdown-menu": "^2.1.7", + "@radix-ui/react-label": "^2.1.4", + "@radix-ui/react-select": "^2.2.2", + "@radix-ui/react-slot": "^1.2.0", + "@radix-ui/react-tabs": "^1.1.7", + "@radix-ui/react-toast": "^1.2.10", + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.9", + "apexcharts": "^4.5.0", + "bcryptjs": "^3.0.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "jose": "^6.0.11", + "jsonwebtoken": "^9.0.2", + "lucide-react": "^0.488.0", + "mysql2": "^3.14.0", + "next": "15.3.0", + "next-auth": "^4.24.11", + "next-themes": "^0.4.6", "react": "^19.0.0", + "react-apexcharts": "^1.7.0", "react-dom": "^19.0.0", - "next": "15.3.0" + "recharts": "^2.15.2", + "tailwind-merge": "^3.2.0", + "tw-animate-css": "^1.2.5", + "vaul": "^1.1.2" }, "devDependencies": { - "typescript": "^5", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4" + "prettier": "^3.5.3", + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/postcss.config.mjs b/postcss.config.mjs index c7bcb4b..ba720fe 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,5 +1,5 @@ const config = { - plugins: ["@tailwindcss/postcss"], + plugins: ['@tailwindcss/postcss'], }; export default config; diff --git a/public/podif-icon.png b/public/podif-icon.png new file mode 100644 index 0000000..b70bb18 Binary files /dev/null and b/public/podif-icon.png differ diff --git a/public/vercel.svg b/public/vercel.svg deleted file mode 100644 index 7705396..0000000 --- a/public/vercel.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/setup_database.js b/setup_database.js new file mode 100644 index 0000000..d7a8837 --- /dev/null +++ b/setup_database.js @@ -0,0 +1,40 @@ +const mysql = require('mysql2/promise'); +const fs = require('fs'); +const path = require('path'); + +async function setupDatabase() { + let connection; + try { + // Create connection without database + connection = await mysql.createConnection({ + host: '127.0.0.1', + port: 3306, + user: 'root', + password: 'semogabisayok321' + }); + + console.log('Connected to MySQL server'); + + // Read and execute check_table.sql + const checkTableSql = fs.readFileSync(path.join(__dirname, 'check_table.sql'), 'utf8'); + const statements = checkTableSql.split(';').filter(stmt => stmt.trim()); + + for (const statement of statements) { + if (statement.trim()) { + console.log('Executing:', statement.substring(0, 50) + '...'); + await connection.query(statement); + } + } + + console.log('Database setup completed successfully'); + } catch (error) { + console.error('Error setting up database:', error); + } finally { + if (connection) { + await connection.end(); + console.log('Database connection closed'); + } + } +} + +setupDatabase(); \ No newline at end of file diff --git a/types/next-auth.d.ts b/types/next-auth.d.ts new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/types/next-auth.d.ts @@ -0,0 +1 @@ + \ No newline at end of file