mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +00:00
add 'biergarten-cassis' and 'biergarten-weizen' themes update CSS variables, and refine .gitignore
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -15,6 +15,11 @@
|
|||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
# project-specific build artifacts
|
||||||
|
/src/Website/build/
|
||||||
|
/src/Website/.react-router/
|
||||||
|
/test-results/
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
@@ -42,6 +47,9 @@ next-env.d.ts
|
|||||||
|
|
||||||
# vscode
|
# vscode
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
/cloudinary-images
|
/cloudinary-images
|
||||||
|
|
||||||
|
|||||||
4
src/Website/.prettierignore
Normal file
4
src/Website/.prettierignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
build
|
||||||
|
node_modules
|
||||||
|
.react-router
|
||||||
|
package-lock.json
|
||||||
10
src/Website/.prettierrc.json
Normal file
10
src/Website/.prettierrc.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/prettierrc",
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
@@ -14,6 +14,9 @@ type Pages = {
|
|||||||
"/": {
|
"/": {
|
||||||
params: {};
|
params: {};
|
||||||
};
|
};
|
||||||
|
"/theme": {
|
||||||
|
params: {};
|
||||||
|
};
|
||||||
"/login": {
|
"/login": {
|
||||||
params: {};
|
params: {};
|
||||||
};
|
};
|
||||||
@@ -43,12 +46,16 @@ type Pages = {
|
|||||||
type RouteFiles = {
|
type RouteFiles = {
|
||||||
"root.tsx": {
|
"root.tsx": {
|
||||||
id: "root";
|
id: "root";
|
||||||
page: "/" | "/login" | "/register" | "/logout" | "/dashboard" | "/confirm" | "/beers" | "/breweries" | "/beer-styles";
|
page: "/" | "/theme" | "/login" | "/register" | "/logout" | "/dashboard" | "/confirm" | "/beers" | "/breweries" | "/beer-styles";
|
||||||
};
|
};
|
||||||
"routes/home.tsx": {
|
"routes/home.tsx": {
|
||||||
id: "routes/home";
|
id: "routes/home";
|
||||||
page: "/";
|
page: "/";
|
||||||
};
|
};
|
||||||
|
"routes/theme.tsx": {
|
||||||
|
id: "routes/theme";
|
||||||
|
page: "/theme";
|
||||||
|
};
|
||||||
"routes/login.tsx": {
|
"routes/login.tsx": {
|
||||||
id: "routes/login";
|
id: "routes/login";
|
||||||
page: "/login";
|
page: "/login";
|
||||||
@@ -86,6 +93,7 @@ type RouteFiles = {
|
|||||||
type RouteModules = {
|
type RouteModules = {
|
||||||
"root": typeof import("./app/root.tsx");
|
"root": typeof import("./app/root.tsx");
|
||||||
"routes/home": typeof import("./app/routes/home.tsx");
|
"routes/home": typeof import("./app/routes/home.tsx");
|
||||||
|
"routes/theme": typeof import("./app/routes/theme.tsx");
|
||||||
"routes/login": typeof import("./app/routes/login.tsx");
|
"routes/login": typeof import("./app/routes/login.tsx");
|
||||||
"routes/register": typeof import("./app/routes/register.tsx");
|
"routes/register": typeof import("./app/routes/register.tsx");
|
||||||
"routes/logout": typeof import("./app/routes/logout.tsx");
|
"routes/logout": typeof import("./app/routes/logout.tsx");
|
||||||
|
|||||||
62
src/Website/.react-router/types/app/routes/+types/theme.ts
Normal file
62
src/Website/.react-router/types/app/routes/+types/theme.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Generated by React Router
|
||||||
|
|
||||||
|
import type { GetInfo, GetAnnotations } from "react-router/internal";
|
||||||
|
|
||||||
|
type Module = typeof import("../theme.js")
|
||||||
|
|
||||||
|
type Info = GetInfo<{
|
||||||
|
file: "routes/theme.tsx",
|
||||||
|
module: Module
|
||||||
|
}>
|
||||||
|
|
||||||
|
type Matches = [{
|
||||||
|
id: "root";
|
||||||
|
module: typeof import("../../root.js");
|
||||||
|
}, {
|
||||||
|
id: "routes/theme";
|
||||||
|
module: typeof import("../theme.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"];
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"hash": "fdf55a9c",
|
|
||||||
"configHash": "c6a852b3",
|
|
||||||
"lockfileHash": "e3b0c442",
|
|
||||||
"browserHash": "58d74d30",
|
|
||||||
"optimized": {},
|
|
||||||
"chunks": {}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "module"
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
themes: biergarten-lager, biergarten-stout;
|
themes: biergarten-lager, biergarten-stout, biergarten-cassis, biergarten-weizen;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans:
|
--font-sans:
|
||||||
"DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
"DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
"Segoe UI Symbol", "Noto Color Emoji";
|
||||||
--font-serif: "Volkhov", ui-serif, Georgia, serif;
|
--font-serif: "Volkhov", ui-serif, Georgia, serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,35 +25,35 @@ h6,
|
|||||||
default: true;
|
default: true;
|
||||||
prefersdark: false;
|
prefersdark: false;
|
||||||
color-scheme: "light";
|
color-scheme: "light";
|
||||||
/* Base — warm parchment / aged paper */
|
/* Base — muted parchment / brushed paper */
|
||||||
--color-base-100: oklch(97% 0.025 80);
|
--color-base-100: oklch(96% 0.012 82);
|
||||||
--color-base-200: oklch(92% 0.04 78);
|
--color-base-200: oklch(92% 0.018 80);
|
||||||
--color-base-300: oklch(86% 0.06 75);
|
--color-base-300: oklch(87% 0.025 78);
|
||||||
--color-base-content: oklch(28% 0.05 55);
|
--color-base-content: oklch(30% 0.025 58);
|
||||||
/* Primary — golden amber lager */
|
/* Primary — mellow amber */
|
||||||
--color-primary: oklch(68% 0.165 60);
|
--color-primary: oklch(65% 0.085 62);
|
||||||
--color-primary-content: oklch(18% 0.04 55);
|
--color-primary-content: oklch(20% 0.02 58);
|
||||||
/* Secondary — deep mahogany ale */
|
/* Secondary — softened mahogany */
|
||||||
--color-secondary: oklch(38% 0.09 40);
|
--color-secondary: oklch(42% 0.05 42);
|
||||||
--color-secondary-content: oklch(95% 0.02 75);
|
--color-secondary-content: oklch(96% 0.01 76);
|
||||||
/* Accent — frothy cream head */
|
/* Accent — frothy cream */
|
||||||
--color-accent: oklch(94% 0.03 90);
|
--color-accent: oklch(93% 0.015 90);
|
||||||
--color-accent-content: oklch(30% 0.05 55);
|
--color-accent-content: oklch(32% 0.02 58);
|
||||||
/* Neutral — roasted stout */
|
/* Neutral — warm roast */
|
||||||
--color-neutral: oklch(24% 0.04 45);
|
--color-neutral: oklch(28% 0.02 46);
|
||||||
--color-neutral-content: oklch(92% 0.025 80);
|
--color-neutral-content: oklch(92% 0.012 80);
|
||||||
/* Info — cool hop green */
|
/* Info — muted hop green */
|
||||||
--color-info: oklch(58% 0.14 145);
|
--color-info: oklch(46% 0.065 145);
|
||||||
--color-info-content: oklch(97% 0.015 145);
|
--color-info-content: oklch(97% 0.008 145);
|
||||||
/* Success — fresh barley */
|
/* Success — soft barley */
|
||||||
--color-success: oklch(72% 0.13 120);
|
--color-success: oklch(70% 0.06 122);
|
||||||
--color-success-content: oklch(20% 0.04 120);
|
--color-success-content: oklch(22% 0.02 122);
|
||||||
/* Warning — amber harvest */
|
/* Warning — toned amber */
|
||||||
--color-warning: oklch(74% 0.19 55);
|
--color-warning: oklch(72% 0.09 56);
|
||||||
--color-warning-content: oklch(18% 0.04 55);
|
--color-warning-content: oklch(20% 0.02 56);
|
||||||
/* Error — deep cherry kriek */
|
/* Error — restrained cherry */
|
||||||
--color-error: oklch(52% 0.2 20);
|
--color-error: oklch(54% 0.09 22);
|
||||||
--color-error-content: oklch(97% 0.012 15);
|
--color-error-content: oklch(97% 0.006 15);
|
||||||
--radius-selector: 0.375rem;
|
--radius-selector: 0.375rem;
|
||||||
--radius-field: 0.5rem;
|
--radius-field: 0.5rem;
|
||||||
--radius-box: 0.875rem;
|
--radius-box: 0.875rem;
|
||||||
@@ -80,7 +80,7 @@ h6,
|
|||||||
--color-primary-content: oklch(14% 0.012 50);
|
--color-primary-content: oklch(14% 0.012 50);
|
||||||
|
|
||||||
/* Secondary — deep mahogany ale */
|
/* Secondary — deep mahogany ale */
|
||||||
--color-secondary: oklch(55% 0.025 40);
|
--color-secondary: oklch(51% 0.025 40);
|
||||||
--color-secondary-content: oklch(97% 0.005 75);
|
--color-secondary-content: oklch(97% 0.005 75);
|
||||||
|
|
||||||
/* Accent — frothy cream head */
|
/* Accent — frothy cream head */
|
||||||
@@ -116,3 +116,111 @@ h6,
|
|||||||
--depth: 1;
|
--depth: 1;
|
||||||
--noise: 1;
|
--noise: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "biergarten-cassis";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "dark";
|
||||||
|
|
||||||
|
/* Base — blackberry-stained barrel, dark purple-black */
|
||||||
|
--color-base-100: oklch(13% 0.022 295);
|
||||||
|
--color-base-200: oklch(17% 0.028 292);
|
||||||
|
--color-base-300: oklch(22% 0.032 290);
|
||||||
|
--color-base-content: oklch(90% 0.014 300);
|
||||||
|
|
||||||
|
/* Primary — cassis berry purple */
|
||||||
|
--color-primary: oklch(52% 0.15 295);
|
||||||
|
--color-primary-content: oklch(97% 0.008 295);
|
||||||
|
|
||||||
|
/* Secondary — sour cherry */
|
||||||
|
--color-secondary: oklch(46% 0.11 10);
|
||||||
|
--color-secondary-content: oklch(97% 0.006 10);
|
||||||
|
|
||||||
|
/* Accent — tart lime zest */
|
||||||
|
--color-accent: oklch(75% 0.1 130);
|
||||||
|
--color-accent-content: oklch(18% 0.04 130);
|
||||||
|
|
||||||
|
/* Neutral — deep blackened grape */
|
||||||
|
--color-neutral: oklch(18% 0.016 290);
|
||||||
|
--color-neutral-content: oklch(88% 0.01 295);
|
||||||
|
|
||||||
|
/* Info — muted indigo */
|
||||||
|
--color-info: oklch(46% 0.065 250);
|
||||||
|
--color-info-content: oklch(97% 0.006 250);
|
||||||
|
|
||||||
|
/* Success — dark elderberry green */
|
||||||
|
--color-success: oklch(50% 0.065 145);
|
||||||
|
--color-success-content: oklch(97% 0.006 145);
|
||||||
|
|
||||||
|
/* Warning — sour apricot */
|
||||||
|
--color-warning: oklch(70% 0.1 65);
|
||||||
|
--color-warning-content: oklch(18% 0.03 65);
|
||||||
|
|
||||||
|
/* Error — kriek red */
|
||||||
|
--color-error: oklch(50% 0.13 22);
|
||||||
|
--color-error-content: oklch(97% 0.006 22);
|
||||||
|
|
||||||
|
--radius-selector: 0.5rem;
|
||||||
|
--radius-field: 0.5rem;
|
||||||
|
--radius-box: 1rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 1;
|
||||||
|
--noise: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@plugin "daisyui/theme" {
|
||||||
|
name: "biergarten-weizen";
|
||||||
|
default: false;
|
||||||
|
prefersdark: false;
|
||||||
|
color-scheme: "light";
|
||||||
|
|
||||||
|
/* Base — near-white with the faintest young-barley green breath */
|
||||||
|
--color-base-100: oklch(99% 0.007 112);
|
||||||
|
--color-base-200: oklch(96% 0.012 114);
|
||||||
|
--color-base-300: oklch(92% 0.019 116);
|
||||||
|
--color-base-content: oklch(20% 0.022 122);
|
||||||
|
|
||||||
|
/* Primary — fresh-cut barley, green-gold */
|
||||||
|
--color-primary: oklch(70% 0.09 118);
|
||||||
|
--color-primary-content: oklch(16% 0.022 118);
|
||||||
|
|
||||||
|
/* Secondary — muted sage stem */
|
||||||
|
--color-secondary: oklch(44% 0.055 128);
|
||||||
|
--color-secondary-content: oklch(97% 0.005 128);
|
||||||
|
|
||||||
|
/* Accent — pale morning dew */
|
||||||
|
--color-accent: oklch(93% 0.03 148);
|
||||||
|
--color-accent-content: oklch(22% 0.022 148);
|
||||||
|
|
||||||
|
/* Neutral — dried straw with green memory */
|
||||||
|
--color-neutral: oklch(88% 0.014 116);
|
||||||
|
--color-neutral-content: oklch(20% 0.02 118);
|
||||||
|
|
||||||
|
/* Info — clear summer sky */
|
||||||
|
--color-info: oklch(46% 0.07 232);
|
||||||
|
--color-info-content: oklch(98% 0.005 232);
|
||||||
|
|
||||||
|
/* Success — vivid young shoot */
|
||||||
|
--color-success: oklch(48% 0.09 145);
|
||||||
|
--color-success-content: oklch(98% 0.005 145);
|
||||||
|
|
||||||
|
/* Warning — ripening grain amber */
|
||||||
|
--color-warning: oklch(68% 0.1 76);
|
||||||
|
--color-warning-content: oklch(18% 0.02 72);
|
||||||
|
|
||||||
|
/* Error — washed dusty rose */
|
||||||
|
--color-error: oklch(52% 0.1 18);
|
||||||
|
--color-error-content: oklch(98% 0.005 15);
|
||||||
|
|
||||||
|
--radius-selector: 2rem;
|
||||||
|
--radius-field: 2rem;
|
||||||
|
--radius-box: 1rem;
|
||||||
|
--size-selector: 0.25rem;
|
||||||
|
--size-field: 0.25rem;
|
||||||
|
--border: 1px;
|
||||||
|
--depth: 0;
|
||||||
|
--noise: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import {
|
||||||
|
Disclosure,
|
||||||
|
DisclosureButton,
|
||||||
|
DisclosurePanel,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuItems,
|
||||||
|
} from "@headlessui/react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
@@ -10,43 +19,95 @@ interface NavbarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Navbar({ auth }: NavbarProps) {
|
export default function Navbar({ auth }: NavbarProps) {
|
||||||
|
const navLinks = [
|
||||||
|
{ to: "/theme", label: "Theme" },
|
||||||
|
{ to: "/beers", label: "Beers" },
|
||||||
|
{ to: "/breweries", label: "Breweries" },
|
||||||
|
{ to: "/beer-styles", label: "Beer Styles" },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50">
|
<Disclosure
|
||||||
<div className="navbar-start">
|
as="nav"
|
||||||
<Link to="/" className="text-xl font-bold px-4">
|
className="sticky top-0 z-50 border-b border-base-300 bg-base-100 shadow-md"
|
||||||
|
>
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div className="navbar mx-auto max-w-7xl px-2 sm:px-4">
|
||||||
|
<div className="navbar-start gap-2">
|
||||||
|
<DisclosureButton
|
||||||
|
className="btn btn-ghost btn-square lg:hidden"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="h-5 w-5 stroke-current"
|
||||||
|
>
|
||||||
|
{open ? (
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
d="M4 6h16M4 12h16M4 18h16"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</DisclosureButton>
|
||||||
|
|
||||||
|
<Link to="/" className="text-xl font-bold">
|
||||||
🍺 The Biergarten App
|
🍺 The Biergarten App
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="navbar-center gap-4">
|
<div className="navbar-center hidden lg:flex gap-2">
|
||||||
<Link to="/beers" className="btn btn-ghost btn-sm">
|
{navLinks.map((link) => (
|
||||||
Beers
|
<Link key={link.to} to={link.to} className="btn btn-ghost btn-sm">
|
||||||
</Link>
|
{link.label}
|
||||||
<Link to="/breweries" className="btn btn-ghost btn-sm">
|
|
||||||
Breweries
|
|
||||||
</Link>
|
|
||||||
<Link to="/beer-styles" className="btn btn-ghost btn-sm">
|
|
||||||
Beer Styles
|
|
||||||
</Link>
|
</Link>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="navbar-end gap-2 pr-4">
|
<div className="navbar-end gap-2">
|
||||||
<Link to="/register" className="btn btn-ghost btn-sm">
|
{!auth && (
|
||||||
|
<Link to="/register" className="btn btn-ghost btn-sm hidden sm:inline-flex">
|
||||||
Register User
|
Register User
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
{auth ? (
|
{auth ? (
|
||||||
<>
|
<>
|
||||||
<Link to="/dashboard" className="btn btn-primary btn-sm">
|
<Link to="/dashboard" className="btn btn-primary btn-sm">
|
||||||
Dashboard
|
Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
<div className="divider divider-horizontal m-0 h-6"></div>
|
|
||||||
<span className="text-sm text-base-content/70">
|
<Menu as="div" className="relative">
|
||||||
{auth.username}
|
<MenuButton className="btn btn-ghost btn-sm">{auth.username}</MenuButton>
|
||||||
</span>
|
<MenuItems className="menu absolute right-0 z-60 mt-2 w-52 rounded-box border border-base-300 bg-base-100 p-2 shadow-xl focus:outline-none">
|
||||||
<Link to="/logout" className="btn btn-ghost btn-sm">
|
<MenuItem>
|
||||||
|
{({ focus }) => (
|
||||||
|
<Link to="/dashboard" className={focus ? "active" : ""}>
|
||||||
|
Dashboard
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
{({ focus }) => (
|
||||||
|
<Link to="/logout" className={focus ? "active" : ""}>
|
||||||
Logout
|
Logout
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItems>
|
||||||
|
</Menu>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Link to="/login" className="btn btn-primary btn-sm">
|
<Link to="/login" className="btn btn-primary btn-sm">
|
||||||
@@ -55,5 +116,23 @@ export default function Navbar({ auth }: NavbarProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DisclosurePanel className="border-t border-base-300 bg-base-100 px-4 py-3 lg:hidden">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{navLinks.map((link) => (
|
||||||
|
<Link key={link.to} to={link.to} className="btn btn-ghost btn-sm justify-start">
|
||||||
|
{link.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
{!auth && (
|
||||||
|
<Link to="/register" className="btn btn-ghost btn-sm justify-start">
|
||||||
|
Register User
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</DisclosurePanel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Disclosure>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
40
src/Website/app/components/forms/FormField.tsx
Normal file
40
src/Website/app/components/forms/FormField.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Description, Field, Label } from "@headlessui/react";
|
||||||
|
|
||||||
|
type FormFieldProps = React.InputHTMLAttributes<HTMLInputElement> & {
|
||||||
|
label: string;
|
||||||
|
error?: string;
|
||||||
|
hint?: string;
|
||||||
|
labelClassName?: string;
|
||||||
|
inputClassName?: string;
|
||||||
|
hintClassName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function FormField({
|
||||||
|
label,
|
||||||
|
error,
|
||||||
|
hint,
|
||||||
|
className,
|
||||||
|
labelClassName,
|
||||||
|
inputClassName,
|
||||||
|
hintClassName,
|
||||||
|
...inputProps
|
||||||
|
}: FormFieldProps) {
|
||||||
|
return (
|
||||||
|
<Field className={className ?? "space-y-1"}>
|
||||||
|
<Label htmlFor={inputProps.id} className={labelClassName ?? "label font-medium"}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
{...inputProps}
|
||||||
|
className={inputClassName ?? `input w-full ${error ? "input-error" : ""}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<Description className={hintClassName ?? "label text-error"}>{error}</Description>
|
||||||
|
) : hint ? (
|
||||||
|
<Description className={hintClassName ?? "label"}>{hint}</Description>
|
||||||
|
) : null}
|
||||||
|
</Field>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/Website/app/components/forms/SubmitButton.tsx
Normal file
31
src/Website/app/components/forms/SubmitButton.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Button } from "@headlessui/react";
|
||||||
|
|
||||||
|
interface SubmitButtonProps {
|
||||||
|
isSubmitting: boolean;
|
||||||
|
idleText: string;
|
||||||
|
submittingText: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SubmitButton({
|
||||||
|
isSubmitting,
|
||||||
|
idleText,
|
||||||
|
submittingText,
|
||||||
|
className,
|
||||||
|
}: SubmitButtonProps) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
className={className ?? "btn btn-primary w-full mt-2"}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<span className="loading loading-spinner loading-sm" /> {submittingText}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
idleText
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { type RouteConfig, index, route } from "@react-router/dev/routes";
|
|||||||
|
|
||||||
export default [
|
export default [
|
||||||
index("routes/home.tsx"),
|
index("routes/home.tsx"),
|
||||||
|
route("theme", "routes/theme.tsx"),
|
||||||
route("login", "routes/login.tsx"),
|
route("login", "routes/login.tsx"),
|
||||||
route("register", "routes/register.tsx"),
|
route("register", "routes/register.tsx"),
|
||||||
route("logout", "routes/logout.tsx"),
|
route("logout", "routes/logout.tsx"),
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { HomeSimpleDoor, LogIn, UserPlus } from "iconoir-react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
||||||
|
import FormField from "../components/forms/FormField";
|
||||||
|
import SubmitButton from "../components/forms/SubmitButton";
|
||||||
import { createAuthSession, getOptionalAuth, login } from "../lib/auth.server";
|
import { createAuthSession, getOptionalAuth, login } from "../lib/auth.server";
|
||||||
import { loginSchema, type LoginSchema } from "../lib/schemas";
|
import { loginSchema, type LoginSchema } from "../lib/schemas";
|
||||||
import type { Route } from "./+types/login";
|
import type { Route } from "./+types/login";
|
||||||
@@ -54,10 +57,11 @@ export default function Login({ actionData }: Route.ComponentProps) {
|
|||||||
<div className="card w-full max-w-md bg-base-100 shadow-xl">
|
<div className="card w-full max-w-md bg-base-100 shadow-xl">
|
||||||
<div className="card-body gap-4">
|
<div className="card-body gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="card-title text-3xl justify-center">Login</h1>
|
<h1 className="card-title text-3xl justify-center gap-2">
|
||||||
<p className="text-base-content/70">
|
<LogIn className="size-7" aria-hidden="true" />
|
||||||
Sign in to your Biergarten account
|
Login
|
||||||
</p>
|
</h1>
|
||||||
|
<p className="text-base-content/70">Sign in to your Biergarten account</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{actionData?.error && (
|
{actionData?.error && (
|
||||||
@@ -67,63 +71,46 @@ export default function Login({ actionData }: Route.ComponentProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={onSubmit} className="space-y-3">
|
<form onSubmit={onSubmit} className="space-y-3">
|
||||||
<fieldset className="fieldset">
|
<FormField
|
||||||
<legend className="fieldset-legend">Username</legend>
|
|
||||||
<input
|
|
||||||
id="username"
|
id="username"
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
placeholder="your_username"
|
placeholder="your_username"
|
||||||
className={`input w-full ${errors.username ? "input-error" : ""}`}
|
label="Username"
|
||||||
|
error={errors.username?.message}
|
||||||
{...register("username")}
|
{...register("username")}
|
||||||
/>
|
/>
|
||||||
{errors.username && (
|
|
||||||
<p className="label text-error">{errors.username.message}</p>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset className="fieldset">
|
<FormField
|
||||||
<legend className="fieldset-legend">Password</legend>
|
|
||||||
<input
|
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
className={`input w-full ${errors.password ? "input-error" : ""}`}
|
label="Password"
|
||||||
|
error={errors.password?.message}
|
||||||
{...register("password")}
|
{...register("password")}
|
||||||
/>
|
/>
|
||||||
{errors.password && (
|
|
||||||
<p className="label text-error">{errors.password.message}</p>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<button
|
<SubmitButton
|
||||||
type="submit"
|
isSubmitting={isSubmitting}
|
||||||
disabled={isSubmitting}
|
idleText="Sign In"
|
||||||
className="btn btn-primary w-full mt-2"
|
submittingText="Signing in..."
|
||||||
>
|
/>
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<span className="loading loading-spinner loading-sm" />{" "}
|
|
||||||
Signing in...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Sign In"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="divider text-xs">New here?</div>
|
<div className="divider text-xs">New here?</div>
|
||||||
|
|
||||||
<div className="text-center space-y-2">
|
<div className="text-center space-y-2">
|
||||||
<Link to="/register" className="btn btn-outline btn-sm w-full">
|
<Link to="/register" className="btn btn-outline btn-sm w-full gap-2">
|
||||||
|
<UserPlus className="size-4" aria-hidden="true" />
|
||||||
Create an account
|
Create an account
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="link link-hover text-sm text-base-content/60"
|
className="link link-hover text-sm text-base-content/60 inline-flex items-center gap-1"
|
||||||
>
|
>
|
||||||
← Back to home
|
<HomeSimpleDoor className="size-4" aria-hidden="true" />
|
||||||
|
Back to home
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
||||||
import {
|
import FormField from "../components/forms/FormField";
|
||||||
createAuthSession,
|
import SubmitButton from "../components/forms/SubmitButton";
|
||||||
getOptionalAuth,
|
import { createAuthSession, getOptionalAuth, register } from "../lib/auth.server";
|
||||||
register,
|
|
||||||
} from "../lib/auth.server";
|
|
||||||
import { registerSchema, type RegisterSchema } from "../lib/schemas";
|
import { registerSchema, type RegisterSchema } from "../lib/schemas";
|
||||||
import type { Route } from "./+types/register";
|
import type { Route } from "./+types/register";
|
||||||
|
|
||||||
@@ -37,7 +35,14 @@ export async function action({ request }: Route.ActionArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { confirmPassword: _, ...body } = result.data;
|
const body = {
|
||||||
|
username: result.data.username,
|
||||||
|
firstName: result.data.firstName,
|
||||||
|
lastName: result.data.lastName,
|
||||||
|
email: result.data.email,
|
||||||
|
dateOfBirth: result.data.dateOfBirth,
|
||||||
|
password: result.data.password,
|
||||||
|
};
|
||||||
const payload = await register(body);
|
const payload = await register(body);
|
||||||
return createAuthSession(payload, "/dashboard");
|
return createAuthSession(payload, "/dashboard");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -69,9 +74,7 @@ export default function Register({ actionData }: Route.ComponentProps) {
|
|||||||
<div className="card-body gap-4">
|
<div className="card-body gap-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1 className="card-title text-3xl justify-center">Register</h1>
|
<h1 className="card-title text-3xl justify-center">Register</h1>
|
||||||
<p className="text-base-content/70">
|
<p className="text-base-content/70">Create your Biergarten account</p>
|
||||||
Create your Biergarten account
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{actionData?.error && (
|
{actionData?.error && (
|
||||||
@@ -80,135 +83,85 @@ export default function Register({ actionData }: Route.ComponentProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={onSubmit} className="space-y-2">
|
<form onSubmit={onSubmit} className="space-y-3">
|
||||||
<fieldset className="fieldset">
|
<FormField
|
||||||
<legend className="fieldset-legend">Username</legend>
|
|
||||||
<input
|
|
||||||
id="username"
|
id="username"
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
placeholder="your_username"
|
placeholder="your_username"
|
||||||
className={`input w-full ${errors.username ? "input-error" : ""}`}
|
label="Username"
|
||||||
|
hint="3-64 characters, alphanumeric and . _ -"
|
||||||
|
error={errors.username?.message}
|
||||||
{...field("username")}
|
{...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">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<fieldset className="fieldset">
|
<FormField
|
||||||
<legend className="fieldset-legend">First Name</legend>
|
|
||||||
<input
|
|
||||||
id="firstName"
|
id="firstName"
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="given-name"
|
autoComplete="given-name"
|
||||||
placeholder="Jane"
|
placeholder="Jane"
|
||||||
className={`input w-full ${errors.firstName ? "input-error" : ""}`}
|
label="First Name"
|
||||||
|
error={errors.firstName?.message}
|
||||||
{...field("firstName")}
|
{...field("firstName")}
|
||||||
/>
|
/>
|
||||||
{errors.firstName && (
|
|
||||||
<p className="label text-error">{errors.firstName.message}</p>
|
<FormField
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
<fieldset className="fieldset">
|
|
||||||
<legend className="fieldset-legend">Last Name</legend>
|
|
||||||
<input
|
|
||||||
id="lastName"
|
id="lastName"
|
||||||
type="text"
|
type="text"
|
||||||
autoComplete="family-name"
|
autoComplete="family-name"
|
||||||
placeholder="Doe"
|
placeholder="Doe"
|
||||||
className={`input w-full ${errors.lastName ? "input-error" : ""}`}
|
label="Last Name"
|
||||||
|
error={errors.lastName?.message}
|
||||||
{...field("lastName")}
|
{...field("lastName")}
|
||||||
/>
|
/>
|
||||||
{errors.lastName && (
|
|
||||||
<p className="label text-error">{errors.lastName.message}</p>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<fieldset className="fieldset">
|
<FormField
|
||||||
<legend className="fieldset-legend">Email</legend>
|
|
||||||
<input
|
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
placeholder="jane@example.com"
|
placeholder="jane@example.com"
|
||||||
className={`input w-full ${errors.email ? "input-error" : ""}`}
|
label="Email"
|
||||||
|
error={errors.email?.message}
|
||||||
{...field("email")}
|
{...field("email")}
|
||||||
/>
|
/>
|
||||||
{errors.email && (
|
|
||||||
<p className="label text-error">{errors.email.message}</p>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<fieldset className="fieldset">
|
<FormField
|
||||||
<legend className="fieldset-legend">Date of Birth</legend>
|
|
||||||
<input
|
|
||||||
id="dateOfBirth"
|
id="dateOfBirth"
|
||||||
type="date"
|
type="date"
|
||||||
className={`input w-full ${errors.dateOfBirth ? "input-error" : ""}`}
|
label="Date of Birth"
|
||||||
|
hint="Must be 19 years or older"
|
||||||
|
error={errors.dateOfBirth?.message}
|
||||||
{...field("dateOfBirth")}
|
{...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">
|
<FormField
|
||||||
<legend className="fieldset-legend">Password</legend>
|
|
||||||
<input
|
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
className={`input w-full ${errors.password ? "input-error" : ""}`}
|
label="Password"
|
||||||
|
hint="8+ chars: uppercase, lowercase, digit, special character"
|
||||||
|
error={errors.password?.message}
|
||||||
{...field("password")}
|
{...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">
|
<FormField
|
||||||
<legend className="fieldset-legend">Confirm Password</legend>
|
|
||||||
<input
|
|
||||||
id="confirmPassword"
|
id="confirmPassword"
|
||||||
type="password"
|
type="password"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
className={`input w-full ${errors.confirmPassword ? "input-error" : ""}`}
|
label="Confirm Password"
|
||||||
|
error={errors.confirmPassword?.message}
|
||||||
{...field("confirmPassword")}
|
{...field("confirmPassword")}
|
||||||
/>
|
/>
|
||||||
{errors.confirmPassword && (
|
|
||||||
<p className="label text-error">
|
|
||||||
{errors.confirmPassword.message}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
<button
|
<SubmitButton
|
||||||
type="submit"
|
isSubmitting={isSubmitting}
|
||||||
disabled={isSubmitting}
|
idleText="Create Account"
|
||||||
className="btn btn-primary w-full mt-2"
|
submittingText="Creating account..."
|
||||||
>
|
/>
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<span className="loading loading-spinner loading-sm" />{" "}
|
|
||||||
Creating account...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
"Create Account"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="divider text-xs">Already have an account?</div>
|
<div className="divider text-xs">Already have an account?</div>
|
||||||
@@ -217,10 +170,7 @@ export default function Register({ actionData }: Route.ComponentProps) {
|
|||||||
<Link to="/login" className="btn btn-outline btn-sm w-full">
|
<Link to="/login" className="btn btn-outline btn-sm w-full">
|
||||||
Sign in
|
Sign in
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link to="/" className="link link-hover text-sm text-base-content/60">
|
||||||
to="/"
|
|
||||||
className="link link-hover text-sm text-base-content/60"
|
|
||||||
>
|
|
||||||
← Back to home
|
← Back to home
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
186
src/Website/app/routes/theme.tsx
Normal file
186
src/Website/app/routes/theme.tsx
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import type { Route } from "./+types/theme";
|
||||||
|
|
||||||
|
interface ThemeOption {
|
||||||
|
value: "biergarten-lager" | "biergarten-stout" | "biergarten-cassis" | "biergarten-weizen";
|
||||||
|
label: string;
|
||||||
|
vibe: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeOptions: ThemeOption[] = [
|
||||||
|
{
|
||||||
|
value: "biergarten-lager",
|
||||||
|
label: "Biergarten Lager",
|
||||||
|
vibe: "Warm parchment, golden amber, daytime beer garden",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "biergarten-stout",
|
||||||
|
label: "Biergarten Stout",
|
||||||
|
vibe: "Charred barrel, deep roast, cozy evening cellar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "biergarten-cassis",
|
||||||
|
label: "Biergarten Cassis",
|
||||||
|
vibe: "Blackberry barrel, sour kriek, dark berry night",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "biergarten-weizen",
|
||||||
|
label: "Biergarten Weizen",
|
||||||
|
vibe: "Hazy straw wheat, banana-clove, sunny afternoon",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const storageKey = "biergarten-theme";
|
||||||
|
|
||||||
|
export function meta({}: Route.MetaArgs) {
|
||||||
|
return [
|
||||||
|
{ title: "Theme | The Biergarten App" },
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
content: "Theme guide and switcher for The Biergarten App",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidTheme(value: string | null): value is ThemeOption["value"] {
|
||||||
|
return themeOptions.some((theme) => theme.value === value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(theme: ThemeOption["value"]) {
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
|
localStorage.setItem(storageKey, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThemePage() {
|
||||||
|
const [selectedTheme, setSelectedTheme] = useState<ThemeOption["value"]>(() => {
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return "biergarten-lager";
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedTheme = localStorage.getItem(storageKey);
|
||||||
|
return isValidTheme(savedTheme) ? savedTheme : "biergarten-lager";
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyTheme(selectedTheme);
|
||||||
|
}, [selectedTheme]);
|
||||||
|
|
||||||
|
const activeTheme =
|
||||||
|
themeOptions.find((theme) => theme.value === selectedTheme) ?? themeOptions[0];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-base-200 px-4 py-8 sm:px-6 lg:px-8">
|
||||||
|
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6">
|
||||||
|
<section className="card border border-base-300 bg-base-100 shadow-xl">
|
||||||
|
<div className="card-body gap-4">
|
||||||
|
<h1 className="card-title text-3xl sm:text-4xl">Theme Guide</h1>
|
||||||
|
<p className="text-base-content/70">
|
||||||
|
Four themes, four moods — from the sun-bleached clarity of a Weizen afternoon to the
|
||||||
|
deep berry dark of a Cassis barrel. Every theme shares the same semantic token
|
||||||
|
structure so components stay consistent while the atmosphere shifts completely.
|
||||||
|
</p>
|
||||||
|
<div className="alert alert-info alert-soft">
|
||||||
|
<span>
|
||||||
|
Active theme: <strong>{activeTheme.label}</strong> — {activeTheme.vibe}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="card border border-base-300 bg-base-100 shadow-xl">
|
||||||
|
<div className="card-body gap-4">
|
||||||
|
<h2 className="card-title text-2xl">Theme switcher</h2>
|
||||||
|
<p className="text-base-content/70">Pick a theme and preview it immediately.</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="join join-vertical sm:join-horizontal"
|
||||||
|
role="radiogroup"
|
||||||
|
aria-label="Theme selector"
|
||||||
|
>
|
||||||
|
{themeOptions.map((theme) => {
|
||||||
|
const checked = selectedTheme === theme.value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={theme.value}
|
||||||
|
className={`btn join-item ${checked ? "btn-primary" : "btn-outline"}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="theme"
|
||||||
|
value={theme.value}
|
||||||
|
className="sr-only"
|
||||||
|
checked={checked}
|
||||||
|
onChange={() => {
|
||||||
|
setSelectedTheme(theme.value);
|
||||||
|
applyTheme(theme.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{theme.label}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
<div className="card border border-base-300 bg-base-100 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title">Brand colors</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm font-medium">
|
||||||
|
<div className="rounded-box bg-primary p-3 text-primary-content">Primary</div>
|
||||||
|
<div className="rounded-box bg-secondary p-3 text-secondary-content">Secondary</div>
|
||||||
|
<div className="rounded-box bg-accent p-3 text-accent-content">Accent</div>
|
||||||
|
<div className="rounded-box bg-neutral p-3 text-neutral-content">Neutral</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card border border-base-300 bg-base-100 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title">Status colors</h3>
|
||||||
|
<div className="space-y-2 text-sm font-medium">
|
||||||
|
<div className="rounded-box bg-info p-3 text-info-content">Info</div>
|
||||||
|
<div className="rounded-box bg-success p-3 text-success-content">Success</div>
|
||||||
|
<div className="rounded-box bg-warning p-3 text-warning-content">Warning</div>
|
||||||
|
<div className="rounded-box bg-error p-3 text-error-content">Error</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card border border-base-300 bg-base-100 shadow-lg md:col-span-2 xl:col-span-1">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title">Core style outline</h3>
|
||||||
|
<ul className="list list-disc space-y-2 pl-5 text-base-content/80">
|
||||||
|
<li>Warm serif headings paired with clear sans-serif body text</li>
|
||||||
|
<li>Rounded, tactile surfaces with subtle depth and grain</li>
|
||||||
|
<li>Semantic token usage to keep contrast consistent in both themes</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className="card border border-base-300 bg-base-100 shadow-xl">
|
||||||
|
<div className="card-body gap-4">
|
||||||
|
<h2 className="card-title text-2xl">Component preview</h2>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<button className="btn btn-primary">Primary action</button>
|
||||||
|
<button className="btn btn-secondary">Secondary action</button>
|
||||||
|
<button className="btn btn-accent">Accent action</button>
|
||||||
|
<button className="btn btn-ghost">Ghost action</button>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2">
|
||||||
|
<div role="alert" className="alert alert-success alert-soft">
|
||||||
|
<span>Theme tokens are applied consistently.</span>
|
||||||
|
</div>
|
||||||
|
<div role="alert" className="alert alert-warning alert-soft">
|
||||||
|
<span>Use semantic colors over hard-coded color values.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
src/Website/eslint.config.mjs
Normal file
48
src/Website/eslint.config.mjs
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import js from "@eslint/js";
|
||||||
|
import prettierConfig from "eslint-config-prettier";
|
||||||
|
import reactHooks from "eslint-plugin-react-hooks";
|
||||||
|
import globals from "globals";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ["build/**", "node_modules/**", ".react-router/**", "coverage/**"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["**/*.{ts,tsx}"],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
...tseslint.configs.stylistic,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
"react-hooks": reactHooks,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
"no-empty-pattern": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
argsIgnorePattern: "^_",
|
||||||
|
varsIgnorePattern: "^_",
|
||||||
|
caughtErrorsIgnorePattern: "^_",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prettierConfig,
|
||||||
|
);
|
||||||
2393
src/Website/package-lock.json
generated
2393
src/Website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,21 +6,29 @@
|
|||||||
"dev": "react-router dev",
|
"dev": "react-router dev",
|
||||||
"build": "react-router build",
|
"build": "react-router build",
|
||||||
"start": "NODE_ENV=production node ./build/server/index.js",
|
"start": "NODE_ENV=production node ./build/server/index.js",
|
||||||
"typecheck": "tsc"
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"format": "prettier . --write",
|
||||||
|
"format:check": "prettier . --check",
|
||||||
|
"typegen": "react-router typegen",
|
||||||
|
"typecheck": "npm run typegen && tsc -p tsconfig.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^2.2.9",
|
||||||
"@hookform/resolvers": "^5.2.2",
|
"@hookform/resolvers": "^5.2.2",
|
||||||
"@react-router/dev": "^7.13.1",
|
"@react-router/dev": "^7.13.1",
|
||||||
"@react-router/express": "^7.13.1",
|
"@react-router/express": "^7.13.1",
|
||||||
"@react-router/node": "^7.13.1",
|
"@react-router/node": "^7.13.1",
|
||||||
|
"iconoir-react": "^7.11.0",
|
||||||
"isbot": "^5.1.36",
|
"isbot": "^5.1.36",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4",
|
"react-dom": "^19.2.4",
|
||||||
"react-hook-form": "^7.71.2",
|
"react-hook-form": "^7.71.2",
|
||||||
"react-router": "7.13.1",
|
"react-router": "^7.13.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.0.0",
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "^25.5.0",
|
"@types/node": "^25.5.0",
|
||||||
@@ -28,9 +36,15 @@
|
|||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"autoprefixer": "^10.4.27",
|
"autoprefixer": "^10.4.27",
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.5.19",
|
||||||
|
"eslint": "^9.0.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"globals": "^17.4.0",
|
||||||
"postcss": "^8.5.8",
|
"postcss": "^8.5.8",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^8.0.0"
|
"typescript-eslint": "^8.57.0",
|
||||||
|
"vite": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"charset": "utf8",
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
@@ -9,10 +8,12 @@
|
|||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
"rootDirs": [".", "./.react-router/types"],
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"types": ["node", "vite/client"],
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": ["app"],
|
"include": ["app", ".react-router/types/**/*", "react-router.config.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,7 @@ import { defineConfig } from "vite";
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [reactRouter()],
|
plugins: [reactRouter()],
|
||||||
|
resolve: {
|
||||||
|
dedupe: ["react", "react-dom"],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user