first commit
This commit is contained in:
Generated
+41
-3
@@ -8,19 +8,23 @@
|
|||||||
"name": "newest-nxtjs-daisyui",
|
"name": "newest-nxtjs-daisyui",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"daisyui": "^5.0.43",
|
||||||
|
"dexie": "^4.0.11",
|
||||||
"next": "15.3.4",
|
"next": "15.3.4",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0",
|
||||||
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.4",
|
"eslint-config-next": "15.3.4",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4.1.11",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2409,6 +2413,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
@@ -2541,6 +2554,12 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/doctrine": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||||
@@ -4940,6 +4959,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -6037,6 +6062,19 @@
|
|||||||
"punycode": "^2.1.0"
|
"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": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
+9
-5
@@ -9,19 +9,23 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"daisyui": "^5.0.43",
|
||||||
|
"dexie": "^4.0.11",
|
||||||
|
"next": "15.3.4",
|
||||||
|
"papaparse": "^5.5.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"next": "15.3.4"
|
"uuid": "^11.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
|
||||||
"tailwindcss": "^4",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.4",
|
"eslint-config-next": "15.3.4",
|
||||||
"@eslint/eslintrc": "^3"
|
"tailwindcss": "^4.1.11",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -1,5 +1,7 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: ["@tailwindcss/postcss"],
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
@@ -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' });
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<button
|
||||||
|
onClick={() => clockToggle(teacherId)}
|
||||||
|
className="px-4 py-2 border rounded"
|
||||||
|
>
|
||||||
|
{nextType === 'in' ? 'Clock In' : 'Clock Out'}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const NavBar = () => {
|
||||||
|
return (
|
||||||
|
<div className="navbar bg-base-100 shadow-sm">
|
||||||
|
<div className="flex-1">
|
||||||
|
<a className="btn btn-ghost text-xl">daisyUI</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex-none">
|
||||||
|
<button className="btn btn-square btn-ghost">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" className="inline-block h-5 w-5 stroke-current">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NavBar;
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+1
-25
@@ -1,26 +1,2 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui";
|
||||||
: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;
|
|
||||||
}
|
|
||||||
@@ -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<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 };
|
||||||
|
}
|
||||||
@@ -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<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 };
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import NavBar from "./components/NavBar";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -27,6 +28,7 @@ export default function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
<NavBar />
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+19
-98
@@ -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 (
|
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="p-4 max-w-screen-sm mx-auto">
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
<h1 className="text-2xl mb-4">Teacher Attendance</h1>
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
{/* Sync local changes to server */}
|
||||||
<a
|
<SyncButton />
|
||||||
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"
|
{/* Roster + clock-in/out UI */}
|
||||||
target="_blank"
|
<TeacherList />
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
{/* Daily totals + CSV export */}
|
||||||
<Image
|
<DailySummary />
|
||||||
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>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Home
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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();
|
||||||
Reference in New Issue
Block a user