Web app ready
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import db from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function POST(_req: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const tx = db.transaction(() => {
|
||||
const row = db.prepare('UPDATE counters SET value = value - 1 WHERE id = ? AND value > 0 RETURNING *').get(Number(id));
|
||||
if (row) db.prepare('INSERT INTO events (counter_id, delta, created_at) VALUES (?, -1, ?)').run(Number(id), Date.now());
|
||||
return row;
|
||||
});
|
||||
const result = tx();
|
||||
if (!result) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import db from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function GET(_req: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const counter = db.prepare('SELECT * FROM counters WHERE id = ?').get(Number(id));
|
||||
if (!counter) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
|
||||
const cutoff = Date.now() - 365 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const dailyActivity = db.prepare(`
|
||||
SELECT
|
||||
date(created_at / 1000, 'unixepoch', 'localtime') AS date,
|
||||
SUM(delta) AS value
|
||||
FROM events
|
||||
WHERE counter_id = ? AND created_at >= ?
|
||||
GROUP BY date
|
||||
HAVING SUM(delta) > 0
|
||||
ORDER BY date ASC
|
||||
`).all(Number(id), cutoff);
|
||||
|
||||
const allTimeTotal = (db.prepare(
|
||||
'SELECT COALESCE(SUM(delta), 0) AS total FROM events WHERE counter_id = ?'
|
||||
).get(Number(id)) as { total: number }).total;
|
||||
|
||||
return NextResponse.json({ counter, dailyActivity, allTimeTotal });
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import db from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function POST(_req: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const tx = db.transaction(() => {
|
||||
const row = db.prepare('UPDATE counters SET value = value + 1 WHERE id = ? RETURNING *').get(Number(id));
|
||||
if (row) db.prepare('INSERT INTO events (counter_id, delta, created_at) VALUES (?, 1, ?)').run(Number(id), Date.now());
|
||||
return row;
|
||||
});
|
||||
const result = tx();
|
||||
if (!result) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(result);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import db, { UPLOADS_DIR } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function GET(_req: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const counter = db.prepare('SELECT * FROM counters WHERE id = ?').get(Number(id));
|
||||
if (!counter) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
return NextResponse.json(counter);
|
||||
}
|
||||
|
||||
export async function PUT(request: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const existing = db.prepare('SELECT * FROM counters WHERE id = ?').get(Number(id)) as Record<string, unknown> | undefined;
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
|
||||
const name = body.name?.trim() ?? existing.name;
|
||||
const value = body.value !== undefined ? body.value : existing.value;
|
||||
const group_id = body.group_id !== undefined ? body.group_id : existing.group_id;
|
||||
const image_path = body.image_path !== undefined ? body.image_path : existing.image_path;
|
||||
|
||||
db.prepare(
|
||||
'UPDATE counters SET name = ?, value = ?, group_id = ?, image_path = ? WHERE id = ?'
|
||||
).run(name, value, group_id, image_path, Number(id));
|
||||
|
||||
const updated = db.prepare('SELECT * FROM counters WHERE id = ?').get(Number(id));
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
export async function DELETE(_req: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const existing = db.prepare('SELECT * FROM counters WHERE id = ?').get(Number(id)) as Record<string, unknown> | undefined;
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
|
||||
// Remove associated image if present
|
||||
if (existing.image_path) {
|
||||
const imgFile = path.join(UPLOADS_DIR, path.basename(existing.image_path as string));
|
||||
try { fs.unlinkSync(imgFile); } catch { /* ignore missing file */ }
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM counters WHERE id = ?').run(Number(id));
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import db from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
const counters = db.prepare('SELECT * FROM counters ORDER BY order_index ASC, id ASC').all();
|
||||
return NextResponse.json(counters);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { name, value = 0, group_id = null, image_path = null } = await request.json();
|
||||
if (!name?.trim()) {
|
||||
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||
}
|
||||
const maxOrder = (db.prepare(
|
||||
'SELECT COALESCE(MAX(order_index), -1) AS m FROM counters'
|
||||
).get() as { m: number }).m;
|
||||
const result = db.prepare(
|
||||
'INSERT INTO counters (name, value, group_id, image_path, order_index) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(name.trim(), value, group_id, image_path, maxOrder + 1);
|
||||
const counter = db.prepare('SELECT * FROM counters WHERE id = ?').get(result.lastInsertRowid);
|
||||
return NextResponse.json(counter, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import db from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
type Params = { params: Promise<{ id: string }> };
|
||||
|
||||
export async function PUT(request: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const { name } = await request.json();
|
||||
const existing = db.prepare('SELECT * FROM groups WHERE id = ?').get(Number(id));
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
if (!name?.trim()) return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||
db.prepare('UPDATE groups SET name = ? WHERE id = ?').run(name.trim(), Number(id));
|
||||
const updated = db.prepare('SELECT * FROM groups WHERE id = ?').get(Number(id));
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
export async function DELETE(_req: Request, { params }: Params) {
|
||||
const { id } = await params;
|
||||
const existing = db.prepare('SELECT * FROM groups WHERE id = ?').get(Number(id));
|
||||
if (!existing) return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
db.transaction(() => {
|
||||
db.prepare('UPDATE counters SET group_id = NULL WHERE group_id = ?').run(Number(id));
|
||||
db.prepare('DELETE FROM groups WHERE id = ?').run(Number(id));
|
||||
})();
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import db from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
const groups = db.prepare('SELECT * FROM groups ORDER BY order_index ASC, id ASC').all();
|
||||
return NextResponse.json(groups);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { name } = await request.json();
|
||||
if (!name?.trim()) {
|
||||
return NextResponse.json({ error: 'Name is required' }, { status: 400 });
|
||||
}
|
||||
const maxOrder = (db.prepare(
|
||||
'SELECT COALESCE(MAX(order_index), -1) AS m FROM groups'
|
||||
).get() as { m: number }).m;
|
||||
const result = db.prepare(
|
||||
'INSERT INTO groups (name, order_index) VALUES (?, ?)'
|
||||
).run(name.trim(), maxOrder + 1);
|
||||
const group = db.prepare('SELECT * FROM groups WHERE id = ?').get(result.lastInsertRowid);
|
||||
return NextResponse.json(group, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import db from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
// Body: { counters?: { id: number; order_index: number; group_id: number | null }[], groups?: { id: number; order_index: number }[] }
|
||||
export async function PUT(request: Request) {
|
||||
const { counters, groups } = await request.json();
|
||||
|
||||
const updateCounters = db.prepare(
|
||||
'UPDATE counters SET order_index = ?, group_id = ? WHERE id = ?'
|
||||
);
|
||||
const updateGroups = db.prepare(
|
||||
'UPDATE groups SET order_index = ? WHERE id = ?'
|
||||
);
|
||||
|
||||
const reorderAll = db.transaction(() => {
|
||||
if (Array.isArray(counters)) {
|
||||
for (const c of counters) {
|
||||
updateCounters.run(c.order_index, c.group_id ?? null, c.id);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(groups)) {
|
||||
for (const g of groups) {
|
||||
updateGroups.run(g.order_index, g.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
reorderAll();
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import path from 'path';
|
||||
import { writeFile } from 'fs/promises';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { UPLOADS_DIR } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const ALLOWED_TYPES: Record<string, string> = {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/gif': 'gif',
|
||||
'image/webp': 'webp',
|
||||
};
|
||||
const MAX_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const formData = await request.formData();
|
||||
const file = formData.get('file');
|
||||
|
||||
if (!file || !(file instanceof File)) {
|
||||
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
||||
}
|
||||
if (!ALLOWED_TYPES[file.type]) {
|
||||
return NextResponse.json({ error: 'Invalid file type' }, { status: 400 });
|
||||
}
|
||||
if (file.size > MAX_SIZE_BYTES) {
|
||||
return NextResponse.json({ error: 'File too large (max 10 MB)' }, { status: 400 });
|
||||
}
|
||||
|
||||
const ext = ALLOWED_TYPES[file.type];
|
||||
const filename = `${randomUUID()}.${ext}`;
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
await writeFile(path.join(UPLOADS_DIR, filename), buffer);
|
||||
|
||||
return NextResponse.json({ filename });
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import path from 'path';
|
||||
import { readFile, stat, access } from 'fs/promises';
|
||||
import { createHash } from 'crypto';
|
||||
import { UPLOADS_DIR } from '@/lib/db';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
type Params = { params: Promise<{ filename: string }> };
|
||||
|
||||
const MIME_MAP: Record<string, string> = {
|
||||
jpg: 'image/jpeg',
|
||||
jpeg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
gif: 'image/gif',
|
||||
webp: 'image/webp',
|
||||
};
|
||||
|
||||
export async function GET(req: Request, { params }: Params) {
|
||||
const { filename } = await params;
|
||||
// Prevent path traversal
|
||||
const safe = path.basename(filename);
|
||||
const filePath = path.join(UPLOADS_DIR, safe);
|
||||
try { await access(filePath); } catch {
|
||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const ext = safe.split('.').pop()?.toLowerCase() ?? '';
|
||||
const contentType = MIME_MAP[ext] ?? 'application/octet-stream';
|
||||
|
||||
const stats = await stat(filePath);
|
||||
const etag = `"${createHash('md5').update(`${stats.size}-${stats.mtimeMs}`).digest('hex')}"`;
|
||||
|
||||
// Return 304 if browser already has this version
|
||||
if (req.headers.get('if-none-match') === etag) {
|
||||
return new NextResponse(null, { status: 304 });
|
||||
}
|
||||
|
||||
const buffer = await readFile(filePath);
|
||||
return new NextResponse(buffer, {
|
||||
headers: {
|
||||
'Content-Type': contentType,
|
||||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||||
'ETag': etag,
|
||||
'Last-Modified': stats.mtime.toUTCString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user