initial project setup

Fastify + Prisma backend, React + Vite frontend, Docker deployment.
Multi-board feedback platform with anonymous cookie auth, passkey
upgrade path, ALTCHA spam protection, plugin system, and full
privacy-first architecture.
This commit is contained in:
2026-03-19 18:05:16 +02:00
commit f07eddf29e
77 changed files with 7031 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export function getCurrentPeriod(resetSchedule: string): string {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
switch (resetSchedule) {
case "weekly": {
const startOfYear = new Date(year, 0, 1);
const days = Math.floor((now.getTime() - startOfYear.getTime()) / 86400000);
const week = Math.ceil((days + startOfYear.getDay() + 1) / 7);
return `${year}-W${String(week).padStart(2, "0")}`;
}
case "quarterly": {
const q = Math.ceil((now.getMonth() + 1) / 3);
return `${year}-Q${q}`;
}
case "yearly":
return `${year}`;
case "never":
return "lifetime";
case "monthly":
default:
return `${year}-${month}`;
}
}
export async function getRemainingBudget(userId: string, boardId: string): Promise<number> {
const board = await prisma.board.findUnique({ where: { id: boardId } });
if (!board) return 0;
if (board.voteBudgetReset === "never" && board.voteBudget === 0) {
return Infinity;
}
const period = getCurrentPeriod(board.voteBudgetReset);
const used = await prisma.vote.aggregate({
where: { voterId: userId, post: { boardId }, budgetPeriod: period },
_sum: { weight: true },
});
const spent = used._sum.weight ?? 0;
return Math.max(0, board.voteBudget - spent);
}
export function getNextResetDate(resetSchedule: string): Date {
const now = new Date();
switch (resetSchedule) {
case "weekly": {
const d = new Date(now);
d.setDate(d.getDate() + (7 - d.getDay()));
d.setHours(0, 0, 0, 0);
return d;
}
case "quarterly": {
const q = Math.ceil((now.getMonth() + 1) / 3);
return new Date(now.getFullYear(), q * 3, 1);
}
case "yearly":
return new Date(now.getFullYear() + 1, 0, 1);
case "never":
return new Date(8640000000000000); // max date
case "monthly":
default: {
const d = new Date(now.getFullYear(), now.getMonth() + 1, 1);
return d;
}
}
}