first commit. working

This commit is contained in:
2025-06-26 13:09:12 -04:00
parent 86df5c354a
commit 45d8efdce6
14 changed files with 4185 additions and 255 deletions
+18
View File
@@ -0,0 +1,18 @@
"use client";
import React from 'react';
import { useAttendance } from '../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 (
<button
onClick={() => clockToggle(teacherId)}
className="px-4 py-2 border rounded"
>
{nextType === 'in' ? 'Clock In' : 'Clock Out'}
</button>
);
}
+38
View File
@@ -0,0 +1,38 @@
"use client";
import React from 'react';
import { useAttendance } from '../hooks/useAttendance';
import { useTeachers } from '../hooks/useTeachers';
import { exportToCsv } from '../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 (
<div>
<h2 className="text-xl mb-2">Daily Summary</h2>
<button
onClick={() => exportToCsv('daily-summary.csv', summary)}
className="mb-2 px-4 py-2 border rounded"
>
Export CSV
</button>
<ul>
{summary.map(s => (
<li key={s.name}>{s.name}: {s.total} hrs</li>
))}
</ul>
</div>
);
}
+19
View File
@@ -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 (
<button
onClick={handleSync}
className="mb-4 px-4 py-2 border rounded"
>
Sync
</button>
);
}
+58
View File
@@ -0,0 +1,58 @@
"use client";
import React, { useState } from 'react';
import { useTeachers } from '../hooks/useTeachers';
import { useAttendance } from '../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 (
<div>
<div className="flex mb-4">
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
placeholder="New teacher name"
className="flex-grow border p-2 mr-2 rounded"
/>
<button
onClick={() => { addTeacher(name); setName(''); }}
className="px-4 py-2 border rounded"
>
Add
</button>
</div>
<ul>
{teachers.map(t => {
const records = todayRecords.filter(r => r.teacherId === t.id);
const lastIn = [...records].reverse().find(r => r.type === 'in');
const lastOut = [...records].reverse().find(r => r.type === 'out');
return (
<li key={t.id} className="flex justify-between items-center mb-2">
<span>{t.name}</span>
<span className="mx-2 text-sm text-gray-500">
In: {formatTime(lastIn?.timestamp)} | Out: {formatTime(lastOut?.timestamp)}
</span>
<ClockButton teacherId={t.id} />
</li>
);
})}
</ul>
</div>
);
}
+31
View File
@@ -0,0 +1,31 @@
import { useState, useEffect } from 'react';
import { db, AttendanceRecord } from '../utils/db';
import { v4 as uuid } from 'uuid';
export function useAttendance() {
const [todayRecords, setTodayRecords] = useState<AttendanceRecord[]>([]);
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 };
}
+18
View File
@@ -0,0 +1,18 @@
import { useState, useEffect } from 'react';
import { db, Teacher } from '../utils/db';
import { v4 as uuid } from 'uuid';
export function useTeachers() {
const [teachers, setTeachers] = useState<Teacher[]>([]);
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 };
}
+11
View File
@@ -0,0 +1,11 @@
/** @type {import('next').NextConfig} */
const withPWA = require('next-pwa')({
dest: 'public',
register: true,
skipWaiting: true,
});
module.exports = withPWA({
reactStrictMode: true,
// you can add more custom Next.js config here
});
+3919 -151
View File
File diff suppressed because it is too large Load Diff
+9 -5
View File
@@ -9,19 +9,23 @@
"lint": "next lint"
},
"dependencies": {
"dexie": "^4.0.11",
"next": "15.3.4",
"next-pwa": "^5.6.0",
"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",
"@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",
"typescript": "^5"
}
}
+6
View File
@@ -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' });
}
+20 -99
View File
@@ -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 (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={180}
height={38}
priority
/>
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
<li className="mb-2 tracking-[-.01em]">
Get started by editing{" "}
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
</ol>
<main className="p-4 max-w-screen-sm mx-auto">
<h1 className="text-2xl mb-4">Teacher Attendance</h1>
<div className="flex gap-4 items-center flex-col sm:flex-row">
<a
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={20}
height={20}
/>
Deploy now
</a>
<a
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Read our docs
</a>
</div>
</main>
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
Learn
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
Examples
</a>
<a
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
Go to nextjs.org
</a>
</footer>
</div>
);
{/* Sync local changes to server */}
<SyncButton />
{/* Roster + clock-in/out UI */}
<TeacherList />
{/* Daily totals + CSV export */}
<DailySummary />
</main>
)
}
export default Home
+1
View File
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "ES2017",
"baseUrl": ".",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
+13
View File
@@ -0,0 +1,13 @@
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();
}
+24
View File
@@ -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<Teacher, string>;
records!: Dexie.Table<AttendanceRecord, string>;
constructor() {
super('AttendanceDB');
this.version(1).stores({
teachers: 'id, name',
records: 'id, teacherId, timestamp, type',
});
}
}
export const db = new AppDB();