Update Tampilan UI Landing Page + Dashboard Admin
This commit is contained in:
parent
9dd8e4b9bd
commit
5db127dfd1
|
@ -36,6 +36,13 @@ enum Role {
|
|||
ADMIN
|
||||
TEACHER
|
||||
STUDENT
|
||||
PARENT
|
||||
}
|
||||
|
||||
enum Gender {
|
||||
MALE
|
||||
FEMALE
|
||||
OTHER
|
||||
}
|
||||
|
||||
// Model untuk Siswa
|
||||
|
@ -44,6 +51,7 @@ model Student {
|
|||
userId String @unique
|
||||
studentNumber String @unique
|
||||
dateOfBirth DateTime
|
||||
gender Gender @default(OTHER)
|
||||
address String
|
||||
phone String?
|
||||
parentName String
|
||||
|
|
|
@ -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
|
||||
const teacherPassword = await hashPassword('guru123')
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -23,9 +23,9 @@ export async function GET(request: NextRequest) {
|
|||
name: string
|
||||
}
|
||||
|
||||
if (decoded.role !== 'ADMIN') {
|
||||
if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') {
|
||||
return NextResponse.json(
|
||||
{ message: 'Access denied. Admin role required.' },
|
||||
{ message: 'Access denied. Admin or Teacher role required.' },
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
@ -38,11 +38,49 @@ export async function GET(request: NextRequest) {
|
|||
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({
|
||||
totalStudents,
|
||||
totalTeachers,
|
||||
totalClasses,
|
||||
totalSubjects,
|
||||
studentsByGender: genderStats,
|
||||
classByGrade,
|
||||
classBySection,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Dashboard stats error:', error)
|
||||
|
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -27,9 +27,18 @@ export default function LoginPage() {
|
|||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
// Store token or handle authentication
|
||||
// Store token and user data
|
||||
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 {
|
||||
const data = await response.json()
|
||||
setError(data.message || 'Login failed')
|
||||
|
@ -71,7 +80,7 @@ export default function LoginPage() {
|
|||
required
|
||||
value={email}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
@ -87,7 +96,7 @@ export default function LoginPage() {
|
|||
required
|
||||
value={password}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
@ -137,9 +146,8 @@ export default function LoginPage() {
|
|||
<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>
|
||||
<div className="text-xs text-gray-600 space-y-1">
|
||||
<p><strong>Admin:</strong> admin@sipintar.com / admin123</p>
|
||||
<p><strong>Guru:</strong> guru@sipintar.com / guru123</p>
|
||||
<p><strong>Siswa:</strong> siswa@sipintar.com / siswa123</p>
|
||||
<p><strong>Admin (Guru):</strong> admin@sipintar.com / admin123</p>
|
||||
<p><strong>Orang Tua:</strong> parent@sipintar.com / parent123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -21,6 +21,19 @@ export default function StudentsPage() {
|
|||
const [students, setStudents] = useState<Student[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
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()
|
||||
|
||||
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) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
|
@ -73,7 +141,7 @@ export default function StudentsPage() {
|
|||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<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"
|
||||
>
|
||||
← Kembali ke Dashboard
|
||||
|
@ -185,18 +253,179 @@ export default function StudentsPage() {
|
|||
{/* Add Student 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-md w-full p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Tambah Siswa Baru</h3>
|
||||
<p className="text-sm text-gray-600 mb-4">
|
||||
Fitur ini akan segera tersedia. Gunakan API endpoint /api/students untuk menambah siswa.
|
||||
</p>
|
||||
<div className="flex justify-end space-x-3">
|
||||
<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 Siswa Baru</h3>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Data Siswa */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Siswa</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-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}
|
||||
>
|
||||
Tutup
|
||||
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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
141
src/app/page.tsx
141
src/app/page.tsx
|
@ -1,142 +1,5 @@
|
|||
import Link from "next/link";
|
||||
import LandingPage from '@/components/LandingPage'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
return <LandingPage />
|
||||
}
|
||||
|
|
|
@ -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>© 2025 SIPINTAR. Semua hak dilindungi.</p>
|
||||
<p className="mt-2 text-sm">Sistem Informasi Pintar untuk Sekolah Modern</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue