mian commit

This commit is contained in:
Kevin Carter 2026-03-01 18:32:49 +00:00
parent c7e1203187
commit 3754ea4c28
135 changed files with 18211 additions and 0 deletions

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

26
Dockerfile Normal file
View File

@ -0,0 +1,26 @@
# Use Node.js as the base image
FROM node:18
# Set the working directory inside the container
WORKDIR /app
# Copy package.json and package-lock.json files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Generate Database
RUN npx prisma migrate dev --name init
# Build the Next.js application
RUN npm run build
# Expose the port the app runs on
EXPOSE 3000
# Start the Next.js application
CMD ["npm", "start"]

27
docker-compose.yml Normal file
View File

@ -0,0 +1,27 @@
version: '3.8'
services:
postgress:
image: postgres:15
container_name: postgres_db
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypassword
POSTGRES_DB: mydb
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
app:
build: .
container_name: nextjs_app
ports:
- '3000:3000'
environment:
- DATABASE_URL=postgresql://myuser:mypassword@192.168.0.48:5432/mydb
depends_on:
- postgress
volumes:
postgres_data:

10
jwt_dump.json Normal file
View File

@ -0,0 +1,10 @@
{
"aud": "authenticated",
"exp": 1772176558,
"iat": 1772176498,
"iss": "https://kind-burro-23.clerk.accounts.dev",
"jti": "0689311f9e0ff06da236",
"nbf": 1772176493,
"role": "admin",
"sub": "user_3AE7OSCzF8rz7XnvTkOUmFV4AKp"
}

8
next.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [{ hostname: "images.pexels.com" }],
},
};
export default nextConfig;

7404
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View File

@ -0,0 +1,46 @@
{
"name": "lama-dev-next-dashboard",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"seed:users": "tsx scripts/seed.ts",
"seed:schedule": "tsx scripts/seed_schedule.ts"
},
"dependencies": {
"@clerk/elements": "^0.14.6",
"@clerk/nextjs": "^5.4.1",
"@hookform/resolvers": "^3.9.0",
"@prisma/client": "^5.19.1",
"@supabase/supabase-js": "^2.98.0",
"@types/react-big-calendar": "^1.8.9",
"moment": "^2.30.1",
"next": "14.2.5",
"next-cloudinary": "^6.13.0",
"prisma": "^5.19.1",
"react": "^18",
"react-big-calendar": "^1.13.2",
"react-calendar": "^5.0.0",
"react-dom": "^18",
"react-hook-form": "^7.52.2",
"react-toastify": "^10.0.5",
"recharts": "^2.12.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"dotenv": "^17.3.1",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"ts-node": "^10.9.2",
"tsx": "^4.21.0",
"typescript": "^5"
}
}

8
postcss.config.mjs Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;

BIN
public/announcement.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/assignment.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/attendance.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
public/avatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/blood.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/calendar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/class.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/close.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/create.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/date.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/delete.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/exam.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/filter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/finance.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
public/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/lesson.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
public/logout.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/mail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 830 B

BIN
public/maleFemale.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
public/message.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/more.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/moreDark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/noAvatar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
public/parent.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/phone.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

BIN
public/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
public/result.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/search.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/setting.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/singleAttendance.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
public/singleBranch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/singleClass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
public/singleLesson.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/sort.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/student.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
public/subject.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/teacher.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
public/update.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/upload.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

BIN
public/view.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

142
scripts/seed-data.json Normal file
View File

@ -0,0 +1,142 @@
{
"teacherMap": {
"1": "user_3AJAkSqshofbdPsW7lZh6q2eCe5",
"2": "user_3AJAkVye7wKBBK8seog6gljyc7L",
"3": "user_3AJAkXA9lrOHxsGcJNoJGOXKNOr",
"4": "user_3AJAkc3f42kZzVPLvaRBFHor14J",
"5": "user_3AJAkbsXvHmZcGu3wFpRZm732LX",
"6": "user_3AJAke4rOtbf26yRM9JoHhtEjyD",
"7": "user_3AJAklVf8Pnm2pH6Ql1PnfQBIID",
"8": "user_3AJAki8tuWw9rWvZZD9xIP6CxVp",
"9": "user_3AJAkn3ET3kFyUrx636ayvHrLds",
"10": "user_3AJAkuiTMWimGNBtw7ymHVUqwVW",
"11": "user_3AJAkvjGTDDsscx7g9dD998d6Gq",
"12": "user_3AJAkr08HMjIKMG1Guzw3HJqB9L",
"13": "user_3AJAl1xiX1ZtMg0ohV7QB2TpHm5",
"14": "user_3AJAl1tyuI5t3DfmleToHAw6HgJ",
"15": "user_3AJAl3Rr5gq7K1W0fhl6AVNFXrY"
},
"parentMap": {
"1": "user_3AJAl6KD5fVP5MbjDZLpyoZ7sDG",
"2": "user_3AJAl9gkapHVzn5Tik7RteIEJVZ",
"3": "user_3AJAl5BLWwfpho7phY16SrCleE8",
"4": "user_3AJAl7BN77L27mpmHzjCp96OlFx",
"5": "user_3AJAlFko0fvXyleuRuyqmGkpgTV",
"6": "user_3AJAlEyIdgY7vVA7yZQUbTwimOC",
"7": "user_3AJAlIlHOwSpcR92ENGiHrz0nCg",
"8": "user_3AJAlKekpw2nrtGnnMBvN0xdh3c",
"9": "user_3AJAlLmCzuvKoqh9yh2ulnmz26h",
"10": "user_3AJAlPbUuestp7PvH2BuX2GQK6R",
"11": "user_3AJAlWR17pNYP0S24hiNnUKI2R1",
"12": "user_3AJAlWTNINh2uIOEueR8dPKXjlc",
"13": "user_3AJAlUx3a2u65hTIoxWLjmDnDuj",
"14": "user_3AJAlac7l5vhi9lbnvuOIEZ2zoz",
"15": "user_3AJAldy0F23q4Jm8kz8olXdoao0",
"16": "user_3AJAlekANUdcMJeZ34EFcCjluAq",
"17": "user_3AJAllI3cBBjs5JbcUimhrLEYvf",
"18": "user_3AJAlp8YJPzQwvcutFoCsHAz3Sn",
"19": "user_3AJAllVI1vR6vwSPlLPCyIb9oZB",
"20": "user_3AJAlwjmMMnvV9la8fFlH7TYuOS",
"21": "user_3AJAlxGYIRJnKZmvkt2HB9ukrlC",
"22": "user_3AJAltSwlQoPrRGcsAZcJpH2sH9",
"23": "user_3AJAlvcnUE4GGbYKK0odLUjwxqu",
"24": "user_3AJAm16YficrMn5CeAOCAxV8iu1",
"25": "user_3AJAlxj9Z9gE4EIwQ5pljEywIV0"
},
"studentMap": {
"1": "user_3AJAm7o0RjL16Gt1jE9YLG0tib7",
"2": "user_3AJAm9SoYdWl5sQfbf8slNI2Qn4",
"3": "user_3AJAm8NIt39YG9DPjnAwq2G2V3G",
"4": "user_3AJAm5KPpNLvz8DjwNU6MRCBO5e",
"5": "user_3AJAmGDojjfnVtn7LrmLVemoCiM",
"6": "user_3AJAmDK9jVHWfe3FjdR7emQ329K",
"7": "user_3AJAmMNLtdeplBIS9kwXYW0FC9Z",
"8": "user_3AJAmNXiIZz0FYILKDOZxq1ET56",
"9": "user_3AJAmMeKtN7oMMrLgcxIEWsm0EV",
"10": "user_3AJAmQsp9Xg8LDpxCkkmccjDCuU",
"11": "user_3AJAmZtzvmAwwu7QJRvIpxZCNYF",
"12": "user_3AJAmXM1fL4b0fONRdM5uIRIxBS",
"13": "user_3AJAmi4pxmDtV24YaIaNmmack33",
"14": "user_3AJAmf864NjSUdQ6ExiMR3zaDCn",
"15": "user_3AJAmbZArRtgzFrBdW77qbd7P1z",
"16": "user_3AJAmpWu9wzc4ve2kwTKPLveFST",
"17": "user_3AJAmoVVKEmMeE8GMd7A2fgn5nY",
"18": "user_3AJAmlYITikQW1km45TqvQ2biBm",
"19": "user_3AJAmuEU2s2CLT9391jnj6Uxb3e",
"20": "user_3AJAmwfQjhHcnd6Y2HGvXWTxTj8",
"21": "user_3AJAmxavPXIvafOo84MWRqKPyRN",
"22": "user_3AJAmxtksJ1U2UjHoP1wzXgk3JB",
"23": "user_3AJAn3gfMCCyfaFrX0yY69XfQfQ",
"24": "user_3AJAn1UL8HJe9pfcZboyDCqg4Zd",
"25": "user_3AJAn4fAYAtcYCLKv1fBkBJSyxw",
"26": "user_3AJAnBlPYjZBRY7HrMaY95WRQ59",
"27": "user_3AJAnD31sahtYeq9Zfzu867VgB8",
"28": "user_3AJAnK8qcNr259cZnIKpH4ZuoRH",
"29": "user_3AJAnECM3SCOTVaHQvwaIkV1YnJ",
"30": "user_3AJAnH4sNG5lbNwuvYHmmbU9J3U",
"31": "user_3AJAnRjFXzyoALI4egtIvZEUFuA",
"32": "user_3AJAnYhwtMvevCuQScoLSJePp79",
"33": "user_3AJAnXQkG2mOAFxFm1lHaOV2aJQ",
"34": "user_3AJAnYU4Frx0bX863nwBE9EJaGH",
"35": "user_3AJAncF3uP2xfkU6WixizrfSSrl",
"36": "user_3AJAngPagGao0QCe5AcGeeGGpDS",
"37": "user_3AJAnazAi3ERTCfqVQr7jDtTOK4",
"38": "user_3AJAnoJnVGxwohyI3ot6cwnZ6iO",
"39": "user_3AJAnoCbmMiBBlXE3PvUN1vg3CT",
"40": "user_3AJAniemPzyyEwPSZZ2h7iXK9lV",
"41": "user_3AJAnvqsElaS1dwqPtmhw4N7dCS",
"42": "user_3AJAnqp0r30RwkxKEJK3nrPJcwR",
"43": "user_3AJAnsBcWHQQGYCpKxLbfWpSWdE",
"44": "user_3AJAnrDUy5R1iFD1Vsb8FNlDO1I",
"45": "user_3AJAo1kdbtoIYYx3gvEcr3D9WT8",
"46": "user_3AJAo0CALCnqJle4D4JGt5VNqHJ",
"47": "user_3AJAo1fZPiFTp8U0NYaprnET0Ls",
"48": "user_3AJAoB35YG2oZHKLvT4UFBQVBqj",
"49": "user_3AJAoDZ1nOyp3f3KO2Lefzgi6Ah",
"50": "user_3AJAo6si4CGdeQ3teM04nPmrJe2"
},
"classes": [
{
"id": 1,
"name": "1A",
"gradeId": 1,
"capacity": 20,
"supervisorId": "user_3AJAkSqshofbdPsW7lZh6q2eCe5"
},
{
"id": 2,
"name": "2A",
"gradeId": 2,
"capacity": 20,
"supervisorId": "user_3AJAkVye7wKBBK8seog6gljyc7L"
},
{
"id": 3,
"name": "3A",
"gradeId": 3,
"capacity": 20,
"supervisorId": "user_3AJAkXA9lrOHxsGcJNoJGOXKNOr"
},
{
"id": 4,
"name": "4A",
"gradeId": 4,
"capacity": 20,
"supervisorId": "user_3AJAkc3f42kZzVPLvaRBFHor14J"
},
{
"id": 5,
"name": "5A",
"gradeId": 5,
"capacity": 20,
"supervisorId": "user_3AJAkbsXvHmZcGu3wFpRZm732LX"
},
{
"id": 6,
"name": "6A",
"gradeId": 6,
"capacity": 20,
"supervisorId": "user_3AJAkSqshofbdPsW7lZh6q2eCe5"
}
]
}

261
scripts/seed.ts Normal file
View File

@ -0,0 +1,261 @@
import "dotenv/config";
import { createClient } from "@supabase/supabase-js";
import { clerkClient } from "@clerk/nextjs/server";
import * as fs from "fs";
import * as path from "path";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
"sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz",
{ auth: { persistSession: false } }
);
const PASSWORD = "&%5C400l&%";
async function cleanClerk() {
console.log("Cleaning up old Clerk users...");
const clerk = clerkClient();
const users = await clerk.users.getUserList({ limit: 500 });
for (const user of users.data) {
if (
user.publicMetadata.role === "teacher" ||
user.publicMetadata.role === "student" ||
user.publicMetadata.role === "parent"
) {
await clerk.users.deleteUser(user.id);
}
}
}
async function cleanSupabase() {
console.log("Cleaning up Supabase tables...");
const tables = [
"Result", "Assignment", "Exam", "Attendance", "Event", "Announcement",
"Lesson", "TeacherSubject", "Student", "Teacher", "Parent", "Class", "Subject", "Grade"
];
for (const table of tables) {
await supabase.from(table).delete().neq("id", "0" as any);
}
}
async function seedAdmin() {
console.log("Syncing Admin...");
const clerk = clerkClient();
const users = await clerk.users.getUserList({ limit: 100 });
const adminUser = users.data.find(u => u.username === "admin" || u.emailAddresses[0]?.emailAddress?.includes("admin"));
if (adminUser) {
await supabase.from("Admin").upsert({ id: adminUser.id, username: adminUser.username || "admin" });
console.log(`Synced Admin ID: ${adminUser.id}`);
}
}
async function main() {
try {
const clerk = clerkClient();
await cleanClerk();
await cleanSupabase();
await seedAdmin();
console.log("Seeding Grades and Subjects...");
const grades = [1, 2, 3, 4, 5, 6].map((level) => ({ id: level, level }));
await supabase.from("Grade").insert(grades);
const subjectsArray = [
{ id: 1, name: "Mathematics" },
{ id: 2, name: "Science" },
{ id: 3, name: "English" },
{ id: 4, name: "History" },
{ id: 5, name: "Geography" },
{ id: 6, name: "Physics" },
{ id: 7, name: "Chemistry" },
{ id: 8, name: "Biology" },
{ id: 9, name: "Computer Science" },
{ id: 10, name: "Art" },
];
await supabase.from("Subject").insert(subjectsArray);
console.log("Creating 15 Teachers...");
const teacherMap: Record<number, string> = {};
for (let i = 1; i <= 15; i++) {
const user = await clerk.users.createUser({
username: `teacher${i}`,
password: PASSWORD,
firstName: `TName${i}`,
lastName: `TSurname${i}`,
publicMetadata: { role: "teacher" }
});
teacherMap[i] = user.id;
await supabase.from("Teacher").insert({
id: user.id,
username: `teacher${i}`,
name: `TName${i}`,
surname: `TSurname${i}`,
email: `teacher${i}@example.com`,
phone: `123-456-789${i}`,
address: `Address${i}`,
bloodType: "A+",
sex: i % 2 === 0 ? "MALE" : "FEMALE",
birthday: "1996-02-27T00:26:35.280Z"
});
await supabase.from("TeacherSubject").insert([
{ subjectId: (i % 10) + 1, teacherId: user.id, isPrimary: true },
{ subjectId: ((i + 1) % 10) + 1, teacherId: user.id }
]);
}
console.log("Creating 6 Classes...");
const classesArray = [
{ id: 1, name: "1A", gradeId: 1, capacity: 20, supervisorId: teacherMap[1] },
{ id: 2, name: "2A", gradeId: 2, capacity: 20, supervisorId: teacherMap[2] },
{ id: 3, name: "3A", gradeId: 3, capacity: 20, supervisorId: teacherMap[3] },
{ id: 4, name: "4A", gradeId: 4, capacity: 20, supervisorId: teacherMap[4] },
{ id: 5, name: "5A", gradeId: 5, capacity: 20, supervisorId: teacherMap[5] },
{ id: 6, name: "6A", gradeId: 6, capacity: 20, supervisorId: teacherMap[1] },
];
await supabase.from("Class").insert(classesArray);
console.log("Creating 25 Parents...");
const parentMap: Record<number, string> = {};
for (let i = 1; i <= 25; i++) {
const user = await clerk.users.createUser({
username: `parent${i}`,
password: PASSWORD,
firstName: `PName${i}`,
lastName: `PSurname${i}`,
publicMetadata: { role: "parent" }
});
parentMap[i] = user.id;
await supabase.from("Parent").insert({
id: user.id,
username: `parent${i}`,
name: `PName${i}`,
surname: `PSurname${i}`,
email: `parent${i}@example.com`,
phone: `123-456-789${i}`,
address: `Address${i}`
});
}
console.log("Creating 50 Students...");
const studentMap: Record<number, string> = {};
for (let i = 1; i <= 50; i++) {
const user = await clerk.users.createUser({
username: `student${i}`,
password: PASSWORD,
firstName: `SName${i}`,
lastName: `SSurname${i}`,
publicMetadata: { role: "student" }
});
studentMap[i] = user.id;
const classInfo = classesArray[(i % 6)];
await supabase.from("Student").insert({
id: user.id,
username: `student${i}`,
name: `SName${i}`,
surname: `SSurname${i}`,
email: `student${i}@example.com`,
phone: `987-654-321${i}`,
address: `Address${i}`,
bloodType: "O-",
sex: i % 2 === 0 ? "MALE" : "FEMALE",
parentId: parentMap[(i % 25) + 1],
gradeId: classInfo.gradeId,
classId: classInfo.id,
birthday: "2016-02-27T00:26:35.281Z"
});
}
console.log("Exporting User IDs to seed-data.json...");
const seedDataPath = path.join(process.cwd(), "scripts", "seed-data.json");
const seedData = {
teacherMap,
parentMap,
studentMap,
classes: classesArray
};
fs.writeFileSync(seedDataPath, JSON.stringify(seedData, null, 2));
console.log("User generation complete! Saved to seed-data.json.");
/*
const daysOfWeek: ("MONDAY" | "TUESDAY" | "WEDNESDAY" | "THURSDAY" | "FRIDAY")[] = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"];
for (let i = 1; i <= 30; i++) {
const classIndex = i % 6;
const classInfo = classesArray[classIndex];
const teacherIndex = (i % 15) + 1;
const teacherId = teacherMap[teacherIndex];
const subjectId = (teacherIndex % 10) + 1;
// Distribute across 5 days (Monday to Friday)
const dayIndex = i % 5;
const dayName = daysOfWeek[dayIndex];
// Distribute across 6 periods (e.g., 8:00 AM to 2:00 PM)
const periodIndex = (i % 6);
// Base date (a Monday): 2026-02-23T00:00:00.000Z
const lessonDate = new Date("2026-02-23T00:00:00.000Z");
lessonDate.setDate(lessonDate.getDate() + dayIndex);
// Create start time and end time (1 hour lesson)
const startHour = 8 + periodIndex;
const startTime = new Date(lessonDate);
startTime.setUTCHours(startHour, 0, 0, 0);
const endTime = new Date(lessonDate);
endTime.setUTCHours(startHour + 1, 0, 0, 0);
await supabase.from("Lesson").insert({
id: i,
name: `Lesson${i}`,
day: dayName,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
subjectId: subjectId,
classId: classInfo.id,
teacherId: teacherId
});
await supabase.from("Exam").insert({
id: i,
title: `Exam ${i}`,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
lessonId: i
});
await supabase.from("Assignment").insert({
id: i,
title: `Assignment ${i}`,
startDate: startTime.toISOString(),
dueDate: new Date(startTime.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(), // Due in 7 days
lessonId: i
});
// Result requires a student from the class 'classIndex'.
// Student 'j' is in class 'j % 6'.
const studentBase = classIndex === 0 ? 6 : classIndex;
const studentIndex = studentBase + 6 * (i % 8); // Ensures variation 1-48
await supabase.from("Result").insert({
id: i,
score: 90 + (i % 10),
studentId: studentMap[studentIndex],
examId: i
});
}
*/
} catch (err) {
console.error("Seed error:", err);
}
}
main();

226
scripts/seed_schedule.ts Normal file
View File

@ -0,0 +1,226 @@
import "dotenv/config";
import { createClient } from "@supabase/supabase-js";
import * as fs from "fs";
import * as path from "path";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
"sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz",
{ auth: { persistSession: false } }
);
const SUBJECTS: Record<string, number> = {
Math: 1,
Science: 2,
English: 3,
History: 4,
Art: 5,
Music: 6,
PE: 7,
Computer: 8,
Geography: 9,
Biology: 10
};
// -------------------------------------------------------------
// CONFIGURATION: Adjust the timeline spread and generation odds
// -------------------------------------------------------------
const CONFIG = {
monthsPast: 3, // How many months backward to generate lessons
monthsFuture: 9, // How many months forward to generate lessons
baseDate: new Date("2026-02-23T00:00:00.000Z"), // "Present" date
examProbability: 0.05, // 5% chance of an Exam per lesson
assignmentProbability: 0.1, // 10% chance of an Assignment per lesson
attendanceProbability: 0.95, // 95% attendance rate for past lessons
};
// Helper to chunk large arrays for Supabase batch inserts
function chunkArray<T>(array: T[], size: number): T[][] {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
async function insertInChunks(table: string, data: any[]) {
if (data.length === 0) return;
console.log(`Inserting ${data.length} records into ${table}...`);
const chunks = chunkArray(data, 1000);
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
const { error } = await supabase.from(table).insert(chunk);
if (error) {
console.error(`Error inserting into ${table} (chunk ${i}):`, error);
}
}
}
async function main() {
try {
console.log("Loading user IDs from seed-data.json...");
const seedDataPath = path.join(process.cwd(), "scripts", "seed-data.json");
if (!fs.existsSync(seedDataPath)) {
throw new Error("seed-data.json not found! Please run `npm run seed:users` first.");
}
const data = JSON.parse(fs.readFileSync(seedDataPath, "utf-8"));
const { teacherMap, studentMap, classes } = data;
console.log("Cleaning old Schedule & Attendance data...");
const tablesToClean = ["Attendance", "Result", "Assignment", "Exam", "Lesson"];
for (const table of tablesToClean) {
await supabase.from(table).delete().neq("id", "0" as any);
}
console.log(`Building full year timetable...`);
console.log(`- Past months: ${CONFIG.monthsPast}`);
console.log(`- Future months: ${CONFIG.monthsFuture}`);
const days = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"];
const periods = [8, 9, 10, 11, 13, 14]; // Expanded periods (8 AM - 2 PM, skipping 12 PM lunch)
let lessonIdCounter = 1;
let examIdCounter = 1;
let assignmentIdCounter = 1;
let resultIdCounter = 1;
let attendanceIdCounter = 1;
const lessonsData = [];
const examsData = [];
const assignmentsData = [];
const resultsData = [];
const attendancesData = [];
// Compute start of generation (aligned to a Monday)
const startDate = new Date(CONFIG.baseDate);
startDate.setMonth(startDate.getMonth() - CONFIG.monthsPast);
const startDayOfWeek = startDate.getDay();
const daysToMonday = startDayOfWeek === 0 ? -6 : 1 - startDayOfWeek;
startDate.setDate(startDate.getDate() + daysToMonday);
// Compute end of generation
const endDate = new Date(CONFIG.baseDate);
endDate.setMonth(endDate.getMonth() + CONFIG.monthsFuture);
let currentWeekStart = new Date(startDate);
while (currentWeekStart < endDate) {
for (let classIndex = 0; classIndex < classes.length; classIndex++) {
const classInfo = classes[classIndex];
// Gather students belonging to this class based on seed logic
const studentList: string[] = [];
for (let i = 1; i <= 50; i++) {
const studentBase = classIndex === 0 ? 6 : classIndex;
const studentIdx = studentBase + 6 * (i % 8);
if (studentMap[studentIdx] && !studentList.includes(studentMap[studentIdx])) {
studentList.push(studentMap[studentIdx]);
}
}
for (let dayOffset = 0; dayOffset < days.length; dayOffset++) {
const dayName = days[dayOffset];
const currentDate = new Date(currentWeekStart);
currentDate.setDate(currentDate.getDate() + dayOffset);
for (let period = 0; period < periods.length; period++) {
const startHour = periods[period];
const subjectIdx = (classIndex + dayOffset + period) % 10;
const subjectKey = Object.keys(SUBJECTS)[subjectIdx];
const subjectId = SUBJECTS[subjectKey];
const teacherId = teacherMap[subjectId]; // Authorized teacher
const startTime = new Date(currentDate);
startTime.setUTCHours(startHour, 0, 0, 0);
const endTime = new Date(currentDate);
endTime.setUTCHours(startHour + 1, 0, 0, 0);
// 1. Insert Lesson
lessonsData.push({
id: lessonIdCounter,
name: `${classInfo.name} ${subjectKey} (${dayName})`,
day: dayName,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
subjectId: subjectId,
classId: classInfo.id,
teacherId: teacherId
});
// 2. Insert Exam / Results (Random Probability)
if (Math.random() < CONFIG.examProbability) {
examsData.push({
id: examIdCounter,
title: `${subjectKey} Assessment`,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
lessonId: lessonIdCounter
});
// Only give results to a subset to prevent table explosions
for (const sId of studentList.slice(0, 4)) {
resultsData.push({
id: resultIdCounter++,
score: 60 + Math.floor(Math.random() * 41), // 60-100
studentId: sId,
examId: examIdCounter
});
}
examIdCounter++;
}
// 3. Insert Assignment
if (Math.random() < CONFIG.assignmentProbability) {
assignmentsData.push({
id: assignmentIdCounter++,
title: `${subjectKey} Practice`,
startDate: startTime.toISOString(),
dueDate: new Date(startTime.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
lessonId: lessonIdCounter
});
}
// 4. Insert Attendance (ONLY for dates occurring in the past)
if (startTime < CONFIG.baseDate) {
for (const sId of studentList) {
attendancesData.push({
id: attendanceIdCounter++,
date: startTime.toISOString(),
present: Math.random() < CONFIG.attendanceProbability,
studentId: sId,
lessonId: lessonIdCounter
});
}
}
lessonIdCounter++;
}
}
}
// Advance to the next week safely
currentWeekStart.setDate(currentWeekStart.getDate() + 7);
}
// Send payload in bulk batches to Supabase to prevent network/memory bottlenecks
await insertInChunks("Lesson", lessonsData);
await insertInChunks("Exam", examsData);
await insertInChunks("Assignment", assignmentsData);
await insertInChunks("Result", resultsData);
await insertInChunks("Attendance", attendancesData);
console.log(`\n✅ Timeline successfully generated!`);
console.log(`- Lessons: ${lessonsData.length}`);
console.log(`- Exams: ${examsData.length}`);
console.log(`- Assignments: ${assignmentsData.length}`);
console.log(`- Results: ${resultsData.length}`);
console.log(`- Attendance Records: ${attendancesData.length}`);
} catch (err) {
console.error("Schedule Seeding Failed:", err);
}
}
main();

17
scripts/test.ts Normal file
View File

@ -0,0 +1,17 @@
import { createClient } from "@supabase/supabase-js";
import dotenv from "dotenv";
dotenv.config({ path: ".env" });
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
"sb_secret_N7UND0UgjKTVK-Uodkm0Hg_xSvEMPvz",
{ auth: { persistSession: false } }
);
async function test() {
const { data, error } = await supabase.from("Admin").select("*");
console.log("Admins:", data);
console.log("Error:", error);
}
test();

View File

@ -0,0 +1,49 @@
import Announcements from "@/components/Announcements";
import AttendanceChartContainer from "@/components/AttendanceChartContainer";
import CountChartContainer from "@/components/CountChartContainer";
import EventCalendarContainer from "@/components/EventCalendarContainer";
import FinanceChart from "@/components/FinanceChart";
import UserCard from "@/components/UserCard";
const AdminPage = ({
searchParams,
}: {
searchParams: { [keys: string]: string | undefined };
}) => {
return (
<div className="p-4 flex gap-4 flex-col md:flex-row">
{/* LEFT */}
<div className="w-full lg:w-2/3 flex flex-col gap-8">
{/* USER CARDS */}
<div className="flex gap-4 justify-between flex-wrap">
<UserCard type="admin" />
<UserCard type="teacher" />
<UserCard type="student" />
<UserCard type="parent" />
</div>
{/* MIDDLE CHARTS */}
<div className="flex gap-4 flex-col lg:flex-row">
{/* COUNT CHART */}
<div className="w-full lg:w-1/3 h-[450px]">
<CountChartContainer />
</div>
{/* ATTENDANCE CHART */}
<div className="w-full lg:w-2/3 h-[450px]">
<AttendanceChartContainer />
</div>
</div>
{/* BOTTOM CHART */}
<div className="w-full h-[500px]">
<FinanceChart />
</div>
</div>
{/* RIGHT */}
<div className="w-full lg:w-1/3 flex flex-col gap-8">
<EventCalendarContainer searchParams={searchParams}/>
<Announcements />
</div>
</div>
);
};
export default AdminPage;

View File

@ -0,0 +1,31 @@
import Menu from "@/components/Menu";
import Navbar from "@/components/Navbar";
import Image from "next/image";
import Link from "next/link";
export default function DashboardLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<div className="h-screen flex">
{/* LEFT */}
<div className="w-[14%] md:w-[8%] lg:w-[16%] xl:w-[14%] p-4">
<Link
href="/"
className="flex items-center justify-center lg:justify-start gap-2"
>
<Image src="/logo.png" alt="logo" width={32} height={32} />
<span className="hidden lg:block font-bold">SchooLama</span>
</Link>
<Menu />
</div>
{/* RIGHT */}
<div className="w-[86%] md:w-[92%] lg:w-[84%] xl:w-[86%] bg-[#F7F8FA] overflow-scroll flex flex-col">
<Navbar />
{children}
</div>
</div>
);
}

View File

@ -0,0 +1,138 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import { auth } from "@clerk/nextjs/server";
type AnnouncementList = Tables<"Announcement"> & { class: Tables<"Class"> | null };
const AnnouncementListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { userId, sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const currentUserId = userId;
const columns = [
{
header: "Title",
accessor: "title",
},
{
header: "Class",
accessor: "class",
},
{
header: "Date",
accessor: "date",
className: "hidden md:table-cell",
},
...(role === "admin"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: AnnouncementList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">{item.title}</td>
<td>{item.class?.name || "-"}</td>
<td className="hidden md:table-cell">
{new Intl.DateTimeFormat("en-US").format(new Date(item.date))}
</td>
<td>
<div className="flex items-center gap-2">
{role === "admin" && (
<>
<FormContainer table="announcement" type="update" data={item} />
<FormContainer table="announcement" type="delete" id={item.id} />
</>
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
let query = supabase
.from("Announcement")
.select("*, class:Class(*)", { count: "exact" });
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "search":
query = query.ilike("title", `%${value}%`);
break;
default:
break;
}
}
}
}
// ROLE CONDITIONS
// Authorization is now handled by Supabase Postgres RLS policies.
// The initialized `supabase` client automatically passes the Clerk user JWT.
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
const { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching announcements from Supabase:", error);
}
const data = (rawData || []) as unknown as AnnouncementList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">
All Announcements
</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{role === "admin" && (
<FormContainer table="announcement" type="create" />
)}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default AnnouncementListPage;

View File

@ -0,0 +1,162 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import { auth } from "@clerk/nextjs/server";
type AssignmentList = Tables<"Assignment"> & {
lesson: Tables<"Lesson"> & {
subject: Tables<"Subject">;
class: Tables<"Class">;
teacher: Tables<"Teacher">;
};
};
const AssignmentListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { userId, sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const currentUserId = userId;
const columns = [
{
header: "Subject Name",
accessor: "name",
},
{
header: "Class",
accessor: "class",
},
{
header: "Teacher",
accessor: "teacher",
className: "hidden md:table-cell",
},
{
header: "Due Date",
accessor: "dueDate",
className: "hidden md:table-cell",
},
...(role === "admin" || role === "teacher"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: AssignmentList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">{item.lesson?.subject?.name || "-"}</td>
<td>{item.lesson?.class?.name || "-"}</td>
<td className="hidden md:table-cell">
{item.lesson?.teacher ? item.lesson.teacher.name + " " + item.lesson.teacher.surname : "-"}
</td>
<td className="hidden md:table-cell">
{new Intl.DateTimeFormat("en-US").format(new Date(item.dueDate))}
</td>
<td>
<div className="flex items-center gap-2">
{(role === "admin" || role === "teacher") && (
<>
<FormContainer table="assignment" type="update" data={item} />
<FormContainer table="assignment" type="delete" id={item.id} />
</>
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
let query = supabase
.from("Assignment")
.select(
"*, lesson:Lesson!inner(*, subject:Subject(*), class:Class(*), teacher:Teacher(*))",
{ count: "exact" }
);
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "classId":
query = query.eq("lesson.classId", parseInt(value));
break;
case "teacherId":
query = query.eq("lesson.teacherId", value);
break;
case "search":
query = query.ilike("lesson.subject.name", `%${value}%`);
break;
default:
break;
}
}
}
}
// ROLE CONDITIONS
// Authorization is now handled by Supabase Postgres RLS policies.
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
const { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching assignments from Supabase:", error);
}
const data = (rawData || []) as unknown as AssignmentList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">
All Assignments
</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{role === "admin" ||
(role === "teacher" && (
<FormContainer table="assignment" type="create" />
))}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default AssignmentListPage;

View File

@ -0,0 +1,141 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import { auth } from "@clerk/nextjs/server";
type ClassList = Tables<"Class"> & { supervisor: Tables<"Teacher"> | null };
const ClassListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const columns = [
{
header: "Class Name",
accessor: "name",
},
{
header: "Capacity",
accessor: "capacity",
className: "hidden md:table-cell",
},
{
header: "Grade",
accessor: "grade",
className: "hidden md:table-cell",
},
{
header: "Supervisor",
accessor: "supervisor",
className: "hidden md:table-cell",
},
...(role === "admin"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: ClassList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">{item.name}</td>
<td className="hidden md:table-cell">{item.capacity}</td>
<td className="hidden md:table-cell">{item.name[0]}</td>
<td className="hidden md:table-cell">
{item.supervisor ? item.supervisor.name + " " + item.supervisor.surname : "-"}
</td>
<td>
<div className="flex items-center gap-2">
{role === "admin" && (
<>
<FormContainer table="class" type="update" data={item} />
<FormContainer table="class" type="delete" id={item.id} />
</>
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
let query = supabase
.from("Class")
.select("*, supervisor:Teacher(*)", { count: "exact" });
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "supervisorId":
query = query.eq("supervisorId", value);
break;
case "search":
query = query.ilike("name", `%${value}%`);
break;
default:
break;
}
}
}
}
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
const { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching classes from Supabase:", error);
}
const data = (rawData || []) as unknown as ClassList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">All Classes</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{role === "admin" && <FormContainer table="class" type="create" />}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default ClassListPage;

View File

@ -0,0 +1,160 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import { auth } from "@clerk/nextjs/server";
type EventList = Tables<"Event"> & { class: Tables<"Class"> | null };
const EventListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { userId, sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const currentUserId = userId;
const columns = [
{
header: "Title",
accessor: "title",
},
{
header: "Class",
accessor: "class",
},
{
header: "Date",
accessor: "date",
className: "hidden md:table-cell",
},
{
header: "Start Time",
accessor: "startTime",
className: "hidden md:table-cell",
},
{
header: "End Time",
accessor: "endTime",
className: "hidden md:table-cell",
},
...(role === "admin"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: EventList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">{item.title}</td>
<td>{item.class?.name || "-"}</td>
<td className="hidden md:table-cell">
{new Intl.DateTimeFormat("en-US").format(new Date(item.startTime))}
</td>
<td className="hidden md:table-cell">
{new Date(item.startTime).toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}
</td>
<td className="hidden md:table-cell">
{new Date(item.endTime).toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}
</td>
<td>
<div className="flex items-center gap-2">
{role === "admin" && (
<>
<FormContainer table="event" type="update" data={item} />
<FormContainer table="event" type="delete" id={item.id} />
</>
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
let query = supabase
.from("Event")
.select("*, class:Class(*)", { count: "exact" });
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "search":
query = query.ilike("title", `%${value}%`);
break;
default:
break;
}
}
}
}
// ROLE CONDITIONS
// Authorization is now handled by Supabase Postgres RLS policies.
// The initialized `supabase` client automatically passes the Clerk user JWT,
// so the database will only return the events this user is allowed to see.
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
const { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching events from Supabase:", error);
}
const data = (rawData || []) as unknown as EventList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">All Events</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{role === "admin" && <FormContainer table="event" type="create" />}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default EventListPage;

View File

@ -0,0 +1,160 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import { auth } from "@clerk/nextjs/server";
type ExamList = Tables<"Exam"> & {
lesson: Tables<"Lesson"> & {
subject: Tables<"Subject">;
class: Tables<"Class">;
teacher: Tables<"Teacher">;
};
};
const ExamListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { userId, sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const currentUserId = userId;
const columns = [
{
header: "Subject Name",
accessor: "name",
},
{
header: "Class",
accessor: "class",
},
{
header: "Teacher",
accessor: "teacher",
className: "hidden md:table-cell",
},
{
header: "Date",
accessor: "date",
className: "hidden md:table-cell",
},
...(role === "admin" || role === "teacher"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: ExamList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">{item.lesson?.subject?.name || "-"}</td>
<td>{item.lesson?.class?.name || "-"}</td>
<td className="hidden md:table-cell">
{item.lesson?.teacher ? item.lesson.teacher.name + " " + item.lesson.teacher.surname : "-"}
</td>
<td className="hidden md:table-cell">
{new Intl.DateTimeFormat("en-US").format(new Date(item.startTime))}
</td>
<td>
<div className="flex items-center gap-2">
{(role === "admin" || role === "teacher") && (
<>
<FormContainer table="exam" type="update" data={item} />
<FormContainer table="exam" type="delete" id={item.id} />
</>
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
let query = supabase
.from("Exam")
.select(
"*, lesson:Lesson!inner(*, subject:Subject(*), class:Class(*), teacher:Teacher(*))",
{ count: "exact" }
);
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "classId":
query = query.eq("lesson.classId", parseInt(value));
break;
case "teacherId":
query = query.eq("lesson.teacherId", value);
break;
case "search":
query = query.ilike("lesson.subject.name", `%${value}%`);
break;
default:
break;
}
}
}
}
// ROLE CONDITIONS
// Authorization is now handled by Supabase Postgres RLS policies.
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
const { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching exams from Supabase:", error);
}
const data = (rawData || []) as unknown as ExamList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">All Exams</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{(role === "admin" || role === "teacher") && (
<FormContainer table="exam" type="create" />
)}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default ExamListPage;

View File

@ -0,0 +1,142 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import { auth } from "@clerk/nextjs/server";
type LessonList = Tables<"Lesson"> & {
subject: Tables<"Subject">;
class: Tables<"Class">;
teacher: Tables<"Teacher">;
};
const LessonListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const columns = [
{
header: "Subject Name",
accessor: "name",
},
{
header: "Class",
accessor: "class",
},
{
header: "Teacher",
accessor: "teacher",
className: "hidden md:table-cell",
},
...(role === "admin"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: LessonList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">{item.subject?.name || "-"}</td>
<td>{item.class?.name || "-"}</td>
<td className="hidden md:table-cell">
{item.teacher ? item.teacher.name + " " + item.teacher.surname : "-"}
</td>
<td>
<div className="flex items-center gap-2">
{role === "admin" && (
<>
<FormContainer table="lesson" type="update" data={item} />
<FormContainer table="lesson" type="delete" id={item.id} />
</>
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
let query = supabase
.from("Lesson")
.select("*, subject:Subject(*), class:Class(*), teacher:Teacher(*)", { count: "exact" });
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "classId":
query = query.eq("classId", parseInt(value));
break;
case "teacherId":
query = query.eq("teacherId", value);
break;
case "search":
query = query.or(`subject.name.ilike.%${value}%,teacher.name.ilike.%${value}%,class.name.ilike.%${value}%`);
break;
default:
break;
}
}
}
}
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
const { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching lessons from Supabase:", error);
}
const data = (rawData || []) as unknown as LessonList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">All Lessons</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{role === "admin" && <FormContainer table="lesson" type="create" />}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default LessonListPage;

View File

@ -0,0 +1,29 @@
const Loading = () => {
return (
<div className="p-8 animate-pulse">
<div className=" rounded-lg overflow-hidden shadow-md">
<div className="p-8 bg-gray-200 flex space-x-32">
<div className="h-6 bg-gray-300 rounded w-1/6"></div>
<div className="h-6 bg-gray-300 rounded w-2/6"></div>
<div className="h-6 bg-gray-300 rounded w-1/6"></div>
<div className="h-6 bg-gray-300 rounded w-1/6"></div>
</div>
<div className="p-4">
{[...Array(10)].map((_, index) => (
<div
key={index}
className="flex items-center justify-between mb-4 py-2 mt-4"
>
<div className="h-8 bg-gray-200 rounded w-1/6 mr-2"></div>
<div className="h-8 bg-gray-200 rounded w-2/6 mr-2"></div>
<div className="h-8 bg-gray-200 rounded w-1/6 mr-2"></div>
<div className="h-8 bg-gray-200 rounded w-1/6"></div>
</div>
))}
</div>
</div>
</div>
);
};
export default Loading;

View File

@ -0,0 +1,144 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import { auth } from "@clerk/nextjs/server";
type ParentList = Tables<"Parent"> & { students: Tables<"Student">[] };
const ParentListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const columns = [
{
header: "Info",
accessor: "info",
},
{
header: "Student Names",
accessor: "students",
className: "hidden md:table-cell",
},
{
header: "Phone",
accessor: "phone",
className: "hidden lg:table-cell",
},
{
header: "Address",
accessor: "address",
className: "hidden lg:table-cell",
},
...(role === "admin"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: ParentList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">
<div className="flex flex-col">
<h3 className="font-semibold">{item.name}</h3>
<p className="text-xs text-gray-500">{item?.email}</p>
</div>
</td>
<td className="hidden md:table-cell">
{item.students.map((student) => student.name).join(",")}
</td>
<td className="hidden md:table-cell">{item.phone}</td>
<td className="hidden md:table-cell">{item.address}</td>
<td>
<div className="flex items-center gap-2">
{role === "admin" && (
<>
<FormContainer table="parent" type="update" data={item} />
<FormContainer table="parent" type="delete" id={item.id} />
</>
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
let query = supabase
.from("Parent")
.select("*, students:Student(*)", { count: "exact" });
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "search":
query = query.ilike("name", `%${value}%`);
break;
default:
break;
}
}
}
}
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
const { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching parents from Supabase:", error);
}
const data = (rawData || []) as unknown as ParentList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">All Parents</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{role === "admin" && <FormContainer table="parent" type="create" />}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default ParentListPage;

View File

@ -0,0 +1,212 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import { auth } from "@clerk/nextjs/server";
type ResultList = {
id: number;
title: string;
studentName: string;
studentSurname: string;
teacherName: string;
teacherSurname: string;
score: number;
className: string;
startTime: Date;
};
const ResultListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { userId, sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const currentUserId = userId;
const columns = [
{
header: "Title",
accessor: "title",
},
{
header: "Student",
accessor: "student",
},
{
header: "Score",
accessor: "score",
className: "hidden md:table-cell",
},
{
header: "Teacher",
accessor: "teacher",
className: "hidden md:table-cell",
},
{
header: "Class",
accessor: "class",
className: "hidden md:table-cell",
},
{
header: "Date",
accessor: "date",
className: "hidden md:table-cell",
},
...(role === "admin" || role === "teacher"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: ResultList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">{item.title}</td>
<td>{item.studentName + " " + item.studentName}</td>
<td className="hidden md:table-cell">{item.score}</td>
<td className="hidden md:table-cell">
{item.teacherName + " " + item.teacherSurname}
</td>
<td className="hidden md:table-cell">{item.className}</td>
<td className="hidden md:table-cell">
{new Intl.DateTimeFormat("en-US").format(new Date(item.startTime))}
</td>
<td>
<div className="flex items-center gap-2">
{(role === "admin" || role === "teacher") && (
<>
<FormContainer table="result" type="update" data={item} />
<FormContainer table="result" type="delete" id={item.id} />
</>
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
let query = supabase
.from("Result")
.select(`
*,
student:Student(*),
exam:Exam(
*,
lesson:Lesson!inner(
class:Class(*),
teacher:Teacher(*)
)
),
assignment:Assignment(
*,
lesson:Lesson!inner(
class:Class(*),
teacher:Teacher(*)
)
)
`, { count: "exact" });
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "studentId":
query = query.eq("studentId", value);
break;
case "search":
// Supabase JS doesn't easily support OR across joined tables via .or() string syntax
// like `exam.title.ilike.%val%,student.name.ilike.%val%`.
// Workaround: We will fetch broadly and filter below, or just rely on RLS.
// For true broad search in Supabase over joins,
// an RPC function is usually required. For now, dropping search.
console.warn("Cross-table search text not supported in standard Supabase JS select without RPC.");
break;
default:
break;
}
}
}
}
// ROLE CONDITIONS: Handled by Supabase Postgres RLS.
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
const { data: dataRes, count, error } = await query;
if (error) {
console.error("Error fetching results from Supabase:", error);
}
const dlist = (dataRes || []) as unknown as any[];
const data = dlist.map((item) => {
const assessment = item.exam || item.assignment;
if (!assessment) return null;
const isExam = "startTime" in assessment;
return {
id: item.id,
title: assessment.title,
studentName: item.student?.name || "Unknown",
studentSurname: item.student?.surname || "",
teacherName: assessment.lesson?.teacher?.name || "-",
teacherSurname: assessment.lesson?.teacher?.surname || "",
score: item.score,
className: assessment.lesson?.class?.name || "-",
startTime: isExam ? assessment.startTime : assessment.startDate,
};
}).filter(item => item !== null) as ResultList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">All Results</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{(role === "admin" || role === "teacher") && (
<FormContainer table="result" type="create" />
)}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default ResultListPage;

View File

@ -0,0 +1,221 @@
import Announcements from "@/components/Announcements";
import BigCalendarContainer from "@/components/BigCalendarContainer";
import FormContainer from "@/components/FormContainer";
import Performance from "@/components/Performance";
import StudentAttendanceCard from "@/components/StudentAttendanceCard";
import { getSupabaseClient } from "@/lib/supabase";
import { auth } from "@clerk/nextjs/server";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Suspense } from "react";
const SingleStudentPage = async ({
params: { id },
}: {
params: { id: string };
}) => {
const { sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const supabase = await getSupabaseClient();
const { data, error } = await supabase
.from("Student")
.select(`
*,
class:Class(
*,
lessons:Lesson(count)
)
`)
.eq("id", id)
.single();
if (error || !data) {
return notFound();
}
// Extract count from the array returned by PostgREST inner select count
const lessonsCount = Array.isArray(data.class?.lessons)
? (data.class?.lessons as any)[0]?.count || 0
: 0;
const student = {
...data,
class: {
...data.class,
_count: { lessons: lessonsCount }
}
} as any;
if (!student) {
return notFound();
}
return (
<div className="flex-1 p-4 flex flex-col gap-4 xl:flex-row">
{/* LEFT */}
<div className="w-full xl:w-2/3">
{/* TOP */}
<div className="flex flex-col lg:flex-row gap-4">
{/* USER INFO CARD */}
<div className="bg-lamaSky py-6 px-4 rounded-md flex-1 flex gap-4">
<div className="w-1/3">
<Image
src={student.img || "/noAvatar.png"}
alt=""
width={144}
height={144}
className="w-36 h-36 rounded-full object-cover"
/>
</div>
<div className="w-2/3 flex flex-col justify-between gap-4">
<div className="flex items-center gap-4">
<h1 className="text-xl font-semibold">
{student.name + " " + student.surname}
</h1>
{role === "admin" && (
<FormContainer table="student" type="update" data={student} />
)}
</div>
<p className="text-sm text-gray-500">
Lorem ipsum, dolor sit amet consectetur adipisicing elit.
</p>
<div className="flex items-center justify-between gap-2 flex-wrap text-xs font-medium">
<div className="w-full md:w-1/3 lg:w-full 2xl:w-1/3 flex items-center gap-2">
<Image src="/blood.png" alt="" width={14} height={14} />
<span>{student.bloodType}</span>
</div>
<div className="w-full md:w-1/3 lg:w-full 2xl:w-1/3 flex items-center gap-2">
<Image src="/date.png" alt="" width={14} height={14} />
<span>
{new Intl.DateTimeFormat("en-GB").format(new Date(student.birthday))}
</span>
</div>
<div className="w-full md:w-1/3 lg:w-full 2xl:w-1/3 flex items-center gap-2">
<Image src="/mail.png" alt="" width={14} height={14} />
<span>{student.email || "-"}</span>
</div>
<div className="w-full md:w-1/3 lg:w-full 2xl:w-1/3 flex items-center gap-2">
<Image src="/phone.png" alt="" width={14} height={14} />
<span>{student.phone || "-"}</span>
</div>
</div>
</div>
</div>
{/* SMALL CARDS */}
<div className="flex-1 flex gap-4 justify-between flex-wrap">
{/* CARD */}
<div className="bg-white p-4 rounded-md flex gap-4 w-full md:w-[48%] xl:w-[45%] 2xl:w-[48%]">
<Image
src="/singleAttendance.png"
alt=""
width={24}
height={24}
className="w-6 h-6"
/>
<Suspense fallback="loading...">
<StudentAttendanceCard id={student.id} />
</Suspense>
</div>
{/* CARD */}
<div className="bg-white p-4 rounded-md flex gap-4 w-full md:w-[48%] xl:w-[45%] 2xl:w-[48%]">
<Image
src="/singleBranch.png"
alt=""
width={24}
height={24}
className="w-6 h-6"
/>
<div className="">
<h1 className="text-xl font-semibold">
{student.class.name.charAt(0)}th
</h1>
<span className="text-sm text-gray-400">Grade</span>
</div>
</div>
{/* CARD */}
<div className="bg-white p-4 rounded-md flex gap-4 w-full md:w-[48%] xl:w-[45%] 2xl:w-[48%]">
<Image
src="/singleLesson.png"
alt=""
width={24}
height={24}
className="w-6 h-6"
/>
<div className="">
<h1 className="text-xl font-semibold">
{student.class._count.lessons}
</h1>
<span className="text-sm text-gray-400">Lessons</span>
</div>
</div>
{/* CARD */}
<div className="bg-white p-4 rounded-md flex gap-4 w-full md:w-[48%] xl:w-[45%] 2xl:w-[48%]">
<Image
src="/singleClass.png"
alt=""
width={24}
height={24}
className="w-6 h-6"
/>
<div className="">
<h1 className="text-xl font-semibold">{student.class.name}</h1>
<span className="text-sm text-gray-400">Class</span>
</div>
</div>
</div>
</div>
{/* BOTTOM */}
<div className="mt-4 bg-white rounded-md p-4 h-[800px]">
<h1>Student&apos;s Schedule</h1>
<BigCalendarContainer type="classId" id={student.class.id} />
</div>
</div>
{/* RIGHT */}
<div className="w-full xl:w-1/3 flex flex-col gap-4">
<div className="bg-white p-4 rounded-md">
<h1 className="text-xl font-semibold">Shortcuts</h1>
<div className="mt-4 flex gap-4 flex-wrap text-xs text-gray-500">
<Link
className="p-3 rounded-md bg-lamaSkyLight"
href={`/list/lessons?classId=${student.class.id}`}
>
Student&apos;s Lessons
</Link>
<Link
className="p-3 rounded-md bg-lamaPurpleLight"
href={`/list/teachers?classId=${student.class.id}`}
>
Student&apos;s Teachers
</Link>
<Link
className="p-3 rounded-md bg-pink-50"
href={`/list/exams?classId=${student.class.id}`}
>
Student&apos;s Exams
</Link>
<Link
className="p-3 rounded-md bg-lamaSkyLight"
href={`/list/assignments?classId=${student.class.id}`}
>
Student&apos;s Assignments
</Link>
<Link
className="p-3 rounded-md bg-lamaYellowLight"
href={`/list/results?studentId=${student.id}`}
>
Student&apos;s Results
</Link>
</div>
</div>
<Performance />
<Announcements />
</div>
</div>
);
};
export default SingleStudentPage;

View File

@ -0,0 +1,186 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import Link from "next/link";
import { auth } from "@clerk/nextjs/server";
type StudentList = Tables<"Student"> & { class: Tables<"Class"> };
const StudentListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const columns = [
{
header: "Info",
accessor: "info",
},
{
header: "Student ID",
accessor: "studentId",
className: "hidden md:table-cell",
},
{
header: "Grade",
accessor: "grade",
className: "hidden md:table-cell",
},
{
header: "Phone",
accessor: "phone",
className: "hidden lg:table-cell",
},
{
header: "Address",
accessor: "address",
className: "hidden lg:table-cell",
},
...(role === "admin"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: StudentList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">
<Image
src={item.img || "/noAvatar.png"}
alt=""
width={40}
height={40}
className="md:hidden xl:block w-10 h-10 rounded-full object-cover"
/>
<div className="flex flex-col">
<h3 className="font-semibold">{item.name}</h3>
<p className="text-xs text-gray-500">{item.class.name}</p>
</div>
</td>
<td className="hidden md:table-cell">{item.username}</td>
<td className="hidden md:table-cell">{item.class.name[0]}</td>
<td className="hidden md:table-cell">{item.phone}</td>
<td className="hidden md:table-cell">{item.address}</td>
<td>
<div className="flex items-center gap-2">
<Link href={`/list/students/${item.id}`}>
<button className="w-7 h-7 flex items-center justify-center rounded-full bg-lamaSky">
<Image src="/view.png" alt="" width={16} height={16} />
</button>
</Link>
{role === "admin" && (
// <button className="w-7 h-7 flex items-center justify-center rounded-full bg-lamaPurple">
// <Image src="/delete.png" alt="" width={16} height={16} />
// </button>
<FormContainer table="student" type="delete" id={item.id} />
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
// Note: we need lessons if teacherId is provided
let query = supabase
.from("Student")
.select("*, class:Class(*, lessons:Lesson(*))", { count: "exact" });
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "teacherId":
// Filter by teacherId within the joined lessons.
// Supabase postgREST filters on JSON: class.lessons.teacherId=eq...
// It's tricky to filter the main rows based on a nested condition.
// Instead we can use an inner join via class!inner(lessons!inner(*)).
query = query.eq("class.lessons.teacherId", value);
// It might require adjustments, but RLS generally restricts this.
break;
case "search":
query = query.ilike("name", `%${value}%`);
break;
default:
break;
}
}
}
}
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
let { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching students from Supabase:", error);
}
// Workaround for `teacherId` query: Since Supabase inner joins
// on deep JSON relationships can sometimes fail or return empty arrays
// when `select` is used like "class(*)", let's filter after fetch if `teacherId` was passed
// (In a real scenario, an RPC is better for complex joins that filter parents by nested children)
if (queryParams.teacherId && rawData) {
rawData = rawData.filter(student =>
// @ts-ignore
student.class?.lessons?.some((lesson: any) => lesson.teacherId === queryParams.teacherId)
);
count = rawData.length;
}
const data = (rawData || []) as unknown as StudentList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">All Students</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{role === "admin" && (
// <button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
// <Image src="/plus.png" alt="" width={14} height={14} />
// </button>
<FormContainer table="student" type="create" />
)}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default StudentListPage;

View File

@ -0,0 +1,129 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import { auth } from "@clerk/nextjs/server";
type SubjectList = Tables<"Subject"> & { teachers: Tables<"Teacher">[] };
const SubjectListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const columns = [
{
header: "Subject Name",
accessor: "name",
},
{
header: "Teachers",
accessor: "teachers",
className: "hidden md:table-cell",
},
{
header: "Actions",
accessor: "action",
},
];
const renderRow = (item: SubjectList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">{item.name}</td>
<td className="hidden md:table-cell">
{item.teachers.map((teacher) => teacher.name).join(",")}
</td>
<td>
<div className="flex items-center gap-2">
{role === "admin" && (
<>
<FormContainer table="subject" type="update" data={item} />
<FormContainer table="subject" type="delete" id={item.id} />
</>
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
let query = supabase
.from("Subject")
.select("*, TeacherSubject(Teacher(*))", { count: "exact" });
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "search":
query = query.ilike("name", `%${value}%`);
break;
default:
break;
}
}
}
}
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
const { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching subjects from Supabase:", error);
}
const data = (rawData || []).map(subject => {
// @ts-ignore
const teachers = subject.TeacherSubject?.map((rel: any) => rel.Teacher) || [];
return {
...subject,
teachers
}
}) as unknown as SubjectList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">All Subjects</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{role === "admin" && (
<FormContainer table="subject" type="create" />
)}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default SubjectListPage;

View File

@ -0,0 +1,221 @@
import Announcements from "@/components/Announcements";
import BigCalendarContainer from "@/components/BigCalendarContainer";
import BigCalendar from "@/components/BigCalender";
import FormContainer from "@/components/FormContainer";
import Performance from "@/components/Performance";
import { getSupabaseClient } from "@/lib/supabase";
import { auth } from "@clerk/nextjs/server";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
const SingleTeacherPage = async ({
params: { id },
}: {
params: { id: string };
}) => {
const { sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const supabase = await getSupabaseClient();
const { data, error } = await supabase
.from("Teacher")
.select(`
*,
TeacherSubject(count),
lessons:Lesson(count),
classes:Class(count)
`)
.eq("id", id)
.single();
if (error || !data) {
return notFound();
}
const subjectsCount = Array.isArray(data.TeacherSubject) ? (data.TeacherSubject as any)[0]?.count || 0 : 0;
const lessonsCount = Array.isArray(data.lessons) ? (data.lessons as any)[0]?.count || 0 : 0;
const classesCount = Array.isArray(data.classes) ? (data.classes as any)[0]?.count || 0 : 0;
const teacher = {
...data,
_count: {
subjects: subjectsCount,
lessons: lessonsCount,
classes: classesCount,
}
} as any;
if (!teacher) {
return notFound();
}
return (
<div className="flex-1 p-4 flex flex-col gap-4 xl:flex-row">
{/* LEFT */}
<div className="w-full xl:w-2/3">
{/* TOP */}
<div className="flex flex-col lg:flex-row gap-4">
{/* USER INFO CARD */}
<div className="bg-lamaSky py-6 px-4 rounded-md flex-1 flex gap-4">
<div className="w-1/3">
<Image
src={teacher.img || "/noAvatar.png"}
alt=""
width={144}
height={144}
className="w-36 h-36 rounded-full object-cover"
/>
</div>
<div className="w-2/3 flex flex-col justify-between gap-4">
<div className="flex items-center gap-4">
<h1 className="text-xl font-semibold">
{teacher.name + " " + teacher.surname}
</h1>
{role === "admin" && (
<FormContainer table="teacher" type="update" data={teacher} />
)}
</div>
<p className="text-sm text-gray-500">
Lorem ipsum, dolor sit amet consectetur adipisicing elit.
</p>
<div className="flex items-center justify-between gap-2 flex-wrap text-xs font-medium">
<div className="w-full md:w-1/3 lg:w-full 2xl:w-1/3 flex items-center gap-2">
<Image src="/blood.png" alt="" width={14} height={14} />
<span>{teacher.bloodType}</span>
</div>
<div className="w-full md:w-1/3 lg:w-full 2xl:w-1/3 flex items-center gap-2">
<Image src="/date.png" alt="" width={14} height={14} />
<span>
{new Intl.DateTimeFormat("en-GB").format(new Date(teacher.birthday))}
</span>
</div>
<div className="w-full md:w-1/3 lg:w-full 2xl:w-1/3 flex items-center gap-2">
<Image src="/mail.png" alt="" width={14} height={14} />
<span>{teacher.email || "-"}</span>
</div>
<div className="w-full md:w-1/3 lg:w-full 2xl:w-1/3 flex items-center gap-2">
<Image src="/phone.png" alt="" width={14} height={14} />
<span>{teacher.phone || "-"}</span>
</div>
</div>
</div>
</div>
{/* SMALL CARDS */}
<div className="flex-1 flex gap-4 justify-between flex-wrap">
{/* CARD */}
<div className="bg-white p-4 rounded-md flex gap-4 w-full md:w-[48%] xl:w-[45%] 2xl:w-[48%]">
<Image
src="/singleAttendance.png"
alt=""
width={24}
height={24}
className="w-6 h-6"
/>
<div className="">
<h1 className="text-xl font-semibold">90%</h1>
<span className="text-sm text-gray-400">Attendance</span>
</div>
</div>
{/* CARD */}
<div className="bg-white p-4 rounded-md flex gap-4 w-full md:w-[48%] xl:w-[45%] 2xl:w-[48%]">
<Image
src="/singleBranch.png"
alt=""
width={24}
height={24}
className="w-6 h-6"
/>
<div className="">
<h1 className="text-xl font-semibold">
{teacher._count.subjects}
</h1>
<span className="text-sm text-gray-400">Branches</span>
</div>
</div>
{/* CARD */}
<div className="bg-white p-4 rounded-md flex gap-4 w-full md:w-[48%] xl:w-[45%] 2xl:w-[48%]">
<Image
src="/singleLesson.png"
alt=""
width={24}
height={24}
className="w-6 h-6"
/>
<div className="">
<h1 className="text-xl font-semibold">
{teacher._count.lessons}
</h1>
<span className="text-sm text-gray-400">Lessons</span>
</div>
</div>
{/* CARD */}
<div className="bg-white p-4 rounded-md flex gap-4 w-full md:w-[48%] xl:w-[45%] 2xl:w-[48%]">
<Image
src="/singleClass.png"
alt=""
width={24}
height={24}
className="w-6 h-6"
/>
<div className="">
<h1 className="text-xl font-semibold">
{teacher._count.classes}
</h1>
<span className="text-sm text-gray-400">Classes</span>
</div>
</div>
</div>
</div>
{/* BOTTOM */}
<div className="mt-4 bg-white rounded-md p-4 h-[800px]">
<h1>Teacher&apos;s Schedule</h1>
<BigCalendarContainer type="teacherId" id={teacher.id} />
</div>
</div>
{/* RIGHT */}
<div className="w-full xl:w-1/3 flex flex-col gap-4">
<div className="bg-white p-4 rounded-md">
<h1 className="text-xl font-semibold">Shortcuts</h1>
<div className="mt-4 flex gap-4 flex-wrap text-xs text-gray-500">
<Link
className="p-3 rounded-md bg-lamaSkyLight"
href={`/list/classes?supervisorId=${teacher.id}`}
>
Teacher&apos;s Classes
</Link>
<Link
className="p-3 rounded-md bg-lamaPurpleLight"
href={`/list/students?teacherId=${teacher.id}`}
>
Teacher&apos;s Students
</Link>
<Link
className="p-3 rounded-md bg-lamaYellowLight"
href={`/list/lessons?teacherId=${teacher.id}`}
>
Teacher&apos;s Lessons
</Link>
<Link
className="p-3 rounded-md bg-pink-50"
href={`/list/exams?teacherId=${teacher.id}`}
>
Teacher&apos;s Exams
</Link>
<Link
className="p-3 rounded-md bg-lamaSkyLight"
href={`/list/assignments?teacherId=${teacher.id}`}
>
Teacher&apos;s Assignments
</Link>
</div>
</div>
<Performance />
<Announcements />
</div>
</div>
);
};
export default SingleTeacherPage;

View File

@ -0,0 +1,190 @@
import FormContainer from "@/components/FormContainer";
import Pagination from "@/components/Pagination";
import Table from "@/components/Table";
import TableSearch from "@/components/TableSearch";
import { getSupabaseClient } from "@/lib/supabase";
import { Tables } from "@/types/supabase";
import Image from "next/image";
import Link from "next/link";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { auth } from "@clerk/nextjs/server";
type TeacherList = Tables<"Teacher"> & { subjects: Tables<"Subject">[] } & { classes: Tables<"Class">[] };
const TeacherListPage = async ({
searchParams,
}: {
searchParams: { [key: string]: string | undefined };
}) => {
const { sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const columns = [
{
header: "Info",
accessor: "info",
},
{
header: "Teacher ID",
accessor: "teacherId",
className: "hidden md:table-cell",
},
{
header: "Subjects",
accessor: "subjects",
className: "hidden md:table-cell",
},
{
header: "Classes",
accessor: "classes",
className: "hidden md:table-cell",
},
{
header: "Phone",
accessor: "phone",
className: "hidden lg:table-cell",
},
{
header: "Address",
accessor: "address",
className: "hidden lg:table-cell",
},
...(role === "admin"
? [
{
header: "Actions",
accessor: "action",
},
]
: []),
];
const renderRow = (item: TeacherList) => (
<tr
key={item.id}
className="border-b border-gray-200 even:bg-slate-50 text-sm hover:bg-lamaPurpleLight"
>
<td className="flex items-center gap-4 p-4">
<Image
src={item.img || "/noAvatar.png"}
alt=""
width={40}
height={40}
className="md:hidden xl:block w-10 h-10 rounded-full object-cover"
/>
<div className="flex flex-col">
<h3 className="font-semibold">{item.name}</h3>
<p className="text-xs text-gray-500">{item?.email}</p>
</div>
</td>
<td className="hidden md:table-cell">{item.username}</td>
<td className="hidden md:table-cell">
{item.subjects.map((subject) => subject.name).join(",")}
</td>
<td className="hidden md:table-cell">
{item.classes.map((classItem) => classItem.name).join(",")}
</td>
<td className="hidden md:table-cell">{item.phone}</td>
<td className="hidden md:table-cell">{item.address}</td>
<td>
<div className="flex items-center gap-2">
<Link href={`/list/teachers/${item.id}`}>
<button className="w-7 h-7 flex items-center justify-center rounded-full bg-lamaSky">
<Image src="/view.png" alt="" width={16} height={16} />
</button>
</Link>
{role === "admin" && (
// <button className="w-7 h-7 flex items-center justify-center rounded-full bg-lamaPurple">
// <Image src="/delete.png" alt="" width={16} height={16} />
// </button>
<FormContainer table="teacher" type="delete" id={item.id} />
)}
</div>
</td>
</tr>
);
const { page, ...queryParams } = searchParams;
const p = page ? parseInt(page) : 1;
const supabase = await getSupabaseClient();
// URL PARAMS CONDITION
// Note: we need lessons if classId is provided
let query = supabase
.from("Teacher")
.select("*, TeacherSubject(Subject(*)), classes:Class(*), lessons:Lesson(*)", { count: "exact" });
if (queryParams) {
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
switch (key) {
case "classId":
// Filter by classId within the joined lessons.
query = query.eq("lessons.classId", parseInt(value));
break;
case "search":
query = query.ilike("name", `%${value}%`);
break;
default:
break;
}
}
}
}
// PAGINATION
query = query.range(ITEM_PER_PAGE * (p - 1), ITEM_PER_PAGE * p - 1);
let { data: rawData, count, error } = await query;
if (error) {
console.error("Error fetching teachers from Supabase:", error);
}
// Workaround for `classId` query:
if (queryParams.classId && rawData) {
rawData = rawData.filter(teacher =>
// @ts-ignore
teacher.lessons?.some((lesson: any) => lesson.classId === parseInt(queryParams.classId!))
);
count = rawData.length;
}
// Map the nested TeacherSubject data back to a flat subjects array for the datatable
const data = (rawData || []).map(teacher => {
// @ts-ignore
const subjects = teacher.TeacherSubject?.map((rel: any) => rel.Subject) || [];
return {
...teacher,
subjects
}
}) as unknown as TeacherList[];
return (
<div className="bg-white p-4 rounded-md flex-1 m-4 mt-0">
{/* TOP */}
<div className="flex items-center justify-between">
<h1 className="hidden md:block text-lg font-semibold">All Teachers</h1>
<div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto">
<TableSearch />
<div className="flex items-center gap-4 self-end">
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/filter.png" alt="" width={14} height={14} />
</button>
<button className="w-8 h-8 flex items-center justify-center rounded-full bg-lamaYellow">
<Image src="/sort.png" alt="" width={14} height={14} />
</button>
{role === "admin" && (
<FormContainer table="teacher" type="create" />
)}
</div>
</div>
</div>
{/* LIST */}
<Table columns={columns} renderRow={renderRow} data={data} />
{/* PAGINATION */}
<Pagination page={p} count={count || 0} />
</div>
);
};
export default TeacherListPage;

View File

@ -0,0 +1,48 @@
import Announcements from "@/components/Announcements";
import BigCalendarContainer from "@/components/BigCalendarContainer";
import { getSupabaseClient } from "@/lib/supabase";
import { auth } from "@clerk/nextjs/server";
import { Tables } from "@/types/supabase";
const ParentPage = async () => {
const { userId } = auth();
const currentUserId = userId;
const supabase = await getSupabaseClient();
const { data: students, error } = await supabase
.from("Student")
.select("*")
// RLS policies should handle identifying parent id
// .eq("parentId", currentUserId!)
if (error) {
console.error("Error fetching parent students:", error);
}
const studentsList = (students || []) as Tables<"Student">[];
return (
<div className="flex-1 p-4 flex gap-4 flex-col xl:flex-row">
{/* LEFT */}
<div className="">
{studentsList.map((student) => (
<div className="w-full xl:w-2/3" key={student.id}>
<div className="h-full bg-white p-4 rounded-md">
<h1 className="text-xl font-semibold">
Schedule ({student.name + " " + student.surname})
</h1>
<BigCalendarContainer type="classId" id={student.classId} />
</div>
</div>
))}
</div>
{/* RIGHT */}
<div className="w-full xl:w-1/3 flex flex-col gap-8">
<Announcements />
</div>
</div>
);
};
export default ParentPage;

View File

@ -0,0 +1,57 @@
import Announcements from "@/components/Announcements";
import BigCalendarContainer from "@/components/BigCalendarContainer";
import BigCalendar from "@/components/BigCalender";
import EventCalendar from "@/components/EventCalendar";
import { getSupabaseClient } from "@/lib/supabase";
import { Tables } from "@/types/supabase";
import { auth } from "@clerk/nextjs/server";
const StudentPage = async () => {
const { userId } = auth();
const supabase = await getSupabaseClient();
const { data: studentItem, error: studentError } = await supabase
.from("Student")
.select("classId")
.eq("id", userId!)
.single();
if (studentError) {
console.error("Error fetching student details:", studentError);
}
const { data: classItems, error } = await supabase
.from("Class")
.select("*")
.eq("id", studentItem?.classId || 0);
if (error) {
console.error("Error fetching student class:", error);
}
const classItem = (classItems || []) as Tables<"Class">[];
const studentClassId = classItem.length > 0 ? classItem[0].id : null;
return (
<div className="p-4 flex gap-4 flex-col xl:flex-row">
{/* LEFT */}
<div className="w-full xl:w-2/3">
<div className="h-full bg-white p-4 rounded-md">
<h1 className="text-xl font-semibold">Schedule (4A)</h1>
{studentClassId ? (
<BigCalendarContainer type="classId" id={studentClassId} />
) : (
<div className="text-gray-500 mt-4">No schedule found for your assigned class.</div>
)}
</div>
</div>
{/* RIGHT */}
<div className="w-full xl:w-1/3 flex flex-col gap-8">
<EventCalendar />
<Announcements />
</div>
</div>
);
};
export default StudentPage;

View File

@ -0,0 +1,24 @@
import Announcements from "@/components/Announcements";
import BigCalendarContainer from "@/components/BigCalendarContainer";
import { auth } from "@clerk/nextjs/server";
const TeacherPage = () => {
const { userId } = auth();
return (
<div className="flex-1 p-4 flex gap-4 flex-col xl:flex-row">
{/* LEFT */}
<div className="w-full xl:w-2/3">
<div className="h-full bg-white p-4 rounded-md">
<h1 className="text-xl font-semibold">Schedule</h1>
<BigCalendarContainer type="teacherId" id={userId!} />
</div>
</div>
{/* RIGHT */}
<div className="w-full xl:w-1/3 flex flex-col gap-8">
<Announcements />
</div>
</div>
);
};
export default TeacherPage;

View File

@ -0,0 +1,70 @@
"use client";
import * as Clerk from "@clerk/elements/common";
import * as SignIn from "@clerk/elements/sign-in";
import { useUser } from "@clerk/nextjs";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
const LoginPage = () => {
const { isLoaded, isSignedIn, user } = useUser();
const router = useRouter();
useEffect(() => {
const role = user?.publicMetadata.role;
if (role) {
router.push(`/${role}`);
}
}, [user, router]);
return (
<div className="h-screen flex items-center justify-center bg-lamaSkyLight">
<SignIn.Root>
<SignIn.Step
name="start"
className="bg-white p-12 rounded-md shadow-2xl flex flex-col gap-2"
>
<h1 className="text-xl font-bold flex items-center gap-2">
<Image src="/logo.png" alt="" width={24} height={24} />
SchooLama
</h1>
<h2 className="text-gray-400">Sign in to your account</h2>
<Clerk.GlobalError className="text-sm text-red-400" />
<Clerk.Field name="identifier" className="flex flex-col gap-2">
<Clerk.Label className="text-xs text-gray-500">
Username
</Clerk.Label>
<Clerk.Input
type="text"
required
className="p-2 rounded-md ring-1 ring-gray-300"
/>
<Clerk.FieldError className="text-xs text-red-400" />
</Clerk.Field>
<Clerk.Field name="password" className="flex flex-col gap-2">
<Clerk.Label className="text-xs text-gray-500">
Password
</Clerk.Label>
<Clerk.Input
type="password"
required
className="p-2 rounded-md ring-1 ring-gray-300"
/>
<Clerk.FieldError className="text-xs text-red-400" />
</Clerk.Field>
<SignIn.Action
submit
className="bg-blue-500 text-white my-1 rounded-md text-sm p-[10px]"
>
Sign In
</SignIn.Action>
</SignIn.Step>
</SignIn.Root>
</div>
);
};
export default LoginPage;

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

110
src/app/globals.css Normal file
View File

@ -0,0 +1,110 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.react-calendar {
width: 100% !important;
border: none !important;
font-family: "Inter", sans-serif !important;
}
.react-calendar__navigation__label__labelText {
font-weight: 600;
}
.react-calendar__tile--active {
background-color: #c3ebfa !important;
color: black !important;
}
.rbc-btn-group:first-child button {
border: none !important;
background-color: #f1f0ff !important;
margin-right: 2px !important;
font-size: 13px !important;
}
.rbc-toolbar-label {
text-align: right !important;
padding: 0px 20px !important;
}
.rbc-btn-group:last-child {
font-size: 13px !important;
}
.rbc-btn-group:last-child button {
border: none !important;
background-color: #f1f0ff !important;
margin-left: 2px !important;
}
.rbc-toolbar button.rbc-active {
background-color: #dbdafe !important;
box-shadow: none !important;
}
.rbc-time-view {
border-color: #eee !important;
}
.rbc-time-gutter.rbc-time-column {
font-size: 12px !important;
}
.rbc-time-gutter.rbc-time-column .rbc-timeslot-group {
padding: 0px 20px !important;
}
.rbc-timeslot-group {
background-color: #f7fdff !important;
}
.rbc-day-slot {
font-size: 14px !important;
}
.rbc-event {
border: none !important;
color: black !important;
padding: 5px !important;
border-radius: 6px !important;
}
.rbc-event-content {
display: block !important;
font-size: 13px;
font-weight: 600;
}
.rbc-event:nth-child(1) {
background-color: #e2f8ff !important;
}
.rbc-event:nth-child(2) {
background-color: #fefce8 !important;
}
.rbc-event:nth-child(3) {
background-color: #f2f1ff !important;
}
.rbc-event:nth-child(4) {
background-color: #fdf2fb !important;
}
.rbc-event:nth-child(5) {
background-color: #e2f8ff !important;
}
.rbc-event:nth-child(6) {
background-color: #fefce8 !important;
}
.rbc-event:nth-child(7) {
background-color: #f2f1ff !important;
}
.rbc-event:nth-child(8) {
background-color: #fdf2fb !important;
}

31
src/app/layout.tsx Normal file
View File

@ -0,0 +1,31 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ClerkProvider } from "@clerk/nextjs";
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import ConsoleSuppressor from "@/components/ConsoleSuppressor";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Lama Dev School Management Dashboard",
description: "Next.js School Management System",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<ClerkProvider>
<html lang="en">
<body className={inter.className}>
<ConsoleSuppressor />
{children} <ToastContainer position="bottom-right" theme="dark" />
</body>
</html>
</ClerkProvider>
);
}

View File

@ -0,0 +1,66 @@
import { getSupabaseClient } from "@/lib/supabase";
import { auth } from "@clerk/nextjs/server";
const Announcements = async () => {
const { userId, sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const supabase = await getSupabaseClient();
const { data: rawData, error } = await supabase
.from("Announcement")
.select("*")
.order("date", { ascending: false })
.limit(3);
if (error) {
console.error("Error fetching announcements:", error);
}
const data = rawData || [];
return (
<div className="bg-white p-4 rounded-md">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold">Announcements</h1>
<span className="text-xs text-gray-400">View All</span>
</div>
<div className="flex flex-col gap-4 mt-4">
{data[0] && (
<div className="bg-lamaSkyLight rounded-md p-4">
<div className="flex items-center justify-between">
<h2 className="font-medium">{data[0].title}</h2>
<span className="text-xs text-gray-400 bg-white rounded-md px-1 py-1">
{new Intl.DateTimeFormat("en-GB").format(new Date(data[0].date))}
</span>
</div>
<p className="text-sm text-gray-400 mt-1">{data[0].description}</p>
</div>
)}
{data[1] && (
<div className="bg-lamaPurpleLight rounded-md p-4">
<div className="flex items-center justify-between">
<h2 className="font-medium">{data[1].title}</h2>
<span className="text-xs text-gray-400 bg-white rounded-md px-1 py-1">
{new Intl.DateTimeFormat("en-GB").format(new Date(data[1].date))}
</span>
</div>
<p className="text-sm text-gray-400 mt-1">{data[1].description}</p>
</div>
)}
{data[2] && (
<div className="bg-lamaYellowLight rounded-md p-4">
<div className="flex items-center justify-between">
<h2 className="font-medium">{data[2].title}</h2>
<span className="text-xs text-gray-400 bg-white rounded-md px-1 py-1">
{new Intl.DateTimeFormat("en-GB").format(new Date(data[2].date))}
</span>
</div>
<p className="text-sm text-gray-400 mt-1">{data[2].description}</p>
</div>
)}
</div>
</div>
);
};
export default Announcements;

View File

@ -0,0 +1,57 @@
"use client";
import Image from "next/image";
import {
BarChart,
Bar,
Rectangle,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
const AttendanceChart = ({
data,
}: {
data: { name: string; present: number; absent: number }[];
}) => {
return (
<ResponsiveContainer width="100%" height="90%">
<BarChart width={500} height={300} data={data} barSize={20}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#ddd" />
<XAxis
dataKey="name"
axisLine={false}
tick={{ fill: "#d1d5db" }}
tickLine={false}
height={30}
/>
<YAxis axisLine={false} tick={{ fill: "#d1d5db" }} tickLine={false} width={40} />
<Tooltip
contentStyle={{ borderRadius: "10px", borderColor: "lightgray" }}
/>
<Legend
align="left"
verticalAlign="top"
wrapperStyle={{ paddingTop: "20px", paddingBottom: "40px" }}
/>
<Bar
dataKey="present"
fill="#FAE27C"
legendType="circle"
radius={[10, 10, 0, 0]}
/>
<Bar
dataKey="absent"
fill="#C3EBFA"
legendType="circle"
radius={[10, 10, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
);
};
export default AttendanceChart;

View File

@ -0,0 +1,71 @@
import Image from "next/image";
import AttendanceChart from "./AttendanceChart";
import { getSupabaseClient } from "@/lib/supabase";
const AttendanceChartContainer = async () => {
const today = new Date();
const dayOfWeek = today.getDay();
const daysSinceMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1;
const lastMonday = new Date(today);
lastMonday.setDate(today.getDate() - daysSinceMonday);
const supabase = await getSupabaseClient();
const { data: rawData, error } = await supabase
.from("Attendance")
.select("date, present")
.gte("date", lastMonday.toISOString());
if (error) {
console.error("Error fetching attendance data:", error);
}
const resData = rawData || [];
// console.log(data)
const daysOfWeek = ["Mon", "Tue", "Wed", "Thu", "Fri"];
const attendanceMap: { [key: string]: { present: number; absent: number } } =
{
Mon: { present: 0, absent: 0 },
Tue: { present: 0, absent: 0 },
Wed: { present: 0, absent: 0 },
Thu: { present: 0, absent: 0 },
Fri: { present: 0, absent: 0 },
};
resData.forEach((item) => {
const itemDate = new Date(item.date);
const dayOfWeek = itemDate.getDay();
if (dayOfWeek >= 1 && dayOfWeek <= 5) {
const dayName = daysOfWeek[dayOfWeek - 1];
if (item.present) {
attendanceMap[dayName].present += 1;
} else {
attendanceMap[dayName].absent += 1;
}
}
});
const data = daysOfWeek.map((day) => ({
name: day,
present: attendanceMap[day].present,
absent: attendanceMap[day].absent,
}));
return (
<div className="bg-white rounded-lg p-4 h-full">
<div className="flex justify-between items-center">
<h1 className="text-lg font-semibold">Attendance</h1>
<Image src="/moreDark.png" alt="" width={20} height={20} />
</div>
<AttendanceChart data={data} />
</div>
);
};
export default AttendanceChartContainer;

View File

@ -0,0 +1,40 @@
import { getSupabaseClient } from "@/lib/supabase";
import BigCalendar from "./BigCalender";
const BigCalendarContainer = async ({
type,
id,
}: {
type: "teacherId" | "classId";
id: string | number;
}) => {
const supabase = await getSupabaseClient();
let query = supabase.from("Lesson").select("*");
if (type === "teacherId") {
query = query.eq("teacherId", id as string);
} else {
query = query.eq("classId", id as number);
}
const { data: rawData, error } = await query;
if (error) {
console.error("Error fetching calendar lessons:", error);
}
const dataRes = rawData || [];
const data = dataRes.map((lesson) => ({
title: lesson.name,
start: new Date(lesson.startTime),
end: new Date(lesson.endTime),
}));
return (
<div className="">
<BigCalendar data={data} />
</div>
);
};
export default BigCalendarContainer;

View File

@ -0,0 +1,41 @@
"use client";
import { Calendar, momentLocalizer, View, Views } from "react-big-calendar";
import moment from "moment";
import "react-big-calendar/lib/css/react-big-calendar.css";
import { useState } from "react";
const localizer = momentLocalizer(moment);
const BigCalendar = ({
data,
}: {
data: { title: string; start: Date; end: Date }[];
}) => {
const [view, setView] = useState<View>(Views.WORK_WEEK);
const [date, setDate] = useState<Date>(new Date());
const handleOnChangeView = (selectedView: View) => {
setView(selectedView);
};
return (
<Calendar
localizer={localizer}
events={data}
components={{}}
startAccessor="start"
endAccessor="end"
views={["month", "work_week", "day"]}
view={view}
date={date}
style={{ height: "98%" }}
onView={handleOnChangeView}
onNavigate={setDate}
min={new Date(2025, 1, 0, 8, 0, 0)}
max={new Date(2025, 1, 0, 17, 0, 0)}
/>
);
};
export default BigCalendar;

View File

@ -0,0 +1,18 @@
"use client";
if (typeof window !== "undefined") {
const originalError = console.error;
console.error = (...args) => {
if (
typeof args[0] === "string" &&
args[0].includes("Support for defaultProps will be removed from function components")
) {
return;
}
originalError(...args);
};
}
export default function ConsoleSuppressor() {
return null;
}

View File

@ -0,0 +1,54 @@
"use client";
import Image from "next/image";
import {
RadialBarChart,
RadialBar,
Legend,
ResponsiveContainer,
} from "recharts";
const CountChart = ({ boys, girls }: { boys: number; girls: number }) => {
const data = [
{
name: "Total",
count: boys+girls,
fill: "white",
},
{
name: "Girls",
count: girls,
fill: "#FAE27C",
},
{
name: "Boys",
count: boys,
fill: "#C3EBFA",
},
];
return (
<div className="relative w-full h-[75%]">
<ResponsiveContainer>
<RadialBarChart
cx="50%"
cy="50%"
innerRadius="40%"
outerRadius="100%"
barSize={32}
data={data}
>
<RadialBar background dataKey="count" />
</RadialBarChart>
</ResponsiveContainer>
<Image
src="/maleFemale.png"
alt=""
width={50}
height={50}
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2"
/>
</div>
);
};
export default CountChart;

View File

@ -0,0 +1,50 @@
import Image from "next/image";
import CountChart from "./CountChart";
import { getSupabaseClient } from "@/lib/supabase";
const CountChartContainer = async () => {
const supabase = await getSupabaseClient();
const { count: boysCount } = await supabase
.from("Student")
.select("*", { count: "exact", head: true })
.eq("sex", "MALE");
const { count: girlsCount } = await supabase
.from("Student")
.select("*", { count: "exact", head: true })
.eq("sex", "FEMALE");
const boys = boysCount || 0;
const girls = girlsCount || 0;
return (
<div className="bg-white rounded-xl w-full h-full p-4">
{/* TITLE */}
<div className="flex justify-between items-center">
<h1 className="text-lg font-semibold">Students</h1>
<Image src="/moreDark.png" alt="" width={20} height={20} />
</div>
{/* CHART */}
<CountChart boys={boys} girls={girls} />
{/* BOTTOM */}
<div className="flex justify-center gap-16">
<div className="flex flex-col gap-1">
<div className="w-5 h-5 bg-lamaSky rounded-full" />
<h1 className="font-bold">{boys}</h1>
<h2 className="text-xs text-gray-300">
Boys ({Math.round((boys / (boys + girls)) * 100)}%)
</h2>
</div>
<div className="flex flex-col gap-1">
<div className="w-5 h-5 bg-lamaYellow rounded-full" />
<h1 className="font-bold">{girls}</h1>
<h2 className="text-xs text-gray-300">
Girls ({Math.round((girls / (boys + girls)) * 100)}%)
</h2>
</div>
</div>
</div>
);
};
export default CountChartContainer;

View File

@ -0,0 +1,38 @@
"use client";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import Calendar from "react-calendar";
import "react-calendar/dist/Calendar.css";
type ValuePiece = Date | null;
type Value = ValuePiece | [ValuePiece, ValuePiece];
const EventCalendar = () => {
const [value, onChange] = useState<Value>(new Date());
const router = useRouter();
// TEMPORARY FIX: Suppress hydration warning for react-calendar
// react-calendar uses browser locale for formatting which mismatches server rendering
useEffect(() => {
if (value instanceof Date) {
router.push(`?date=${value}`);
}
}, [value, router]);
// Prevent rendering on the server to avoid hydration mismatch
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
if (!isClient) {
return null; // or a loading skeleton
}
return <Calendar onChange={onChange} value={value} />;
};
export default EventCalendar;

View File

@ -0,0 +1,25 @@
import Image from "next/image";
import EventCalendar from "./EventCalendar";
import EventList from "./EventList";
const EventCalendarContainer = async ({
searchParams,
}: {
searchParams: { [keys: string]: string | undefined };
}) => {
const { date } = searchParams;
return (
<div className="bg-white p-4 rounded-md">
<EventCalendar />
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold my-4">Events</h1>
<Image src="/moreDark.png" alt="" width={20} height={20} />
</div>
<div className="flex flex-col gap-4">
<EventList dateParam={date} />
</div>
</div>
);
};
export default EventCalendarContainer;

View File

@ -0,0 +1,45 @@
import { getSupabaseClient } from "@/lib/supabase";
const EventList = async ({ dateParam }: { dateParam: string | undefined }) => {
const date = dateParam ? new Date(dateParam) : new Date();
const startOfDay = new Date(date);
startOfDay.setHours(0, 0, 0, 0);
const endOfDay = new Date(date);
endOfDay.setHours(23, 59, 59, 999);
const supabase = await getSupabaseClient();
const { data: rawData, error } = await supabase
.from("Event")
.select("*")
.gte("startTime", startOfDay.toISOString())
.lte("startTime", endOfDay.toISOString());
if (error) {
console.error("Error fetching events:", error);
}
const data = rawData || [];
return data.map((event) => (
<div
className="p-5 rounded-md border-2 border-gray-100 border-t-4 odd:border-t-lamaSky even:border-t-lamaPurple"
key={event.id}
>
<div className="flex items-center justify-between">
<h1 className="font-semibold text-gray-600">{event.title}</h1>
<span className="text-gray-300 text-xs">
{new Date(event.startTime).toLocaleTimeString("en-UK", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})}
</span>
</div>
<p className="mt-2 text-gray-400 text-sm">{event.description}</p>
</div>
));
};
export default EventList;

View File

@ -0,0 +1,126 @@
"use client";
import Image from "next/image";
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from "recharts";
const data = [
{
name: "Jan",
income: 4000,
expense: 2400,
},
{
name: "Feb",
income: 3000,
expense: 1398,
},
{
name: "Mar",
income: 2000,
expense: 9800,
},
{
name: "Apr",
income: 2780,
expense: 3908,
},
{
name: "May",
income: 1890,
expense: 4800,
},
{
name: "Jun",
income: 2390,
expense: 3800,
},
{
name: "Jul",
income: 3490,
expense: 4300,
},
{
name: "Aug",
income: 3490,
expense: 4300,
},
{
name: "Sep",
income: 3490,
expense: 4300,
},
{
name: "Oct",
income: 3490,
expense: 4300,
},
{
name: "Nov",
income: 3490,
expense: 4300,
},
{
name: "Dec",
income: 3490,
expense: 4300,
},
];
const FinanceChart = () => {
return (
<div className="bg-white rounded-xl w-full h-full p-4">
<div className="flex justify-between items-center">
<h1 className="text-lg font-semibold">Finance</h1>
<Image src="/moreDark.png" alt="" width={20} height={20} />
</div>
<ResponsiveContainer width="100%" height="90%">
<LineChart
width={500}
height={300}
data={data}
margin={{
top: 5,
right: 30,
left: 20,
bottom: 5,
}}
>
<CartesianGrid strokeDasharray="3 3" stroke="#ddd" />
<XAxis
dataKey="name"
axisLine={false}
tick={{ fill: "#d1d5db" }}
tickLine={false}
tickMargin={10}
height={30}
/>
<YAxis axisLine={false} tick={{ fill: "#d1d5db" }} tickLine={false} tickMargin={20} width={40} />
<Tooltip />
<Legend
align="center"
verticalAlign="top"
wrapperStyle={{ paddingTop: "10px", paddingBottom: "30px" }}
/>
<Line
type="monotone"
dataKey="income"
stroke="#C3EBFA"
strokeWidth={5}
/>
<Line type="monotone" dataKey="expense" stroke="#CFCEFF" strokeWidth={5} />
</LineChart>
</ResponsiveContainer>
</div>
);
};
export default FinanceChart;

View File

@ -0,0 +1,117 @@
import { getSupabaseClient } from "@/lib/supabase";
import FormModal from "./FormModal";
import { auth } from "@clerk/nextjs/server";
export type FormContainerProps = {
table:
| "teacher"
| "student"
| "parent"
| "subject"
| "class"
| "lesson"
| "exam"
| "assignment"
| "result"
| "attendance"
| "event"
| "announcement";
type: "create" | "update" | "delete";
data?: any;
id?: number | string;
};
const FormContainer = async ({ table, type, data, id }: FormContainerProps) => {
let relatedData = {};
const { userId, sessionClaims } = auth();
const role = (sessionClaims?.metadata as { role?: string })?.role;
const currentUserId = userId;
if (type !== "delete") {
const supabase = await getSupabaseClient();
switch (table) {
case "subject": {
const { data: subjectTeachers } = await supabase.from("Teacher").select("id, name, surname");
relatedData = { teachers: subjectTeachers };
break;
}
case "class": {
const { data: classGrades } = await supabase.from("Grade").select("id, level");
const { data: classTeachers } = await supabase.from("Teacher").select("id, name, surname");
relatedData = { teachers: classTeachers, grades: classGrades };
break;
}
case "teacher": {
const { data: teacherSubjects } = await supabase.from("Subject").select("id, name");
relatedData = { subjects: teacherSubjects };
break;
}
case "student": {
const { data: studentGrades } = await supabase.from("Grade").select("id, level");
const { data: studentClasses } = await supabase.from("Class").select("*, students:Student(count)");
const classesWithCount = studentClasses?.map(c => ({
...c,
_count: { students: Array.isArray(c.students) ? (c.students as any)[0]?.count || 0 : 0 }
}));
relatedData = { classes: classesWithCount, grades: studentGrades };
break;
}
case "lesson": {
const { data: lessonSubjects } = await supabase.from("Subject").select("id, name");
const { data: lessonClasses } = await supabase.from("Class").select("id, name");
const { data: lessonTeachers } = await supabase.from("Teacher").select("id, name, surname");
relatedData = { subjects: lessonSubjects, classes: lessonClasses, teachers: lessonTeachers };
break;
}
case "assignment": {
const { data: assignmentLessons } = await supabase.from("Lesson").select("id, name");
relatedData = { lessons: assignmentLessons };
break;
}
case "result": {
const { data: resultStudents } = await supabase.from("Student").select("id, name, surname");
const { data: resultExams } = await supabase.from("Exam").select("id, title");
const { data: resultAssignments } = await supabase.from("Assignment").select("id, title");
relatedData = { students: resultStudents, exams: resultExams, assignments: resultAssignments };
break;
}
case "exam": {
let query = supabase.from("Lesson").select("id, name");
if (role === "teacher") {
query = query.eq("teacherId", currentUserId!);
}
const { data: examLessons } = await query;
relatedData = { lessons: examLessons };
break;
}
case "event": {
const { data: eventClasses } = await supabase.from("Class").select("id, name");
relatedData = { classes: eventClasses };
break;
}
case "announcement": {
const { data: announcementClasses } = await supabase.from("Class").select("id, name");
relatedData = { classes: announcementClasses };
break;
}
default:
break;
}
}
return (
<div className="">
<FormModal
table={table}
type={type}
data={data}
id={id}
relatedData={relatedData}
/>
</div>
);
};
export default FormContainer;

View File

@ -0,0 +1,246 @@
"use client";
import {
deleteClass,
deleteExam,
deleteStudent,
deleteSubject,
deleteTeacher,
deleteLesson,
deleteAssignment,
deleteResult,
deleteEvent,
deleteAnnouncement,
} from "@/lib/actions";
import dynamic from "next/dynamic";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { useFormState } from "react-dom";
import { toast } from "react-toastify";
import { FormContainerProps } from "./FormContainer";
const deleteActionMap = {
subject: deleteSubject,
class: deleteClass,
teacher: deleteTeacher,
student: deleteStudent,
exam: deleteExam,
// TODO: OTHER DELETE ACTIONS
parent: deleteSubject,
lesson: deleteLesson,
assignment: deleteAssignment,
result: deleteResult,
attendance: deleteSubject,
event: deleteEvent,
announcement: deleteAnnouncement,
};
// USE LAZY LOADING
// import TeacherForm from "./forms/TeacherForm";
// import StudentForm from "./forms/StudentForm";
const TeacherForm = dynamic(() => import("./forms/TeacherForm"), {
loading: () => <h1>Loading...</h1>,
});
const StudentForm = dynamic(() => import("./forms/StudentForm"), {
loading: () => <h1>Loading...</h1>,
});
const SubjectForm = dynamic(() => import("./forms/SubjectForm"), {
loading: () => <h1>Loading...</h1>,
});
const ClassForm = dynamic(() => import("./forms/ClassForm"), {
loading: () => <h1>Loading...</h1>,
});
const ExamForm = dynamic(() => import("./forms/ExamForm"), {
loading: () => <h1>Loading...</h1>,
});
const LessonForm = dynamic(() => import("./forms/LessonForm"), {
loading: () => <h1>Loading...</h1>,
});
const AssignmentForm = dynamic(() => import("./forms/AssignmentForm"), {
loading: () => <h1>Loading...</h1>,
});
const ResultForm = dynamic(() => import("./forms/ResultForm"), {
loading: () => <h1>Loading...</h1>,
});
const EventForm = dynamic(() => import("./forms/EventForm"), {
loading: () => <h1>Loading...</h1>,
});
const AnnouncementForm = dynamic(() => import("./forms/AnnouncementForm"), {
loading: () => <h1>Loading...</h1>,
});
// TODO: OTHER FORMS
const forms: {
[key: string]: (
setOpen: Dispatch<SetStateAction<boolean>>,
type: "create" | "update",
data?: any,
relatedData?: any
) => JSX.Element;
} = {
subject: (setOpen, type, data, relatedData) => (
<SubjectForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
),
class: (setOpen, type, data, relatedData) => (
<ClassForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
),
teacher: (setOpen, type, data, relatedData) => (
<TeacherForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
),
student: (setOpen, type, data, relatedData) => (
<StudentForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
),
exam: (setOpen, type, data, relatedData) => (
<ExamForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
),
lesson: (setOpen, type, data, relatedData) => (
<LessonForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
),
assignment: (setOpen, type, data, relatedData) => (
<AssignmentForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
),
result: (setOpen, type, data, relatedData) => (
<ResultForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
),
event: (setOpen, type, data, relatedData) => (
<EventForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
),
announcement: (setOpen, type, data, relatedData) => (
<AnnouncementForm
type={type}
data={data}
setOpen={setOpen}
relatedData={relatedData}
/>
// TODO OTHER LIST ITEMS
),
};
const FormModal = ({
table,
type,
data,
id,
relatedData,
}: FormContainerProps & { relatedData?: any }) => {
const size = type === "create" ? "w-8 h-8" : "w-7 h-7";
const bgColor =
type === "create"
? "bg-lamaYellow"
: type === "update"
? "bg-lamaSky"
: "bg-lamaPurple";
const [open, setOpen] = useState(false);
const Form = () => {
const [state, formAction] = useFormState(deleteActionMap[table], {
success: false,
error: false,
});
const router = useRouter();
useEffect(() => {
if (state.success) {
toast(`${table} has been deleted!`);
setOpen(false);
router.refresh();
}
}, [state, router]);
return type === "delete" && id ? (
<form action={formAction} className="p-4 flex flex-col gap-4">
<input type="text | number" name="id" defaultValue={id} hidden />
<span className="text-center font-medium">
All data will be lost. Are you sure you want to delete this {table}?
</span>
<button className="bg-red-700 text-white py-2 px-4 rounded-md border-none w-max self-center">
Delete
</button>
</form>
) : type === "create" || type === "update" ? (
forms[table] ? (
forms[table](setOpen, type, data, relatedData)
) : (
"Form not found!"
)
) : (
"Form not found!"
);
};
return (
<>
<button
className={`${size} flex items-center justify-center rounded-full ${bgColor}`}
onClick={() => setOpen(true)}
>
<Image src={`/${type}.png`} alt="" width={16} height={16} />
</button>
{open && (
<div className="w-screen h-screen absolute left-0 top-0 bg-black bg-opacity-60 z-50 flex items-center justify-center">
<div className="bg-white p-4 rounded-md relative w-[90%] md:w-[70%] lg:w-[60%] xl:w-[50%] 2xl:w-[40%]">
<Form />
<div
className="absolute top-4 right-4 cursor-pointer"
onClick={() => setOpen(false)}
>
<Image src="/close.png" alt="" width={14} height={14} />
</div>
</div>
</div>
)}
</>
);
};
export default FormModal;

View File

@ -0,0 +1,41 @@
import { FieldError } from "react-hook-form";
type InputFieldProps = {
label: string;
type?: string;
register: any;
name: string;
defaultValue?: string;
error?: FieldError;
hidden?: boolean;
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
};
const InputField = ({
label,
type = "text",
register,
name,
defaultValue,
error,
hidden,
inputProps,
}: InputFieldProps) => {
return (
<div className={hidden ? "hidden" : "flex flex-col gap-2 w-full md:w-1/4"}>
<label className="text-xs text-gray-500">{label}</label>
<input
type={type}
{...register(name)}
className="ring-[1.5px] ring-gray-300 p-2 rounded-md text-sm w-full"
{...inputProps}
defaultValue={defaultValue}
/>
{error?.message && (
<p className="text-xs text-red-400">{error.message.toString()}</p>
)}
</div>
);
};
export default InputField;

150
src/components/Menu.tsx Normal file
View File

@ -0,0 +1,150 @@
import { currentUser } from "@clerk/nextjs/server";
import Image from "next/image";
import Link from "next/link";
const menuItems = [
{
title: "MENU",
items: [
{
icon: "/home.png",
label: "Home",
href: "/",
visible: ["admin", "teacher", "student", "parent"],
},
{
icon: "/teacher.png",
label: "Teachers",
href: "/list/teachers",
visible: ["admin", "teacher"],
},
{
icon: "/student.png",
label: "Students",
href: "/list/students",
visible: ["admin", "teacher"],
},
{
icon: "/parent.png",
label: "Parents",
href: "/list/parents",
visible: ["admin", "teacher"],
},
{
icon: "/subject.png",
label: "Subjects",
href: "/list/subjects",
visible: ["admin"],
},
{
icon: "/class.png",
label: "Classes",
href: "/list/classes",
visible: ["admin", "teacher"],
},
{
icon: "/lesson.png",
label: "Lessons",
href: "/list/lessons",
visible: ["admin", "teacher"],
},
{
icon: "/exam.png",
label: "Exams",
href: "/list/exams",
visible: ["admin", "teacher", "student", "parent"],
},
{
icon: "/assignment.png",
label: "Assignments",
href: "/list/assignments",
visible: ["admin", "teacher", "student", "parent"],
},
{
icon: "/result.png",
label: "Results",
href: "/list/results",
visible: ["admin", "teacher", "student", "parent"],
},
{
icon: "/attendance.png",
label: "Attendance",
href: "/list/attendance",
visible: ["admin", "teacher", "student", "parent"],
},
{
icon: "/calendar.png",
label: "Events",
href: "/list/events",
visible: ["admin", "teacher", "student", "parent"],
},
{
icon: "/message.png",
label: "Messages",
href: "/list/messages",
visible: ["admin", "teacher", "student", "parent"],
},
{
icon: "/announcement.png",
label: "Announcements",
href: "/list/announcements",
visible: ["admin", "teacher", "student", "parent"],
},
],
},
{
title: "OTHER",
items: [
{
icon: "/profile.png",
label: "Profile",
href: "/profile",
visible: ["admin", "teacher", "student", "parent"],
},
{
icon: "/setting.png",
label: "Settings",
href: "/settings",
visible: ["admin", "teacher", "student", "parent"],
},
{
icon: "/logout.png",
label: "Logout",
href: "/logout",
visible: ["admin", "teacher", "student", "parent"],
},
],
},
];
const Menu = async () => {
const user = await currentUser();
const role = user?.publicMetadata.role as string;
return (
<div className="mt-4 text-sm">
{menuItems.map((i) => (
<div className="flex flex-col gap-2" key={i.title}>
<span className="hidden lg:block text-gray-400 font-light my-4">
{i.title}
</span>
{i.items.map((item) => {
if (item.visible.includes(role)) {
return (
<Link
href={item.href}
key={item.label}
className="flex items-center justify-center lg:justify-start gap-4 text-gray-500 py-2 md:px-2 rounded-md hover:bg-lamaSkyLight"
>
<Image src={item.icon} alt="" width={20} height={20} />
<span className="hidden lg:block">{item.label}</span>
</Link>
);
}
})}
</div>
))}
</div>
);
};
export default Menu;

42
src/components/Navbar.tsx Normal file
View File

@ -0,0 +1,42 @@
import { UserButton } from "@clerk/nextjs";
import { currentUser } from "@clerk/nextjs/server";
import Image from "next/image";
const Navbar = async () => {
const user = await currentUser();
return (
<div className="flex items-center justify-between p-4">
{/* SEARCH BAR */}
<div className="hidden md:flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-gray-300 px-2">
<Image src="/search.png" alt="" width={14} height={14} />
<input
type="text"
placeholder="Search..."
className="w-[200px] p-2 bg-transparent outline-none"
/>
</div>
{/* ICONS AND USER */}
<div className="flex items-center gap-6 justify-end w-full">
<div className="bg-white rounded-full w-7 h-7 flex items-center justify-center cursor-pointer">
<Image src="/message.png" alt="" width={20} height={20} />
</div>
<div className="bg-white rounded-full w-7 h-7 flex items-center justify-center cursor-pointer relative">
<Image src="/announcement.png" alt="" width={20} height={20} />
<div className="absolute -top-3 -right-3 w-5 h-5 flex items-center justify-center bg-purple-500 text-white rounded-full text-xs">
1
</div>
</div>
<div className="flex flex-col">
<span className="text-xs leading-3 font-medium">John Doe</span>
<span className="text-[10px] text-gray-500 text-right">
{user?.publicMetadata?.role as string}
</span>
</div>
{/* <Image src="/avatar.png" alt="" width={36} height={36} className="rounded-full"/> */}
<UserButton />
</div>
</div>
);
};
export default Navbar;

View File

@ -0,0 +1,62 @@
"use client";
import { ITEM_PER_PAGE } from "@/lib/settings";
import { useRouter } from "next/navigation";
const Pagination = ({ page, count }: { page: number; count: number }) => {
const router = useRouter();
const hasPrev = ITEM_PER_PAGE * (page - 1) > 0;
const hasNext = ITEM_PER_PAGE * (page - 1) + ITEM_PER_PAGE < count;
const changePage = (newPage: number) => {
const params = new URLSearchParams(window.location.search);
params.set("page", newPage.toString());
router.push(`${window.location.pathname}?${params}`);
};
return (
<div className="p-4 flex items-center justify-between text-gray-500">
<button
disabled={!hasPrev}
className="py-2 px-4 rounded-md bg-slate-200 text-xs font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => {
changePage(page - 1);
}}
>
Prev
</button>
<div className="flex items-center gap-2 text-sm">
{Array.from(
{ length: Math.ceil(count / ITEM_PER_PAGE) },
(_, index) => {
const pageIndex = index + 1;
return (
<button
key={pageIndex}
className={`px-2 rounded-sm ${
page === pageIndex ? "bg-lamaSky" : ""
}`}
onClick={() => {
changePage(pageIndex);
}}
>
{pageIndex}
</button>
);
}
)}
</div>
<button
className="py-2 px-4 rounded-md bg-slate-200 text-xs font-semibold disabled:opacity-50 disabled:cursor-not-allowed"
disabled={!hasNext}
onClick={() => {
changePage(page + 1);
}}
>
Next
</button>
</div>
);
};
export default Pagination;

View File

@ -0,0 +1,40 @@
"use client";
import Image from "next/image";
import { PieChart, Pie, Sector, Cell, ResponsiveContainer } from "recharts";
const data = [
{ name: "Group A", value: 92, fill: "#C3EBFA" },
{ name: "Group B", value: 8, fill: "#FAE27C" },
];
const Performance = () => {
return (
<div className="bg-white p-4 rounded-md h-80 relative">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold">Performance</h1>
<Image src="/moreDark.png" alt="" width={16} height={16} />
</div>
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
dataKey="value"
startAngle={180}
endAngle={0}
data={data}
cx="50%"
cy="50%"
innerRadius={70}
fill="#8884d8"
/>
</PieChart>
</ResponsiveContainer>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-center">
<h1 className="text-3xl font-bold">9.2</h1>
<p className="text-xs text-gray-300">of 10 max LTS</p>
</div>
<h2 className="font-medium absolute bottom-16 left-0 right-0 m-auto text-center">1st Semester - 2nd Semester</h2>
</div>
);
};
export default Performance;

View File

@ -0,0 +1,28 @@
import { getSupabaseClient } from "@/lib/supabase";
const StudentAttendanceCard = async ({ id }: { id: string }) => {
const supabase = await getSupabaseClient();
const { data: attendanceData, error } = await supabase
.from("Attendance")
.select("present")
.eq("studentId", id)
.gte("date", new Date(new Date().getFullYear(), 0, 1).toISOString());
if (error) {
console.error("Error fetching student attendance:", error);
}
const attendance = attendanceData || [];
const totalDays = attendance.length;
const presentDays = attendance.filter((day) => day.present).length;
const percentage = (presentDays / totalDays) * 100;
return (
<div className="">
<h1 className="text-xl font-semibold">{percentage || "-"}%</h1>
<span className="text-sm text-gray-400">Attendance</span>
</div>
);
};
export default StudentAttendanceCard;

24
src/components/Table.tsx Normal file
View File

@ -0,0 +1,24 @@
const Table = ({
columns,
renderRow,
data,
}: {
columns: { header: string; accessor: string; className?: string }[];
renderRow: (item: any) => React.ReactNode;
data: any[];
}) => {
return (
<table className="w-full mt-4">
<thead>
<tr className="text-left text-gray-500 text-sm">
{columns.map((col) => (
<th key={col.accessor} className={col.className}>{col.header}</th>
))}
</tr>
</thead>
<tbody>{data.map((item) => renderRow(item))}</tbody>
</table>
);
};
export default Table;

View File

@ -0,0 +1,34 @@
"use client";
import Image from "next/image";
import { useRouter } from "next/navigation";
const TableSearch = () => {
const router = useRouter();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const value = (e.currentTarget[0] as HTMLInputElement).value;
const params = new URLSearchParams(window.location.search);
params.set("search", value);
router.push(`${window.location.pathname}?${params}`);
};
return (
<form
onSubmit={handleSubmit}
className="w-full md:w-auto flex items-center gap-2 text-xs rounded-full ring-[1.5px] ring-gray-300 px-2"
>
<Image src="/search.png" alt="" width={14} height={14} />
<input
type="text"
placeholder="Search..."
className="w-[200px] p-2 bg-transparent outline-none"
/>
</form>
);
};
export default TableSearch;

View File

@ -0,0 +1,38 @@
import { getSupabaseClient } from "@/lib/supabase";
import Image from "next/image";
const UserCard = async ({
type,
}: {
type: "admin" | "teacher" | "student" | "parent";
}) => {
const supabase = await getSupabaseClient();
const tableMap: Record<typeof type, any> = {
admin: "Admin",
teacher: "Teacher",
student: "Student",
parent: "Parent",
};
const tableName = tableMap[type] as "Admin" | "Teacher" | "Student" | "Parent";
const { count } = await supabase
.from(tableName)
.select("*", { count: "exact", head: true });
const data = count || 0;
return (
<div className="rounded-2xl odd:bg-lamaPurple even:bg-lamaYellow p-4 flex-1 min-w-[130px]">
<div className="flex justify-between items-center">
<span className="text-[10px] bg-white px-2 py-1 rounded-full text-green-600">
2024/25
</span>
<Image src="/more.png" alt="" width={20} height={20} />
</div>
<h1 className="text-2xl font-semibold my-4">{data}</h1>
<h2 className="capitalize text-sm font-medium text-gray-500">{type}s</h2>
</div>
);
};
export default UserCard;

Some files were not shown because too many files have changed in this diff Show More