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
+86
View File
@@ -0,0 +1,86 @@
"use client";
import { useEffect, useRef, useState } from "react";
type Variant =
| "fade-up"
| "fade-down"
| "fade-in"
| "fade-left"
| "fade-right"
| "scale-up";
interface AnimateInProps {
children: React.ReactNode;
variant?: Variant;
/** Delay (ms) after the element enters the viewport before animating */
delay?: number;
/** Animation duration in ms */
duration?: number;
/** Extra classes to put on the wrapper div */
className?: string;
/** If true (default) the animation only fires once */
once?: boolean;
}
const transforms: Record<Variant, { hidden: string; visible: string }> = {
"fade-up": { hidden: "opacity-0 translate-y-10", visible: "opacity-100 translate-y-0" },
"fade-down": { hidden: "opacity-0 -translate-y-10", visible: "opacity-100 translate-y-0" },
"fade-in": { hidden: "opacity-0", visible: "opacity-100" },
"fade-left": { hidden: "opacity-0 -translate-x-10", visible: "opacity-100 translate-x-0" },
"fade-right": { hidden: "opacity-0 translate-x-10", visible: "opacity-100 translate-x-0" },
"scale-up": { hidden: "opacity-0 scale-95", visible: "opacity-100 scale-100" },
};
export default function AnimateIn({
children,
variant = "fade-up",
delay = 0,
duration = 650,
className = "",
once = true,
}: AnimateInProps) {
const ref = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
// Respect the user's motion preference
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setVisible(true);
return;
}
const el = ref.current;
if (!el) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setVisible(true);
if (once) observer.disconnect();
} else if (!once) {
setVisible(false);
}
},
{ threshold: 0.08, rootMargin: "0px 0px -60px 0px" },
);
observer.observe(el);
return () => observer.disconnect();
}, [once]);
const { hidden, visible: vis } = transforms[variant];
return (
<div
ref={ref}
className={`${visible ? vis : hidden} ${className}`}
style={{
transition: `opacity ${duration}ms cubic-bezier(0.22,1,0.36,1) ${delay}ms, transform ${duration}ms cubic-bezier(0.22,1,0.36,1) ${delay}ms`,
willChange: "opacity, transform",
}}
>
{children}
</div>
);
}