Update Tampilan UI Landing Page + Dashboard Admin

This commit is contained in:
Azis 2025-08-08 21:55:28 +07:00
parent 9dd8e4b9bd
commit 5db127dfd1
15 changed files with 2951 additions and 161 deletions

View File

@ -36,6 +36,13 @@ enum Role {
ADMIN ADMIN
TEACHER TEACHER
STUDENT STUDENT
PARENT
}
enum Gender {
MALE
FEMALE
OTHER
} }
// Model untuk Siswa // Model untuk Siswa
@ -44,6 +51,7 @@ model Student {
userId String @unique userId String @unique
studentNumber String @unique studentNumber String @unique
dateOfBirth DateTime dateOfBirth DateTime
gender Gender @default(OTHER)
address String address String
phone String? phone String?
parentName String parentName String

View File

@ -17,6 +17,17 @@ async function main() {
}, },
}) })
// Create Parent User
const parentPassword = await hashPassword('parent123')
await prisma.user.create({
data: {
email: 'parent@sipintar.com',
name: 'Budi Hartono',
password: parentPassword,
role: 'PARENT',
},
})
// Create Teacher Users and Teacher profiles // Create Teacher Users and Teacher profiles
const teacherPassword = await hashPassword('guru123') const teacherPassword = await hashPassword('guru123')

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View File

@ -0,0 +1,191 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import jwt from 'jsonwebtoken'
export async function GET(request: NextRequest) {
try {
// Get token from Authorization header
const authHeader = request.headers.get('authorization')
const token = authHeader?.replace('Bearer ', '')
if (!token) {
return NextResponse.json(
{ message: 'No token provided' },
{ status: 401 }
)
}
// Verify JWT token
const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as {
userId: string
role: string
email: string
name: string
}
// Check if user has permission (Admin or Teacher)
if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') {
return NextResponse.json(
{ message: 'Insufficient permissions' },
{ status: 403 }
)
}
// Get all classes with related data
const classes = await prisma.class.findMany({
include: {
teacher: {
include: {
user: {
select: {
name: true,
},
},
},
},
subject: {
select: {
name: true,
code: true,
},
},
_count: {
select: {
students: true,
},
},
},
orderBy: [
{ grade: 'asc' },
{ section: 'asc' },
{ name: 'asc' },
],
})
return NextResponse.json(classes)
} catch (error) {
console.error('Get classes error:', error)
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
// Get token from Authorization header
const authHeader = request.headers.get('authorization')
const token = authHeader?.replace('Bearer ', '')
if (!token) {
return NextResponse.json(
{ message: 'No token provided' },
{ status: 401 }
)
}
// Verify JWT token
const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as {
userId: string
role: string
email: string
name: string
}
// Check if user has permission (Admin only)
if (decoded.role !== 'ADMIN') {
return NextResponse.json(
{ message: 'Insufficient permissions' },
{ status: 403 }
)
}
const {
name,
grade,
section,
maxStudents,
room,
teacherId,
subjectId,
} = await request.json()
// Validate required fields
if (!name || !grade || !section || !maxStudents || !room || !teacherId || !subjectId) {
return NextResponse.json(
{ message: 'Missing required fields' },
{ status: 400 }
)
}
// Check if teacher and subject exist
const teacher = await prisma.teacher.findUnique({
where: { id: teacherId },
})
if (!teacher) {
return NextResponse.json(
{ message: 'Teacher not found' },
{ status: 400 }
)
}
const subject = await prisma.subject.findUnique({
where: { id: subjectId },
})
if (!subject) {
return NextResponse.json(
{ message: 'Subject not found' },
{ status: 400 }
)
}
// Create class
const newClass = await prisma.class.create({
data: {
name,
grade,
section,
maxStudents: parseInt(maxStudents),
room,
teacherId,
subjectId,
},
include: {
teacher: {
include: {
user: {
select: {
name: true,
},
},
},
},
subject: {
select: {
name: true,
code: true,
},
},
_count: {
select: {
students: true,
},
},
},
})
return NextResponse.json({
message: 'Class created successfully',
class: newClass,
})
} catch (error) {
console.error('Create class error:', error)
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@ -23,9 +23,9 @@ export async function GET(request: NextRequest) {
name: string name: string
} }
if (decoded.role !== 'ADMIN') { if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') {
return NextResponse.json( return NextResponse.json(
{ message: 'Access denied. Admin role required.' }, { message: 'Access denied. Admin or Teacher role required.' },
{ status: 403 } { status: 403 }
) )
} }
@ -38,11 +38,49 @@ export async function GET(request: NextRequest) {
prisma.subject.count({ where: { isActive: true } }), prisma.subject.count({ where: { isActive: true } }),
]) ])
// Get class distribution by grade and section
const classDistribution = await prisma.class.groupBy({
by: ['grade', 'section'],
where: { isActive: true },
_count: {
id: true,
},
})
// Format class distribution data
const classByGrade = classDistribution.reduce((acc: Record<string, number>, curr) => {
const key = curr.grade
if (!acc[key]) {
acc[key] = 0
}
acc[key] += curr._count.id
return acc
}, {})
const classBySection = classDistribution.reduce((acc: Record<string, number>, curr) => {
const key = curr.section
if (!acc[key]) {
acc[key] = 0
}
acc[key] += curr._count.id
return acc
}, {})
// Mock gender statistics for now (until Prisma client is regenerated)
const genderStats = {
male: Math.floor(totalStudents * 0.58), // 58% male
female: Math.floor(totalStudents * 0.42), // 42% female
other: 0 // Remove other category
}
return NextResponse.json({ return NextResponse.json({
totalStudents, totalStudents,
totalTeachers, totalTeachers,
totalClasses, totalClasses,
totalSubjects, totalSubjects,
studentsByGender: genderStats,
classByGrade,
classBySection,
}) })
} catch (error) { } catch (error) {
console.error('Dashboard stats error:', error) console.error('Dashboard stats error:', error)

View File

@ -0,0 +1,172 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import jwt from 'jsonwebtoken'
export async function GET(request: NextRequest) {
try {
// Get token from Authorization header
const authHeader = request.headers.get('authorization')
const token = authHeader?.replace('Bearer ', '')
if (!token) {
return NextResponse.json(
{ message: 'No token provided' },
{ status: 401 }
)
}
// Verify JWT token
const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as {
userId: string
role: string
email: string
name: string
}
// Check if user has permission (Admin or Teacher)
if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') {
return NextResponse.json(
{ message: 'Insufficient permissions' },
{ status: 403 }
)
}
// Get all subjects
const subjects = await prisma.subject.findMany({
where: {
isActive: true,
},
include: {
teacher: {
include: {
user: {
select: {
name: true,
},
},
},
},
_count: {
select: {
classes: true,
},
},
},
orderBy: {
name: 'asc',
},
})
return NextResponse.json(subjects)
} catch (error) {
console.error('Get subjects error:', error)
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
// Get token from Authorization header
const authHeader = request.headers.get('authorization')
const token = authHeader?.replace('Bearer ', '')
if (!token) {
return NextResponse.json(
{ message: 'No token provided' },
{ status: 401 }
)
}
// Verify JWT token
const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as {
userId: string
role: string
email: string
name: string
}
// Check if user has permission (Admin only)
if (decoded.role !== 'ADMIN') {
return NextResponse.json(
{ message: 'Insufficient permissions' },
{ status: 403 }
)
}
const {
name,
code,
description,
credits,
teacherId,
} = await request.json()
// Validate required fields
if (!name || !code || !teacherId) {
return NextResponse.json(
{ message: 'Missing required fields' },
{ status: 400 }
)
}
// Check if code already exists
const existingSubject = await prisma.subject.findUnique({
where: { code },
})
if (existingSubject) {
return NextResponse.json(
{ message: 'Subject code already exists' },
{ status: 400 }
)
}
// Check if teacher exists
const teacher = await prisma.teacher.findUnique({
where: { id: teacherId },
})
if (!teacher) {
return NextResponse.json(
{ message: 'Teacher not found' },
{ status: 400 }
)
}
// Create subject
const subject = await prisma.subject.create({
data: {
name,
code,
description,
credits: parseInt(credits) || 1,
teacherId,
},
include: {
teacher: {
include: {
user: {
select: {
name: true,
},
},
},
},
},
})
return NextResponse.json({
message: 'Subject created successfully',
subject,
})
} catch (error) {
console.error('Create subject error:', error)
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@ -0,0 +1,192 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
import { hashPassword } from '@/lib/auth'
import jwt from 'jsonwebtoken'
export async function GET(request: NextRequest) {
try {
// Get token from Authorization header
const authHeader = request.headers.get('authorization')
const token = authHeader?.replace('Bearer ', '')
if (!token) {
return NextResponse.json(
{ message: 'No token provided' },
{ status: 401 }
)
}
// Verify JWT token
const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as {
userId: string
role: string
email: string
name: string
}
// Check if user has permission (Admin or Teacher)
if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') {
return NextResponse.json(
{ message: 'Insufficient permissions' },
{ status: 403 }
)
}
// Get all teachers with user information
const teachers = await prisma.teacher.findMany({
include: {
user: {
select: {
id: true,
name: true,
email: true,
isActive: true,
},
},
subjects: {
select: {
name: true,
code: true,
},
},
},
orderBy: {
teacherNumber: 'asc',
},
})
return NextResponse.json(teachers)
} catch (error) {
console.error('Get teachers error:', error)
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
// Get token from Authorization header
const authHeader = request.headers.get('authorization')
const token = authHeader?.replace('Bearer ', '')
if (!token) {
return NextResponse.json(
{ message: 'No token provided' },
{ status: 401 }
)
}
// Verify JWT token
const decoded = jwt.verify(token, process.env.NEXTAUTH_SECRET || 'fallback-secret') as {
userId: string
role: string
email: string
name: string
}
// Check if user has permission (Admin only)
if (decoded.role !== 'ADMIN') {
return NextResponse.json(
{ message: 'Insufficient permissions' },
{ status: 403 }
)
}
const {
name,
email,
password,
teacherNumber,
specialization,
qualification,
experience,
phone,
address,
} = await request.json()
// Validate required fields
if (!name || !email || !password || !teacherNumber || !specialization || !qualification || experience === undefined) {
return NextResponse.json(
{ message: 'Missing required fields' },
{ status: 400 }
)
}
// Check if email or teacher number already exists
const existingUser = await prisma.user.findUnique({
where: { email },
})
if (existingUser) {
return NextResponse.json(
{ message: 'Email already exists' },
{ status: 400 }
)
}
const existingTeacher = await prisma.teacher.findUnique({
where: { teacherNumber },
})
if (existingTeacher) {
return NextResponse.json(
{ message: 'Teacher number already exists' },
{ status: 400 }
)
}
// Hash password
const hashedPassword = await hashPassword(password)
// Create user and teacher in a transaction
const result = await prisma.$transaction(async (tx) => {
// Create user
const user = await tx.user.create({
data: {
email,
name,
password: hashedPassword,
role: 'TEACHER',
},
})
// Create teacher profile
const teacher = await tx.teacher.create({
data: {
userId: user.id,
teacherNumber,
specialization,
qualification,
experience: parseInt(experience),
phone,
address,
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
isActive: true,
},
},
},
})
return teacher
})
return NextResponse.json({
message: 'Teacher created successfully',
teacher: result,
})
} catch (error) {
console.error('Create teacher error:', error)
return NextResponse.json(
{ message: 'Internal server error' },
{ status: 500 }
)
}
}

View File

@ -27,9 +27,18 @@ export default function LoginPage() {
if (response.ok) { if (response.ok) {
const data = await response.json() const data = await response.json()
// Store token or handle authentication // Store token and user data
localStorage.setItem('token', data.token) localStorage.setItem('token', data.token)
router.push('/dashboard') localStorage.setItem('user', JSON.stringify(data.user))
// Redirect based on user role
if (data.user.role === 'ADMIN' || data.user.role === 'TEACHER') {
router.push('/dashboard/admin')
} else if (data.user.role === 'PARENT') {
router.push('/dashboard/parent')
} else {
router.push('/dashboard') // fallback
}
} else { } else {
const data = await response.json() const data = await response.json()
setError(data.message || 'Login failed') setError(data.message || 'Login failed')
@ -71,7 +80,7 @@ export default function LoginPage() {
required required
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500" className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="Masukkan email Anda" placeholder="Masukkan email Anda"
/> />
</div> </div>
@ -87,7 +96,7 @@ export default function LoginPage() {
required required
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500" className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
placeholder="Masukkan password Anda" placeholder="Masukkan password Anda"
/> />
</div> </div>
@ -137,9 +146,8 @@ export default function LoginPage() {
<div className="mt-8 p-4 bg-gray-50 rounded-md"> <div className="mt-8 p-4 bg-gray-50 rounded-md">
<h4 className="text-sm font-medium text-gray-900 mb-2">Demo Accounts:</h4> <h4 className="text-sm font-medium text-gray-900 mb-2">Demo Accounts:</h4>
<div className="text-xs text-gray-600 space-y-1"> <div className="text-xs text-gray-600 space-y-1">
<p><strong>Admin:</strong> admin@sipintar.com / admin123</p> <p><strong>Admin (Guru):</strong> admin@sipintar.com / admin123</p>
<p><strong>Guru:</strong> guru@sipintar.com / guru123</p> <p><strong>Orang Tua:</strong> parent@sipintar.com / parent123</p>
<p><strong>Siswa:</strong> siswa@sipintar.com / siswa123</p>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,681 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import Image from 'next/image'
interface User {
id: string
name: string
email: string
role: string
}
export default function AdminDashboard() {
const [user, setUser] = useState<User | null>(null)
const [stats, setStats] = useState({
totalStudents: 0,
totalTeachers: 0,
totalClasses: 0,
totalSubjects: 0,
studentsByGender: {
male: 0,
female: 0,
other: 0
},
classByGrade: {} as Record<string, number>,
classBySection: {} as Record<string, number>,
})
const [sidebarOpen, setSidebarOpen] = useState(false)
const [sidebarHovered, setSidebarHovered] = useState(false)
const router = useRouter()
useEffect(() => {
const token = localStorage.getItem('token')
const userData = localStorage.getItem('user')
if (!token || !userData) {
router.push('/auth/login')
return
}
const parsedUser = JSON.parse(userData)
if (parsedUser.role !== 'ADMIN' && parsedUser.role !== 'TEACHER') {
router.push('/auth/login')
return
}
setUser(parsedUser)
fetchStats()
}, [router])
const fetchStats = async () => {
try {
const token = localStorage.getItem('token')
const response = await fetch('/api/dashboard/stats', {
headers: { 'Authorization': `Bearer ${token}` }
})
if (response.ok) {
const data = await response.json()
setStats(data)
}
} catch (error) {
console.error('Error fetching stats:', error)
}
}
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
router.push('/')
}
if (!user) {
return <div className="flex items-center justify-center min-h-screen">Loading...</div>
}
return (
<div className="min-h-screen bg-gray-50 flex">
{/* Hover Sidebar */}
<div
className="hidden lg:flex lg:flex-shrink-0 group"
onMouseEnter={() => setSidebarHovered(true)}
onMouseLeave={() => setSidebarHovered(false)}
>
<div className={`flex flex-col transition-all duration-300 ease-in-out ${sidebarHovered ? 'w-64' : 'w-16'}`}>
<div className="flex flex-col h-0 flex-1 shadow-lg" style={{ backgroundColor: '#0D1320' }}>
{/* Logo */}
<div className="flex items-center h-16 flex-shrink-0 px-4" style={{ backgroundColor: '#0D1320' }}>
<Image
src="/Logo Geometris dengan Topi Wisuda.png"
alt="SIPINTAR Logo"
width={32}
height={32}
className={`transition-all duration-300 ${sidebarHovered ? 'mr-3' : 'mx-auto'}`}
/>
<h1 className={`text-xl font-bold text-white transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
SIPINTAR
</h1>
</div>
{/* Navigation */}
<nav className="mt-5 flex-1 px-2 space-y-1">
<Link
href="/dashboard/admin"
className="bg-blue-800 bg-opacity-50 text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
title="Dashboard"
>
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 5a2 2 0 012-2h4a2 2 0 012 2v6H8V5z" />
</svg>
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
Dashboard
</span>
</Link>
<Link
href="/dashboard/students"
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
title="Siswa"
>
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
Siswa
</span>
</Link>
<Link
href="/dashboard/teachers"
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
title="Guru"
>
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
Guru
</span>
</Link>
<Link
href="/dashboard/classes"
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
title="Kelas"
>
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
Kelas
</span>
</Link>
<Link
href="/dashboard/subjects"
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
title="Mata Pelajaran"
>
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
Mata Pelajaran
</span>
</Link>
<Link
href="/dashboard/attendance"
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
title="Absensi"
>
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
</svg>
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
Absensi
</span>
</Link>
<Link
href="/dashboard/grades"
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
title="Nilai"
>
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
Nilai
</span>
</Link>
<Link
href="/dashboard/reports"
className="text-gray-300 hover:bg-gray-700 hover:bg-opacity-50 hover:text-white group flex items-center px-2 py-2 text-sm font-medium rounded-md"
title="Laporan"
>
<svg className="flex-shrink-0 h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<span className={`ml-3 transition-all duration-300 ${sidebarHovered ? 'opacity-100' : 'opacity-0 w-0'} overflow-hidden`}>
Laporan
</span>
</Link>
</nav>
</div>
</div>
</div>
{/* Mobile sidebar */}
<div className={`lg:hidden fixed inset-0 flex z-40 ${sidebarOpen ? 'block' : 'hidden'}`}>
<div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setSidebarOpen(false)}></div>
<div className="relative flex-1 flex flex-col max-w-xs w-full" style={{ backgroundColor: '#0D1320' }}>
<div className="absolute top-0 right-0 -mr-12 pt-2">
<button
onClick={() => setSidebarOpen(false)}
className="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"
>
<svg className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Mobile navigation - same as desktop */}
<div className="flex-1 h-0 pt-5 pb-4 overflow-y-auto">
<div className="flex items-center flex-shrink-0 px-4 py-4" style={{ backgroundColor: '#0D1320' }}>
<Image
src="/Logo Geometris dengan Topi Wisuda.png"
alt="SIPINTAR Logo"
width={32}
height={32}
className="mr-3"
/>
<h1 className="text-xl font-bold text-white">
SIPINTAR
</h1>
</div>
<nav className="mt-5 px-2 space-y-1">
{/* Same navigation items as desktop */}
</nav>
</div>
</div>
</div>
{/* Main content */}
<div className="flex flex-col w-0 flex-1 overflow-hidden">
{/* Top header */}
<div className="relative z-10 flex-shrink-0 flex h-16 bg-white shadow">
<button
onClick={() => setSidebarOpen(true)}
className="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500 lg:hidden"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
<div className="flex-1 px-4 flex justify-between items-center">
<div className="flex-1">
<h1 className="text-2xl font-semibold text-gray-900">Dashboard Admin</h1>
</div>
<div className="ml-4 flex items-center md:ml-6">
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="text-sm font-medium text-gray-900">{user.name}</p>
<p className="text-xs text-gray-500">{user.role}</p>
</div>
<button
onClick={handleLogout}
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors"
>
Logout
</button>
</div>
</div>
</div>
</div>
{/* Main content area */}
<main className="flex-1 relative overflow-y-auto focus:outline-none">
<div className="py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
{/* Welcome Section */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Selamat Datang, {user.name}!
</h2>
<p className="text-gray-600">Kelola sistem sekolah dengan mudah dan efisien</p>
</div>
{/* Stats Cards and Gender Statistics */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
{/* Gender Statistics Circle Chart */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Total Murid</h3>
<div className="flex flex-col items-center">
<div className="relative w-48 h-48 mb-4">
{/* SVG Circle Chart */}
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 200 200">
{/* Background circle */}
<circle
cx="100"
cy="100"
r="80"
fill="none"
stroke="#f3f4f6"
strokeWidth="20"
/>
{/* Male segment */}
{stats.studentsByGender.male > 0 && (
<circle
cx="100"
cy="100"
r="80"
fill="none"
stroke="#3b82f6"
strokeWidth="20"
strokeDasharray={`${(stats.studentsByGender.male / stats.totalStudents) * 502.65} 502.65`}
strokeDashoffset="0"
strokeLinecap="round"
/>
)}
{/* Female segment */}
{stats.studentsByGender.female > 0 && (
<circle
cx="100"
cy="100"
r="80"
fill="none"
stroke="#ec4899"
strokeWidth="20"
strokeDasharray={`${(stats.studentsByGender.female / stats.totalStudents) * 502.65} 502.65`}
strokeDashoffset={`-${(stats.studentsByGender.male / stats.totalStudents) * 502.65}`}
strokeLinecap="round"
/>
)}
{/* Center number */}
<text x="100" y="108" textAnchor="middle" style={{ fill: '#111827', fontSize: '28px', fontWeight: '700', fontStyle: 'normal' }} transform="rotate(90 100 100)">
{stats.totalStudents}
</text>
</svg>
</div>
{/* Legend */}
<div className="space-y-2 w-full">
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="w-3 h-3 bg-blue-500 rounded-full mr-2"></div>
<span className="text-sm text-gray-700">Laki-laki</span>
</div>
<div className="text-right">
<span className="text-sm font-bold text-gray-900">{stats.studentsByGender.male}</span>
<span className="text-xs text-gray-500 ml-1">
({stats.totalStudents > 0 ? Math.round((stats.studentsByGender.male / stats.totalStudents) * 100) : 0}%)
</span>
</div>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center">
<div className="w-3 h-3 bg-pink-500 rounded-full mr-2"></div>
<span className="text-sm text-gray-700">Perempuan</span>
</div>
<div className="text-right">
<span className="text-sm font-bold text-gray-900">{stats.studentsByGender.female}</span>
<span className="text-xs text-gray-500 ml-1">
({stats.totalStudents > 0 ? Math.round((stats.studentsByGender.female / stats.totalStudents) * 100) : 0}%)
</span>
</div>
</div>
</div>
</div>
</div>
{/* Stats Cards */}
<div className="lg:col-span-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Total Siswa</p>
<p className="text-2xl font-bold text-gray-900">{stats.totalStudents}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Total Guru</p>
<p className="text-2xl font-bold text-gray-900">{stats.totalTeachers}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-purple-500 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2-2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Total Kelas</p>
<p className="text-2xl font-bold text-gray-900">{stats.totalClasses}</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className="w-8 h-8 bg-orange-500 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Total Mata Pelajaran</p>
<p className="text-2xl font-bold text-gray-900">{stats.totalSubjects}</p>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h3 className="text-lg font-medium text-gray-900 mb-4">Aksi Cepat</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Link
href="/dashboard/students"
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-4">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Kelola Siswa</p>
<p className="text-sm text-gray-500">Tambah, edit, atau lihat data siswa</p>
</div>
</Link>
<Link
href="/dashboard/teachers"
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center mr-4">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Kelola Guru</p>
<p className="text-sm text-gray-500">Tambah, edit, atau lihat data guru</p>
</div>
</Link>
<Link
href="/dashboard/classes"
className="flex items-center p-4 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
>
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center mr-4">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Kelola Kelas</p>
<p className="text-sm text-gray-500">Atur kelas dan mata pelajaran</p>
</div>
</Link>
</div>
</div>
{/* Class Distribution Bar Chart */}
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h3 className="text-lg font-medium text-gray-900 mb-6">Distribusi Kelas</h3>
<div className="space-y-6">
{/* Stacked Bar Chart by Grade with Sections */}
<div>
<h4 className="text-md font-medium text-gray-700 mb-4">Distribusi Kelas Berdasarkan Tingkat dan Jurusan</h4>
{/* Legend */}
<div className="flex flex-wrap gap-4 mb-6">
<div className="flex items-center">
<div className="w-4 h-4 bg-blue-500 rounded mr-2"></div>
<span className="text-sm text-gray-700">IPA</span>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-green-500 rounded mr-2"></div>
<span className="text-sm text-gray-700">IPS</span>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-orange-500 rounded mr-2"></div>
<span className="text-sm text-gray-700">BAHASA</span>
</div>
<div className="flex items-center">
<div className="w-4 h-4 bg-purple-500 rounded mr-2"></div>
<span className="text-sm text-gray-700">Lainnya</span>
</div>
</div>
{/* Stacked Bars */}
<div className="space-y-4">
{(() => {
// Group classes by grade and section
const gradeData: Record<string, Record<string, number>> = {}
// Initialize grade data structure
Object.entries(stats.classByGrade).forEach(([grade]) => {
gradeData[grade] = { IPA: 0, IPS: 0, BAHASA: 0, OTHER: 0 }
})
// This would ideally come from a more detailed API
// For now, we'll simulate the distribution
Object.entries(stats.classByGrade).forEach(([grade, total]) => {
if (!gradeData[grade]) gradeData[grade] = { IPA: 0, IPS: 0, BAHASA: 0, OTHER: 0 }
// Simulate distribution (this would come from real data)
const ipaCount = Math.floor(total * 0.4)
const ipsCount = Math.floor(total * 0.35)
const bahasaCount = Math.floor(total * 0.15)
const otherCount = total - ipaCount - ipsCount - bahasaCount
gradeData[grade] = {
IPA: ipaCount,
IPS: ipsCount,
BAHASA: bahasaCount,
OTHER: Math.max(0, otherCount)
}
})
const maxTotal = Math.max(...Object.entries(gradeData).map(([, sections]) =>
Object.values(sections).reduce((sum, count) => sum + count, 0)
))
return Object.entries(gradeData).map(([grade, sections]) => {
const total = Object.values(sections).reduce((sum, count) => sum + count, 0)
const totalPercentage = maxTotal > 0 ? (total / maxTotal) * 100 : 0
return (
<div key={grade} className="flex items-center">
{/* Grade Label */}
<div className="w-20 text-sm font-medium text-gray-700 text-right mr-4">
Kelas {grade}
</div>
{/* Stacked Bar Container */}
<div className="flex-1 relative">
<div
className="flex h-8 bg-gray-100 rounded overflow-hidden"
style={{ width: `${Math.max(totalPercentage, 20)}%` }}
>
{/* IPA Segment */}
{sections.IPA > 0 && (
<div
className="bg-blue-500 flex items-center justify-center text-white text-xs font-medium"
style={{ width: `${(sections.IPA / total) * 100}%` }}
>
{sections.IPA > 0 && total > 5 && sections.IPA}
</div>
)}
{/* IPS Segment */}
{sections.IPS > 0 && (
<div
className="bg-green-500 flex items-center justify-center text-white text-xs font-medium"
style={{ width: `${(sections.IPS / total) * 100}%` }}
>
{sections.IPS > 0 && total > 5 && sections.IPS}
</div>
)}
{/* BAHASA Segment */}
{sections.BAHASA > 0 && (
<div
className="bg-orange-500 flex items-center justify-center text-white text-xs font-medium"
style={{ width: `${(sections.BAHASA / total) * 100}%` }}
>
{sections.BAHASA > 0 && total > 3 && sections.BAHASA}
</div>
)}
{/* OTHER Segment */}
{sections.OTHER > 0 && (
<div
className="bg-purple-500 flex items-center justify-center text-white text-xs font-medium"
style={{ width: `${(sections.OTHER / total) * 100}%` }}
>
{sections.OTHER > 0 && total > 3 && sections.OTHER}
</div>
)}
</div>
</div>
{/* Total Count */}
<div className="w-12 text-right text-sm text-gray-600 ml-4">
{total}
</div>
</div>
)
})
})()}
</div>
{Object.keys(stats.classByGrade).length === 0 && (
<div className="text-center py-8">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<p className="mt-2 text-gray-500 text-sm">Belum ada data kelas untuk ditampilkan</p>
</div>
)}
</div>
</div>
</div>
{/* Recent Activity */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Aktivitas Terbaru</h3>
<div className="space-y-4">
<div className="flex items-center p-4 bg-gray-50 rounded-lg">
<div className="w-2 h-2 bg-green-500 rounded-full mr-3"></div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">Siswa baru ditambahkan</p>
<p className="text-xs text-gray-500">2 jam yang lalu</p>
</div>
</div>
<div className="flex items-center p-4 bg-gray-50 rounded-lg">
<div className="w-2 h-2 bg-blue-500 rounded-full mr-3"></div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">Laporan absensi bulan ini</p>
<p className="text-xs text-gray-500">5 jam yang lalu</p>
</div>
</div>
<div className="flex items-center p-4 bg-gray-50 rounded-lg">
<div className="w-2 h-2 bg-yellow-500 rounded-full mr-3"></div>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">Nilai ujian diperbarui</p>
<p className="text-xs text-gray-500">1 hari yang lalu</p>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
)
}

View File

@ -0,0 +1,448 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
interface Class {
id: string
name: string
grade: string
section: string
maxStudents: number
room: string
teacher: {
user: {
name: string
}
teacherNumber: string
}
subject: {
name: string
code: string
}
_count: {
students: number
}
}
interface Teacher {
id: string
user: {
name: string
}
teacherNumber: string
}
interface Subject {
id: string
name: string
code: string
}
export default function ClassesPage() {
const [classes, setClasses] = useState<Class[]>([])
const [teachers, setTeachers] = useState<Teacher[]>([])
const [subjects, setSubjects] = useState<Subject[]>([])
const [isLoading, setIsLoading] = useState(true)
const [showAddForm, setShowAddForm] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [formData, setFormData] = useState({
name: '',
grade: '',
section: '',
maxStudents: '',
room: '',
teacherId: '',
subjectId: '',
})
const router = useRouter()
useEffect(() => {
const token = localStorage.getItem('token')
if (!token) {
router.push('/auth/login')
return
}
fetchData()
}, [router])
const fetchData = async () => {
try {
const token = localStorage.getItem('token')
// Fetch classes, teachers, and subjects
const [classesRes, teachersRes, subjectsRes] = await Promise.all([
fetch('/api/classes', {
headers: { 'Authorization': `Bearer ${token}` },
}),
fetch('/api/teachers', {
headers: { 'Authorization': `Bearer ${token}` },
}),
fetch('/api/subjects', {
headers: { 'Authorization': `Bearer ${token}` },
})
])
if (classesRes.ok) {
const data = await classesRes.json()
setClasses(data)
}
if (teachersRes.ok) {
const data = await teachersRes.json()
setTeachers(data)
}
if (subjectsRes.ok) {
const data = await subjectsRes.json()
setSubjects(data)
}
} catch (error) {
console.error('Failed to fetch data:', error)
} finally {
setIsLoading(false)
}
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value } = e.target
setFormData(prev => ({
...prev,
[name]: value
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
const token = localStorage.getItem('token')
const response = await fetch('/api/classes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
...formData,
maxStudents: parseInt(formData.maxStudents) || 30
}),
})
if (response.ok) {
// Reset form and close modal
setFormData({
name: '',
grade: '',
section: '',
maxStudents: '',
room: '',
teacherId: '',
subjectId: '',
})
setShowAddForm(false)
// Refresh classes list
await fetchData()
alert('Kelas berhasil ditambahkan!')
} else {
const error = await response.json()
alert(error.message || 'Gagal menambahkan kelas')
}
} catch (error) {
console.error('Error adding class:', error)
alert('Terjadi kesalahan saat menambahkan kelas')
} finally {
setIsSubmitting(false)
}
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-purple-600"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900">SIPINTAR</h1>
<span className="ml-2 text-sm text-gray-500">Manajemen Kelas</span>
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => router.push('/dashboard/admin')}
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
Kembali ke Dashboard
</button>
</div>
</div>
</div>
</nav>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-bold text-gray-900">Manajemen Kelas</h2>
<p className="text-gray-600">Kelola kelas dan mata pelajaran</p>
</div>
<button
onClick={() => setShowAddForm(true)}
className="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-md text-sm font-medium"
>
+ Tambah Kelas
</button>
</div>
{/* Classes Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{classes.length === 0 ? (
<div className="col-span-full">
<div className="text-center py-12">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">Belum ada kelas</h3>
<p className="mt-1 text-sm text-gray-500">Mulai dengan menambahkan kelas baru.</p>
<div className="mt-6">
<button
onClick={() => setShowAddForm(true)}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-purple-600 hover:bg-purple-700"
>
+ Tambah Kelas Pertama
</button>
</div>
</div>
</div>
) : (
classes.map((classItem) => (
<div key={classItem.id} className="bg-white rounded-lg shadow-sm border p-6 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between">
<div className="flex-1">
<h3 className="text-lg font-medium text-gray-900 mb-2">
{classItem.name}
</h3>
<div className="space-y-2 text-sm text-gray-600">
<div className="flex items-center">
<span className="font-medium">Tingkat:</span>
<span className="ml-2">{classItem.grade} - {classItem.section}</span>
</div>
<div className="flex items-center">
<span className="font-medium">Ruang:</span>
<span className="ml-2">{classItem.room}</span>
</div>
<div className="flex items-center">
<span className="font-medium">Wali Kelas:</span>
<span className="ml-2">{classItem.teacher.user.name}</span>
</div>
<div className="flex items-center">
<span className="font-medium">Mata Pelajaran:</span>
<span className="ml-2">{classItem.subject.name} ({classItem.subject.code})</span>
</div>
<div className="flex items-center">
<span className="font-medium">Siswa:</span>
<span className="ml-2">
{classItem._count.students} / {classItem.maxStudents}
</span>
</div>
</div>
</div>
<div className="ml-4">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${classItem._count.students >= classItem.maxStudents
? 'bg-red-100 text-red-800'
: classItem._count.students >= classItem.maxStudents * 0.8
? 'bg-yellow-100 text-yellow-800'
: 'bg-green-100 text-green-800'
}`}>
{classItem._count.students >= classItem.maxStudents
? 'Penuh'
: classItem._count.students >= classItem.maxStudents * 0.8
? 'Hampir Penuh'
: 'Tersedia'
}
</span>
</div>
</div>
<div className="mt-6 flex space-x-3">
<button className="text-purple-600 hover:text-purple-900 text-sm font-medium">
Edit
</button>
<button className="text-red-600 hover:text-red-900 text-sm font-medium">
Hapus
</button>
</div>
</div>
))
)}
</div>
{/* Add Class Modal */}
{showAddForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<h3 className="text-lg font-medium text-gray-900 mb-6">Tambah Kelas Baru</h3>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nama Kelas *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
placeholder="Contoh: X-IPA-1"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tingkat/Grade *
</label>
<select
name="grade"
value={formData.grade}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Pilih Tingkat</option>
<option value="X">X (Sepuluh)</option>
<option value="XI">XI (Sebelas)</option>
<option value="XII">XII (Dua Belas)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Jurusan *
</label>
<select
name="section"
value={formData.section}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Pilih Jurusan</option>
<option value="IPA">IPA</option>
<option value="IPS">IPS</option>
<option value="BAHASA">BAHASA</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Ruang Kelas *
</label>
<input
type="text"
name="room"
value={formData.room}
onChange={handleInputChange}
required
placeholder="Contoh: R101"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Maksimal Siswa *
</label>
<input
type="number"
name="maxStudents"
value={formData.maxStudents}
onChange={handleInputChange}
required
min="1"
max="50"
placeholder="30"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Wali Kelas *
</label>
<select
name="teacherId"
value={formData.teacherId}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Pilih Wali Kelas</option>
{teachers.map((teacher) => (
<option key={teacher.id} value={teacher.id}>
{teacher.user.name} ({teacher.teacherNumber})
</option>
))}
</select>
</div>
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
Mata Pelajaran *
</label>
<select
name="subjectId"
value={formData.subjectId}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500"
>
<option value="">Pilih Mata Pelajaran</option>
{subjects.map((subject) => (
<option key={subject.id} value={subject.id}>
{subject.name} ({subject.code})
</option>
))}
</select>
</div>
</div>
<div className="flex justify-end space-x-3 pt-6 border-t">
<button
type="button"
onClick={() => setShowAddForm(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
disabled={isSubmitting}
>
Batal
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSubmitting}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Kelas'}
</button>
</div>
</form>
</div>
</div>
</div>
)}
</div>
</div>
)
}

View File

@ -0,0 +1,263 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Image from 'next/image'
interface User {
id: string
name: string
email: string
role: string
}
interface Child {
id: string
name: string
studentNumber: string
class: string
attendance: number
lastGrade: string
}
export default function ParentDashboard() {
const [user, setUser] = useState<User | null>(null)
const [children] = useState<Child[]>([
{
id: '1',
name: 'Ahmad Pratama',
studentNumber: 'SW001',
class: 'X IPA 1',
attendance: 95,
lastGrade: 'A'
}
])
const router = useRouter()
useEffect(() => {
const token = localStorage.getItem('token')
const userData = localStorage.getItem('user')
if (!token || !userData) {
router.push('/auth/login')
return
}
const parsedUser = JSON.parse(userData)
if (parsedUser.role !== 'PARENT') {
router.push('/auth/login')
return
}
setUser(parsedUser)
}, [router])
const handleLogout = () => {
localStorage.removeItem('token')
localStorage.removeItem('user')
router.push('/')
}
if (!user) {
return <div className="flex items-center justify-center min-h-screen">Loading...</div>
}
return (
<div className="min-h-screen bg-gray-50">
{/* Header */}
<header className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
<div className="flex items-center space-x-3">
<Image
src="/Logo Geometris dengan Topi Wisuda.png"
alt="SIPINTAR Logo"
width={32}
height={32}
/>
<h1 className="text-xl font-bold text-blue-700">SIPINTAR</h1>
<span className="text-gray-500">|</span>
<span className="text-gray-600">Dashboard Orang Tua</span>
</div>
<div className="flex items-center space-x-4">
<div className="text-right">
<p className="text-sm font-medium text-gray-900">{user.name}</p>
<p className="text-xs text-gray-500">Orang Tua</p>
</div>
<button
onClick={handleLogout}
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors"
>
Logout
</button>
</div>
</div>
</div>
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Welcome Section */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Selamat Datang, {user.name}!
</h2>
<p className="text-gray-600">Monitor perkembangan akademik anak Anda</p>
</div>
{/* Children Cards */}
<div className="grid gap-6 mb-8">
{children.map((child) => (
<div key={child.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-xl font-bold text-gray-900">{child.name}</h3>
<p className="text-gray-600">NIS: {child.studentNumber} | Kelas: {child.class}</p>
</div>
<div className="text-right">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
<svg className="w-8 h-8 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div className="bg-green-50 p-4 rounded-lg border border-green-200">
<div className="flex items-center">
<div className="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<p className="text-sm font-medium text-green-700">Kehadiran</p>
<p className="text-xl font-bold text-green-900">{child.attendance}%</p>
</div>
</div>
</div>
<div className="bg-blue-50 p-4 rounded-lg border border-blue-200">
<div className="flex items-center">
<div className="w-8 h-8 bg-blue-100 rounded-full flex items-center justify-center mr-3">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<div>
<p className="text-sm font-medium text-blue-700">Nilai Terakhir</p>
<p className="text-xl font-bold text-blue-900">{child.lastGrade}</p>
</div>
</div>
</div>
<div className="bg-purple-50 p-4 rounded-lg border border-purple-200">
<div className="flex items-center">
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center mr-3">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.746 0 3.332.477 4.5 1.253v13C19.832 18.477 18.246 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
<div>
<p className="text-sm font-medium text-purple-700">Mata Pelajaran</p>
<p className="text-xl font-bold text-purple-900">12</p>
</div>
</div>
</div>
</div>
{/* Quick Actions for this child */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<button className="flex items-center justify-center p-3 border border-blue-200 rounded-lg hover:bg-blue-50 transition-colors">
<svg className="w-5 h-5 text-blue-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span className="text-sm font-medium text-blue-700">Lihat Nilai</span>
</button>
<button className="flex items-center justify-center p-3 border border-green-200 rounded-lg hover:bg-green-50 transition-colors">
<svg className="w-5 h-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-sm font-medium text-green-700">Absensi</span>
</button>
<button className="flex items-center justify-center p-3 border border-purple-200 rounded-lg hover:bg-purple-50 transition-colors">
<svg className="w-5 h-5 text-purple-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3a4 4 0 118 0v4m-4 6v6m0 0v3m0-3h6m-6 0H6" />
</svg>
<span className="text-sm font-medium text-purple-700">Jadwal</span>
</button>
</div>
</div>
))}
</div>
{/* Recent Updates */}
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h3 className="text-lg font-medium text-gray-900 mb-4">Pemberitahuan Terbaru</h3>
<div className="space-y-4">
<div className="flex items-start p-4 bg-blue-50 rounded-lg border border-blue-200">
<div className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3"></div>
<div className="flex-1">
<p className="text-sm font-medium text-blue-900">Nilai Ujian Matematika</p>
<p className="text-xs text-blue-700 mt-1">Ahmad Pratama mendapat nilai A untuk ujian matematika</p>
<p className="text-xs text-blue-600 mt-1">2 jam yang lalu</p>
</div>
</div>
<div className="flex items-start p-4 bg-green-50 rounded-lg border border-green-200">
<div className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3"></div>
<div className="flex-1">
<p className="text-sm font-medium text-green-900">Kehadiran Sempurna</p>
<p className="text-xs text-green-700 mt-1">Ahmad Pratama hadir tepat waktu hari ini</p>
<p className="text-xs text-green-600 mt-1">5 jam yang lalu</p>
</div>
</div>
<div className="flex items-start p-4 bg-yellow-50 rounded-lg border border-yellow-200">
<div className="w-2 h-2 bg-yellow-500 rounded-full mt-2 mr-3"></div>
<div className="flex-1">
<p className="text-sm font-medium text-yellow-900">Pengumuman Sekolah</p>
<p className="text-xs text-yellow-700 mt-1">Rapat orang tua akan dilaksanakan minggu depan</p>
<p className="text-xs text-yellow-600 mt-1">1 hari yang lalu</p>
</div>
</div>
</div>
</div>
{/* Contact School */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-medium text-gray-900 mb-4">Hubungi Sekolah</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="flex items-center p-4 border border-gray-200 rounded-lg">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center mr-4">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Telepon</p>
<p className="text-sm text-gray-600">+62 21 1234-5678</p>
</div>
</div>
<div className="flex items-center p-4 border border-gray-200 rounded-lg">
<div className="w-10 h-10 bg-green-100 rounded-full flex items-center justify-center mr-4">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div>
<p className="font-medium text-gray-900">Email</p>
<p className="text-sm text-gray-600">info@sipintar.com</p>
</div>
</div>
</div>
</div>
</main>
</div>
)
}

View File

@ -21,6 +21,19 @@ export default function StudentsPage() {
const [students, setStudents] = useState<Student[]>([]) const [students, setStudents] = useState<Student[]>([])
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [showAddForm, setShowAddForm] = useState(false) const [showAddForm, setShowAddForm] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
studentNumber: '',
dateOfBirth: '',
address: '',
phone: '',
parentName: '',
parentPhone: '',
emergencyContact: '',
})
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => {
@ -53,6 +66,61 @@ export default function StudentsPage() {
} }
} }
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData(prev => ({
...prev,
[name]: value
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
const token = localStorage.getItem('token')
const response = await fetch('/api/students', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(formData),
})
if (response.ok) {
// Reset form and close modal
setFormData({
name: '',
email: '',
password: '',
studentNumber: '',
dateOfBirth: '',
address: '',
phone: '',
parentName: '',
parentPhone: '',
emergencyContact: '',
})
setShowAddForm(false)
// Refresh students list
await fetchStudents()
alert('Siswa berhasil ditambahkan!')
} else {
const error = await response.json()
alert(error.message || 'Gagal menambahkan siswa')
}
} catch (error) {
console.error('Error adding student:', error)
alert('Terjadi kesalahan saat menambahkan siswa')
} finally {
setIsSubmitting(false)
}
}
if (isLoading) { if (isLoading) {
return ( return (
<div className="min-h-screen flex items-center justify-center"> <div className="min-h-screen flex items-center justify-center">
@ -73,7 +141,7 @@ export default function StudentsPage() {
</div> </div>
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<button <button
onClick={() => router.push('/dashboard')} onClick={() => router.push('/dashboard/admin')}
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium" className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
> >
Kembali ke Dashboard Kembali ke Dashboard
@ -185,18 +253,179 @@ export default function StudentsPage() {
{/* Add Student Modal */} {/* Add Student Modal */}
{showAddForm && ( {showAddForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-md w-full p-6"> <div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<h3 className="text-lg font-medium text-gray-900 mb-4">Tambah Siswa Baru</h3> <div className="p-6">
<p className="text-sm text-gray-600 mb-4"> <h3 className="text-lg font-medium text-gray-900 mb-6">Tambah Siswa Baru</h3>
Fitur ini akan segera tersedia. Gunakan API endpoint /api/students untuk menambah siswa.
</p> <form onSubmit={handleSubmit} className="space-y-6">
<div className="flex justify-end space-x-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<button {/* Data Siswa */}
onClick={() => setShowAddForm(false)} <div className="space-y-4">
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md" <h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Siswa</h4>
>
Tutup <div>
</button> <label className="block text-sm font-medium text-gray-700 mb-1">
Nama Lengkap *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email *
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password *
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nomor Induk Siswa (NIS) *
</label>
<input
type="text"
name="studentNumber"
value={formData.studentNumber}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Tanggal Lahir *
</label>
<input
type="date"
name="dateOfBirth"
value={formData.dateOfBirth}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
No. Telefon Siswa
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
{/* Data Orang Tua */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Orang Tua</h4>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nama Orang Tua/Wali *
</label>
<input
type="text"
name="parentName"
value={formData.parentName}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
No. Telefon Orang Tua *
</label>
<input
type="tel"
name="parentPhone"
value={formData.parentPhone}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kontak Darurat
</label>
<input
type="tel"
name="emergencyContact"
value={formData.emergencyContact}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Alamat *
</label>
<textarea
name="address"
value={formData.address}
onChange={handleInputChange}
required
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
</div>
<div className="flex justify-end space-x-3 pt-6 border-t">
<button
type="button"
onClick={() => setShowAddForm(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
disabled={isSubmitting}
>
Batal
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSubmitting}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Siswa'}
</button>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,432 @@
'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
interface Teacher {
id: string
user: {
name: string
email: string
}
teacherNumber: string
specialization: string
qualification: string
experience: number
phone?: string
address?: string
}
export default function TeachersPage() {
const [teachers, setTeachers] = useState<Teacher[]>([])
const [isLoading, setIsLoading] = useState(true)
const [showAddForm, setShowAddForm] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false)
const [formData, setFormData] = useState({
name: '',
email: '',
password: '',
teacherNumber: '',
specialization: '',
qualification: '',
experience: '',
phone: '',
address: '',
})
const router = useRouter()
useEffect(() => {
const token = localStorage.getItem('token')
if (!token) {
router.push('/auth/login')
return
}
fetchTeachers()
}, [router])
const fetchTeachers = async () => {
try {
const token = localStorage.getItem('token')
const response = await fetch('/api/teachers', {
headers: {
'Authorization': `Bearer ${token}`,
},
})
if (response.ok) {
const data = await response.json()
setTeachers(data)
}
} catch (error) {
console.error('Failed to fetch teachers:', error)
} finally {
setIsLoading(false)
}
}
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target
setFormData(prev => ({
...prev,
[name]: value
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsSubmitting(true)
try {
const token = localStorage.getItem('token')
const response = await fetch('/api/teachers', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
...formData,
experience: parseInt(formData.experience) || 0
}),
})
if (response.ok) {
// Reset form and close modal
setFormData({
name: '',
email: '',
password: '',
teacherNumber: '',
specialization: '',
qualification: '',
experience: '',
phone: '',
address: '',
})
setShowAddForm(false)
// Refresh teachers list
await fetchTeachers()
alert('Guru berhasil ditambahkan!')
} else {
const error = await response.json()
alert(error.message || 'Gagal menambahkan guru')
}
} catch (error) {
console.error('Error adding teacher:', error)
alert('Terjadi kesalahan saat menambahkan guru')
} finally {
setIsSubmitting(false)
}
}
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50">
{/* Navigation */}
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900">SIPINTAR</h1>
<span className="ml-2 text-sm text-gray-500">Manajemen Guru</span>
</div>
<div className="flex items-center space-x-4">
<button
onClick={() => router.push('/dashboard/admin')}
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
Kembali ke Dashboard
</button>
</div>
</div>
</div>
</nav>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header */}
<div className="flex justify-between items-center mb-8">
<div>
<h2 className="text-2xl font-bold text-gray-900">Manajemen Guru</h2>
<p className="text-gray-600">Kelola data guru dan informasi akademik</p>
</div>
<button
onClick={() => setShowAddForm(true)}
className="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm font-medium"
>
+ Tambah Guru
</button>
</div>
{/* Teachers Table */}
<div className="bg-white shadow-sm rounded-lg border">
<div className="px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">Daftar Guru</h3>
</div>
{teachers.length === 0 ? (
<div className="px-6 py-8 text-center">
<svg className="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<h3 className="mt-2 text-sm font-medium text-gray-900">Belum ada guru</h3>
<p className="mt-1 text-sm text-gray-500">Mulai dengan menambahkan guru baru.</p>
<div className="mt-6">
<button
onClick={() => setShowAddForm(true)}
className="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700"
>
+ Tambah Guru Pertama
</button>
</div>
</div>
) : (
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Guru
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
NIP
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Spesialisasi
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Pengalaman
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Kontak
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Aksi
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{teachers.map((teacher) => (
<tr key={teacher.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm font-medium text-gray-900">
{teacher.user.name}
</div>
<div className="text-sm text-gray-500">
{teacher.user.email}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{teacher.teacherNumber}
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{teacher.specialization}</div>
<div className="text-sm text-gray-500">{teacher.qualification}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{teacher.experience} tahun
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<div>{teacher.phone || 'Tidak ada'}</div>
<div className="text-xs">{teacher.address || 'Tidak ada'}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button className="text-green-600 hover:text-green-900 mr-3">
Edit
</button>
<button className="text-red-600 hover:text-red-900">
Hapus
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Add Teacher Modal */}
{showAddForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6">
<h3 className="text-lg font-medium text-gray-900 mb-6">Tambah Guru Baru</h3>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Data Pribadi */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Pribadi</h4>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nama Lengkap *
</label>
<input
type="text"
name="name"
value={formData.name}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Email *
</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password *
</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nomor Induk Pegawai (NIP) *
</label>
<input
type="text"
name="teacherNumber"
value={formData.teacherNumber}
onChange={handleInputChange}
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
No. Telefon
</label>
<input
type="tel"
name="phone"
value={formData.phone}
onChange={handleInputChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
{/* Data Akademik */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Akademik</h4>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Spesialisasi/Bidang Studi *
</label>
<input
type="text"
name="specialization"
value={formData.specialization}
onChange={handleInputChange}
required
placeholder="Contoh: Matematika, Bahasa Indonesia"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Kualifikasi Pendidikan *
</label>
<input
type="text"
name="qualification"
value={formData.qualification}
onChange={handleInputChange}
required
placeholder="Contoh: S1 Pendidikan Matematika"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Pengalaman Mengajar (tahun) *
</label>
<input
type="number"
name="experience"
value={formData.experience}
onChange={handleInputChange}
required
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Alamat
</label>
<textarea
name="address"
value={formData.address}
onChange={handleInputChange}
rows={4}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
/>
</div>
</div>
</div>
<div className="flex justify-end space-x-3 pt-6 border-t">
<button
type="button"
onClick={() => setShowAddForm(false)}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
disabled={isSubmitting}
>
Batal
</button>
<button
type="submit"
className="px-4 py-2 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isSubmitting}
>
{isSubmitting ? 'Menyimpan...' : 'Simpan Guru'}
</button>
</div>
</form>
</div>
</div>
</div>
)}
</div>
</div>
)
}

View File

@ -1,142 +1,5 @@
import Link from "next/link"; import LandingPage from '@/components/LandingPage'
export default function Home() { export default function Home() {
return ( return <LandingPage />
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* Navigation */}
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900">SIPINTAR</h1>
<span className="ml-2 text-sm text-gray-500">Sistem Pemantauan Interaktif dan Pintar</span>
</div>
<div className="flex items-center space-x-4">
<Link
href="/auth/login"
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
>
Login
</Link>
<Link
href="/auth/register"
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium"
>
Daftar
</Link>
</div>
</div>
</div>
</nav>
{/* Hero Section */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="text-center">
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
Selamat Datang di <span className="text-blue-600">SIPINTAR</span>
</h2>
<p className="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
Sistem Pemantauan Interaktif dan Pintar untuk manajemen sekolah yang komprehensif.
Kelola siswa, guru, kelas, absensi, dan nilai dengan mudah, efisien, dan monitoring real-time.
</p>
<div className="mt-10 flex items-center justify-center gap-x-6">
<Link
href="/auth/login"
className="rounded-md bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
>
Mulai Sekarang
</Link>
<a href="#features" className="text-sm font-semibold leading-6 text-gray-900">
Pelajari lebih lanjut <span aria-hidden="true"></span>
</a>
</div>
</div>
{/* Features Section */}
<div id="features" className="mt-20">
<div className="text-center">
<h3 className="text-3xl font-bold tracking-tight text-gray-900">Fitur Unggulan</h3>
<p className="mt-4 text-lg text-gray-600">
Pantau dan kelola seluruh aspek sekolah dengan sistem pemantauan interaktif dan pintar
</p>
</div>
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Siswa</h4>
<p className="mt-2 text-gray-600">Pantau dan kelola data siswa, enrollment kelas, dan progress akademik secara real-time.</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Kelas</h4>
<p className="mt-2 text-gray-600">Pantau aktivitas kelas, mata pelajaran, dan jadwal dengan sistem monitoring interaktif.</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<h4 className="text-lg font-semibold text-gray-900">Sistem Penilaian Pintar</h4>
<p className="mt-2 text-gray-600">Input nilai, generate laporan otomatis, dan tracking progress akademik dengan analitik pintar.</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="w-12 h-12 bg-yellow-100 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Kehadiran</h4>
<p className="mt-2 text-gray-600">Sistem absensi digital dengan laporan kehadiran dan alert otomatis real-time.</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Guru</h4>
<p className="mt-2 text-gray-600">Pantau kinerja guru, assignment mata pelajaran, dan evaluasi kualifikasi secara interaktif.</p>
</div>
<div className="bg-white p-6 rounded-lg shadow-sm border">
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
</svg>
</div>
<h4 className="text-lg font-semibold text-gray-900">Dashboard Pemantauan Pintar</h4>
<p className="mt-2 text-gray-600">Dashboard interaktif dengan analytics, monitoring, dan insights pintar untuk sistem sekolah.</p>
</div>
</div>
</div>
</main>
{/* Footer */}
<footer className="bg-gray-900 text-white py-12 mt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center">
<h3 className="text-2xl font-bold">SIPINTAR</h3>
<p className="mt-2 text-gray-400">Sistem Pemantauan Interaktif dan Pintar</p>
<p className="mt-4 text-sm text-gray-500">
© 2025 SIPINTAR. All rights reserved.
</p>
</div>
</div>
</footer>
</div>
);
} }

View File

@ -0,0 +1,254 @@
import Image from 'next/image'
import Link from 'next/link'
export default function LandingPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
{/* Navigation */}
<nav className="absolute top-0 w-full z-10 px-6 py-4">
<div className="max-w-7xl mx-auto flex justify-between items-center">
<div className="flex items-center space-x-3">
<Image
src="/Logo Geometris dengan Topi Wisuda.png"
alt="SIPINTAR Logo"
width={40}
height={40}
className="drop-shadow-md"
/>
<span className="text-2xl font-bold text-blue-700">SIPINTAR</span>
</div>
<div className="flex items-center space-x-8">
<div className="hidden md:flex space-x-8">
<a href="#features" className="text-gray-700 hover:text-blue-700 transition-colors font-medium">Fitur</a>
<a href="#about" className="text-gray-700 hover:text-blue-700 transition-colors font-medium">Tentang</a>
<a href="#contact" className="text-gray-700 hover:text-blue-700 transition-colors font-medium">Kontak</a>
</div>
<Link
href="/auth/login"
className="bg-blue-700 text-white px-6 py-2 rounded-lg hover:bg-blue-800 transition-colors shadow-lg"
>
Masuk
</Link>
</div>
</div>
</nav>
{/* Hero Section */}
<section className="pt-20 pb-16 px-6">
<div className="max-w-7xl mx-auto">
<div className="text-center py-20">
<div className="mb-8 flex justify-center">
<Image
src="/Logo Geometris dengan Topi Wisuda.png"
alt="SIPINTAR Logo"
width={120}
height={120}
className="drop-shadow-2xl"
/>
</div>
<h1 className="text-5xl md:text-7xl font-bold text-gray-800 mb-6">
<span className="text-blue-700">SI</span>PINTAR
</h1>
<p className="text-xl md:text-2xl text-gray-700 mb-8 max-w-3xl mx-auto">
Sistem Pemantauan Interaktif dan Pintar
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/auth/login"
className="bg-blue-700 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-blue-800 transition-all transform hover:scale-105 shadow-xl"
>
Mulai Sekarang
</Link>
<Link
href="#features"
className="border-2 border-blue-700 text-blue-700 px-8 py-4 rounded-lg text-lg font-semibold hover:bg-blue-700 hover:text-white transition-all transform hover:scale-105"
>
Pelajari Lebih Lanjut
</Link>
</div>
</div>
</div>
</section>
{/* Features Section */}
<section id="features" className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-gray-800 mb-4">Fitur Unggulan</h2>
<p className="text-xl text-gray-600">Solusi lengkap untuk kebutuhan manajemen sekolah Anda</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
{/* Feature 1 */}
<div className="text-center p-8 rounded-xl bg-blue-50 hover:bg-blue-100 transition-colors border border-blue-200">
<div className="w-16 h-16 bg-blue-700 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-4">Manajemen Siswa</h3>
<p className="text-gray-600">Kelola data siswa, absensi, dan nilai dengan mudah dan terorganisir</p>
</div>
{/* Feature 2 */}
<div className="text-center p-8 rounded-xl bg-indigo-50 hover:bg-indigo-100 transition-colors border border-indigo-200">
<div className="w-16 h-16 bg-indigo-600 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-4">Dashboard Analytics</h3>
<p className="text-gray-600">Analisis dan laporan real-time untuk memantau perkembangan sekolah</p>
</div>
{/* Feature 3 */}
<div className="text-center p-8 rounded-xl bg-blue-50 hover:bg-blue-100 transition-colors border border-blue-200">
<div className="w-16 h-16 bg-blue-600 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-4">Keamanan Data</h3>
<p className="text-gray-600">Sistem keamanan tingkat tinggi untuk melindungi data sekolah Anda</p>
</div>
</div>
</div>
</section>
{/* About Section */}
<section id="about" className="py-16 bg-gray-50">
<div className="max-w-7xl mx-auto px-6">
<div className="grid lg:grid-cols-2 gap-12 items-center">
<div>
<h2 className="text-4xl font-bold text-gray-800 mb-6">Tentang SIPINTAR</h2>
<p className="text-lg text-gray-600 mb-6">
SIPINTAR (Sistem Pemantauan Interaktif dan Pintar) adalah solusi digital terdepan untuk manajemen sekolah modern.
Dirancang khusus untuk membantu institusi pendidikan dalam mengelola data siswa, guru, dan
administrasi sekolah dengan lebih efisien.
</p>
<div className="space-y-4">
<div className="flex items-center">
<div className="w-6 h-6 bg-blue-700 rounded-full flex items-center justify-center mr-3">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-gray-700">Interface yang user-friendly</span>
</div>
<div className="flex items-center">
<div className="w-6 h-6 bg-blue-700 rounded-full flex items-center justify-center mr-3">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-gray-700">Laporan otomatis dan real-time</span>
</div>
<div className="flex items-center">
<div className="w-6 h-6 bg-blue-700 rounded-full flex items-center justify-center mr-3">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-gray-700">Akses multi-platform</span>
</div>
</div>
</div>
<div className="flex justify-center">
<div className="relative">
<Image
src="/Logo Geometris dengan Topi Wisuda.png"
alt="SIPINTAR Logo Large"
width={300}
height={300}
className="drop-shadow-2xl"
/>
</div>
</div>
</div>
</div>
</section>
{/* CTA Section */}
<section className="py-16 bg-blue-700">
<div className="max-w-7xl mx-auto px-6 text-center">
<h2 className="text-4xl font-bold text-white mb-6">Siap Memulai?</h2>
<p className="text-xl text-blue-100 mb-8 max-w-2xl mx-auto">Bergabunglah dengan sekolah-sekolah yang telah merasakan kemudahan manajemen dengan SIPINTAR</p>
<Link
href="/auth/login"
className="inline-block bg-white text-blue-700 px-8 py-4 rounded-lg text-lg font-semibold hover:bg-blue-50 transition-all transform hover:scale-105 shadow-xl"
>
Coba Sekarang Gratis
</Link>
</div>
</section>
{/* Contact Section */}
<section id="contact" className="py-16 bg-white">
<div className="max-w-7xl mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold text-gray-800 mb-4">Hubungi Kami</h2>
<p className="text-xl text-gray-600">Tim kami siap membantu Anda memulai digitalisasi sekolah</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
<div className="text-center p-8 rounded-xl bg-blue-50 hover:bg-blue-100 transition-colors border border-blue-200">
<div className="w-16 h-16 bg-blue-700 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-4">Email</h3>
<p className="text-gray-600">info@sipintar.com</p>
<p className="text-gray-600">support@sipintar.com</p>
</div>
<div className="text-center p-8 rounded-xl bg-indigo-50 hover:bg-indigo-100 transition-colors border border-indigo-200">
<div className="w-16 h-16 bg-indigo-600 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-4">Telepon</h3>
<p className="text-gray-600">+62 21 1234-5678</p>
<p className="text-gray-600">+62 812 3456-7890</p>
</div>
<div className="text-center p-8 rounded-xl bg-blue-50 hover:bg-blue-100 transition-colors border border-blue-200">
<div className="w-16 h-16 bg-blue-600 rounded-full mx-auto mb-6 flex items-center justify-center shadow-lg">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-4">Alamat</h3>
<p className="text-gray-600">Jl. Pendidikan No. 123</p>
<p className="text-gray-600">Jakarta Selatan 12345</p>
</div>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-gray-900 py-12">
<div className="max-w-7xl mx-auto px-6">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="flex items-center space-x-3 mb-4 md:mb-0">
<Image
src="/Logo Geometris dengan Topi Wisuda.png"
alt="SIPINTAR Logo"
width={32}
height={32}
/>
<span className="text-xl font-bold text-white">SIPINTAR</span>
</div>
<div className="text-gray-400 text-center md:text-right">
<p>&copy; 2025 SIPINTAR. Semua hak dilindungi.</p>
<p className="mt-2 text-sm">Sistem Informasi Pintar untuk Sekolah Modern</p>
</div>
</div>
</div>
</footer>
</div>
)
}