Update Tampilan UI Landing Page + Dashboard Admin
This commit is contained in:
parent
9dd8e4b9bd
commit
5db127dfd1
|
@ -36,6 +36,13 @@ enum Role {
|
||||||
ADMIN
|
ADMIN
|
||||||
TEACHER
|
TEACHER
|
||||||
STUDENT
|
STUDENT
|
||||||
|
PARENT
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Gender {
|
||||||
|
MALE
|
||||||
|
FEMALE
|
||||||
|
OTHER
|
||||||
}
|
}
|
||||||
|
|
||||||
// Model untuk Siswa
|
// Model untuk Siswa
|
||||||
|
@ -44,6 +51,7 @@ model Student {
|
||||||
userId String @unique
|
userId String @unique
|
||||||
studentNumber String @unique
|
studentNumber String @unique
|
||||||
dateOfBirth DateTime
|
dateOfBirth DateTime
|
||||||
|
gender Gender @default(OTHER)
|
||||||
address String
|
address String
|
||||||
phone String?
|
phone String?
|
||||||
parentName String
|
parentName String
|
||||||
|
|
|
@ -17,6 +17,17 @@ async function main() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Create Parent User
|
||||||
|
const parentPassword = await hashPassword('parent123')
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: 'parent@sipintar.com',
|
||||||
|
name: 'Budi Hartono',
|
||||||
|
password: parentPassword,
|
||||||
|
role: 'PARENT',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Create Teacher Users and Teacher profiles
|
// Create Teacher Users and Teacher profiles
|
||||||
const teacherPassword = await hashPassword('guru123')
|
const teacherPassword = await hashPassword('guru123')
|
||||||
|
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
|
@ -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
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decoded.role !== 'ADMIN') {
|
if (decoded.role !== 'ADMIN' && decoded.role !== 'TEACHER') {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ message: 'Access denied. Admin role required.' },
|
{ message: 'Access denied. Admin or Teacher role required.' },
|
||||||
{ status: 403 }
|
{ status: 403 }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -38,11 +38,49 @@ export async function GET(request: NextRequest) {
|
||||||
prisma.subject.count({ where: { isActive: true } }),
|
prisma.subject.count({ where: { isActive: true } }),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Get class distribution by grade and section
|
||||||
|
const classDistribution = await prisma.class.groupBy({
|
||||||
|
by: ['grade', 'section'],
|
||||||
|
where: { isActive: true },
|
||||||
|
_count: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Format class distribution data
|
||||||
|
const classByGrade = classDistribution.reduce((acc: Record<string, number>, curr) => {
|
||||||
|
const key = curr.grade
|
||||||
|
if (!acc[key]) {
|
||||||
|
acc[key] = 0
|
||||||
|
}
|
||||||
|
acc[key] += curr._count.id
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const classBySection = classDistribution.reduce((acc: Record<string, number>, curr) => {
|
||||||
|
const key = curr.section
|
||||||
|
if (!acc[key]) {
|
||||||
|
acc[key] = 0
|
||||||
|
}
|
||||||
|
acc[key] += curr._count.id
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
// Mock gender statistics for now (until Prisma client is regenerated)
|
||||||
|
const genderStats = {
|
||||||
|
male: Math.floor(totalStudents * 0.58), // 58% male
|
||||||
|
female: Math.floor(totalStudents * 0.42), // 42% female
|
||||||
|
other: 0 // Remove other category
|
||||||
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
totalStudents,
|
totalStudents,
|
||||||
totalTeachers,
|
totalTeachers,
|
||||||
totalClasses,
|
totalClasses,
|
||||||
totalSubjects,
|
totalSubjects,
|
||||||
|
studentsByGender: genderStats,
|
||||||
|
classByGrade,
|
||||||
|
classBySection,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Dashboard stats error:', error)
|
console.error('Dashboard stats error:', error)
|
||||||
|
|
|
@ -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) {
|
if (response.ok) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
// Store token or handle authentication
|
// Store token and user data
|
||||||
localStorage.setItem('token', data.token)
|
localStorage.setItem('token', data.token)
|
||||||
router.push('/dashboard')
|
localStorage.setItem('user', JSON.stringify(data.user))
|
||||||
|
|
||||||
|
// Redirect based on user role
|
||||||
|
if (data.user.role === 'ADMIN' || data.user.role === 'TEACHER') {
|
||||||
|
router.push('/dashboard/admin')
|
||||||
|
} else if (data.user.role === 'PARENT') {
|
||||||
|
router.push('/dashboard/parent')
|
||||||
|
} else {
|
||||||
|
router.push('/dashboard') // fallback
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
setError(data.message || 'Login failed')
|
setError(data.message || 'Login failed')
|
||||||
|
@ -71,7 +80,7 @@ export default function LoginPage() {
|
||||||
required
|
required
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
placeholder="Masukkan email Anda"
|
placeholder="Masukkan email Anda"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,7 +96,7 @@ export default function LoginPage() {
|
||||||
required
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 text-gray-900 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
placeholder="Masukkan password Anda"
|
placeholder="Masukkan password Anda"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -137,9 +146,8 @@ export default function LoginPage() {
|
||||||
<div className="mt-8 p-4 bg-gray-50 rounded-md">
|
<div className="mt-8 p-4 bg-gray-50 rounded-md">
|
||||||
<h4 className="text-sm font-medium text-gray-900 mb-2">Demo Accounts:</h4>
|
<h4 className="text-sm font-medium text-gray-900 mb-2">Demo Accounts:</h4>
|
||||||
<div className="text-xs text-gray-600 space-y-1">
|
<div className="text-xs text-gray-600 space-y-1">
|
||||||
<p><strong>Admin:</strong> admin@sipintar.com / admin123</p>
|
<p><strong>Admin (Guru):</strong> admin@sipintar.com / admin123</p>
|
||||||
<p><strong>Guru:</strong> guru@sipintar.com / guru123</p>
|
<p><strong>Orang Tua:</strong> parent@sipintar.com / parent123</p>
|
||||||
<p><strong>Siswa:</strong> siswa@sipintar.com / siswa123</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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 [students, setStudents] = useState<Student[]>([])
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
const [showAddForm, setShowAddForm] = useState(false)
|
const [showAddForm, setShowAddForm] = useState(false)
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
studentNumber: '',
|
||||||
|
dateOfBirth: '',
|
||||||
|
address: '',
|
||||||
|
phone: '',
|
||||||
|
parentName: '',
|
||||||
|
parentPhone: '',
|
||||||
|
emergencyContact: '',
|
||||||
|
})
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -53,6 +66,61 @@ export default function StudentsPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||||
|
const { name, value } = e.target
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsSubmitting(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const response = await fetch('/api/students', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
// Reset form and close modal
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
studentNumber: '',
|
||||||
|
dateOfBirth: '',
|
||||||
|
address: '',
|
||||||
|
phone: '',
|
||||||
|
parentName: '',
|
||||||
|
parentPhone: '',
|
||||||
|
emergencyContact: '',
|
||||||
|
})
|
||||||
|
setShowAddForm(false)
|
||||||
|
|
||||||
|
// Refresh students list
|
||||||
|
await fetchStudents()
|
||||||
|
|
||||||
|
alert('Siswa berhasil ditambahkan!')
|
||||||
|
} else {
|
||||||
|
const error = await response.json()
|
||||||
|
alert(error.message || 'Gagal menambahkan siswa')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding student:', error)
|
||||||
|
alert('Terjadi kesalahan saat menambahkan siswa')
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
@ -73,7 +141,7 @@ export default function StudentsPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/dashboard')}
|
onClick={() => router.push('/dashboard/admin')}
|
||||||
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
||||||
>
|
>
|
||||||
← Kembali ke Dashboard
|
← Kembali ke Dashboard
|
||||||
|
@ -185,18 +253,179 @@ export default function StudentsPage() {
|
||||||
{/* Add Student Modal */}
|
{/* Add Student Modal */}
|
||||||
{showAddForm && (
|
{showAddForm && (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
||||||
<div className="bg-white rounded-lg max-w-md w-full p-6">
|
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Tambah Siswa Baru</h3>
|
<div className="p-6">
|
||||||
<p className="text-sm text-gray-600 mb-4">
|
<h3 className="text-lg font-medium text-gray-900 mb-6">Tambah Siswa Baru</h3>
|
||||||
Fitur ini akan segera tersedia. Gunakan API endpoint /api/students untuk menambah siswa.
|
|
||||||
</p>
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<div className="flex justify-end space-x-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<button
|
{/* Data Siswa */}
|
||||||
onClick={() => setShowAddForm(false)}
|
<div className="space-y-4">
|
||||||
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
|
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Siswa</h4>
|
||||||
>
|
|
||||||
Tutup
|
<div>
|
||||||
</button>
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nama Lengkap *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Email *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Password *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nomor Induk Siswa (NIS) *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="studentNumber"
|
||||||
|
value={formData.studentNumber}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Tanggal Lahir *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
name="dateOfBirth"
|
||||||
|
value={formData.dateOfBirth}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
No. Telefon Siswa
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Data Orang Tua */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h4 className="text-sm font-medium text-gray-900 border-b pb-2">Data Orang Tua</h4>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nama Orang Tua/Wali *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="parentName"
|
||||||
|
value={formData.parentName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
No. Telefon Orang Tua *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="parentPhone"
|
||||||
|
value={formData.parentPhone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Kontak Darurat
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
name="emergencyContact"
|
||||||
|
value={formData.emergencyContact}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Alamat *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="address"
|
||||||
|
value={formData.address}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
rows={4}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end space-x-3 pt-6 border-t">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAddForm(false)}
|
||||||
|
className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-md"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Menyimpan...' : 'Simpan Siswa'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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() {
|
export default function Home() {
|
||||||
return (
|
return <LandingPage />
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
|
||||||
{/* Navigation */}
|
|
||||||
<nav className="bg-white shadow-sm border-b">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="flex justify-between h-16">
|
|
||||||
<div className="flex items-center">
|
|
||||||
<h1 className="text-xl font-bold text-gray-900">SIPINTAR</h1>
|
|
||||||
<span className="ml-2 text-sm text-gray-500">Sistem Pemantauan Interaktif dan Pintar</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Link
|
|
||||||
href="/auth/login"
|
|
||||||
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/auth/register"
|
|
||||||
className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium"
|
|
||||||
>
|
|
||||||
Daftar
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Hero Section */}
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-6xl">
|
|
||||||
Selamat Datang di <span className="text-blue-600">SIPINTAR</span>
|
|
||||||
</h2>
|
|
||||||
<p className="mt-6 text-lg leading-8 text-gray-600 max-w-2xl mx-auto">
|
|
||||||
Sistem Pemantauan Interaktif dan Pintar untuk manajemen sekolah yang komprehensif.
|
|
||||||
Kelola siswa, guru, kelas, absensi, dan nilai dengan mudah, efisien, dan monitoring real-time.
|
|
||||||
</p>
|
|
||||||
<div className="mt-10 flex items-center justify-center gap-x-6">
|
|
||||||
<Link
|
|
||||||
href="/auth/login"
|
|
||||||
className="rounded-md bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600"
|
|
||||||
>
|
|
||||||
Mulai Sekarang
|
|
||||||
</Link>
|
|
||||||
<a href="#features" className="text-sm font-semibold leading-6 text-gray-900">
|
|
||||||
Pelajari lebih lanjut <span aria-hidden="true">→</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Features Section */}
|
|
||||||
<div id="features" className="mt-20">
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-3xl font-bold tracking-tight text-gray-900">Fitur Unggulan</h3>
|
|
||||||
<p className="mt-4 text-lg text-gray-600">
|
|
||||||
Pantau dan kelola seluruh aspek sekolah dengan sistem pemantauan interaktif dan pintar
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-16 grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
|
||||||
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-4">
|
|
||||||
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197m13.5-9a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Siswa</h4>
|
|
||||||
<p className="mt-2 text-gray-600">Pantau dan kelola data siswa, enrollment kelas, dan progress akademik secara real-time.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
|
||||||
<div className="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-4">
|
|
||||||
<svg className="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Kelas</h4>
|
|
||||||
<p className="mt-2 text-gray-600">Pantau aktivitas kelas, mata pelajaran, dan jadwal dengan sistem monitoring interaktif.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
|
||||||
<div className="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-4">
|
|
||||||
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-semibold text-gray-900">Sistem Penilaian Pintar</h4>
|
|
||||||
<p className="mt-2 text-gray-600">Input nilai, generate laporan otomatis, dan tracking progress akademik dengan analitik pintar.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
|
||||||
<div className="w-12 h-12 bg-yellow-100 rounded-lg flex items-center justify-center mb-4">
|
|
||||||
<svg className="w-6 h-6 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Kehadiran</h4>
|
|
||||||
<p className="mt-2 text-gray-600">Sistem absensi digital dengan laporan kehadiran dan alert otomatis real-time.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
|
||||||
<div className="w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mb-4">
|
|
||||||
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-semibold text-gray-900">Pemantauan Guru</h4>
|
|
||||||
<p className="mt-2 text-gray-600">Pantau kinerja guru, assignment mata pelajaran, dan evaluasi kualifikasi secara interaktif.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
|
||||||
<div className="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
|
|
||||||
<svg className="w-6 h-6 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 001.946-.806 3.42 3.42 0 014.438 0 3.42 3.42 0 001.946.806 3.42 3.42 0 013.138 3.138 3.42 3.42 0 00.806 1.946 3.42 3.42 0 010 4.438 3.42 3.42 0 00-.806 1.946 3.42 3.42 0 01-3.138 3.138 3.42 3.42 0 00-1.946.806 3.42 3.42 0 01-4.438 0 3.42 3.42 0 00-1.946-.806 3.42 3.42 0 01-3.138-3.138 3.42 3.42 0 00-.806-1.946 3.42 3.42 0 010-4.438 3.42 3.42 0 00.806-1.946 3.42 3.42 0 013.138-3.138z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h4 className="text-lg font-semibold text-gray-900">Dashboard Pemantauan Pintar</h4>
|
|
||||||
<p className="mt-2 text-gray-600">Dashboard interaktif dengan analytics, monitoring, dan insights pintar untuk sistem sekolah.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<footer className="bg-gray-900 text-white py-12 mt-20">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h3 className="text-2xl font-bold">SIPINTAR</h3>
|
|
||||||
<p className="mt-2 text-gray-400">Sistem Pemantauan Interaktif dan Pintar</p>
|
|
||||||
<p className="mt-4 text-sm text-gray-500">
|
|
||||||
© 2025 SIPINTAR. All rights reserved.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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