mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-04-05 18:09:04 +00:00
Add auth demo
This commit is contained in:
18
src/Website/app/routes/beer-styles.tsx
Normal file
18
src/Website/app/routes/beer-styles.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Route } from "./+types/beer-styles";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "Beer Styles | The Biergarten App" }];
|
||||
}
|
||||
|
||||
export default function BeerStyles() {
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200">
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-4xl font-bold mb-4">Beer Styles</h1>
|
||||
<p className="text-base-content/70">
|
||||
Learn about different beer styles.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/Website/app/routes/beers.tsx
Normal file
16
src/Website/app/routes/beers.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Route } from "./+types/beers";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "Beers | The Biergarten App" }];
|
||||
}
|
||||
|
||||
export default function Beers() {
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200">
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-4xl font-bold mb-4">Beers</h1>
|
||||
<p className="text-base-content/70">Explore our collection of beers.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/Website/app/routes/breweries.tsx
Normal file
16
src/Website/app/routes/breweries.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Route } from "./+types/breweries";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "Breweries | The Biergarten App" }];
|
||||
}
|
||||
|
||||
export default function Breweries() {
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200">
|
||||
<div className="container mx-auto p-4">
|
||||
<h1 className="text-4xl font-bold mb-4">Breweries</h1>
|
||||
<p className="text-base-content/70">Discover our partner breweries.</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
src/Website/app/routes/confirm.tsx
Normal file
80
src/Website/app/routes/confirm.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Link } from "react-router";
|
||||
import { confirmEmail, requireAuth } from "../lib/auth.server";
|
||||
import type { Route } from "./+types/confirm";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "Confirm Email | The Biergarten App" }];
|
||||
}
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const auth = await requireAuth(request);
|
||||
const url = new URL(request.url);
|
||||
const token = url.searchParams.get("token");
|
||||
|
||||
if (!token) {
|
||||
return { success: false as const, error: "Missing confirmation token." };
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await confirmEmail(token, auth.accessToken);
|
||||
return {
|
||||
success: true as const,
|
||||
confirmedDate: payload.confirmedDate,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
success: false as const,
|
||||
error: err instanceof Error ? err.message : "Confirmation failed.",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Confirm({ loaderData }: Route.ComponentProps) {
|
||||
return (
|
||||
<div className="hero min-h-screen bg-base-200">
|
||||
<div className="card w-full max-w-md bg-base-100 shadow-xl">
|
||||
<div className="card-body items-center text-center gap-4">
|
||||
{loaderData.success ? (
|
||||
<>
|
||||
<div className="text-success text-6xl">✓</div>
|
||||
<h1 className="card-title text-2xl">Email Confirmed!</h1>
|
||||
<p className="text-base-content/70">
|
||||
Your email address has been successfully verified.
|
||||
</p>
|
||||
<div className="bg-base-200 rounded-box w-full p-3 text-sm text-left">
|
||||
<span className="text-base-content/50 text-xs uppercase tracking-widest font-semibold">
|
||||
Confirmed at
|
||||
</span>
|
||||
<p className="font-mono mt-1">
|
||||
{new Date(loaderData.confirmedDate).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="card-actions w-full pt-2">
|
||||
<Link to="/dashboard" className="btn btn-primary w-full">
|
||||
Go to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-error text-6xl">✕</div>
|
||||
<h1 className="card-title text-2xl">Confirmation Failed</h1>
|
||||
<div role="alert" className="alert alert-error alert-soft w-full">
|
||||
<span>{loaderData.error}</span>
|
||||
</div>
|
||||
<p className="text-base-content/70 text-sm">
|
||||
The confirmation link may have expired (valid for 30 minutes) or
|
||||
already been used.
|
||||
</p>
|
||||
<div className="card-actions w-full pt-2 flex-col gap-2">
|
||||
<Link to="/dashboard" className="btn btn-primary w-full">
|
||||
Back to Dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
110
src/Website/app/routes/dashboard.tsx
Normal file
110
src/Website/app/routes/dashboard.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { requireAuth } from "../lib/auth.server";
|
||||
import type { Route } from "./+types/dashboard";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "Dashboard | The Biergarten App" }];
|
||||
}
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const auth = await requireAuth(request);
|
||||
return {
|
||||
username: auth.username,
|
||||
userAccountId: auth.userAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
export default function Dashboard({ loaderData }: Route.ComponentProps) {
|
||||
const { username, userAccountId } = loaderData;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200">
|
||||
<div className="mx-auto max-w-4xl px-6 py-10 space-y-6">
|
||||
<div className="card bg-base-100 shadow">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title text-2xl">Welcome, {username}!</h2>
|
||||
<p className="text-base-content/70">
|
||||
You are successfully authenticated. This is a protected page that
|
||||
requires a valid session.
|
||||
</p>
|
||||
|
||||
<div className="bg-base-200 rounded-box p-4 mt-2">
|
||||
<p className="text-xs font-semibold uppercase tracking-widest text-base-content/50 mb-3">
|
||||
Session Info
|
||||
</p>
|
||||
<div className="stats stats-vertical w-full">
|
||||
<div className="stat py-2">
|
||||
<div className="stat-title">Username</div>
|
||||
<div className="stat-value text-lg font-mono">{username}</div>
|
||||
</div>
|
||||
<div className="stat py-2">
|
||||
<div className="stat-title">User ID</div>
|
||||
<div className="stat-desc font-mono text-xs mt-1">
|
||||
{userAccountId}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-base-100 shadow">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">Auth Flow Demo</h2>
|
||||
<p className="text-sm text-base-content/70">
|
||||
This demo showcases the following authentication features:
|
||||
</p>
|
||||
<ul className="list">
|
||||
<li className="list-row">
|
||||
<div>
|
||||
<p className="font-semibold">Login</p>
|
||||
<p className="text-sm text-base-content/60">
|
||||
POST to <code className="kbd kbd-sm">/api/auth/login</code>{" "}
|
||||
with username & password
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="list-row">
|
||||
<div>
|
||||
<p className="font-semibold">Register</p>
|
||||
<p className="text-sm text-base-content/60">
|
||||
POST to{" "}
|
||||
<code className="kbd kbd-sm">/api/auth/register</code> with
|
||||
full user details
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="list-row">
|
||||
<div>
|
||||
<p className="font-semibold">Session</p>
|
||||
<p className="text-sm text-base-content/60">
|
||||
JWT access & refresh tokens stored in an HTTP-only
|
||||
cookie
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="list-row">
|
||||
<div>
|
||||
<p className="font-semibold">Protected Routes</p>
|
||||
<p className="text-sm text-base-content/60">
|
||||
This dashboard requires authentication via{" "}
|
||||
<code className="kbd kbd-sm">requireAuth()</code>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li className="list-row">
|
||||
<div>
|
||||
<p className="font-semibold">Token Refresh</p>
|
||||
<p className="text-sm text-base-content/60">
|
||||
POST to{" "}
|
||||
<code className="kbd kbd-sm">/api/auth/refresh</code> with
|
||||
refresh token
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
src/Website/app/routes/home.tsx
Normal file
56
src/Website/app/routes/home.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Link } from "react-router";
|
||||
import { getOptionalAuth } from "../lib/auth.server";
|
||||
import type { Route } from "./+types/home";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [
|
||||
{ title: "The Biergarten App" },
|
||||
{ name: "description", content: "Welcome to The Biergarten App" },
|
||||
];
|
||||
}
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const auth = await getOptionalAuth(request);
|
||||
return { username: auth?.username ?? null };
|
||||
}
|
||||
|
||||
export default function Home({ loaderData }: Route.ComponentProps) {
|
||||
const { username } = loaderData;
|
||||
|
||||
return (
|
||||
<div className="hero min-h-screen bg-base-200">
|
||||
<div className="hero-content text-center">
|
||||
<div className="max-w-md space-y-6">
|
||||
<h1 className="text-5xl font-bold">🍺 The Biergarten App</h1>
|
||||
<p className="text-lg text-base-content/70">Authentication Demo</p>
|
||||
|
||||
{username ? (
|
||||
<>
|
||||
<p className="text-base-content/80">
|
||||
Welcome back,{" "}
|
||||
<span className="font-semibold text-primary">{username}</span>!
|
||||
</p>
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Link to="/dashboard" className="btn btn-primary">
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link to="/logout" className="btn btn-ghost">
|
||||
Logout
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex gap-3 justify-center">
|
||||
<Link to="/login" className="btn btn-primary">
|
||||
Login
|
||||
</Link>
|
||||
<Link to="/register" className="btn btn-outline">
|
||||
Register
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
133
src/Website/app/routes/login.tsx
Normal file
133
src/Website/app/routes/login.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
||||
import { createAuthSession, getOptionalAuth, login } from "../lib/auth.server";
|
||||
import { loginSchema, type LoginSchema } from "../lib/schemas";
|
||||
import type { Route } from "./+types/login";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "Login | The Biergarten App" }];
|
||||
}
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const auth = await getOptionalAuth(request);
|
||||
if (auth) throw redirect("/dashboard");
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function action({ request }: Route.ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const result = loginSchema.safeParse({
|
||||
username: formData.get("username"),
|
||||
password: formData.get("password"),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
return { error: result.error.issues[0].message };
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await login(result.data.username, result.data.password);
|
||||
return createAuthSession(payload, "/dashboard");
|
||||
} catch (err) {
|
||||
return { error: err instanceof Error ? err.message : "Login failed." };
|
||||
}
|
||||
}
|
||||
|
||||
export default function Login({ actionData }: Route.ComponentProps) {
|
||||
const navigation = useNavigation();
|
||||
const submit = useSubmit();
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<LoginSchema>({ resolver: zodResolver(loginSchema) });
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
submit(data, { method: "post" });
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="hero min-h-screen bg-base-200">
|
||||
<div className="card w-full max-w-md bg-base-100 shadow-xl">
|
||||
<div className="card-body gap-4">
|
||||
<div className="text-center">
|
||||
<h1 className="card-title text-3xl justify-center">Login</h1>
|
||||
<p className="text-base-content/70">
|
||||
Sign in to your Biergarten account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{actionData?.error && (
|
||||
<div role="alert" className="alert alert-error alert-soft">
|
||||
<span>{actionData.error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-3">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Username</legend>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
placeholder="your_username"
|
||||
className={`input w-full ${errors.username ? "input-error" : ""}`}
|
||||
{...register("username")}
|
||||
/>
|
||||
{errors.username && (
|
||||
<p className="label text-error">{errors.username.message}</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Password</legend>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
placeholder="••••••••"
|
||||
className={`input w-full ${errors.password ? "input-error" : ""}`}
|
||||
{...register("password")}
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="label text-error">{errors.password.message}</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="btn btn-primary w-full mt-2"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="loading loading-spinner loading-sm" />{" "}
|
||||
Signing in...
|
||||
</>
|
||||
) : (
|
||||
"Sign In"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="divider text-xs">New here?</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<Link to="/register" className="btn btn-outline btn-sm w-full">
|
||||
Create an account
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="link link-hover text-sm text-base-content/60"
|
||||
>
|
||||
← Back to home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
src/Website/app/routes/logout.tsx
Normal file
10
src/Website/app/routes/logout.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { redirect } from "react-router";
|
||||
import { destroySession, getSession } from "../lib/auth.server";
|
||||
import type { Route } from "./+types/logout";
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const session = await getSession(request);
|
||||
return redirect("/", {
|
||||
headers: { "Set-Cookie": await destroySession(session) },
|
||||
});
|
||||
}
|
||||
231
src/Website/app/routes/register.tsx
Normal file
231
src/Website/app/routes/register.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Link, redirect, useNavigation, useSubmit } from "react-router";
|
||||
import {
|
||||
createAuthSession,
|
||||
getOptionalAuth,
|
||||
register,
|
||||
} from "../lib/auth.server";
|
||||
import { registerSchema, type RegisterSchema } from "../lib/schemas";
|
||||
import type { Route } from "./+types/register";
|
||||
|
||||
export function meta({}: Route.MetaArgs) {
|
||||
return [{ title: "Register | The Biergarten App" }];
|
||||
}
|
||||
|
||||
export async function loader({ request }: Route.LoaderArgs) {
|
||||
const auth = await getOptionalAuth(request);
|
||||
if (auth) throw redirect("/dashboard");
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function action({ request }: Route.ActionArgs) {
|
||||
const formData = await request.formData();
|
||||
const result = registerSchema.safeParse({
|
||||
username: formData.get("username"),
|
||||
firstName: formData.get("firstName"),
|
||||
lastName: formData.get("lastName"),
|
||||
email: formData.get("email"),
|
||||
dateOfBirth: formData.get("dateOfBirth"),
|
||||
password: formData.get("password"),
|
||||
confirmPassword: formData.get("confirmPassword"),
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
const fieldErrors = result.error.flatten().fieldErrors;
|
||||
return { error: null, fieldErrors };
|
||||
}
|
||||
|
||||
try {
|
||||
const { confirmPassword: _, ...body } = result.data;
|
||||
const payload = await register(body);
|
||||
return createAuthSession(payload, "/dashboard");
|
||||
} catch (err) {
|
||||
return {
|
||||
error: err instanceof Error ? err.message : "Registration failed.",
|
||||
fieldErrors: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Register({ actionData }: Route.ComponentProps) {
|
||||
const navigation = useNavigation();
|
||||
const submit = useSubmit();
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
|
||||
const {
|
||||
register: field,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<RegisterSchema>({ resolver: zodResolver(registerSchema) });
|
||||
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
submit(data, { method: "post" });
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-base-200 flex items-center justify-center p-4">
|
||||
<div className="card w-full max-w-lg bg-base-100 shadow-xl">
|
||||
<div className="card-body gap-4">
|
||||
<div className="text-center">
|
||||
<h1 className="card-title text-3xl justify-center">Register</h1>
|
||||
<p className="text-base-content/70">
|
||||
Create your Biergarten account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{actionData?.error && (
|
||||
<div role="alert" className="alert alert-error alert-soft">
|
||||
<span>{actionData.error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={onSubmit} className="space-y-2">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Username</legend>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
autoComplete="username"
|
||||
placeholder="your_username"
|
||||
className={`input w-full ${errors.username ? "input-error" : ""}`}
|
||||
{...field("username")}
|
||||
/>
|
||||
{errors.username ? (
|
||||
<p className="label text-error">{errors.username.message}</p>
|
||||
) : (
|
||||
<p className="label">3-64 characters, alphanumeric and . _ -</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">First Name</legend>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
autoComplete="given-name"
|
||||
placeholder="Jane"
|
||||
className={`input w-full ${errors.firstName ? "input-error" : ""}`}
|
||||
{...field("firstName")}
|
||||
/>
|
||||
{errors.firstName && (
|
||||
<p className="label text-error">{errors.firstName.message}</p>
|
||||
)}
|
||||
</fieldset>
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Last Name</legend>
|
||||
<input
|
||||
id="lastName"
|
||||
type="text"
|
||||
autoComplete="family-name"
|
||||
placeholder="Doe"
|
||||
className={`input w-full ${errors.lastName ? "input-error" : ""}`}
|
||||
{...field("lastName")}
|
||||
/>
|
||||
{errors.lastName && (
|
||||
<p className="label text-error">{errors.lastName.message}</p>
|
||||
)}
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Email</legend>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder="jane@example.com"
|
||||
className={`input w-full ${errors.email ? "input-error" : ""}`}
|
||||
{...field("email")}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="label text-error">{errors.email.message}</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Date of Birth</legend>
|
||||
<input
|
||||
id="dateOfBirth"
|
||||
type="date"
|
||||
className={`input w-full ${errors.dateOfBirth ? "input-error" : ""}`}
|
||||
{...field("dateOfBirth")}
|
||||
/>
|
||||
{errors.dateOfBirth ? (
|
||||
<p className="label text-error">{errors.dateOfBirth.message}</p>
|
||||
) : (
|
||||
<p className="label">Must be 19 years or older</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Password</legend>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder="••••••••"
|
||||
className={`input w-full ${errors.password ? "input-error" : ""}`}
|
||||
{...field("password")}
|
||||
/>
|
||||
{errors.password ? (
|
||||
<p className="label text-error">{errors.password.message}</p>
|
||||
) : (
|
||||
<p className="label">
|
||||
8+ chars: uppercase, lowercase, digit, special character
|
||||
</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">Confirm Password</legend>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
autoComplete="new-password"
|
||||
placeholder="••••••••"
|
||||
className={`input w-full ${errors.confirmPassword ? "input-error" : ""}`}
|
||||
{...field("confirmPassword")}
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<p className="label text-error">
|
||||
{errors.confirmPassword.message}
|
||||
</p>
|
||||
)}
|
||||
</fieldset>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="btn btn-primary w-full mt-2"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<span className="loading loading-spinner loading-sm" />{" "}
|
||||
Creating account...
|
||||
</>
|
||||
) : (
|
||||
"Create Account"
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="divider text-xs">Already have an account?</div>
|
||||
|
||||
<div className="text-center space-y-2">
|
||||
<Link to="/login" className="btn btn-outline btn-sm w-full">
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="link link-hover text-sm text-base-content/60"
|
||||
>
|
||||
← Back to home
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user