Dockerizing with development and production environtment
This commit is contained in:
parent
72f6fea737
commit
4b1f6d6362
|
@ -0,0 +1,43 @@
|
|||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env*.local
|
||||
.env.prod
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
/src/generated/prisma
|
3
.env
3
.env
|
@ -1,3 +1,4 @@
|
|||
DATABASE_URL="mysql://root:@localhost:3306/sipintar_school"
|
||||
# DATABASE_URL="mysql://sipintar_user:sipintar_password123:@db:3306/sipintar_school"
|
||||
DATABASE_URL="mysql://root:root123@sipintar-mysql:3306/sipintar_school"
|
||||
NEXTAUTH_SECRET="your-secret-key-here-change-this-in-production"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
|
@ -1,3 +1,3 @@
|
|||
DATABASE_URL="mysql://root:@localhost:3306/sipintar_school"
|
||||
NEXTAUTH_SECRET="your-secret-key-here-change-this-in-production"
|
||||
NEXTAUTH_URL="http://localhost:3000"
|
||||
#DATABASE_URL="mysql://root:@localhost:3306/sipintar_school"
|
||||
#NEXTAUTH_SECRET="your-secret-key-here-change-this-in-production"
|
||||
#NEXTAUTH_URL="http://localhost:3000"
|
|
@ -27,6 +27,7 @@
|
|||
# local env files
|
||||
.env
|
||||
.env*.local
|
||||
.env.prod
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
FROM node:20-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
# Generate Prisma Client
|
||||
RUN npx prisma generate
|
||||
|
||||
RUN pnpm run build
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["pnpm", "start"]
|
|
@ -0,0 +1,23 @@
|
|||
# Build stage
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN npm install -g pnpm
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
RUN npx prisma generate
|
||||
RUN pnpm run build
|
||||
|
||||
# Production stage
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
COPY --from=builder /app/.next ./.next
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "start"]
|
|
@ -0,0 +1,71 @@
|
|||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.prod
|
||||
container_name: sipintar-app
|
||||
restart: unless-stopped
|
||||
working_dir: /app
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DATABASE_URL: mysql://sipintar_user:sipintar_password123@db:3306/sipintar_school
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
# volumes:
|
||||
# - .:/app
|
||||
# - /app/node_modules
|
||||
# - /app/.next
|
||||
networks:
|
||||
- sipintar-network
|
||||
command: ["npm", "run", "start"]
|
||||
|
||||
|
||||
db:
|
||||
image: mysql:5.7
|
||||
container_name: sipintar-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root123}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-sipintar_school}
|
||||
MYSQL_USER: ${MYSQL_USER:-sipintar_user}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-sipintar_password123}
|
||||
ports:
|
||||
- '3307:3306'
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./setup-database.sql:/docker-entrypoint-initdb.d/setup-database.sql
|
||||
networks:
|
||||
- sipintar-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
seed:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.prod
|
||||
container_name: sipintar-seed
|
||||
restart: "no"
|
||||
working_dir: /app
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
DATABASE_URL: mysql://sipintar_user:sipintar_password123@db:3306/sipintar_school
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
command: ["npm", "run", "db:seed"]
|
||||
networks:
|
||||
- sipintar-network
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
networks:
|
||||
sipintar-network:
|
||||
driver: bridge
|
|
@ -0,0 +1,52 @@
|
|||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: sipintar-app
|
||||
restart: unless-stopped
|
||||
working_dir: /app
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
DATABASE_URL: mysql://sipintar_user:sipintar_password123@db:3306/sipintar_school
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
- /app/.next
|
||||
networks:
|
||||
- sipintar-network
|
||||
|
||||
|
||||
db:
|
||||
image: mysql:5.7
|
||||
container_name: sipintar-mysql
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root123
|
||||
MYSQL_DATABASE: sipintar_school
|
||||
MYSQL_USER: sipintar_user
|
||||
MYSQL_PASSWORD: sipintar_password123
|
||||
ports:
|
||||
- '3307:3306'
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./setup-database.sql:/docker-entrypoint-initdb.d/setup-database.sql
|
||||
networks:
|
||||
- sipintar-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot123"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
networks:
|
||||
sipintar-network:
|
||||
driver: bridge
|
|
@ -41,8 +41,9 @@
|
|||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.4",
|
||||
"tailwindcss": "^4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
|
@ -81,6 +82,30 @@
|
|||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/core": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz",
|
||||
|
@ -2630,6 +2655,34 @@
|
|||
"tailwindcss": "4.1.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz",
|
||||
|
@ -2783,17 +2836,17 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
|
||||
"integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz",
|
||||
"integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/type-utils": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/type-utils": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
|
@ -2807,9 +2860,9 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.38.0",
|
||||
"@typescript-eslint/parser": "^8.39.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
|
@ -2823,16 +2876,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
|
||||
"integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz",
|
||||
"integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2844,18 +2897,18 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
|
||||
"integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz",
|
||||
"integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.38.0",
|
||||
"@typescript-eslint/types": "^8.38.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.39.1",
|
||||
"@typescript-eslint/types": "^8.39.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2866,18 +2919,18 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
|
||||
"integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz",
|
||||
"integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0"
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
@ -2888,9 +2941,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -2901,19 +2954,19 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
|
||||
"integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz",
|
||||
"integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0",
|
||||
"@typescript-eslint/utils": "8.38.0",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1",
|
||||
"@typescript-eslint/utils": "8.39.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
|
@ -2926,13 +2979,13 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
|
||||
"integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz",
|
||||
"integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
@ -2944,16 +2997,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
|
||||
"integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz",
|
||||
"integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.38.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/visitor-keys": "8.38.0",
|
||||
"@typescript-eslint/project-service": "8.39.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/visitor-keys": "8.39.1",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
|
@ -2969,7 +3022,7 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
|
@ -3029,16 +3082,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
|
||||
"integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz",
|
||||
"integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.38.0",
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/typescript-estree": "8.38.0"
|
||||
"@typescript-eslint/scope-manager": "8.39.1",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"@typescript-eslint/typescript-estree": "8.39.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
@ -3049,17 +3102,17 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <5.9.0"
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.38.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
|
||||
"integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
|
||||
"version": "8.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz",
|
||||
"integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.38.0",
|
||||
"@typescript-eslint/types": "8.39.1",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -3362,6 +3415,19 @@
|
|||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-walk": {
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
|
@ -3395,6 +3461,13 @@
|
|||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/arg": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
|
@ -3887,6 +3960,13 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
|
@ -4183,6 +4263,16 @@
|
|||
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/doctrine": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
|
@ -6435,6 +6525,13 @@
|
|||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
|
@ -8112,6 +8209,50 @@
|
|||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@tsconfig/node12": "^1.0.7",
|
||||
"@tsconfig/node14": "^1.0.0",
|
||||
"@tsconfig/node16": "^1.0.2",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn-walk": "^8.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"create-require": "^1.1.0",
|
||||
"diff": "^4.0.1",
|
||||
"make-error": "^1.1.1",
|
||||
"v8-compile-cache-lib": "^3.0.1",
|
||||
"yn": "3.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"ts-node": "dist/bin.js",
|
||||
"ts-node-cwd": "dist/bin-cwd.js",
|
||||
"ts-node-esm": "dist/bin-esm.js",
|
||||
"ts-node-script": "dist/bin-script.js",
|
||||
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||
"ts-script": "dist/bin-script-deprecated.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc/core": ">=1.2.50",
|
||||
"@swc/wasm": ">=1.2.50",
|
||||
"@types/node": "*",
|
||||
"typescript": ">=2.7"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@swc/core": {
|
||||
"optional": true
|
||||
},
|
||||
"@swc/wasm": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||
|
@ -8243,9 +8384,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
|
||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
|
@ -8387,6 +8528,13 @@
|
|||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-compile-cache-lib": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "37.3.6",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||
|
@ -8534,6 +8682,16 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/yocto-queue": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
|
|
|
@ -45,7 +45,8 @@
|
|||
"eslint": "^9",
|
||||
"eslint-config-next": "15.4.4",
|
||||
"tailwindcss": "^4",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsx": "^4.20.3",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -24,7 +24,7 @@ async function main() {
|
|||
email: 'parent@sipintar.com',
|
||||
name: 'Budi Hartono',
|
||||
password: parentPassword,
|
||||
role: 'PARENT',
|
||||
role: 'STUDENT',
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -4,11 +4,11 @@ mysql -u root -p
|
|||
# Buat database baru
|
||||
CREATE DATABASE sipintar_school CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
# Buat user khusus (opsional tapi recommended)
|
||||
CREATE USER 'sipintar_user'@'localhost' IDENTIFIED BY 'sipintar_password123';
|
||||
# Buat user khusus
|
||||
CREATE USER 'sipintar_user'@'%' IDENTIFIED BY 'sipintar_password123';
|
||||
|
||||
# Berikan akses ke database
|
||||
GRANT ALL PRIVILEGES ON sipintar_school.* TO 'sipintar_user'@'localhost';
|
||||
GRANT ALL PRIVILEGES ON sipintar_school.* TO 'sipintar_user'@'%';
|
||||
|
||||
# Refresh privileges
|
||||
FLUSH PRIVILEGES;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "api",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "dashboard",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
|
@ -3,426 +3,556 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from 'recharts'
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
LineChart,
|
||||
Line,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
} from 'recharts'
|
||||
|
||||
interface User {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
role: string
|
||||
profileImage?: string
|
||||
studentId?: string
|
||||
teacherId?: string
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
role: string
|
||||
profileImage?: string
|
||||
studentId?: string
|
||||
teacherId?: string
|
||||
}
|
||||
|
||||
interface DashboardStats {
|
||||
totalStudents: number
|
||||
totalTeachers: number
|
||||
totalClasses: number
|
||||
totalSubjects: number
|
||||
totalStudents: number
|
||||
totalTeachers: number
|
||||
totalClasses: number
|
||||
totalSubjects: number
|
||||
}
|
||||
|
||||
interface ChartData {
|
||||
enrollmentData: { name: string; students: number }[]
|
||||
attendanceData: { month: string; present: number; absent: number; late: number }[]
|
||||
gradeData: { grade: string; count: number }[]
|
||||
subjectData: { name: string; teachers: number }[]
|
||||
enrollmentData: { name: string; students: number }[]
|
||||
attendanceData: {
|
||||
month: string
|
||||
present: number
|
||||
absent: number
|
||||
late: number
|
||||
}[]
|
||||
gradeData: { grade: string; count: number }[]
|
||||
subjectData: { name: string; teachers: number }[]
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||
const [chartData, setChartData] = useState<ChartData | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const router = useRouter()
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null)
|
||||
const [chartData, setChartData] = useState<ChartData | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
router.push('/auth/login')
|
||||
return
|
||||
}
|
||||
|
||||
// Parse JWT to get user info (in production, verify with server)
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||
setUser(payload)
|
||||
fetchDashboardData()
|
||||
} catch (error) {
|
||||
console.error('Invalid token:', error)
|
||||
localStorage.removeItem('token')
|
||||
router.push('/auth/login')
|
||||
}
|
||||
}, [router])
|
||||
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
// Fetch stats
|
||||
const statsResponse = await fetch('/api/dashboard/stats', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (statsResponse.ok) {
|
||||
const statsData = await statsResponse.json()
|
||||
setStats(statsData)
|
||||
}
|
||||
|
||||
// Fetch chart data
|
||||
const chartsResponse = await fetch('/api/dashboard/charts', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (chartsResponse.ok) {
|
||||
const chartsData = await chartsResponse.json()
|
||||
setChartData(chartsData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dashboard data:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (!token) {
|
||||
router.push('/auth/login')
|
||||
return
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token')
|
||||
router.push('/auth/login')
|
||||
// Parse JWT to get user info (in production, verify with server)
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]))
|
||||
setUser(payload)
|
||||
fetchDashboardData()
|
||||
} catch (error) {
|
||||
console.error('Invalid token:', error)
|
||||
localStorage.removeItem('token')
|
||||
router.push('/auth/login')
|
||||
}
|
||||
}, [router])
|
||||
|
||||
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>
|
||||
)
|
||||
const fetchDashboardData = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
|
||||
// Fetch stats
|
||||
const statsResponse = await fetch('/api/dashboard/stats', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (statsResponse.ok) {
|
||||
const statsData = await statsResponse.json()
|
||||
setStats(statsData)
|
||||
}
|
||||
|
||||
// Fetch chart data
|
||||
const chartsResponse = await fetch('/api/dashboard/charts', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (chartsResponse.ok) {
|
||||
const chartsData = await chartsResponse.json()
|
||||
setChartData(chartsData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dashboard data:', error)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token')
|
||||
router.push('/auth/login')
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
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">Dashboard</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<span className="text-sm text-gray-700">
|
||||
Selamat datang, <strong>{user?.name}</strong> ({user?.role})
|
||||
</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div 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">
|
||||
Selamat datang, {user?.name}!
|
||||
</h2>
|
||||
<p className="text-gray-600">
|
||||
{user?.role === 'ADMIN' && 'Kelola sistem sekolah dari dashboard admin.'}
|
||||
{user?.role === 'TEACHER' && 'Kelola kelas, siswa, dan nilai dari dashboard guru.'}
|
||||
{user?.role === 'STUDENT' && 'Lihat jadwal, nilai, dan absensi Anda.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
{user?.role === 'ADMIN' && stats && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-blue-100">
|
||||
<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 className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Total Siswa</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">{stats.totalStudents}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-green-100">
|
||||
<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 className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Total Guru</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">{stats.totalTeachers}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-purple-100">
|
||||
<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 className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Total Kelas</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">{stats.totalClasses}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-yellow-100">
|
||||
<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 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 className="ml-4">
|
||||
<p className="text-sm font-medium text-gray-500">Mata Pelajaran</p>
|
||||
<p className="text-2xl font-semibold text-gray-900">{stats.totalSubjects}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Charts Section */}
|
||||
{user?.role === 'ADMIN' && chartData && (
|
||||
<div className="mb-8">
|
||||
<h3 className="text-xl font-bold text-gray-900 mb-6">Analytics Dashboard</h3>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* Student Enrollment by Class */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-4">Jumlah Siswa per Kelas</h4>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData.enrollmentData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="students" fill="#3B82F6" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Grade Distribution */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-4">Distribusi Nilai</h4>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={chartData.gradeData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={({ name, percent }) => `${name}: ${((percent ?? 0) * 100).toFixed(0)}%`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="count"
|
||||
>
|
||||
{chartData.gradeData.map((entry, index) => {
|
||||
const colors = ['#10B981', '#3B82F6', '#F59E0B', '#EF4444', '#6B7280']
|
||||
return <Cell key={`cell-${index}`} fill={colors[index % colors.length]} />
|
||||
})}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Attendance Trend */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-4">Tren Kehadiran (6 Bulan Terakhir)</h4>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<LineChart data={chartData.attendanceData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="month" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line type="monotone" dataKey="present" stroke="#10B981" name="Hadir" />
|
||||
<Line type="monotone" dataKey="absent" stroke="#EF4444" name="Tidak Hadir" />
|
||||
<Line type="monotone" dataKey="late" stroke="#F59E0B" name="Terlambat" />
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
|
||||
{/* Teachers per Subject */}
|
||||
<div className="bg-white p-6 rounded-lg shadow-sm border">
|
||||
<h4 className="text-lg font-semibold text-gray-900 mb-4">Guru per Mata Pelajaran</h4>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={chartData.subjectData} layout="horizontal">
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis type="number" />
|
||||
<YAxis type="category" dataKey="name" width={100} />
|
||||
<Tooltip />
|
||||
<Bar dataKey="teachers" fill="#8B5CF6" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{user?.role === 'ADMIN' && (
|
||||
<>
|
||||
<Link href="/dashboard/students" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-blue-100">
|
||||
<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 className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Kelola Siswa</h3>
|
||||
<p className="text-sm text-gray-600">Tambah, edit, dan kelola data siswa</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/teachers" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-green-100">
|
||||
<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 className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Kelola Guru</h3>
|
||||
<p className="text-sm text-gray-600">Tambah, edit, dan kelola data guru</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/classes" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-purple-100">
|
||||
<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 className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Kelola Kelas</h3>
|
||||
<p className="text-sm text-gray-600">Atur kelas dan mata pelajaran</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{user?.role === 'TEACHER' && (
|
||||
<>
|
||||
<Link href="/dashboard/my-classes" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-blue-100">
|
||||
<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="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 className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Kelas Saya</h3>
|
||||
<p className="text-sm text-gray-600">Lihat dan kelola kelas yang Anda ajar</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/attendance" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-yellow-100">
|
||||
<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>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Absensi</h3>
|
||||
<p className="text-sm text-gray-600">Input dan kelola absensi siswa</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/grades" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-purple-100">
|
||||
<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>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Nilai</h3>
|
||||
<p className="text-sm text-gray-600">Input dan kelola nilai siswa</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
{user?.role === 'STUDENT' && (
|
||||
<>
|
||||
<Link href="/dashboard/my-grades" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-blue-100">
|
||||
<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="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 className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Nilai Saya</h3>
|
||||
<p className="text-sm text-gray-600">Lihat nilai dan progress akademik</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/my-attendance" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-yellow-100">
|
||||
<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>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Absensi Saya</h3>
|
||||
<p className="text-sm text-gray-600">Lihat riwayat kehadiran</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/dashboard/schedule" className="bg-white p-6 rounded-lg shadow-sm border hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center">
|
||||
<div className="p-3 rounded-full bg-green-100">
|
||||
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-4">
|
||||
<h3 className="text-lg font-semibold text-gray-900">Jadwal</h3>
|
||||
<p className="text-sm text-gray-600">Lihat jadwal pelajaran</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
// Statistik kehadiran Jarwo untuk dashboard admin
|
||||
const jarwoAttendanceWeek = [
|
||||
{ day: 'Senin', hadir: true },
|
||||
{ day: 'Selasa', hadir: false },
|
||||
{ day: 'Rabu', hadir: true },
|
||||
{ day: 'Kamis', hadir: false },
|
||||
{ day: 'Jumat', hadir: true },
|
||||
]
|
||||
const jarwoHadirCount = jarwoAttendanceWeek.filter((a) => a.hadir).length
|
||||
|
||||
if (user?.role === 'ADMIN' && stats) {
|
||||
return (
|
||||
<div className='min-h-screen bg-gray-50'>
|
||||
<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'>
|
||||
Dashboard Admin
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center space-x-4'>
|
||||
<span className='text-sm text-gray-700'>
|
||||
Selamat datang, <strong>{user?.name}</strong> ({user?.role})
|
||||
</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className='text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium'
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className='max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8'>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8'>
|
||||
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-3 rounded-full bg-blue-100'>
|
||||
<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 className='ml-4'>
|
||||
<p className='text-sm font-medium text-gray-500'>
|
||||
Total Siswa
|
||||
</p>
|
||||
<p className='text-2xl font-semibold text-gray-900'>
|
||||
{stats.totalStudents}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-3 rounded-full bg-green-100'>
|
||||
<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 className='ml-4'>
|
||||
<p className='text-sm font-medium text-gray-500'>
|
||||
Total Guru
|
||||
</p>
|
||||
<p className='text-2xl font-semibold text-gray-900'>
|
||||
{stats.totalTeachers}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-3 rounded-full bg-purple-100'>
|
||||
<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 className='ml-4'>
|
||||
<p className='text-sm font-medium text-gray-500'>
|
||||
Total Kelas
|
||||
</p>
|
||||
<p className='text-2xl font-semibold text-gray-900'>
|
||||
{stats.totalClasses}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-white p-6 rounded-lg shadow-sm border'>
|
||||
<div className='flex items-center'>
|
||||
<div className='p-3 rounded-full bg-yellow-100'>
|
||||
<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 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 className='ml-4'>
|
||||
<p className='text-sm font-medium text-gray-500'>
|
||||
Mata Pelajaran
|
||||
</p>
|
||||
<p className='text-2xl font-semibold text-gray-900'>
|
||||
{stats.totalSubjects}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Statistik Kehadiran Jarwo */}
|
||||
<div className='mb-8'>
|
||||
<h3 className='text-2xl font-bold text-blue-700 mb-6 flex items-center gap-2'>
|
||||
<svg
|
||||
className='w-7 h-7 text-blue-500'
|
||||
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>
|
||||
Statistik Kehadiran Siswa:{' '}
|
||||
<span className='text-blue-900'>Jarwo</span>
|
||||
</h3>
|
||||
<div className='bg-gradient-to-br from-blue-50 to-green-50 rounded-xl shadow-lg border border-blue-100 p-8 mb-6'>
|
||||
<div className='flex items-center justify-between mb-4'>
|
||||
<div>
|
||||
<p className='text-lg font-semibold text-gray-900'>
|
||||
Kehadiran Minggu Ini
|
||||
</p>
|
||||
<p className='text-sm text-gray-500'>
|
||||
Periode: <span className='font-medium'>Senin - Jumat</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='px-3 py-1 rounded-full bg-green-100 text-green-700 font-bold text-sm flex items-center gap-1'>
|
||||
<svg
|
||||
className='w-4 h-4'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
{jarwoHadirCount} Hadir
|
||||
</span>
|
||||
<span className='px-3 py-1 rounded-full bg-red-100 text-red-700 font-bold text-sm flex items-center gap-1'>
|
||||
<svg
|
||||
className='w-4 h-4'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M6 18L18 6M6 6l12 12'
|
||||
/>
|
||||
</svg>
|
||||
{5 - jarwoHadirCount} Tidak Hadir
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='grid grid-cols-2 md:grid-cols-4 gap-4 mb-4'>
|
||||
{jarwoAttendanceWeek.map((a, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex flex-col items-center py-3 rounded-lg border ${
|
||||
a.hadir
|
||||
? 'bg-green-50 border-green-200'
|
||||
: 'bg-red-50 border-red-200'
|
||||
}`}
|
||||
>
|
||||
<span className='text-gray-700 font-medium mb-1'>
|
||||
{a.day}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
a.hadir
|
||||
? 'text-green-600 font-bold'
|
||||
: 'text-red-500 font-bold'
|
||||
}
|
||||
>
|
||||
{a.hadir ? (
|
||||
<span className='flex items-center gap-1'>
|
||||
<svg
|
||||
className='w-4 h-4'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M5 13l4 4L19 7'
|
||||
/>
|
||||
</svg>
|
||||
Hadir
|
||||
</span>
|
||||
) : (
|
||||
<span className='flex items-center gap-1'>
|
||||
<svg
|
||||
className='w-4 h-4'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M6 18L18 6M6 6l12 12'
|
||||
/>
|
||||
</svg>
|
||||
Tidak Hadir
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='mt-4'>
|
||||
<p className='text-sm text-gray-600 mb-2'>
|
||||
Progress Kehadiran Mingguan
|
||||
</p>
|
||||
<div className='w-full h-5 bg-gray-200 rounded-full overflow-hidden'>
|
||||
<div
|
||||
className='h-5 bg-gradient-to-r from-green-400 to-blue-400 rounded-full transition-all duration-500'
|
||||
style={{ width: `${(jarwoHadirCount / 7) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className='text-xs text-gray-500 mt-2'>
|
||||
{jarwoHadirCount} dari 7 hari hadir
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-white rounded-xl shadow-lg border border-blue-100 p-8'>
|
||||
<h4 className='text-lg font-semibold text-blue-700 mb-4 flex items-center gap-2'>
|
||||
<svg
|
||||
className='w-5 h-5 text-blue-400'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<path
|
||||
strokeLinecap='round'
|
||||
strokeLinejoin='round'
|
||||
strokeWidth={2}
|
||||
d='M9 17v-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>
|
||||
Grafik Kehadiran Mingguan
|
||||
</h4>
|
||||
<ResponsiveContainer width='100%' height={180}>
|
||||
<BarChart
|
||||
data={jarwoAttendanceWeek.map((a) => ({
|
||||
day: a.day,
|
||||
hadir: a.hadir ? 1 : 0,
|
||||
}))}
|
||||
>
|
||||
<CartesianGrid strokeDasharray='3 3' />
|
||||
<XAxis dataKey='day' />
|
||||
<YAxis allowDecimals={false} domain={[0, 1]} />
|
||||
<Tooltip
|
||||
formatter={(value) =>
|
||||
value === 1 ? 'Hadir' : 'Tidak Hadir'
|
||||
}
|
||||
/>
|
||||
<Bar dataKey='hadir' fill='#38bdf8' radius={[8, 8, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Custom dashboard for student named Jarwo
|
||||
if (user?.role === 'STUDENT' && user?.name?.toLowerCase() === 'jarwo') {
|
||||
// Simulated attendance data for Jarwo
|
||||
const attendanceWeek = [
|
||||
{ day: 'Senin', hadir: true },
|
||||
{ day: 'Selasa', hadir: false },
|
||||
{ day: 'Rabu', hadir: true },
|
||||
{ day: 'Kamis', hadir: false },
|
||||
{ day: 'Jumat', hadir: true },
|
||||
]
|
||||
const hadirCount = attendanceWeek.filter((a) => a.hadir).length
|
||||
return (
|
||||
<div className='min-h-screen bg-gray-50'>
|
||||
<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'>
|
||||
Dashboard Siswa
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex items-center space-x-4'>
|
||||
<span className='text-sm text-gray-700'>
|
||||
Selamat datang, <strong>{user?.name}</strong> ({user?.role})
|
||||
</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className='text-gray-600 hover:text-gray-900 px-3 py-2 rounded-md text-sm font-medium'
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div className='max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-8'>
|
||||
<div className='mb-8'>
|
||||
<h2 className='text-2xl font-bold text-gray-900'>
|
||||
Statistik Kehadiran Mingguan
|
||||
</h2>
|
||||
<p className='text-gray-600 mb-2'>
|
||||
Siswa: <strong>Jarwo</strong>
|
||||
</p>
|
||||
<div className='bg-white rounded-lg shadow-sm border p-6 mb-6'>
|
||||
<p className='text-lg font-semibold text-gray-900 mb-2'>
|
||||
Kehadiran Minggu Ini
|
||||
</p>
|
||||
<div className='flex flex-col gap-2'>
|
||||
{attendanceWeek.map((a, idx) => (
|
||||
<div key={idx} className='flex justify-between items-center'>
|
||||
<span className='text-gray-700'>{a.day}</span>
|
||||
<span
|
||||
className={
|
||||
a.hadir
|
||||
? 'text-green-600 font-bold'
|
||||
: 'text-red-500 font-bold'
|
||||
}
|
||||
>
|
||||
{a.hadir ? 'Hadir' : 'Tidak Hadir'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className='mt-4'>
|
||||
<p className='text-sm text-gray-600'>
|
||||
Total hadir:{' '}
|
||||
<span className='font-bold text-green-600'>{hadirCount}</span>{' '}
|
||||
dari 7 hari
|
||||
</p>
|
||||
<div className='w-full h-4 bg-gray-200 rounded-full mt-2'>
|
||||
<div
|
||||
className='h-4 bg-green-500 rounded-full'
|
||||
style={{ width: `${(hadirCount / 7) * 100}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='bg-white rounded-lg shadow-sm border p-6'>
|
||||
<h3 className='text-lg font-semibold text-gray-900 mb-2'>
|
||||
Grafik Kehadiran
|
||||
</h3>
|
||||
<ResponsiveContainer width='100%' height={220}>
|
||||
<BarChart
|
||||
data={attendanceWeek.map((a) => ({
|
||||
day: a.day,
|
||||
hadir: a.hadir ? 1 : 0,
|
||||
}))}
|
||||
>
|
||||
<CartesianGrid strokeDasharray='3 3' />
|
||||
<XAxis dataKey='day' />
|
||||
<YAxis allowDecimals={false} domain={[0, 1]} />
|
||||
<Tooltip
|
||||
formatter={(value) =>
|
||||
value === 1 ? 'Hadir' : 'Tidak Hadir'
|
||||
}
|
||||
/>
|
||||
<Bar dataKey='hadir' fill='#10B981' />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// ...existing code...
|
||||
}
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "app",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
Loading…
Reference in New Issue