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
|
||||
/build
|
||||
|
||||
# project-specific build artifacts
|
||||
/src/Website/build/
|
||||
/src/Website/.react-router/
|
||||
/test-results/
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
@@ -42,6 +47,9 @@ next-env.d.ts
|
||||
|
||||
# vscode
|
||||
.vscode
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
/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: {};
|
||||
};
|
||||
"/theme": {
|
||||
params: {};
|
||||
};
|
||||
"/login": {
|
||||
params: {};
|
||||
};
|
||||
@@ -43,12 +46,16 @@ type Pages = {
|
||||
type RouteFiles = {
|
||||
"root.tsx": {
|
||||
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": {
|
||||
id: "routes/home";
|
||||
page: "/";
|
||||
};
|
||||
"routes/theme.tsx": {
|
||||
id: "routes/theme";
|
||||
page: "/theme";
|
||||
};
|
||||
"routes/login.tsx": {
|
||||
id: "routes/login";
|
||||
page: "/login";
|
||||
@@ -86,6 +93,7 @@ type RouteFiles = {
|
||||
type RouteModules = {
|
||||
"root": typeof import("./app/root.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/register": typeof import("./app/routes/register.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";
|
||||
@plugin "daisyui" {
|
||||
themes: biergarten-lager, biergarten-stout;
|
||||
themes: biergarten-lager, biergarten-stout, biergarten-cassis, biergarten-weizen;
|
||||
}
|
||||
|
||||
@theme {
|
||||
--font-sans:
|
||||
"DM Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
|
||||
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
"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;
|
||||
}
|
||||
|
||||
@@ -25,35 +25,35 @@ h6,
|
||||
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);
|
||||
/* Base — muted parchment / brushed paper */
|
||||
--color-base-100: oklch(96% 0.012 82);
|
||||
--color-base-200: oklch(92% 0.018 80);
|
||||
--color-base-300: oklch(87% 0.025 78);
|
||||
--color-base-content: oklch(30% 0.025 58);
|
||||
/* Primary — mellow amber */
|
||||
--color-primary: oklch(65% 0.085 62);
|
||||
--color-primary-content: oklch(20% 0.02 58);
|
||||
/* Secondary — softened mahogany */
|
||||
--color-secondary: oklch(42% 0.05 42);
|
||||
--color-secondary-content: oklch(96% 0.01 76);
|
||||
/* Accent — frothy cream */
|
||||
--color-accent: oklch(93% 0.015 90);
|
||||
--color-accent-content: oklch(32% 0.02 58);
|
||||
/* Neutral — warm roast */
|
||||
--color-neutral: oklch(28% 0.02 46);
|
||||
--color-neutral-content: oklch(92% 0.012 80);
|
||||
/* Info — muted hop green */
|
||||
--color-info: oklch(46% 0.065 145);
|
||||
--color-info-content: oklch(97% 0.008 145);
|
||||
/* Success — soft barley */
|
||||
--color-success: oklch(70% 0.06 122);
|
||||
--color-success-content: oklch(22% 0.02 122);
|
||||
/* Warning — toned amber */
|
||||
--color-warning: oklch(72% 0.09 56);
|
||||
--color-warning-content: oklch(20% 0.02 56);
|
||||
/* Error — restrained cherry */
|
||||
--color-error: oklch(54% 0.09 22);
|
||||
--color-error-content: oklch(97% 0.006 15);
|
||||
--radius-selector: 0.375rem;
|
||||
--radius-field: 0.5rem;
|
||||
--radius-box: 0.875rem;
|
||||
@@ -80,7 +80,7 @@ h6,
|
||||
--color-primary-content: oklch(14% 0.012 50);
|
||||
|
||||
/* 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);
|
||||
|
||||
/* Accent — frothy cream head */
|
||||
@@ -116,3 +116,111 @@ h6,
|
||||
--depth: 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";
|
||||
|
||||
interface NavbarProps {
|
||||
@@ -10,43 +19,95 @@ interface 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 (
|
||||
<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">
|
||||
<Disclosure
|
||||
as="nav"
|
||||
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
|
||||
</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
|
||||
<div className="navbar-center hidden lg:flex gap-2">
|
||||
{navLinks.map((link) => (
|
||||
<Link key={link.to} to={link.to} className="btn btn-ghost btn-sm">
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="navbar-end gap-2 pr-4">
|
||||
<Link to="/register" className="btn btn-ghost btn-sm">
|
||||
<div className="navbar-end gap-2">
|
||||
{!auth && (
|
||||
<Link to="/register" className="btn btn-ghost btn-sm hidden sm:inline-flex">
|
||||
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">
|
||||
|
||||
<Menu as="div" className="relative">
|
||||
<MenuButton className="btn btn-ghost btn-sm">{auth.username}</MenuButton>
|
||||
<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">
|
||||
<MenuItem>
|
||||
{({ focus }) => (
|
||||
<Link to="/dashboard" className={focus ? "active" : ""}>
|
||||
Dashboard
|
||||
</Link>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
{({ focus }) => (
|
||||
<Link to="/logout" className={focus ? "active" : ""}>
|
||||
Logout
|
||||
</Link>
|
||||
)}
|
||||
</MenuItem>
|
||||
</MenuItems>
|
||||
</Menu>
|
||||
</>
|
||||
) : (
|
||||
<Link to="/login" className="btn btn-primary btn-sm">
|
||||
@@ -55,5 +116,23 @@ export default function Navbar({ auth }: NavbarProps) {
|
||||
)}
|
||||
</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 [
|
||||
index("routes/home.tsx"),
|
||||
route("theme", "routes/theme.tsx"),
|
||||
route("login", "routes/login.tsx"),
|
||||
route("register", "routes/register.tsx"),
|
||||
route("logout", "routes/logout.tsx"),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { HomeSimpleDoor, LogIn, UserPlus } from "iconoir-react";
|
||||
import { useForm } from "react-hook-form";
|
||||
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 { loginSchema, type LoginSchema } from "../lib/schemas";
|
||||
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-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>
|
||||
<h1 className="card-title text-3xl justify-center gap-2">
|
||||
<LogIn className="size-7" aria-hidden="true" />
|
||||
Login
|
||||
</h1>
|
||||
<p className="text-base-content/70">Sign in to your Biergarten account</p>
|
||||
</div>
|
||||
|
||||
{actionData?.error && (
|
||||
@@ -67,63 +71,46 @@ export default function Login({ actionData }: Route.ComponentProps) {
|
||||
)}
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-3">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Username</legend>
|
||||
<input
|
||||
<FormField
|
||||
id="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
placeholder="your_username"
|
||||
className={`input w-full ${errors.username ? "input-error" : ""}`}
|
||||
label="Username"
|
||||
error={errors.username?.message}
|
||||
{...register("username")}
|
||||
/>
|
||||
{errors.username && (
|
||||
<p className="label text-error">{errors.username.message}</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Password</legend>
|
||||
<input
|
||||
<FormField
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="••••••••"
|
||||
className={`input w-full ${errors.password ? "input-error" : ""}`}
|
||||
label="Password"
|
||||
error={errors.password?.message}
|
||||
{...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>
|
||||
<SubmitButton
|
||||
isSubmitting={isSubmitting}
|
||||
idleText="Sign In"
|
||||
submittingText="Signing in..."
|
||||
/>
|
||||
</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">
|
||||
<Link to="/register" className="btn btn-outline btn-sm w-full gap-2">
|
||||
<UserPlus className="size-4" aria-hidden="true" />
|
||||
Create an account
|
||||
</Link>
|
||||
<Link
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
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 FormField from "../components/forms/FormField";
|
||||
import SubmitButton from "../components/forms/SubmitButton";
|
||||
import { createAuthSession, getOptionalAuth, register } from "../lib/auth.server";
|
||||
import { registerSchema, type RegisterSchema } from "../lib/schemas";
|
||||
import type { Route } from "./+types/register";
|
||||
|
||||
@@ -37,7 +35,14 @@ export async function action({ request }: Route.ActionArgs) {
|
||||
}
|
||||
|
||||
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);
|
||||
return createAuthSession(payload, "/dashboard");
|
||||
} catch (err) {
|
||||
@@ -69,9 +74,7 @@ export default function Register({ actionData }: Route.ComponentProps) {
|
||||
<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>
|
||||
<p className="text-base-content/70">Create your Biergarten account</p>
|
||||
</div>
|
||||
|
||||
{actionData?.error && (
|
||||
@@ -80,135 +83,85 @@ export default function Register({ actionData }: Route.ComponentProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-2">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Username</legend>
|
||||
<input
|
||||
<form onSubmit={onSubmit} className="space-y-3">
|
||||
<FormField
|
||||
id="username"
|
||||
type="text"
|
||||
autoComplete="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")}
|
||||
/>
|
||||
{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
|
||||
<FormField
|
||||
id="firstName"
|
||||
type="text"
|
||||
autoComplete="given-name"
|
||||
placeholder="Jane"
|
||||
className={`input w-full ${errors.firstName ? "input-error" : ""}`}
|
||||
label="First Name"
|
||||
error={errors.firstName?.message}
|
||||
{...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
|
||||
|
||||
<FormField
|
||||
id="lastName"
|
||||
type="text"
|
||||
autoComplete="family-name"
|
||||
placeholder="Doe"
|
||||
className={`input w-full ${errors.lastName ? "input-error" : ""}`}
|
||||
label="Last Name"
|
||||
error={errors.lastName?.message}
|
||||
{...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
|
||||
<FormField
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder="jane@example.com"
|
||||
className={`input w-full ${errors.email ? "input-error" : ""}`}
|
||||
label="Email"
|
||||
error={errors.email?.message}
|
||||
{...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
|
||||
<FormField
|
||||
id="dateOfBirth"
|
||||
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")}
|
||||
/>
|
||||
{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
|
||||
<FormField
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
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")}
|
||||
/>
|
||||
{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
|
||||
<FormField
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder="••••••••"
|
||||
className={`input w-full ${errors.confirmPassword ? "input-error" : ""}`}
|
||||
label="Confirm Password"
|
||||
error={errors.confirmPassword?.message}
|
||||
{...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>
|
||||
<SubmitButton
|
||||
isSubmitting={isSubmitting}
|
||||
idleText="Create Account"
|
||||
submittingText="Creating account..."
|
||||
/>
|
||||
</form>
|
||||
|
||||
<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">
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="link link-hover text-sm text-base-content/60"
|
||||
>
|
||||
<Link to="/" className="link link-hover text-sm text-base-content/60">
|
||||
← Back to home
|
||||
</Link>
|
||||
</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",
|
||||
"build": "react-router build",
|
||||
"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": {
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@react-router/dev": "^7.13.1",
|
||||
"@react-router/express": "^7.13.1",
|
||||
"@react-router/node": "^7.13.1",
|
||||
"iconoir-react": "^7.11.0",
|
||||
"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"
|
||||
"react-router": "^7.13.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@tailwindcss/postcss": "^4.2.1",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^25.5.0",
|
||||
@@ -28,9 +36,15 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"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",
|
||||
"prettier": "^3.8.1",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
"typescript-eslint": "^8.57.0",
|
||||
"vite": "^7.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"charset": "utf8",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
@@ -9,10 +8,12 @@
|
||||
"moduleResolution": "bundler",
|
||||
"module": "ESNext",
|
||||
"noEmit": true,
|
||||
"rootDirs": [".", "./.react-router/types"],
|
||||
"resolveJsonModule": true,
|
||||
"types": ["node", "vite/client"],
|
||||
"target": "ES2022",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["app"],
|
||||
"include": ["app", ".react-router/types/**/*", "react-router.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -3,4 +3,7 @@ import { defineConfig } from "vite";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [reactRouter()],
|
||||
resolve: {
|
||||
dedupe: ["react", "react-dom"],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user