diff --git a/package-lock.json b/package-lock.json index 9a34cb4..0478134 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,19 +8,23 @@ "name": "newest-nxtjs-daisyui", "version": "0.1.0", "dependencies": { + "daisyui": "^5.0.43", + "dexie": "^4.0.11", "next": "15.3.4", + "papaparse": "^5.5.3", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "uuid": "^11.1.0" }, "devDependencies": { "@eslint/eslintrc": "^3", - "@tailwindcss/postcss": "^4", + "@tailwindcss/postcss": "^4.1.11", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", "eslint": "^9", "eslint-config-next": "15.3.4", - "tailwindcss": "^4", + "tailwindcss": "^4.1.11", "typescript": "^5" } }, @@ -2409,6 +2413,15 @@ "dev": true, "license": "MIT" }, + "node_modules/daisyui": { + "version": "5.0.43", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.43.tgz", + "integrity": "sha512-2pshHJ73vetSpsbAyaOncGnNYL0mwvgseS1EWy1I9Qpw8D11OuBoDNIWrPIME4UFcq2xuff3A9x+eXbuFR9fUQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2541,6 +2554,12 @@ "node": ">=8" } }, + "node_modules/dexie": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.11.tgz", + "integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==", + "license": "Apache-2.0" + }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -4940,6 +4959,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/papaparse": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", + "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6037,6 +6062,19 @@ "punycode": "^2.1.0" } }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index de93302..9677c1d 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,23 @@ "lint": "next lint" }, "dependencies": { + "daisyui": "^5.0.43", + "dexie": "^4.0.11", + "next": "15.3.4", + "papaparse": "^5.5.3", "react": "^19.0.0", "react-dom": "^19.0.0", - "next": "15.3.4" + "uuid": "^11.1.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4.1.11", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.3.4", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4.1.11", + "typescript": "^5" } } diff --git a/postcss.config.mjs b/postcss.config.mjs index c7bcb4b..25de907 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,5 +1,7 @@ +/** @type {import('postcss-load-config').Config} */ const config = { - plugins: ["@tailwindcss/postcss"], + plugins: { + '@tailwindcss/postcss': {}, + }, }; - -export default config; +export default config; \ No newline at end of file diff --git a/src/app/api/sync.ts b/src/app/api/sync.ts new file mode 100644 index 0000000..6f63e20 --- /dev/null +++ b/src/app/api/sync.ts @@ -0,0 +1,6 @@ +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default function handler(req: NextApiRequest, res: NextApiResponse) { + // stub endpoint for future sync logic + res.status(200).json({ message: 'Sync not implemented yet' }); +} diff --git a/src/app/components/ClockButton.tsx b/src/app/components/ClockButton.tsx new file mode 100644 index 0000000..b0bd9fc --- /dev/null +++ b/src/app/components/ClockButton.tsx @@ -0,0 +1,18 @@ +"use client"; +import React from 'react'; +import { useAttendance } from '@/app/hooks/useAttendance'; + +export default function ClockButton({ teacherId }: { teacherId: string }) { + const { todayRecords, clockToggle } = useAttendance(); + const last = todayRecords.filter(r => r.teacherId === teacherId).pop(); + const nextType = last?.type === 'in' ? 'out' : 'in'; + + return ( + + ); +} \ No newline at end of file diff --git a/src/app/components/DailySummary.tsx b/src/app/components/DailySummary.tsx new file mode 100644 index 0000000..a858899 --- /dev/null +++ b/src/app/components/DailySummary.tsx @@ -0,0 +1,38 @@ +"use client"; +import React from 'react'; +import { useAttendance } from '@/app/hooks/useAttendance'; +import { useTeachers } from '@/app/hooks/useTeachers'; +import { exportToCsv } from '@/app/utils/csvExport'; + +export default function DailySummary() { + const { todayRecords } = useAttendance(); + const { teachers } = useTeachers(); + + const summary = teachers.map(t => { + const recs = todayRecords.filter(r => r.teacherId === t.id); + let total = 0; + for (let i = 0; i < recs.length; i += 2) { + if (recs[i+1]) { + total += (recs[i+1].timestamp - recs[i].timestamp) / (1000*60*60); + } + } + return { name: t.name, total: total.toFixed(2) }; + }); + + return ( +
+

Daily Summary

+ + +
+ ); +} diff --git a/src/app/components/NavBar.tsx b/src/app/components/NavBar.tsx new file mode 100644 index 0000000..7cbc3a8 --- /dev/null +++ b/src/app/components/NavBar.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +const NavBar = () => { + return ( +
+
+ daisyUI +
+
+ +
+
+ ); +}; + +export default NavBar; \ No newline at end of file diff --git a/src/app/components/SyncButton.tsx b/src/app/components/SyncButton.tsx new file mode 100644 index 0000000..3582f36 --- /dev/null +++ b/src/app/components/SyncButton.tsx @@ -0,0 +1,19 @@ +"use client"; +import React from 'react'; + +export default function SyncButton() { + const handleSync = async () => { + const res = await fetch('/api/sync'); + const json = await res.json(); + alert(json.message); + }; + + return ( + + ); +} diff --git a/src/app/components/TeacherList.tsx b/src/app/components/TeacherList.tsx new file mode 100644 index 0000000..149af3e --- /dev/null +++ b/src/app/components/TeacherList.tsx @@ -0,0 +1,58 @@ +"use client"; +import React, { useState } from 'react'; +import { useTeachers } from '@/app/hooks/useTeachers'; +import { useAttendance } from '@/app/hooks/useAttendance'; +import ClockButton from './ClockButton'; + +function roundTo15Minutes(date = new Date()) { + const ms = 1000 * 60 * 15; // 15 minutes in ms + return new Date(Math.round(date.getTime() / ms) * ms); +} + +function formatTime(ts?: number) { + if (!ts) return '--:--'; + const date = new Date(ts); + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} + +export default function TeacherList() { + const { teachers, addTeacher } = useTeachers(); + const { todayRecords } = useAttendance(); + const [name, setName] = useState(''); + + return ( +
+
+ setName(e.target.value)} + placeholder="New teacher name" + className="flex-grow border p-2 mr-2 rounded" + /> + +
+ +
+ ); +} diff --git a/src/app/globals.css b/src/app/globals.css index a2dc41e..74d8662 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,26 +1,2 @@ @import "tailwindcss"; - -:root { - --background: #ffffff; - --foreground: #171717; -} - -@theme inline { - --color-background: var(--background); - --color-foreground: var(--foreground); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); -} - -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} - -body { - background: var(--background); - color: var(--foreground); - font-family: Arial, Helvetica, sans-serif; -} +@plugin "daisyui"; \ No newline at end of file diff --git a/src/app/hooks/useAttendance.ts b/src/app/hooks/useAttendance.ts new file mode 100644 index 0000000..3abdccb --- /dev/null +++ b/src/app/hooks/useAttendance.ts @@ -0,0 +1,31 @@ +import { useState, useEffect } from 'react'; +import { db, AttendanceRecord } from '@/app/utils/db'; +import { v4 as uuid } from 'uuid'; + +export function useAttendance() { + const [todayRecords, setTodayRecords] = useState([]); + + useEffect(() => { + const dayStart = new Date(); + dayStart.setHours(0,0,0,0); + db.records + .where('timestamp') + .above(dayStart.getTime()) + .toArray() + .then(setTodayRecords); + }, []); + + const clockToggle = (teacherId: string) => { + const recs = todayRecords.filter(r => r.teacherId === teacherId); + const next: 'in' | 'out' = recs.pop()?.type === 'in' ? 'out' : 'in'; + const rec: AttendanceRecord = { + id: uuid(), + teacherId, + timestamp: Date.now(), + type: next + }; + db.records.add(rec).then(() => setTodayRecords(prev => [...prev, rec])); + }; + + return { todayRecords, clockToggle }; +} diff --git a/src/app/hooks/useTeachers.ts b/src/app/hooks/useTeachers.ts new file mode 100644 index 0000000..bf12138 --- /dev/null +++ b/src/app/hooks/useTeachers.ts @@ -0,0 +1,18 @@ +import { useState, useEffect } from 'react'; +import { db, Teacher } from '@/app/utils/db'; +import { v4 as uuid } from 'uuid'; + +export function useTeachers() { + const [teachers, setTeachers] = useState([]); + + useEffect(() => { + db.teachers.toArray().then(setTeachers); + }, []); + + const addTeacher = (name: string) => { + const t: Teacher = { id: uuid(), name }; + db.teachers.add(t).then(() => setTeachers(prev => [...prev, t])); + }; + + return { teachers, addTeacher }; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..eb349e3 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import NavBar from "./components/NavBar"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -27,6 +28,7 @@ export default function RootLayout({ + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index e68abe6..0fa2a4e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,24 @@ -import Image from "next/image"; +// pages/index.tsx +import React from 'react' +import SyncButton from './components/SyncButton' +import TeacherList from './components/TeacherList' +import DailySummary from './components/DailySummary' -export default function Home() { +const Home: React.FC = () => { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+
+

Teacher Attendance

- -
- -
- ); + {/* Sync local changes to server */} + + + {/* Roster + clock-in/out UI */} + + + {/* Daily totals + CSV export */} + + + ) } + +export default Home \ No newline at end of file diff --git a/src/app/utils/csvExport.ts b/src/app/utils/csvExport.ts new file mode 100644 index 0000000..b777f3b --- /dev/null +++ b/src/app/utils/csvExport.ts @@ -0,0 +1,14 @@ +export function exportToCsv(filename: string, rows: any[]) { + if (!rows.length) return; + const keys = Object.keys(rows[0]); + const csv = keys.join(',') + '\\n' + + rows.map(r => keys.map(k => JSON.stringify(r[k] || '')).join(',')).join('\\n'); + const blob = new Blob([csv], { type: 'text/csv' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = filename; + document.body.appendChild(link); + link.click(); + link.remove(); + } + \ No newline at end of file diff --git a/src/app/utils/db.ts b/src/app/utils/db.ts new file mode 100644 index 0000000..5758fc1 --- /dev/null +++ b/src/app/utils/db.ts @@ -0,0 +1,24 @@ +import Dexie from 'dexie'; + +export interface Teacher { id: string; name: string } +export interface AttendanceRecord { + id: string; + teacherId: string; + timestamp: number; + type: 'in' | 'out'; +} + +export class AppDB extends Dexie { + teachers!: Dexie.Table; + records!: Dexie.Table; + + constructor() { + super('AttendanceDB'); + this.version(1).stores({ + teachers: 'id, name', + records: 'id, teacherId, timestamp, type', + }); + } +} + +export const db = new AppDB();