feat: implement user authentication and admin management

- Add user management API endpoints for listing and creating users.
- Implement login and logout functionality with rate limiting.
- Create UI components for login form, logout button, and animated elements.
- Add session management with JWT and secure cookie handling.
- Seed initial admin user if no users exist.
- Introduce utility functions for password hashing and user authentication.
- Set up proxy middleware for route protection and security headers.
- Create a JSON file for user data storage.
- Add links configuration for external resources.
This commit is contained in:
Anders Böttcher
2026-05-12 14:58:11 +02:00
parent 50566f2a22
commit 0d58fafe4e
34 changed files with 4263 additions and 5012 deletions
+82
View File
@@ -0,0 +1,82 @@
"use client";
import { useEffect, useRef, useState } from "react";
interface CountUpProps {
/** Target number to count up to */
to: number;
/** String appended after the number (e.g. "%", " MB", "+") */
suffix?: string;
/** String prepended before the number */
prefix?: string;
/** Animation duration in ms */
duration?: number;
/** Decimal places to show */
decimals?: number;
}
export default function CountUp({
to,
suffix = "",
prefix = "",
duration = 1800,
decimals = 0,
}: CountUpProps) {
const ref = useRef<HTMLSpanElement>(null);
const [current, setCurrent] = useState(0);
const [started, setStarted] = useState(false);
useEffect(() => {
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !started) {
setStarted(true);
observer.disconnect();
}
},
{ threshold: 0.5 },
);
observer.observe(el);
return () => observer.disconnect();
}, [started]);
useEffect(() => {
if (!started) return;
// Skip animation if user prefers reduced motion
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setCurrent(to);
return;
}
const startTime = performance.now();
let raf: number;
function tick(now: number) {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
// Ease-out quart — fast start, smooth finish
const eased = 1 - Math.pow(1 - progress, 4);
setCurrent(eased * to);
if (progress < 1) raf = requestAnimationFrame(tick);
}
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [started, to, duration]);
const display =
decimals > 0 ? current.toFixed(decimals) : Math.floor(current).toString();
return (
<span ref={ref}>
{prefix}
{display}
{suffix}
</span>
);
}