Add auth demo

This commit is contained in:
Aaron Po
2026-03-12 13:31:08 -04:00
parent 95b9d7d52a
commit 60b784e365
37 changed files with 6555 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
// Generated by React Router
import "react-router";
declare module "react-router" {
interface Future {
v8_middleware: false
}
}

View File

@@ -0,0 +1,97 @@
// Generated by React Router
import "react-router"
declare module "react-router" {
interface Register {
pages: Pages
routeFiles: RouteFiles
routeModules: RouteModules
}
}
type Pages = {
"/": {
params: {};
};
"/login": {
params: {};
};
"/register": {
params: {};
};
"/logout": {
params: {};
};
"/dashboard": {
params: {};
};
"/confirm": {
params: {};
};
"/beers": {
params: {};
};
"/breweries": {
params: {};
};
"/beer-styles": {
params: {};
};
};
type RouteFiles = {
"root.tsx": {
id: "root";
page: "/" | "/login" | "/register" | "/logout" | "/dashboard" | "/confirm" | "/beers" | "/breweries" | "/beer-styles";
};
"routes/home.tsx": {
id: "routes/home";
page: "/";
};
"routes/login.tsx": {
id: "routes/login";
page: "/login";
};
"routes/register.tsx": {
id: "routes/register";
page: "/register";
};
"routes/logout.tsx": {
id: "routes/logout";
page: "/logout";
};
"routes/dashboard.tsx": {
id: "routes/dashboard";
page: "/dashboard";
};
"routes/confirm.tsx": {
id: "routes/confirm";
page: "/confirm";
};
"routes/beers.tsx": {
id: "routes/beers";
page: "/beers";
};
"routes/breweries.tsx": {
id: "routes/breweries";
page: "/breweries";
};
"routes/beer-styles.tsx": {
id: "routes/beer-styles";
page: "/beer-styles";
};
};
type RouteModules = {
"root": typeof import("./app/root.tsx");
"routes/home": typeof import("./app/routes/home.tsx");
"routes/login": typeof import("./app/routes/login.tsx");
"routes/register": typeof import("./app/routes/register.tsx");
"routes/logout": typeof import("./app/routes/logout.tsx");
"routes/dashboard": typeof import("./app/routes/dashboard.tsx");
"routes/confirm": typeof import("./app/routes/confirm.tsx");
"routes/beers": typeof import("./app/routes/beers.tsx");
"routes/breweries": typeof import("./app/routes/breweries.tsx");
"routes/beer-styles": typeof import("./app/routes/beer-styles.tsx");
};

View File

@@ -0,0 +1,18 @@
// Generated by React Router
declare module "virtual:react-router/server-build" {
import { ServerBuild } from "react-router";
export const assets: ServerBuild["assets"];
export const assetsBuildDirectory: ServerBuild["assetsBuildDirectory"];
export const basename: ServerBuild["basename"];
export const entry: ServerBuild["entry"];
export const future: ServerBuild["future"];
export const isSpaMode: ServerBuild["isSpaMode"];
export const prerender: ServerBuild["prerender"];
export const publicPath: ServerBuild["publicPath"];
export const routeDiscovery: ServerBuild["routeDiscovery"];
export const routes: ServerBuild["routes"];
export const ssr: ServerBuild["ssr"];
export const allowedActionOrigins: ServerBuild["allowedActionOrigins"];
export const unstable_getCriticalCss: ServerBuild["unstable_getCriticalCss"];
}

View File

@@ -0,0 +1,59 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../root.js")
type Info = GetInfo<{
file: "root.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../root.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../beer-styles.js")
type Info = GetInfo<{
file: "routes/beer-styles.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/beer-styles";
module: typeof import("../beer-styles.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../beers.js")
type Info = GetInfo<{
file: "routes/beers.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/beers";
module: typeof import("../beers.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../breweries.js")
type Info = GetInfo<{
file: "routes/breweries.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/breweries";
module: typeof import("../breweries.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../confirm.js")
type Info = GetInfo<{
file: "routes/confirm.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/confirm";
module: typeof import("../confirm.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../dashboard.js")
type Info = GetInfo<{
file: "routes/dashboard.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/dashboard";
module: typeof import("../dashboard.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../home.js")
type Info = GetInfo<{
file: "routes/home.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/home";
module: typeof import("../home.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../login.js")
type Info = GetInfo<{
file: "routes/login.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/login";
module: typeof import("../login.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../logout.js")
type Info = GetInfo<{
file: "routes/logout.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/logout";
module: typeof import("../logout.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,62 @@
// Generated by React Router
import type { GetInfo, GetAnnotations } from "react-router/internal";
type Module = typeof import("../register.js")
type Info = GetInfo<{
file: "routes/register.tsx",
module: Module
}>
type Matches = [{
id: "root";
module: typeof import("../../root.js");
}, {
id: "routes/register";
module: typeof import("../register.js");
}];
type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
export namespace Route {
// links
export type LinkDescriptors = Annotations["LinkDescriptors"];
export type LinksFunction = Annotations["LinksFunction"];
// meta
export type MetaArgs = Annotations["MetaArgs"];
export type MetaDescriptors = Annotations["MetaDescriptors"];
export type MetaFunction = Annotations["MetaFunction"];
// headers
export type HeadersArgs = Annotations["HeadersArgs"];
export type HeadersFunction = Annotations["HeadersFunction"];
// middleware
export type MiddlewareFunction = Annotations["MiddlewareFunction"];
// clientMiddleware
export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
// loader
export type LoaderArgs = Annotations["LoaderArgs"];
// clientLoader
export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
// action
export type ActionArgs = Annotations["ActionArgs"];
// clientAction
export type ClientActionArgs = Annotations["ClientActionArgs"];
// HydrateFallback
export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
// Component
export type ComponentProps = Annotations["ComponentProps"];
// ErrorBoundary
export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
}

View File

@@ -0,0 +1,8 @@
{
"hash": "fdf55a9c",
"configHash": "c6a852b3",
"lockfileHash": "e3b0c442",
"browserHash": "58d74d30",
"optimized": {},
"chunks": {}
}

View File

@@ -0,0 +1,3 @@
{
"type": "module"
}

118
src/Website/app/app.css Normal file
View File

@@ -0,0 +1,118 @@
@import "tailwindcss";
@plugin "daisyui" {
themes: biergarten-lager, biergarten-stout;
}
@theme {
--font-sans:
"DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-serif: "Volkhov", ui-serif, Georgia, serif;
}
h1,
h2,
h3,
h4,
h5,
h6,
.card-title {
font-family: var(--font-serif);
}
@plugin "daisyui/theme" {
name: "biergarten-lager";
default: true;
prefersdark: false;
color-scheme: "light";
/* Base — warm parchment / aged paper */
--color-base-100: oklch(97% 0.025 80);
--color-base-200: oklch(92% 0.04 78);
--color-base-300: oklch(86% 0.06 75);
--color-base-content: oklch(28% 0.05 55);
/* Primary — golden amber lager */
--color-primary: oklch(68% 0.165 60);
--color-primary-content: oklch(18% 0.04 55);
/* Secondary — deep mahogany ale */
--color-secondary: oklch(38% 0.09 40);
--color-secondary-content: oklch(95% 0.02 75);
/* Accent — frothy cream head */
--color-accent: oklch(94% 0.03 90);
--color-accent-content: oklch(30% 0.05 55);
/* Neutral — roasted stout */
--color-neutral: oklch(24% 0.04 45);
--color-neutral-content: oklch(92% 0.025 80);
/* Info — cool hop green */
--color-info: oklch(58% 0.14 145);
--color-info-content: oklch(97% 0.015 145);
/* Success — fresh barley */
--color-success: oklch(72% 0.13 120);
--color-success-content: oklch(20% 0.04 120);
/* Warning — amber harvest */
--color-warning: oklch(74% 0.19 55);
--color-warning-content: oklch(18% 0.04 55);
/* Error — deep cherry kriek */
--color-error: oklch(52% 0.2 20);
--color-error-content: oklch(97% 0.012 15);
--radius-selector: 0.375rem;
--radius-field: 0.5rem;
--radius-box: 0.875rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 1;
}
@plugin "daisyui/theme" {
name: "biergarten-stout";
default: false;
prefersdark: true;
color-scheme: "dark";
/* Base — charred barrel / roasted malt darkness */
--color-base-100: oklch(14% 0.006 45);
--color-base-200: oklch(18% 0.008 43);
--color-base-300: oklch(23% 0.01 42);
--color-base-content: oklch(88% 0.008 75);
/* Primary — golden amber lager */
--color-primary: oklch(68% 0.055 60);
--color-primary-content: oklch(14% 0.012 50);
/* Secondary — deep mahogany ale */
--color-secondary: oklch(55% 0.025 40);
--color-secondary-content: oklch(97% 0.005 75);
/* Accent — frothy cream head */
--color-accent: oklch(82% 0.01 88);
--color-accent-content: oklch(20% 0.01 55);
/* Neutral — near-black with warmth */
--color-neutral: oklch(20% 0.008 45);
--color-neutral-content: oklch(88% 0.007 78);
/* Info — cool hop green */
--color-info: oklch(54% 0.04 145);
--color-info-content: oklch(97% 0.005 145);
/* Success — fresh barley */
--color-success: oklch(66% 0.038 120);
--color-success-content: oklch(14% 0.012 120);
/* Warning — amber harvest */
--color-warning: oklch(70% 0.055 55);
--color-warning-content: oklch(14% 0.012 55);
/* Error — deep cherry kriek */
--color-error: oklch(50% 0.06 20);
--color-error-content: oklch(97% 0.004 15);
--radius-selector: 0.375rem;
--radius-field: 0.5rem;
--radius-box: 0.875rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 1;
}

View File

@@ -0,0 +1,59 @@
import { Link } from "react-router";
interface NavbarProps {
auth: {
username: string;
accessToken: string;
refreshToken: string;
userAccountId: string;
} | null;
}
export default function Navbar({ auth }: NavbarProps) {
return (
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50">
<div className="navbar-start">
<Link to="/" className="text-xl font-bold px-4">
🍺 The Biergarten App
</Link>
</div>
<div className="navbar-center gap-4">
<Link to="/beers" className="btn btn-ghost btn-sm">
Beers
</Link>
<Link to="/breweries" className="btn btn-ghost btn-sm">
Breweries
</Link>
<Link to="/beer-styles" className="btn btn-ghost btn-sm">
Beer Styles
</Link>
</div>
<div className="navbar-end gap-2 pr-4">
<Link to="/register" className="btn btn-ghost btn-sm">
Register User
</Link>
{auth ? (
<>
<Link to="/dashboard" className="btn btn-primary btn-sm">
Dashboard
</Link>
<div className="divider divider-horizontal m-0 h-6"></div>
<span className="text-sm text-base-content/70">
{auth.username}
</span>
<Link to="/logout" className="btn btn-ghost btn-sm">
Logout
</Link>
</>
) : (
<Link to="/login" className="btn btn-primary btn-sm">
Login
</Link>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,175 @@
import { createCookieSessionStorage, redirect } from "react-router";
const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:8080";
export interface AuthTokens {
accessToken: string;
refreshToken: string;
userAccountId: string;
username: string;
}
interface ApiResponse<T> {
message: string;
payload: T;
}
interface LoginPayload {
userAccountId: string;
username: string;
refreshToken: string;
accessToken: string;
}
interface RegistrationPayload extends LoginPayload {
confirmationEmailSent: boolean;
}
const sessionStorage = createCookieSessionStorage({
cookie: {
name: "__session",
httpOnly: true,
maxAge: 60 * 60 * 24 * 21, // 21 days (matches refresh token)
path: "/",
sameSite: "lax",
secrets: [process.env.SESSION_SECRET || "dev-secret-change-me"],
secure: process.env.NODE_ENV === "production",
},
});
export async function getSession(request: Request) {
return sessionStorage.getSession(request.headers.get("Cookie"));
}
export async function commitSession(
session: Awaited<ReturnType<typeof getSession>>,
) {
return sessionStorage.commitSession(session);
}
export async function destroySession(
session: Awaited<ReturnType<typeof getSession>>,
) {
return sessionStorage.destroySession(session);
}
export async function requireAuth(request: Request): Promise<AuthTokens> {
const session = await getSession(request);
const accessToken = session.get("accessToken");
const refreshToken = session.get("refreshToken");
if (!accessToken || !refreshToken) {
throw redirect("/login");
}
return {
accessToken,
refreshToken,
userAccountId: session.get("userAccountId"),
username: session.get("username"),
};
}
export async function getOptionalAuth(
request: Request,
): Promise<AuthTokens | null> {
const session = await getSession(request);
const accessToken = session.get("accessToken");
if (!accessToken) return null;
return {
accessToken,
refreshToken: session.get("refreshToken"),
userAccountId: session.get("userAccountId"),
username: session.get("username"),
};
}
export async function login(username: string, password: string) {
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `Login failed (${res.status})`);
}
const data: ApiResponse<LoginPayload> = await res.json();
return data.payload;
}
export async function register(body: {
username: string;
firstName: string;
lastName: string;
email: string;
dateOfBirth: string;
password: string;
}) {
const res = await fetch(`${API_BASE_URL}/api/auth/register`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text();
throw new Error(text || `Registration failed (${res.status})`);
}
const data: ApiResponse<RegistrationPayload> = await res.json();
return data.payload;
}
export async function refreshTokens(refreshToken: string) {
const res = await fetch(`${API_BASE_URL}/api/auth/refresh`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken }),
});
if (!res.ok) {
throw new Error("Token refresh failed");
}
const data: ApiResponse<LoginPayload> = await res.json();
return data.payload;
}
export async function confirmEmail(token: string, accessToken: string) {
const res = await fetch(
`${API_BASE_URL}/api/auth/confirm?token=${encodeURIComponent(token)}`,
{
method: "POST",
headers: { Authorization: `Bearer ${accessToken}` },
},
);
if (!res.ok) {
const text = await res.text();
throw new Error(text || `Confirmation failed (${res.status})`);
}
const data: ApiResponse<{ userAccountId: string; confirmedDate: string }> =
await res.json();
return data.payload;
}
export async function createAuthSession(
payload: LoginPayload,
redirectTo: string,
) {
const session = await sessionStorage.getSession();
session.set("accessToken", payload.accessToken);
session.set("refreshToken", payload.refreshToken);
session.set("userAccountId", payload.userAccountId);
session.set("username", payload.username);
return redirect(redirectTo, {
headers: { "Set-Cookie": await commitSession(session) },
});
}

View File

@@ -0,0 +1,33 @@
import { z } from "zod";
export const loginSchema = z.object({
username: z.string().min(1, "Username is required"),
password: z.string().min(1, "Password is required"),
});
export type LoginSchema = z.infer<typeof loginSchema>;
export const registerSchema = z
.object({
username: z
.string()
.min(3, "Username must be at least 3 characters")
.max(20, "Username must be at most 20 characters"),
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
email: z.string().email("Invalid email address"),
dateOfBirth: z.string().min(1, "Date of birth is required"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Password must contain an uppercase letter")
.regex(/[a-z]/, "Password must contain a lowercase letter")
.regex(/[0-9]/, "Password must contain a number"),
confirmPassword: z.string().min(1, "Please confirm your password"),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords must match",
path: ["confirmPassword"],
});
export type RegisterSchema = z.infer<typeof registerSchema>;

88
src/Website/app/root.tsx Normal file
View File

@@ -0,0 +1,88 @@
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";
import type { Route } from "./+types/root";
import "./app.css";
import Navbar from "./components/Navbar";
import { getOptionalAuth } from "./lib/auth.server";
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..900;1,9..40,100..900&family=Volkhov:ital,wght@0,400;0,700;1,400;1,700&display=swap",
},
];
export const loader = async ({ request }: Route.LoaderArgs) => {
const auth = await getOptionalAuth(request);
return { auth };
};
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}
export default function App({ loaderData }: Route.ComponentProps) {
const { auth } = loaderData;
return (
<>
<Navbar auth={auth} />
<Outlet />
</>
);
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error";
details =
error.status === 404
? "The requested page could not be found."
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}
return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
);
}

13
src/Website/app/routes.ts Normal file
View File

@@ -0,0 +1,13 @@
import { type RouteConfig, index, route } from "@react-router/dev/routes";
export default [
index("routes/home.tsx"),
route("login", "routes/login.tsx"),
route("register", "routes/register.tsx"),
route("logout", "routes/logout.tsx"),
route("dashboard", "routes/dashboard.tsx"),
route("confirm", "routes/confirm.tsx"),
route("beers", "routes/beers.tsx"),
route("breweries", "routes/breweries.tsx"),
route("beer-styles", "routes/beer-styles.tsx"),
] satisfies RouteConfig;

View File

@@ -0,0 +1,18 @@
import type { Route } from "./+types/beer-styles";
export function meta({}: Route.MetaArgs) {
return [{ title: "Beer Styles | The Biergarten App" }];
}
export default function BeerStyles() {
return (
<div className="min-h-screen bg-base-200">
<div className="container mx-auto p-4">
<h1 className="text-4xl font-bold mb-4">Beer Styles</h1>
<p className="text-base-content/70">
Learn about different beer styles.
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import type { Route } from "./+types/beers";
export function meta({}: Route.MetaArgs) {
return [{ title: "Beers | The Biergarten App" }];
}
export default function Beers() {
return (
<div className="min-h-screen bg-base-200">
<div className="container mx-auto p-4">
<h1 className="text-4xl font-bold mb-4">Beers</h1>
<p className="text-base-content/70">Explore our collection of beers.</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,16 @@
import type { Route } from "./+types/breweries";
export function meta({}: Route.MetaArgs) {
return [{ title: "Breweries | The Biergarten App" }];
}
export default function Breweries() {
return (
<div className="min-h-screen bg-base-200">
<div className="container mx-auto p-4">
<h1 className="text-4xl font-bold mb-4">Breweries</h1>
<p className="text-base-content/70">Discover our partner breweries.</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,80 @@
import { Link } from "react-router";
import { confirmEmail, requireAuth } from "../lib/auth.server";
import type { Route } from "./+types/confirm";
export function meta({}: Route.MetaArgs) {
return [{ title: "Confirm Email | The Biergarten App" }];
}
export async function loader({ request }: Route.LoaderArgs) {
const auth = await requireAuth(request);
const url = new URL(request.url);
const token = url.searchParams.get("token");
if (!token) {
return { success: false as const, error: "Missing confirmation token." };
}
try {
const payload = await confirmEmail(token, auth.accessToken);
return {
success: true as const,
confirmedDate: payload.confirmedDate,
};
} catch (err) {
return {
success: false as const,
error: err instanceof Error ? err.message : "Confirmation failed.",
};
}
}
export default function Confirm({ loaderData }: Route.ComponentProps) {
return (
<div className="hero min-h-screen bg-base-200">
<div className="card w-full max-w-md bg-base-100 shadow-xl">
<div className="card-body items-center text-center gap-4">
{loaderData.success ? (
<>
<div className="text-success text-6xl"></div>
<h1 className="card-title text-2xl">Email Confirmed!</h1>
<p className="text-base-content/70">
Your email address has been successfully verified.
</p>
<div className="bg-base-200 rounded-box w-full p-3 text-sm text-left">
<span className="text-base-content/50 text-xs uppercase tracking-widest font-semibold">
Confirmed at
</span>
<p className="font-mono mt-1">
{new Date(loaderData.confirmedDate).toLocaleString()}
</p>
</div>
<div className="card-actions w-full pt-2">
<Link to="/dashboard" className="btn btn-primary w-full">
Go to Dashboard
</Link>
</div>
</>
) : (
<>
<div className="text-error text-6xl"></div>
<h1 className="card-title text-2xl">Confirmation Failed</h1>
<div role="alert" className="alert alert-error alert-soft w-full">
<span>{loaderData.error}</span>
</div>
<p className="text-base-content/70 text-sm">
The confirmation link may have expired (valid for 30 minutes) or
already been used.
</p>
<div className="card-actions w-full pt-2 flex-col gap-2">
<Link to="/dashboard" className="btn btn-primary w-full">
Back to Dashboard
</Link>
</div>
</>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,110 @@
import { requireAuth } from "../lib/auth.server";
import type { Route } from "./+types/dashboard";
export function meta({}: Route.MetaArgs) {
return [{ title: "Dashboard | The Biergarten App" }];
}
export async function loader({ request }: Route.LoaderArgs) {
const auth = await requireAuth(request);
return {
username: auth.username,
userAccountId: auth.userAccountId,
};
}
export default function Dashboard({ loaderData }: Route.ComponentProps) {
const { username, userAccountId } = loaderData;
return (
<div className="min-h-screen bg-base-200">
<div className="mx-auto max-w-4xl px-6 py-10 space-y-6">
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title text-2xl">Welcome, {username}!</h2>
<p className="text-base-content/70">
You are successfully authenticated. This is a protected page that
requires a valid session.
</p>
<div className="bg-base-200 rounded-box p-4 mt-2">
<p className="text-xs font-semibold uppercase tracking-widest text-base-content/50 mb-3">
Session Info
</p>
<div className="stats stats-vertical w-full">
<div className="stat py-2">
<div className="stat-title">Username</div>
<div className="stat-value text-lg font-mono">{username}</div>
</div>
<div className="stat py-2">
<div className="stat-title">User ID</div>
<div className="stat-desc font-mono text-xs mt-1">
{userAccountId}
</div>
</div>
</div>
</div>
</div>
</div>
<div className="card bg-base-100 shadow">
<div className="card-body">
<h2 className="card-title">Auth Flow Demo</h2>
<p className="text-sm text-base-content/70">
This demo showcases the following authentication features:
</p>
<ul className="list">
<li className="list-row">
<div>
<p className="font-semibold">Login</p>
<p className="text-sm text-base-content/60">
POST to <code className="kbd kbd-sm">/api/auth/login</code>{" "}
with username &amp; password
</p>
</div>
</li>
<li className="list-row">
<div>
<p className="font-semibold">Register</p>
<p className="text-sm text-base-content/60">
POST to{" "}
<code className="kbd kbd-sm">/api/auth/register</code> with
full user details
</p>
</div>
</li>
<li className="list-row">
<div>
<p className="font-semibold">Session</p>
<p className="text-sm text-base-content/60">
JWT access &amp; refresh tokens stored in an HTTP-only
cookie
</p>
</div>
</li>
<li className="list-row">
<div>
<p className="font-semibold">Protected Routes</p>
<p className="text-sm text-base-content/60">
This dashboard requires authentication via{" "}
<code className="kbd kbd-sm">requireAuth()</code>
</p>
</div>
</li>
<li className="list-row">
<div>
<p className="font-semibold">Token Refresh</p>
<p className="text-sm text-base-content/60">
POST to{" "}
<code className="kbd kbd-sm">/api/auth/refresh</code> with
refresh token
</p>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,56 @@
import { Link } from "react-router";
import { getOptionalAuth } from "../lib/auth.server";
import type { Route } from "./+types/home";
export function meta({}: Route.MetaArgs) {
return [
{ title: "The Biergarten App" },
{ name: "description", content: "Welcome to The Biergarten App" },
];
}
export async function loader({ request }: Route.LoaderArgs) {
const auth = await getOptionalAuth(request);
return { username: auth?.username ?? null };
}
export default function Home({ loaderData }: Route.ComponentProps) {
const { username } = loaderData;
return (
<div className="hero min-h-screen bg-base-200">
<div className="hero-content text-center">
<div className="max-w-md space-y-6">
<h1 className="text-5xl font-bold">🍺 The Biergarten App</h1>
<p className="text-lg text-base-content/70">Authentication Demo</p>
{username ? (
<>
<p className="text-base-content/80">
Welcome back,{" "}
<span className="font-semibold text-primary">{username}</span>!
</p>
<div className="flex gap-3 justify-center">
<Link to="/dashboard" className="btn btn-primary">
Dashboard
</Link>
<Link to="/logout" className="btn btn-ghost">
Logout
</Link>
</div>
</>
) : (
<div className="flex gap-3 justify-center">
<Link to="/login" className="btn btn-primary">
Login
</Link>
<Link to="/register" className="btn btn-outline">
Register
</Link>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,133 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Link, redirect, useNavigation, useSubmit } from "react-router";
import { createAuthSession, getOptionalAuth, login } from "../lib/auth.server";
import { loginSchema, type LoginSchema } from "../lib/schemas";
import type { Route } from "./+types/login";
export function meta({}: Route.MetaArgs) {
return [{ title: "Login | The Biergarten App" }];
}
export async function loader({ request }: Route.LoaderArgs) {
const auth = await getOptionalAuth(request);
if (auth) throw redirect("/dashboard");
return null;
}
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const result = loginSchema.safeParse({
username: formData.get("username"),
password: formData.get("password"),
});
if (!result.success) {
return { error: result.error.issues[0].message };
}
try {
const payload = await login(result.data.username, result.data.password);
return createAuthSession(payload, "/dashboard");
} catch (err) {
return { error: err instanceof Error ? err.message : "Login failed." };
}
}
export default function Login({ actionData }: Route.ComponentProps) {
const navigation = useNavigation();
const submit = useSubmit();
const isSubmitting = navigation.state === "submitting";
const {
register,
handleSubmit,
formState: { errors },
} = useForm<LoginSchema>({ resolver: zodResolver(loginSchema) });
const onSubmit = handleSubmit((data) => {
submit(data, { method: "post" });
});
return (
<div className="hero min-h-screen bg-base-200">
<div className="card w-full max-w-md bg-base-100 shadow-xl">
<div className="card-body gap-4">
<div className="text-center">
<h1 className="card-title text-3xl justify-center">Login</h1>
<p className="text-base-content/70">
Sign in to your Biergarten account
</p>
</div>
{actionData?.error && (
<div role="alert" className="alert alert-error alert-soft">
<span>{actionData.error}</span>
</div>
)}
<form onSubmit={onSubmit} className="space-y-3">
<fieldset className="fieldset">
<legend className="fieldset-legend">Username</legend>
<input
id="username"
type="text"
autoComplete="username"
placeholder="your_username"
className={`input w-full ${errors.username ? "input-error" : ""}`}
{...register("username")}
/>
{errors.username && (
<p className="label text-error">{errors.username.message}</p>
)}
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend">Password</legend>
<input
id="password"
type="password"
autoComplete="current-password"
placeholder="••••••••"
className={`input w-full ${errors.password ? "input-error" : ""}`}
{...register("password")}
/>
{errors.password && (
<p className="label text-error">{errors.password.message}</p>
)}
</fieldset>
<button
type="submit"
disabled={isSubmitting}
className="btn btn-primary w-full mt-2"
>
{isSubmitting ? (
<>
<span className="loading loading-spinner loading-sm" />{" "}
Signing in...
</>
) : (
"Sign In"
)}
</button>
</form>
<div className="divider text-xs">New here?</div>
<div className="text-center space-y-2">
<Link to="/register" className="btn btn-outline btn-sm w-full">
Create an account
</Link>
<Link
to="/"
className="link link-hover text-sm text-base-content/60"
>
Back to home
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,10 @@
import { redirect } from "react-router";
import { destroySession, getSession } from "../lib/auth.server";
import type { Route } from "./+types/logout";
export async function loader({ request }: Route.LoaderArgs) {
const session = await getSession(request);
return redirect("/", {
headers: { "Set-Cookie": await destroySession(session) },
});
}

View File

@@ -0,0 +1,231 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { Link, redirect, useNavigation, useSubmit } from "react-router";
import {
createAuthSession,
getOptionalAuth,
register,
} from "../lib/auth.server";
import { registerSchema, type RegisterSchema } from "../lib/schemas";
import type { Route } from "./+types/register";
export function meta({}: Route.MetaArgs) {
return [{ title: "Register | The Biergarten App" }];
}
export async function loader({ request }: Route.LoaderArgs) {
const auth = await getOptionalAuth(request);
if (auth) throw redirect("/dashboard");
return null;
}
export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const result = registerSchema.safeParse({
username: formData.get("username"),
firstName: formData.get("firstName"),
lastName: formData.get("lastName"),
email: formData.get("email"),
dateOfBirth: formData.get("dateOfBirth"),
password: formData.get("password"),
confirmPassword: formData.get("confirmPassword"),
});
if (!result.success) {
const fieldErrors = result.error.flatten().fieldErrors;
return { error: null, fieldErrors };
}
try {
const { confirmPassword: _, ...body } = result.data;
const payload = await register(body);
return createAuthSession(payload, "/dashboard");
} catch (err) {
return {
error: err instanceof Error ? err.message : "Registration failed.",
fieldErrors: null,
};
}
}
export default function Register({ actionData }: Route.ComponentProps) {
const navigation = useNavigation();
const submit = useSubmit();
const isSubmitting = navigation.state === "submitting";
const {
register: field,
handleSubmit,
formState: { errors },
} = useForm<RegisterSchema>({ resolver: zodResolver(registerSchema) });
const onSubmit = handleSubmit((data) => {
submit(data, { method: "post" });
});
return (
<div className="min-h-screen bg-base-200 flex items-center justify-center p-4">
<div className="card w-full max-w-lg bg-base-100 shadow-xl">
<div className="card-body gap-4">
<div className="text-center">
<h1 className="card-title text-3xl justify-center">Register</h1>
<p className="text-base-content/70">
Create your Biergarten account
</p>
</div>
{actionData?.error && (
<div role="alert" className="alert alert-error alert-soft">
<span>{actionData.error}</span>
</div>
)}
<form onSubmit={onSubmit} className="space-y-2">
<fieldset className="fieldset">
<legend className="fieldset-legend">Username</legend>
<input
id="username"
type="text"
autoComplete="username"
placeholder="your_username"
className={`input w-full ${errors.username ? "input-error" : ""}`}
{...field("username")}
/>
{errors.username ? (
<p className="label text-error">{errors.username.message}</p>
) : (
<p className="label">3-64 characters, alphanumeric and . _ -</p>
)}
</fieldset>
<div className="grid grid-cols-2 gap-3">
<fieldset className="fieldset">
<legend className="fieldset-legend">First Name</legend>
<input
id="firstName"
type="text"
autoComplete="given-name"
placeholder="Jane"
className={`input w-full ${errors.firstName ? "input-error" : ""}`}
{...field("firstName")}
/>
{errors.firstName && (
<p className="label text-error">{errors.firstName.message}</p>
)}
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend">Last Name</legend>
<input
id="lastName"
type="text"
autoComplete="family-name"
placeholder="Doe"
className={`input w-full ${errors.lastName ? "input-error" : ""}`}
{...field("lastName")}
/>
{errors.lastName && (
<p className="label text-error">{errors.lastName.message}</p>
)}
</fieldset>
</div>
<fieldset className="fieldset">
<legend className="fieldset-legend">Email</legend>
<input
id="email"
type="email"
autoComplete="email"
placeholder="jane@example.com"
className={`input w-full ${errors.email ? "input-error" : ""}`}
{...field("email")}
/>
{errors.email && (
<p className="label text-error">{errors.email.message}</p>
)}
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend">Date of Birth</legend>
<input
id="dateOfBirth"
type="date"
className={`input w-full ${errors.dateOfBirth ? "input-error" : ""}`}
{...field("dateOfBirth")}
/>
{errors.dateOfBirth ? (
<p className="label text-error">{errors.dateOfBirth.message}</p>
) : (
<p className="label">Must be 19 years or older</p>
)}
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend">Password</legend>
<input
id="password"
type="password"
autoComplete="new-password"
placeholder="••••••••"
className={`input w-full ${errors.password ? "input-error" : ""}`}
{...field("password")}
/>
{errors.password ? (
<p className="label text-error">{errors.password.message}</p>
) : (
<p className="label">
8+ chars: uppercase, lowercase, digit, special character
</p>
)}
</fieldset>
<fieldset className="fieldset">
<legend className="fieldset-legend">Confirm Password</legend>
<input
id="confirmPassword"
type="password"
autoComplete="new-password"
placeholder="••••••••"
className={`input w-full ${errors.confirmPassword ? "input-error" : ""}`}
{...field("confirmPassword")}
/>
{errors.confirmPassword && (
<p className="label text-error">
{errors.confirmPassword.message}
</p>
)}
</fieldset>
<button
type="submit"
disabled={isSubmitting}
className="btn btn-primary w-full mt-2"
>
{isSubmitting ? (
<>
<span className="loading loading-spinner loading-sm" />{" "}
Creating account...
</>
) : (
"Create Account"
)}
</button>
</form>
<div className="divider text-xs">Already have an account?</div>
<div className="text-center space-y-2">
<Link to="/login" className="btn btn-outline btn-sm w-full">
Sign in
</Link>
<Link
to="/"
className="link link-hover text-sm text-base-content/60"
>
Back to home
</Link>
</div>
</div>
</div>
</div>
);
}

4569
src/Website/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

36
src/Website/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "biergarten-website",
"type": "module",
"version": "0.0.0",
"scripts": {
"dev": "react-router dev",
"build": "react-router build",
"start": "NODE_ENV=production node ./build/server/index.js",
"typecheck": "tsc"
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@react-router/dev": "^7.13.1",
"@react-router/express": "^7.13.1",
"@react-router/node": "^7.13.1",
"isbot": "^5.1.36",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-hook-form": "^7.71.2",
"react-router": "7.13.1",
"zod": "^3.23.8"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.1",
"@types/express": "^5.0.6",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"autoprefixer": "^10.4.27",
"daisyui": "^5.5.19",
"postcss": "^8.5.8",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"vite": "^8.0.0"
}
}

View File

@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

View File

@@ -0,0 +1,5 @@
import type { Config } from "@react-router/dev/config";
export default {
ssr: true,
} satisfies Config;

View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./app/**/*.{ts,tsx}"],
theme: {
extend: {},
},
plugins: [require("daisyui")],
};

18
src/Website/tsconfig.json Normal file
View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"charset": "utf8",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"jsx": "react-jsx",
"lib": ["DOM", "DOM.Iterable", "ES2022"],
"moduleResolution": "bundler",
"module": "ESNext",
"noEmit": true,
"resolveJsonModule": true,
"target": "ES2022",
"skipLibCheck": true
},
"include": ["app"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,6 @@
import { reactRouter } from "@react-router/dev/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [reactRouter()],
});