metadata and titles

This commit is contained in:
Michael Freno
2025-12-19 17:05:24 -05:00
parent 69c95c3060
commit e24e53f8d7
19 changed files with 1936 additions and 1750 deletions

View File

@@ -1,4 +1,4 @@
import { Title } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start"; import { HttpStatusCode } from "@solidjs/start";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { createEffect, createSignal, For } from "solid-js"; import { createEffect, createSignal, For } from "solid-js";
@@ -47,7 +47,11 @@ export default function Page_401() {
return ( return (
<> <>
<Title>401 - Unauthorized</Title> <Title>401 Unauthorized | Michael Freno</Title>
<Meta
name="description"
content="401 - Unauthorized access. Please log in to access this page."
/>
<HttpStatusCode code={401} /> <HttpStatusCode code={401} />
<div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-slate-900 via-amber-950/20 to-slate-900 dark:from-black dark:via-amber-950/30 dark:to-black"> <div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-slate-900 via-amber-950/20 to-slate-900 dark:from-black dark:via-amber-950/30 dark:to-black">
{/* Animated particle background */} {/* Animated particle background */}

View File

@@ -1,4 +1,4 @@
import { Title } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start"; import { HttpStatusCode } from "@solidjs/start";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { createEffect, createSignal, For } from "solid-js"; import { createEffect, createSignal, For } from "solid-js";
@@ -43,7 +43,11 @@ export default function NotFound() {
return ( return (
<> <>
<Title>404 - Not Found</Title> <Title>404 Not Found | Michael Freno</Title>
<Meta
name="description"
content="404 - Page not found. The page you're looking for doesn't exist."
/>
<HttpStatusCode code={404} /> <HttpStatusCode code={404} />
<div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 dark:from-black dark:via-slate-900 dark:to-black"> <div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 dark:from-black dark:via-slate-900 dark:to-black">
{/* Animated particle background */} {/* Animated particle background */}

View File

@@ -1,10 +1,17 @@
import { Title } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
export default function About() { export default function About() {
return ( return (
<>
<Title>About | Michael Freno</Title>
<Meta
name="description"
content="Learn more about Michael Freno - Software Engineer, game developer, and open source contributor."
/>
<main> <main>
<Title>About</Title>
<h1>About</h1> <h1>About</h1>
</main> </main>
</>
); );
} }

View File

@@ -1,4 +1,5 @@
import { createSignal, createEffect, Show, onMount } from "solid-js"; import { createSignal, createEffect, Show, onMount } from "solid-js";
import { Title, Meta } from "@solidjs/meta";
import { useNavigate, cache, redirect } from "@solidjs/router"; import { useNavigate, cache, redirect } from "@solidjs/router";
import { getEvent } from "vinxi/http"; import { getEvent } from "vinxi/http";
import Eye from "~/components/icons/Eye"; import Eye from "~/components/icons/Eye";
@@ -453,6 +454,13 @@ export default function AccountPage() {
}; };
return ( return (
<>
<Title>Account | Michael Freno</Title>
<Meta
name="description"
content="Manage your account settings, update profile information, and configure preferences."
/>
<div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36"> <div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36">
<div class="pt-24"> <div class="pt-24">
<Show <Show
@@ -507,7 +515,9 @@ export default function AccountPage() {
: "bg-blue hover:brightness-125 active:scale-90" : "bg-blue hover:brightness-125 active:scale-90"
} mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`} } mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
> >
{profileImageSetLoading() ? "Uploading..." : "Set Image"} {profileImageSetLoading()
? "Uploading..."
: "Set Image"}
</button> </button>
</form> </form>
<Show when={showImageSuccess()}> <Show when={showImageSuccess()}>
@@ -618,7 +628,8 @@ export default function AccountPage() {
/> />
<span class="bar"></span> <span class="bar"></span>
<label class="underlinedInputLabel"> <label class="underlinedInputLabel">
Set {currentUser().displayName ? "New " : ""}Display Name Set {currentUser().displayName ? "New " : ""}Display
Name
</label> </label>
</div> </div>
<div class="flex justify-end"> <div class="flex justify-end">
@@ -631,7 +642,9 @@ export default function AccountPage() {
: "bg-blue hover:brightness-125 active:scale-90" : "bg-blue hover:brightness-125 active:scale-90"
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`} } mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
> >
{displayNameButtonLoading() ? "Submitting..." : "Submit"} {displayNameButtonLoading()
? "Submitting..."
: "Submit"}
</button> </button>
</div> </div>
<Show when={showDisplayNameSuccess()}> <Show when={showDisplayNameSuccess()}>
@@ -673,7 +686,10 @@ export default function AccountPage() {
} }
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125" class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
> >
<Show when={showOldPasswordInput()} fallback={<Eye />}> <Show
when={showOldPasswordInput()}
fallback={<Eye />}
>
<EyeSlash /> <EyeSlash />
</Show> </Show>
</button> </button>
@@ -695,7 +711,9 @@ export default function AccountPage() {
<label class="underlinedInputLabel">New Password</label> <label class="underlinedInputLabel">New Password</label>
<button <button
type="button" type="button"
onClick={() => setShowPasswordInput(!showPasswordInput())} onClick={() =>
setShowPasswordInput(!showPasswordInput())
}
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125" class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
> >
<Show when={showPasswordInput()} fallback={<Eye />}> <Show when={showPasswordInput()} fallback={<Eye />}>
@@ -837,5 +855,6 @@ export default function AccountPage() {
</Show> </Show>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -1,6 +1,6 @@
import { Show, Suspense, For } from "solid-js"; import { Show, Suspense, For } from "solid-js";
import { useParams, A, Navigate, query } from "@solidjs/router"; import { useParams, A, Navigate, query } from "@solidjs/router";
import { Title } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
import { createAsync } from "@solidjs/router"; import { createAsync } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web"; import { getRequestEvent } from "solid-js/web";
import SessionDependantLike from "~/components/blog/SessionDependantLike"; import SessionDependantLike from "~/components/blog/SessionDependantLike";
@@ -159,6 +159,13 @@ export default function PostPage() {
<Title> <Title>
{p().title.replaceAll("_", " ")} | Michael Freno {p().title.replaceAll("_", " ")} | Michael Freno
</Title> </Title>
<Meta
name="description"
content={
p().subtitle ||
`Read ${p().title.replaceAll("_", " ")} by Michael Freno on the freno.me blog.`
}
/>
<div class="relative overflow-x-hidden"> <div class="relative overflow-x-hidden">
{/* Fixed banner image background */} {/* Fixed banner image background */}

View File

@@ -1,6 +1,6 @@
import { Show, createSignal, createEffect, onCleanup } from "solid-js"; import { Show, createSignal, createEffect, onCleanup } from "solid-js";
import { useNavigate, query } from "@solidjs/router"; import { useNavigate, query } from "@solidjs/router";
import { Title } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
import { createAsync } from "@solidjs/router"; import { createAsync } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web"; import { getRequestEvent } from "solid-js/web";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
@@ -195,6 +195,10 @@ export default function CreatePost() {
return ( return (
<> <>
<Title>Create Blog Post | Michael Freno</Title> <Title>Create Blog Post | Michael Freno</Title>
<Meta
name="description"
content="Create a new blog post with rich text editing, image uploads, and tag management."
/>
<Show <Show
when={authState()?.privilegeLevel === "admin"} when={authState()?.privilegeLevel === "admin"}

View File

@@ -1,6 +1,6 @@
import { Show, createSignal, createEffect, onCleanup } from "solid-js"; import { Show, createSignal, createEffect, onCleanup } from "solid-js";
import { useParams, useNavigate, query } from "@solidjs/router"; import { useParams, useNavigate, query } from "@solidjs/router";
import { Title } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
import { createAsync } from "@solidjs/router"; import { createAsync } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web"; import { getRequestEvent } from "solid-js/web";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
@@ -241,6 +241,10 @@ export default function EditPost() {
return ( return (
<> <>
<Title>Edit Post | Michael Freno</Title> <Title>Edit Post | Michael Freno</Title>
<Meta
name="description"
content="Edit your blog post with rich text editing, image management, and tag updates."
/>
<Show <Show
when={data()?.privilegeLevel === "admin"} when={data()?.privilegeLevel === "admin"}

View File

@@ -1,23 +1,32 @@
import { Title, Meta } from "@solidjs/meta";
import DeletionForm from "~/components/DeletionForm"; import DeletionForm from "~/components/DeletionForm";
export default function LifeAndLinageDeletionForm() { export default function LifeAndLinageDeletionForm() {
return ( return (
<>
<Title>Account Deletion - Life and Lineage | Michael Freno</Title>
<Meta
name="description"
content="Request account deletion for Life and Lineage. Remove all your data from our system with a 24-hour grace period."
/>
<div class="pt-20"> <div class="pt-20">
<div class="mx-auto p-4 md:p-6 lg:p-12"> <div class="mx-auto p-4 md:p-6 lg:p-12">
<div class="w-full justify-center text-text"> <div class="text-text w-full justify-center">
<div class="text-xl"> <div class="text-xl">
<em>What will happen</em>: <em>What will happen</em>:
</div> </div>
Once you send, if a match to the email provided is found in our Once you send, if a match to the email provided is found in our
system, a 24hr grace period is started where you can request a system, a 24hr grace period is started where you can request a
cancellation of the account deletion. Once the grace period ends, the cancellation of the account deletion. Once the grace period ends,
account's entry in our central database will be completely removed, the account's entry in our central database will be completely
and your individual database storing your remote saves will also be removed, and your individual database storing your remote saves will
deleted. No data related to the account is retained in any way. also be deleted. No data related to the account is retained in any
way.
</div> </div>
<DeletionForm /> <DeletionForm />
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -1,3 +1,4 @@
import { Title, Meta } from "@solidjs/meta";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore"; import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
import GitHub from "~/components/icons/GitHub"; import GitHub from "~/components/icons/GitHub";
@@ -21,6 +22,13 @@ export default function DownloadsPage() {
}; };
return ( return (
<>
<Title>Downloads | Michael Freno</Title>
<Meta
name="description"
content="Download Life and Lineage, Shapes with Abigail, and Cork for macOS. Available on iOS, Android, and macOS."
/>
<div class="bg-base min-h-screen pt-[15vh] pb-12"> <div class="bg-base min-h-screen pt-[15vh] pb-12">
<div class="text-text text-center text-3xl tracking-widest"> <div class="text-text text-center text-3xl tracking-widest">
Downloads Downloads
@@ -163,5 +171,6 @@ export default function DownloadsPage() {
</ul> </ul>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -1,8 +1,16 @@
import { Title, Meta } from "@solidjs/meta";
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore"; import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
import { Typewriter } from "~/components/Typewriter"; import { Typewriter } from "~/components/Typewriter";
export default function Home() { export default function Home() {
return ( return (
<>
<Title>Home | Michael Freno</Title>
<Meta
name="description"
content="Michael Freno - Software Engineer based in Brooklyn, NY. Passionate about dev tooling, game development, and open source software."
/>
<main class="flex h-full flex-col gap-8 p-4 text-xl"> <main class="flex h-full flex-col gap-8 p-4 text-xl">
<div class="flex-1"> <div class="flex-1">
<Typewriter speed={30} keepAlive={2000}> <Typewriter speed={30} keepAlive={2000}>
@@ -151,5 +159,6 @@ export default function Home() {
</Typewriter> </Typewriter>
</div> </div>
</main> </main>
</>
); );
} }

View File

@@ -6,6 +6,7 @@ import {
cache, cache,
redirect redirect
} from "@solidjs/router"; } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import { getEvent } from "vinxi/http"; import { getEvent } from "vinxi/http";
import GoogleLogo from "~/components/icons/GoogleLogo"; import GoogleLogo from "~/components/icons/GoogleLogo";
import GitHub from "~/components/icons/GitHub"; import GitHub from "~/components/icons/GitHub";
@@ -320,6 +321,12 @@ export default function LoginPage() {
}; };
return ( return (
<>
<Title>Login | Michael Freno</Title>
<Meta
name="description"
content="Sign in to your account or register for a new account to access personalized features and manage your profile."
/>
<div class="flex h-dvh flex-row justify-evenly"> <div class="flex h-dvh flex-row justify-evenly">
{/* Logo section - hidden on mobile */} {/* Logo section - hidden on mobile */}
{/* <div class="hidden md:flex"> {/* <div class="hidden md:flex">
@@ -639,5 +646,6 @@ export default function LoginPage() {
</div> </div>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -1,5 +1,6 @@
import { createSignal, createEffect, Show } from "solid-js"; import { createSignal, createEffect, Show } from "solid-js";
import { A, useNavigate, useSearchParams } from "@solidjs/router"; import { A, useNavigate, useSearchParams } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import CountdownCircleTimer from "~/components/CountdownCircleTimer"; import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import Eye from "~/components/icons/Eye"; import Eye from "~/components/icons/Eye";
import EyeSlash from "~/components/icons/EyeSlash"; import EyeSlash from "~/components/icons/EyeSlash";
@@ -13,8 +14,10 @@ export default function PasswordResetPage() {
const [passwordBlurred, setPasswordBlurred] = createSignal(false); const [passwordBlurred, setPasswordBlurred] = createSignal(false);
const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false); const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false);
const [passwordsMatch, setPasswordsMatch] = createSignal(false); const [passwordsMatch, setPasswordsMatch] = createSignal(false);
const [showPasswordLengthWarning, setShowPasswordLengthWarning] = createSignal(false); const [showPasswordLengthWarning, setShowPasswordLengthWarning] =
const [passwordLengthSufficient, setPasswordLengthSufficient] = createSignal(false); createSignal(false);
const [passwordLengthSufficient, setPasswordLengthSufficient] =
createSignal(false);
const [showRequestNewEmail, setShowRequestNewEmail] = createSignal(false); const [showRequestNewEmail, setShowRequestNewEmail] = createSignal(false);
const [countDown, setCountDown] = createSignal(false); const [countDown, setCountDown] = createSignal(false);
const [error, setError] = createSignal(""); const [error, setError] = createSignal("");
@@ -70,8 +73,8 @@ export default function PasswordResetPage() {
body: JSON.stringify({ body: JSON.stringify({
token: token, token: token,
newPassword, newPassword,
newPasswordConfirmation: newPasswordConf, newPasswordConfirmation: newPasswordConf
}), })
}); });
const result = await response.json(); const result = await response.json();
@@ -158,14 +161,26 @@ export default function PasswordResetPage() {
} }
return ( return (
<div class="timer text-center"> <div class="timer text-center">
<div class="text-sm text-slate-700 dark:text-slate-300">Change Successful!</div> <div class="text-sm text-slate-700 dark:text-slate-300">
<div class="value py-1 text-3xl text-blue-500 dark:text-blue-400">{timeRemaining}</div> Change Successful!
<div class="text-sm text-slate-700 dark:text-slate-300">Redirecting...</div> </div>
<div class="value py-1 text-3xl text-blue-500 dark:text-blue-400">
{timeRemaining}
</div>
<div class="text-sm text-slate-700 dark:text-slate-300">
Redirecting...
</div>
</div> </div>
); );
}; };
return ( return (
<>
<Title>Reset Password | Michael Freno</Title>
<Meta
name="description"
content="Set a new password for your account to regain access to your profile and personalized features."
/>
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> <div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
<div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100"> <div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
Set New Password Set New Password
@@ -175,9 +190,9 @@ export default function PasswordResetPage() {
onSubmit={(e) => setNewPasswordTrigger(e)} onSubmit={(e) => setNewPasswordTrigger(e)}
class="mt-4 flex w-full justify-center" class="mt-4 flex w-full justify-center"
> >
<div class="flex flex-col justify-center max-w-md w-full px-4"> <div class="flex w-full max-w-md flex-col justify-center px-4">
{/* New Password Input */} {/* New Password Input */}
<div class="input-group mx-4 relative"> <div class="input-group relative mx-4">
<input <input
ref={newPasswordRef} ref={newPasswordRef}
name="newPassword" name="newPassword"
@@ -194,7 +209,7 @@ export default function PasswordResetPage() {
<button <button
type="button" type="button"
onClick={() => setShowPasswordInput(!showPasswordInput())} onClick={() => setShowPasswordInput(!showPasswordInput())}
class="absolute right-0 top-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200" class="absolute top-2 right-0 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
> >
<Show when={showPasswordInput()} fallback={<Eye />}> <Show when={showPasswordInput()} fallback={<Eye />}>
<EyeSlash /> <EyeSlash />
@@ -205,14 +220,14 @@ export default function PasswordResetPage() {
{/* Password Length Warning */} {/* Password Length Warning */}
<div <div
class={`${ class={`${
showPasswordLengthWarning() ? "" : "select-none opacity-0" showPasswordLengthWarning() ? "" : "opacity-0 select-none"
} transition-opacity text-center text-red-500 text-sm duration-200 ease-in-out mt-2`} } mt-2 text-center text-sm text-red-500 transition-opacity duration-200 ease-in-out`}
> >
Password too short! Min Length: 8 Password too short! Min Length: 8
</div> </div>
{/* Password Confirmation Input */} {/* Password Confirmation Input */}
<div class="input-group mx-4 mt-6 relative"> <div class="input-group relative mx-4 mt-6">
<input <input
ref={newPasswordConfRef} ref={newPasswordConfRef}
name="newPasswordConf" name="newPasswordConf"
@@ -227,8 +242,10 @@ export default function PasswordResetPage() {
<label class="underlinedInputLabel">Password Confirmation</label> <label class="underlinedInputLabel">Password Confirmation</label>
<button <button
type="button" type="button"
onClick={() => setShowPasswordConfInput(!showPasswordConfInput())} onClick={() =>
class="absolute right-0 top-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200" setShowPasswordConfInput(!showPasswordConfInput())
}
class="absolute top-2 right-0 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
> >
<Show when={showPasswordConfInput()} fallback={<Eye />}> <Show when={showPasswordConfInput()} fallback={<Eye />}>
<EyeSlash /> <EyeSlash />
@@ -244,8 +261,8 @@ export default function PasswordResetPage() {
newPasswordConfRef && newPasswordConfRef &&
newPasswordConfRef.value.length >= 6 newPasswordConfRef.value.length >= 6
? "" ? ""
: "select-none opacity-0" : "opacity-0 select-none"
} transition-opacity text-center text-red-500 text-sm duration-200 ease-in-out mt-2`} } mt-2 text-center text-sm text-red-500 transition-opacity duration-200 ease-in-out`}
> >
Passwords do not match! Passwords do not match!
</div> </div>
@@ -259,9 +276,9 @@ export default function PasswordResetPage() {
disabled={passwordChangeLoading() || !passwordsMatch()} disabled={passwordChangeLoading() || !passwordsMatch()}
class={`${ class={`${
passwordChangeLoading() || !passwordsMatch() passwordChangeLoading() || !passwordsMatch()
? "bg-zinc-400 cursor-not-allowed" ? "cursor-not-allowed bg-zinc-400"
: "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700" : "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700"
} flex justify-center rounded transition-all duration-300 ease-out my-6 px-4 py-2 text-white font-medium`} } my-6 flex justify-center rounded px-4 py-2 font-medium text-white transition-all duration-300 ease-out`}
> >
{passwordChangeLoading() ? "Setting..." : "Set New Password"} {passwordChangeLoading() ? "Setting..." : "Set New Password"}
</button> </button>
@@ -285,16 +302,16 @@ export default function PasswordResetPage() {
{/* Error Message */} {/* Error Message */}
<Show when={error() && !showRequestNewEmail()}> <Show when={error() && !showRequestNewEmail()}>
<div class="flex justify-center mt-4"> <div class="mt-4 flex justify-center">
<div class="text-red-500 text-sm italic">{error()}</div> <div class="text-sm text-red-500 italic">{error()}</div>
</div> </div>
</Show> </Show>
{/* Token Expired Message */} {/* Token Expired Message */}
<div <div
class={`${ class={`${
showRequestNewEmail() ? "" : "select-none opacity-0" showRequestNewEmail() ? "" : "opacity-0 select-none"
} text-red-500 italic transition-opacity flex justify-center duration-300 ease-in-out px-4`} } flex justify-center px-4 text-red-500 italic transition-opacity duration-300 ease-in-out`}
> >
Token has expired, request a new one{" "} Token has expired, request a new one{" "}
<A <A
@@ -307,15 +324,16 @@ export default function PasswordResetPage() {
{/* Back to Login Link */} {/* Back to Login Link */}
<Show when={!countDown()}> <Show when={!countDown()}>
<div class="flex justify-center mt-6"> <div class="mt-6 flex justify-center">
<A <A
href="/login" href="/login"
class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 underline underline-offset-4 transition-colors" class="text-blue-500 underline underline-offset-4 transition-colors hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
> >
Back to Login Back to Login
</A> </A>
</div> </div>
</Show> </Show>
</div> </div>
</>
); );
} }

View File

@@ -1,5 +1,6 @@
import { createSignal, createEffect, onCleanup, Show } from "solid-js"; import { createSignal, createEffect, onCleanup, Show } from "solid-js";
import { A, useNavigate } from "@solidjs/router"; import { A, useNavigate } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import CountdownCircleTimer from "~/components/CountdownCircleTimer"; import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import { isValidEmail } from "~/lib/validation"; import { isValidEmail } from "~/lib/validation";
import { getClientCookie } from "~/lib/cookies.client"; import { getClientCookie } from "~/lib/cookies.client";
@@ -37,7 +38,10 @@ export default function RequestPasswordResetPage() {
createEffect(() => { createEffect(() => {
const timer = getClientCookie("passwordResetRequested"); const timer = getClientCookie("passwordResetRequested");
if (timer) { if (timer) {
timerInterval = setInterval(() => calcRemainder(timer), 1000) as unknown as number; timerInterval = setInterval(
() => calcRemainder(timer),
1000
) as unknown as number;
onCleanup(() => { onCleanup(() => {
if (timerInterval) { if (timerInterval) {
clearInterval(timerInterval); clearInterval(timerInterval);
@@ -71,7 +75,7 @@ export default function RequestPasswordResetPage() {
const response = await fetch("/api/trpc/auth.requestPasswordReset", { const response = await fetch("/api/trpc/auth.requestPasswordReset", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }), body: JSON.stringify({ email })
}); });
const result = await response.json(); const result = await response.json();
@@ -115,6 +119,12 @@ export default function RequestPasswordResetPage() {
}; };
return ( return (
<>
<Title>Request Password Reset | Michael Freno</Title>
<Meta
name="description"
content="Request a password reset link to regain access to your account. Enter your email to receive reset instructions."
/>
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800"> <div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
<div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100"> <div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
Password Reset Request Password Reset Request
@@ -151,7 +161,7 @@ export default function RequestPasswordResetPage() {
loading() loading()
? "bg-zinc-400" ? "bg-zinc-400"
: "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700" : "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700"
} flex justify-center rounded transition-all duration-300 ease-out my-6 px-4 py-2 text-white font-medium`} } my-6 flex justify-center rounded px-4 py-2 font-medium text-white transition-all duration-300 ease-out`}
> >
{loading() ? "Sending..." : "Request Password Reset"} {loading() ? "Sending..." : "Request Password Reset"}
</button> </button>
@@ -177,28 +187,29 @@ export default function RequestPasswordResetPage() {
{/* Success Message */} {/* Success Message */}
<div <div
class={`${ class={`${
showSuccessMessage() ? "" : "select-none opacity-0" showSuccessMessage() ? "" : "opacity-0 select-none"
} text-green-500 italic transition-opacity flex justify-center duration-300 ease-in-out`} } flex justify-center text-green-500 italic transition-opacity duration-300 ease-in-out`}
> >
If email exists, you will receive an email shortly! If email exists, you will receive an email shortly!
</div> </div>
{/* Error Message */} {/* Error Message */}
<Show when={error()}> <Show when={error()}>
<div class="flex justify-center mt-4"> <div class="mt-4 flex justify-center">
<div class="text-red-500 text-sm italic">{error()}</div> <div class="text-sm text-red-500 italic">{error()}</div>
</div> </div>
</Show> </Show>
{/* Back to Login Link */} {/* Back to Login Link */}
<div class="flex justify-center mt-6"> <div class="mt-6 flex justify-center">
<A <A
href="/login" href="/login"
class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 underline underline-offset-4 transition-colors" class="text-blue-500 underline underline-offset-4 transition-colors hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
> >
Back to Login Back to Login
</A> </A>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -1,11 +1,18 @@
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import SimpleParallax from "~/components/SimpleParallax"; import SimpleParallax from "~/components/SimpleParallax";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark"; import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
export default function LifeAndLineageMarketing() { export default function LifeAndLineageMarketing() {
return ( return (
<>
<Title>Life and Lineage | Michael Freno</Title>
<Meta
name="description"
content="A dark fantasy adventure mobile game. Download Life and Lineage on the App Store and Google Play."
/>
<SimpleParallax> <SimpleParallax>
<div class="flex flex-col items-center justify-center h-full text-white"> <div class="flex h-full flex-col items-center justify-center text-white">
<div> <div>
<img <img
src="/LineageIcon.png" src="/LineageIcon.png"
@@ -15,10 +22,8 @@ export default function LifeAndLineageMarketing() {
class="object-cover object-center" class="object-cover object-center"
/> />
</div> </div>
<h1 class="text-5xl font-bold mb-4 text-center"> <h1 class="mb-4 text-center text-5xl font-bold">Life and Lineage</h1>
Life and Lineage <p class="mb-8 text-xl">A dark fantasy adventure</p>
</h1>
<p class="text-xl mb-8">A dark fantasy adventure</p>
<div class="flex space-x-4"> <div class="flex space-x-4">
<a <a
class="my-auto transition-all duration-200 ease-out active:scale-95" class="my-auto transition-all duration-200 ease-out active:scale-95"
@@ -42,5 +47,6 @@ export default function LifeAndLineageMarketing() {
</div> </div>
</div> </div>
</SimpleParallax> </SimpleParallax>
</>
); );
} }

View File

@@ -1,7 +1,14 @@
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
export default function PrivacyPolicy() { export default function PrivacyPolicy() {
return ( return (
<>
<Title>Privacy Policy - Life and Lineage | Michael Freno</Title>
<Meta
name="description"
content="Privacy policy for Life and Lineage mobile game, outlining data collection, usage, and user rights."
/>
<div class="min-h-screen px-[8vw] py-[10vh]"> <div class="min-h-screen px-[8vw] py-[10vh]">
<div class="py-4 text-xl">Life and Lineage&apos;s Privacy Policy</div> <div class="py-4 text-xl">Life and Lineage&apos;s Privacy Policy</div>
<div class="py-2">Last Updated: October 22, 2024</div> <div class="py-2">Last Updated: October 22, 2024</div>
@@ -18,13 +25,13 @@ export default function PrivacyPolicy() {
</div> </div>
<div class="pl-4"> <div class="pl-4">
<div class="pb-2"> <div class="pb-2">
<div class="-ml-6">(a) Collection of Personal Data:</div> Life and <div class="-ml-6">(a) Collection of Personal Data:</div> Life
Lineage collects and stores personal data only if users opt to use and Lineage collects and stores personal data only if users opt
the remote saving feature. The information collected includes to use the remote saving feature. The information collected
email address, and if using an OAuth provider - first name, and includes email address, and if using an OAuth provider - first
last name. This information is used solely for the purpose of name, and last name. This information is used solely for the
providing and managing the remote saving feature. It is and never purpose of providing and managing the remote saving feature. It
will be shared with a third party. is and never will be shared with a third party.
</div> </div>
<div class="pb-2"> <div class="pb-2">
<div class="-ml-6">(b) Data Removal:</div> Users can request the <div class="-ml-6">(b) Data Removal:</div> Users can request the
@@ -59,11 +66,11 @@ export default function PrivacyPolicy() {
<span class="-ml-4 pr-2">3.</span> Security <span class="-ml-4 pr-2">3.</span> Security
</div> </div>
<div class="pb-2 pl-4"> <div class="pb-2 pl-4">
<div class="-ml-6">(a) Data Protection:</div>Life and Lineage takes <div class="-ml-6">(a) Data Protection:</div>Life and Lineage
appropriate measures to protect the personal information of users takes appropriate measures to protect the personal information of
who opt for the remote saving feature. We implement users who opt for the remote saving feature. We implement
industry-standard security protocols to prevent unauthorized access, industry-standard security protocols to prevent unauthorized
disclosure, alteration, or destruction of user data. access, disclosure, alteration, or destruction of user data.
</div> </div>
</div> </div>
@@ -85,8 +92,8 @@ export default function PrivacyPolicy() {
</div> </div>
<div class="pb-2 pl-4"> <div class="pb-2 pl-4">
<div class="-ml-6">(a) Reaching Out:</div> If there are any <div class="-ml-6">(a) Reaching Out:</div> If there are any
questions or comments regarding this privacy policy, you can contact questions or comments regarding this privacy policy, you can
us{" "} contact us{" "}
<A href="/contact" class="text-blue hover-underline-animation"> <A href="/contact" class="text-blue hover-underline-animation">
here here
</A> </A>
@@ -95,5 +102,6 @@ export default function PrivacyPolicy() {
</div> </div>
</ol> </ol>
</div> </div>
</>
); );
} }

View File

@@ -1,7 +1,14 @@
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
export default function PrivacyPolicy() { export default function PrivacyPolicy() {
return ( return (
<>
<Title>Privacy Policy - Shapes with Abigail | Michael Freno</Title>
<Meta
name="description"
content="Privacy policy for Shapes with Abigail app, explaining our commitment to child safety and non-collection of personal data."
/>
<div class="bg-zinc-100 dark:bg-zinc-900"> <div class="bg-zinc-100 dark:bg-zinc-900">
<div class="min-h-screen px-[8vw] py-[8vh]"> <div class="min-h-screen px-[8vw] py-[8vh]">
<div class="py-4 text-xl"> <div class="py-4 text-xl">
@@ -24,9 +31,7 @@ export default function PrivacyPolicy() {
</div> </div>
<div class="pl-4"> <div class="pl-4">
<div class="pb-2"> <div class="pb-2">
<div class="-ml-6"> <div class="-ml-6">(a) Non-Collection of Personal Data:</div>{" "}
(a) Non-Collection of Personal Data:
</div>{" "}
Shapes with Abigail! does not collect nor store personal data. Shapes with Abigail! does not collect nor store personal data.
We respect the privacy of our users, especially considering We respect the privacy of our users, especially considering
the age of our users. We believe that no information, whether the age of our users. We believe that no information, whether
@@ -41,10 +46,10 @@ export default function PrivacyPolicy() {
<span class="-ml-4 pr-2">2.</span> Third-Party Access <span class="-ml-4 pr-2">2.</span> Third-Party Access
</div> </div>
<div class="pb-2 pl-4"> <div class="pb-2 pl-4">
<div class="-ml-6">(a) No Third-Party Access:</div> Since we <div class="-ml-6">(a) No Third-Party Access:</div> Since we do
do not collect or store any user data, there is no possibility not collect or store any user data, there is no possibility of
of sharing or selling our users&apos; information to third sharing or selling our users&apos; information to third parties.
parties. Our priority is the safety and privacy of our users. Our priority is the safety and privacy of our users.
</div> </div>
</div> </div>
@@ -63,14 +68,13 @@ export default function PrivacyPolicy() {
<div class="py-2"> <div class="py-2">
<div class="pb-2 text-lg"> <div class="pb-2 text-lg">
<span class="-ml-4 pr-2">4.</span> Changes to the Privacy <span class="-ml-4 pr-2">4.</span> Changes to the Privacy Policy
Policy
</div> </div>
<div class="pb-2 pl-4"> <div class="pb-2 pl-4">
<div class="-ml-6">(a) Updates:</div> We may update this <div class="-ml-6">(a) Updates:</div> We may update this privacy
privacy policy periodically. Any changes to this privacy policy policy periodically. Any changes to this privacy policy will be
will be posted on this page. However, since we do not collect posted on this page. However, since we do not collect any
any personal data, these updates are likely to be insignificant. personal data, these updates are likely to be insignificant.
</div> </div>
</div> </div>
@@ -94,5 +98,6 @@ export default function PrivacyPolicy() {
</ol> </ol>
</div> </div>
</div> </div>
</>
); );
} }

View File

@@ -1,7 +1,14 @@
import { Title } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
export default function Resume() { export default function Resume() {
return ( return (
<>
<Title>Resume | Michael Freno</Title>
<Meta
name="description"
content="View Michael Freno's resume - Software Engineer with expertise in full-stack development, game development, and open source."
/>
<main class="flex h-screen w-full flex-col"> <main class="flex h-screen w-full flex-col">
<Title>Resume - Freno.dev</Title> <Title>Resume - Freno.dev</Title>
<div class="flex h-full w-full items-center justify-center"> <div class="flex h-full w-full items-center justify-center">
@@ -12,5 +19,6 @@ export default function Resume() {
/> />
</div> </div>
</main> </main>
</>
); );
} }

View File

@@ -1,7 +1,12 @@
import { createSignal } from "solid-js"; import { createSignal } from "solid-js";
import { Title, Meta } from "@solidjs/meta";
import Input from "~/components/ui/Input"; import Input from "~/components/ui/Input";
import Button from "~/components/ui/Button"; import Button from "~/components/ui/Button";
import { isValidEmail, validatePassword, passwordsMatch } from "~/lib/validation"; import {
isValidEmail,
validatePassword,
passwordsMatch
} from "~/lib/validation";
/** /**
* Test page to validate Task 01 components and utilities * Test page to validate Task 01 components and utilities
@@ -36,24 +41,30 @@ export default function TestUtilsPage() {
setLoading(true); setLoading(true);
// Simulate API call // Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
alert(`Form submitted!\nEmail: ${email()}\nPassword: ${password()}`); alert(`Form submitted!\nEmail: ${email()}\nPassword: ${password()}`);
setLoading(false); setLoading(false);
}; };
return ( return (
<>
<Title>Utility Testing | Michael Freno</Title>
<Meta
name="description"
content="Testing page for form components and validation utilities."
/>
<main class="min-h-screen bg-gray-100 p-8"> <main class="min-h-screen bg-gray-100 p-8">
<div class="max-w-2xl mx-auto"> <div class="mx-auto max-w-2xl">
<div class="bg-white rounded-lg shadow-lg p-6 mb-6"> <div class="mb-6 rounded-lg bg-white p-6 shadow-lg">
<h1 class="text-3xl font-bold mb-2">Task 01 - Utility Testing</h1> <h1 class="mb-2 text-3xl font-bold">Task 01 - Utility Testing</h1>
<p class="text-gray-600 mb-4"> <p class="mb-4 text-gray-600">
Testing shared utilities, types, and UI components Testing shared utilities, types, and UI components
</p> </p>
</div> </div>
<div class="bg-white rounded-lg shadow p-6 mb-6"> <div class="mb-6 rounded-lg bg-white p-6 shadow">
<h2 class="text-xl font-bold mb-4">Form Components & Validation</h2> <h2 class="mb-4 text-xl font-bold">Form Components & Validation</h2>
<form onSubmit={handleSubmit} class="space-y-4"> <form onSubmit={handleSubmit} class="space-y-4">
<Input <Input
@@ -130,30 +141,42 @@ export default function TestUtilsPage() {
</form> </form>
</div> </div>
<div class="bg-white rounded-lg shadow p-6"> <div class="rounded-lg bg-white p-6 shadow">
<h2 class="text-xl font-bold mb-4">Validation Status</h2> <h2 class="mb-4 text-xl font-bold">Validation Status</h2>
<div class="space-y-2 text-sm"> <div class="space-y-2 text-sm">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class={`w-3 h-3 rounded-full ${isValidEmail(email()) ? "bg-green-500" : "bg-gray-300"}`} /> <span
class={`h-3 w-3 rounded-full ${isValidEmail(email()) ? "bg-green-500" : "bg-gray-300"}`}
/>
<span>Email Valid: {isValidEmail(email()) ? "✓" : "✗"}</span> <span>Email Valid: {isValidEmail(email()) ? "✓" : "✗"}</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class={`w-3 h-3 rounded-full ${validatePassword(password()).isValid ? "bg-green-500" : "bg-gray-300"}`} /> <span
<span>Password Valid: {validatePassword(password()).isValid ? "✓" : "✗"}</span> class={`h-3 w-3 rounded-full ${validatePassword(password()).isValid ? "bg-green-500" : "bg-gray-300"}`}
/>
<span>
Password Valid:{" "}
{validatePassword(password()).isValid ? "✓" : "✗"}
</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class={`w-3 h-3 rounded-full ${passwordsMatch(password(), passwordConf()) ? "bg-green-500" : "bg-gray-300"}`} /> <span
<span>Passwords Match: {passwordsMatch(password(), passwordConf()) ? "✓" : "✗"}</span> class={`h-3 w-3 rounded-full ${passwordsMatch(password(), passwordConf()) ? "bg-green-500" : "bg-gray-300"}`}
/>
<span>
Passwords Match:{" "}
{passwordsMatch(password(), passwordConf()) ? "✓" : "✗"}
</span>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-blue-50 border border-blue-200 rounded p-4 mt-6"> <div class="mt-6 rounded border border-blue-200 bg-blue-50 p-4">
<h3 class="font-bold text-blue-800 mb-2"> Task 01 Complete</h3> <h3 class="mb-2 font-bold text-blue-800"> Task 01 Complete</h3>
<ul class="text-sm text-blue-700 space-y-1"> <ul class="space-y-1 text-sm text-blue-700">
<li> User types created</li> <li> User types created</li>
<li> Cookie utilities created</li> <li> Cookie utilities created</li>
<li> Validation helpers created</li> <li> Validation helpers created</li>
@@ -165,5 +188,6 @@ export default function TestUtilsPage() {
</div> </div>
</div> </div>
</main> </main>
</>
); );
} }

View File

@@ -1,5 +1,6 @@
import { createSignal, For, Show } from "solid-js"; import { createSignal, For, Show } from "solid-js";
import { query, createAsync } from "@solidjs/router"; import { query, createAsync } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import { getRequestEvent } from "solid-js/web"; import { getRequestEvent } from "solid-js/web";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
@@ -932,6 +933,12 @@ export default function TestPage() {
}; };
return ( return (
<>
<Title>API Testing | Michael Freno</Title>
<Meta
name="description"
content="tRPC API testing dashboard for developers to test endpoints and verify functionality."
/>
<Show <Show
when={authState()?.privilegeLevel === "admin"} when={authState()?.privilegeLevel === "admin"}
fallback={ fallback={
@@ -946,7 +953,9 @@ export default function TestPage() {
<main class="min-h-screen p-8"> <main class="min-h-screen p-8">
<div class="mx-auto max-w-6xl"> <div class="mx-auto max-w-6xl">
<div class="bg-surface0 mb-6 rounded-lg p-6 shadow-lg"> <div class="bg-surface0 mb-6 rounded-lg p-6 shadow-lg">
<h1 class="mb-2 text-3xl font-bold">tRPC API Testing Dashboard</h1> <h1 class="mb-2 text-3xl font-bold">
tRPC API Testing Dashboard
</h1>
<p class="text-text mb-4"> <p class="text-text mb-4">
Complete API coverage: Example, Auth, Database, User, Misc, and Complete API coverage: Example, Auth, Database, User, Misc, and
Lineage routers Lineage routers
@@ -994,7 +1003,8 @@ export default function TestPage() {
<For each={section.endpoints}> <For each={section.endpoints}>
{(endpoint) => { {(endpoint) => {
const key = `${endpoint.router}.${endpoint.procedure}`; const key = `${endpoint.router}.${endpoint.procedure}`;
const hasInput = endpoint.sampleInput !== undefined; const hasInput =
endpoint.sampleInput !== undefined;
const displayInput = () => { const displayInput = () => {
if (inputEdits()[key]) { if (inputEdits()[key]) {
return inputEdits()[key]; return inputEdits()[key];
@@ -1069,7 +1079,10 @@ export default function TestPage() {
<textarea <textarea
value={displayInput()} value={displayInput()}
onInput={(e) => onInput={(e) =>
updateInput(key, e.currentTarget.value) updateInput(
key,
e.currentTarget.value
)
} }
class="border-lavender bg-crust min-h-[100px] w-full rounded border p-2 font-mono text-xs" class="border-lavender bg-crust min-h-[100px] w-full rounded border p-2 font-mono text-xs"
spellcheck={false} spellcheck={false}
@@ -1096,7 +1109,11 @@ export default function TestPage() {
Response: Response:
</p> </p>
<pre class="max-h-60 overflow-auto text-xs text-green-400"> <pre class="max-h-60 overflow-auto text-xs text-green-400">
{JSON.stringify(results()[key], null, 2)} {JSON.stringify(
results()[key],
null,
2
)}
</pre> </pre>
</div> </div>
</Show> </Show>
@@ -1118,18 +1135,20 @@ export default function TestPage() {
<div class="space-y-4 text-base"> <div class="space-y-4 text-base">
<div> <div>
<h3 class="mb-2 text-lg font-semibold">🟢 No Auth Required</h3> <h3 class="mb-2 text-lg font-semibold">
🟢 No Auth Required
</h3>
<ul class="ml-6 list-disc space-y-1 text-sm"> <ul class="ml-6 list-disc space-y-1 text-sm">
<li> <li>
<strong>Example Router</strong> - Hello endpoint <strong>Example Router</strong> - Hello endpoint
</li> </li>
<li> <li>
<strong>Lineage JSON Service</strong> - All 6 endpoints work <strong>Lineage JSON Service</strong> - All 6 endpoints
immediately work immediately
</li> </li>
<li> <li>
<strong>Database</strong> - All endpoints (comments, posts, <strong>Database</strong> - All endpoints (comments,
users, reactions, likes) posts, users, reactions, likes)
</li> </li>
<li> <li>
<strong>Misc</strong> - Downloads, S3 operations, password <strong>Misc</strong> - Downloads, S3 operations, password
@@ -1179,30 +1198,32 @@ export default function TestPage() {
<strong>Example Router</strong> - Admin Dashboard <strong>Example Router</strong> - Admin Dashboard
</li> </li>
<li> <li>
<strong>Lineage Maintenance</strong> - Find Loose Databases, <strong>Lineage Maintenance</strong> - Find Loose
Cleanup Expired Databases, Cleanup Expired
</li> </li>
</ul> </ul>
</div> </div>
<div> <div>
<h3 class="mb-2 text-lg font-semibold">📝 Typical Workflows</h3> <h3 class="mb-2 text-lg font-semibold">
📝 Typical Workflows
</h3>
<ol class="ml-6 list-decimal space-y-2 text-sm"> <ol class="ml-6 list-decimal space-y-2 text-sm">
<li> <li>
<strong>Test public endpoints:</strong> Start with Example <strong>Test public endpoints:</strong> Start with Example
Hello, Lineage JSON Service, or Database queries Hello, Lineage JSON Service, or Database queries
</li> </li>
<li> <li>
<strong>OAuth flow:</strong> Use Auth router callbacks with <strong>OAuth flow:</strong> Use Auth router callbacks
OAuth codes from GitHub/Google with OAuth codes from GitHub/Google
</li> </li>
<li> <li>
<strong>Email auth flow:</strong> Register verify email <strong>Email auth flow:</strong> Register verify email
login use JWT login use JWT
</li> </li>
<li> <li>
<strong>Blog/Project management:</strong> Create posts add <strong>Blog/Project management:</strong> Create posts
comments/likes upload images via S3 add comments/likes upload images via S3
</li> </li>
<li> <li>
<strong>Lineage game data:</strong> Fetch JSON data <strong>Lineage game data:</strong> Fetch JSON data
@@ -1214,9 +1235,9 @@ export default function TestPage() {
<div class="border-rosewater bg-rosewater mt-4 rounded border p-4"> <div class="border-rosewater bg-rosewater mt-4 rounded border p-4">
<p class="text-crust text-sm"> <p class="text-crust text-sm">
<strong>Note:</strong> Some endpoints require specific setup <strong>Note:</strong> Some endpoints require specific setup
(e.g., OAuth codes, existing database records, valid S3 keys). (e.g., OAuth codes, existing database records, valid S3
Check the sample input to understand what data each endpoint keys). Check the sample input to understand what data each
expects. endpoint expects.
</p> </p>
</div> </div>
</div> </div>
@@ -1224,5 +1245,6 @@ export default function TestPage() {
</div> </div>
</main> </main>
</Show> </Show>
</>
); );
} }