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 (
<main> <>
<Title>About</Title> <Title>About | Michael Freno</Title>
<h1>About</h1> <Meta
</main> name="description"
content="Learn more about Michael Freno - Software Engineer, game developer, and open source contributor."
/>
<main>
<h1>About</h1>
</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,389 +454,407 @@ export default function AccountPage() {
}; };
return ( return (
<div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36"> <>
<div class="pt-24"> <Title>Account | Michael Freno</Title>
<Show <Meta
when={!loading() && user()} name="description"
fallback={ content="Manage your account settings, update profile information, and configure preferences."
<div class="mt-[35vh] flex w-full justify-center"> />
<div class="text-text text-xl">Loading...</div>
</div>
}
>
{(currentUser) => (
<>
<div class="text-text mb-8 text-center text-3xl font-bold">
Account Settings
</div>
{/* Profile Image Section */} <div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36">
<div class="mx-auto mb-8 flex max-w-md justify-center"> <div class="pt-24">
<div class="flex flex-col py-4"> <Show
<div class="mb-2 text-center text-lg font-semibold"> when={!loading() && user()}
Profile Image fallback={
</div> <div class="mt-[35vh] flex w-full justify-center">
<div class="flex items-start"> <div class="text-text text-xl">Loading...</div>
<Dropzone </div>
onDrop={handleImageDrop} }
acceptedFiles="image/jpg, image/jpeg, image/png" >
fileHolder={profileImageHolder()} {(currentUser) => (
preSet={preSetHolder() || currentUser().image || null} <>
/> <div class="text-text mb-8 text-center text-3xl font-bold">
<button Account Settings
type="button" </div>
onClick={removeImage}
class="z-20 -ml-6 h-fit rounded-full transition-all hover:brightness-125" {/* Profile Image Section */}
> <div class="mx-auto mb-8 flex max-w-md justify-center">
<XCircle <div class="flex flex-col py-4">
height={36} <div class="mb-2 text-center text-lg font-semibold">
width={36} Profile Image
stroke="currentColor" </div>
strokeWidth={1} <div class="flex items-start">
<Dropzone
onDrop={handleImageDrop}
acceptedFiles="image/jpg, image/jpeg, image/png"
fileHolder={profileImageHolder()}
preSet={preSetHolder() || currentUser().image || null}
/> />
</button> <button
</div> type="button"
<form onSubmit={setUserImage}> onClick={removeImage}
<button class="z-20 -ml-6 h-fit rounded-full transition-all hover:brightness-125"
type="submit" >
disabled={ <XCircle
profileImageSetLoading() || !profileImageStateChange() height={36}
} width={36}
class={`${ stroke="currentColor"
profileImageSetLoading() || !profileImageStateChange() strokeWidth={1}
? "bg-blue cursor-not-allowed brightness-75" />
: "bg-blue hover:brightness-125 active:scale-90" </button>
} mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
>
{profileImageSetLoading() ? "Uploading..." : "Set Image"}
</button>
</form>
<Show when={showImageSuccess()}>
<div class="text-green mt-2 text-center text-sm">
Profile image updated!
</div> </div>
</Show> <form onSubmit={setUserImage}>
</div> <button
</div> type="submit"
disabled={
<hr class="mx-auto mb-8 max-w-4xl" /> profileImageSetLoading() || !profileImageStateChange()
}
{/* Email Section */} class={`${
<div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2"> profileImageSetLoading() || !profileImageStateChange()
<div class="flex items-center justify-center text-lg md:justify-normal"> ? "bg-blue cursor-not-allowed brightness-75"
<div class="flex flex-col lg:flex-row"> : "bg-blue hover:brightness-125 active:scale-90"
<div class="pr-1 font-semibold whitespace-nowrap"> } mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
Current email: >
</div> {profileImageSetLoading()
{currentUser().email ? ( ? "Uploading..."
<span>{currentUser().email}</span> : "Set Image"}
) : ( </button>
<span class="font-light italic underline underline-offset-4"> </form>
None Set <Show when={showImageSuccess()}>
</span> <div class="text-green mt-2 text-center text-sm">
)} Profile image updated!
</div> </div>
<Show </Show>
when={currentUser().email && !currentUser().emailVerified}
>
<button
onClick={sendEmailVerification}
class="text-red ml-2 text-sm underline transition-all hover:brightness-125"
>
Verify Email
</button>
</Show>
</div>
<form onSubmit={setEmailTrigger} class="mx-auto">
<div class="input-group mx-4">
<input
ref={emailRef}
type="email"
required
disabled={
emailButtonLoading() ||
(currentUser().email !== null &&
!currentUser().emailVerified)
}
placeholder=" "
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Set New Email</label>
</div>
<div class="flex justify-end">
<button
type="submit"
disabled={
emailButtonLoading() ||
(currentUser().email !== null &&
!currentUser().emailVerified)
}
class={`${
emailButtonLoading() ||
(currentUser().email !== null &&
!currentUser().emailVerified)
? "bg-blue cursor-not-allowed brightness-75"
: "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`}
>
{emailButtonLoading() ? "Submitting..." : "Submit"}
</button>
</div>
<Show when={showEmailSuccess()}>
<div class="text-green mt-2 text-center text-sm">
Email updated!
</div>
</Show>
</form>
{/* Display Name Section */}
<div class="flex items-center justify-center text-lg md:justify-normal">
<div class="flex flex-col lg:flex-row">
<div class="pr-1 font-semibold whitespace-nowrap">
Display Name:
</div>
{currentUser().displayName ? (
<span>{currentUser().displayName}</span>
) : (
<span class="font-light italic underline underline-offset-4">
None Set
</span>
)}
</div> </div>
</div> </div>
<form onSubmit={setDisplayNameTrigger} class="mx-auto"> <hr class="mx-auto mb-8 max-w-4xl" />
<div class="input-group mx-4">
<input {/* Email Section */}
ref={displayNameRef} <div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2">
type="text" <div class="flex items-center justify-center text-lg md:justify-normal">
required <div class="flex flex-col lg:flex-row">
disabled={displayNameButtonLoading()} <div class="pr-1 font-semibold whitespace-nowrap">
placeholder=" " Current email:
class="underlinedInput bg-transparent" </div>
/> {currentUser().email ? (
<span class="bar"></span> <span>{currentUser().email}</span>
<label class="underlinedInputLabel"> ) : (
Set {currentUser().displayName ? "New " : ""}Display Name <span class="font-light italic underline underline-offset-4">
</label> None Set
</div> </span>
<div class="flex justify-end"> )}
<button
type="submit"
disabled={displayNameButtonLoading()}
class={`${
displayNameButtonLoading()
? "bg-blue cursor-not-allowed brightness-75"
: "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`}
>
{displayNameButtonLoading() ? "Submitting..." : "Submit"}
</button>
</div>
<Show when={showDisplayNameSuccess()}>
<div class="text-green mt-2 text-center text-sm">
Display name updated!
</div> </div>
</Show> <Show
</form> when={currentUser().email && !currentUser().emailVerified}
</div> >
<button
{/* Password Change/Set Section */} onClick={sendEmailVerification}
<form class="text-red ml-2 text-sm underline transition-all hover:brightness-125"
onSubmit={handlePasswordSubmit} >
class="mt-8 flex w-full justify-center" Verify Email
> </button>
<div class="flex w-full max-w-md flex-col justify-center"> </Show>
<div class="mb-4 text-center text-xl font-semibold">
{currentUser().hasPassword
? "Change Password"
: "Set Password"}
</div> </div>
<Show when={currentUser().hasPassword}> <form onSubmit={setEmailTrigger} class="mx-auto">
<div class="input-group relative mx-4 mb-6"> <div class="input-group mx-4">
<input <input
ref={oldPasswordRef} ref={emailRef}
type={showOldPasswordInput() ? "text" : "password"} type="email"
required required
disabled={
emailButtonLoading() ||
(currentUser().email !== null &&
!currentUser().emailVerified)
}
placeholder=" "
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Set New Email</label>
</div>
<div class="flex justify-end">
<button
type="submit"
disabled={
emailButtonLoading() ||
(currentUser().email !== null &&
!currentUser().emailVerified)
}
class={`${
emailButtonLoading() ||
(currentUser().email !== null &&
!currentUser().emailVerified)
? "bg-blue cursor-not-allowed brightness-75"
: "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`}
>
{emailButtonLoading() ? "Submitting..." : "Submit"}
</button>
</div>
<Show when={showEmailSuccess()}>
<div class="text-green mt-2 text-center text-sm">
Email updated!
</div>
</Show>
</form>
{/* Display Name Section */}
<div class="flex items-center justify-center text-lg md:justify-normal">
<div class="flex flex-col lg:flex-row">
<div class="pr-1 font-semibold whitespace-nowrap">
Display Name:
</div>
{currentUser().displayName ? (
<span>{currentUser().displayName}</span>
) : (
<span class="font-light italic underline underline-offset-4">
None Set
</span>
)}
</div>
</div>
<form onSubmit={setDisplayNameTrigger} class="mx-auto">
<div class="input-group mx-4">
<input
ref={displayNameRef}
type="text"
required
disabled={displayNameButtonLoading()}
placeholder=" "
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
Set {currentUser().displayName ? "New " : ""}Display
Name
</label>
</div>
<div class="flex justify-end">
<button
type="submit"
disabled={displayNameButtonLoading()}
class={`${
displayNameButtonLoading()
? "bg-blue cursor-not-allowed brightness-75"
: "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`}
>
{displayNameButtonLoading()
? "Submitting..."
: "Submit"}
</button>
</div>
<Show when={showDisplayNameSuccess()}>
<div class="text-green mt-2 text-center text-sm">
Display name updated!
</div>
</Show>
</form>
</div>
{/* Password Change/Set Section */}
<form
onSubmit={handlePasswordSubmit}
class="mt-8 flex w-full justify-center"
>
<div class="flex w-full max-w-md flex-col justify-center">
<div class="mb-4 text-center text-xl font-semibold">
{currentUser().hasPassword
? "Change Password"
: "Set Password"}
</div>
<Show when={currentUser().hasPassword}>
<div class="input-group relative mx-4 mb-6">
<input
ref={oldPasswordRef}
type={showOldPasswordInput() ? "text" : "password"}
required
disabled={passwordChangeLoading()}
placeholder=" "
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Old Password</label>
<button
type="button"
onClick={() =>
setShowOldPasswordInput(!showOldPasswordInput())
}
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
>
<Show
when={showOldPasswordInput()}
fallback={<Eye />}
>
<EyeSlash />
</Show>
</button>
</div>
</Show>
<div class="input-group relative mx-4 mb-2">
<input
ref={newPasswordRef}
type={showPasswordInput() ? "text" : "password"}
required
onInput={handleNewPasswordChange}
onBlur={handlePasswordBlur}
disabled={passwordChangeLoading()} disabled={passwordChangeLoading()}
placeholder=" " placeholder=" "
class="underlinedInput w-full bg-transparent pr-10" class="underlinedInput w-full bg-transparent pr-10"
/> />
<span class="bar"></span> <span class="bar"></span>
<label class="underlinedInputLabel">Old Password</label> <label class="underlinedInputLabel">New Password</label>
<button <button
type="button" type="button"
onClick={() => onClick={() =>
setShowOldPasswordInput(!showOldPasswordInput()) 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={showOldPasswordInput()} fallback={<Eye />}> <Show when={showPasswordInput()} fallback={<Eye />}>
<EyeSlash /> <EyeSlash />
</Show> </Show>
</button> </button>
</div> </div>
</Show>
<div class="input-group relative mx-4 mb-2"> <Show when={showPasswordLengthWarning()}>
<input <div class="text-red mb-4 text-center text-sm">
ref={newPasswordRef} Password too short! Min Length: 8
type={showPasswordInput() ? "text" : "password"}
required
onInput={handleNewPasswordChange}
onBlur={handlePasswordBlur}
disabled={passwordChangeLoading()}
placeholder=" "
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">New Password</label>
<button
type="button"
onClick={() => setShowPasswordInput(!showPasswordInput())}
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
>
<Show when={showPasswordInput()} fallback={<Eye />}>
<EyeSlash />
</Show>
</button>
</div>
<Show when={showPasswordLengthWarning()}>
<div class="text-red mb-4 text-center text-sm">
Password too short! Min Length: 8
</div>
</Show>
<div class="input-group relative mx-4 mb-2">
<input
ref={newPasswordConfRef}
type={showPasswordConfInput() ? "text" : "password"}
required
onInput={handlePasswordConfChange}
disabled={passwordChangeLoading()}
placeholder=" "
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
Password Confirmation
</label>
<button
type="button"
onClick={() =>
setShowPasswordConfInput(!showPasswordConfInput())
}
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
>
<Show when={showPasswordConfInput()} fallback={<Eye />}>
<EyeSlash />
</Show>
</button>
</div>
<Show
when={
!passwordsMatch() &&
passwordLengthSufficient() &&
newPasswordConfRef &&
newPasswordConfRef.value.length >= 6
}
>
<div class="text-red mb-4 text-center text-sm">
Passwords do not match!
</div>
</Show>
<button
type="submit"
disabled={passwordChangeLoading() || !passwordsMatch()}
class={`${
passwordChangeLoading() || !passwordsMatch()
? "bg-blue cursor-not-allowed brightness-75"
: "bg-blue hover:brightness-125 active:scale-90"
} my-6 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
>
{passwordChangeLoading() ? "Setting..." : "Set"}
</button>
<Show when={passwordError()}>
<div class="text-red text-center text-sm">
{currentUser().hasPassword
? "Password did not match record"
: "Error setting password"}
</div>
</Show>
<Show when={showPasswordSuccess()}>
<div class="text-green text-center text-sm">
Password {currentUser().hasPassword ? "changed" : "set"}{" "}
successfully!
</div>
</Show>
</div>
</form>
<hr class="mt-8 mb-8" />
{/* Delete Account Section */}
<div class="mx-auto max-w-2xl py-8">
<div class="bg-red w-full rounded-md px-6 pt-8 pb-4 shadow-md brightness-75">
<div class="pb-4 text-center text-xl font-semibold">
Delete Account
</div>
<div class="text-crust mb-4 text-center text-sm">
Warning: This will delete all account information and is
irreversible
</div>
<form onSubmit={deleteAccountTrigger}>
<div class="flex w-full justify-center">
<div class="input-group delete mx-4">
<input
ref={deleteAccountPasswordRef}
type="password"
required
disabled={deleteAccountButtonLoading()}
placeholder=" "
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
Enter Password
</label>
</div> </div>
</Show>
<div class="input-group relative mx-4 mb-2">
<input
ref={newPasswordConfRef}
type={showPasswordConfInput() ? "text" : "password"}
required
onInput={handlePasswordConfChange}
disabled={passwordChangeLoading()}
placeholder=" "
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
Password Confirmation
</label>
<button
type="button"
onClick={() =>
setShowPasswordConfInput(!showPasswordConfInput())
}
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
>
<Show when={showPasswordConfInput()} fallback={<Eye />}>
<EyeSlash />
</Show>
</button>
</div> </div>
<Show
when={
!passwordsMatch() &&
passwordLengthSufficient() &&
newPasswordConfRef &&
newPasswordConfRef.value.length >= 6
}
>
<div class="text-red mb-4 text-center text-sm">
Passwords do not match!
</div>
</Show>
<button <button
type="submit" type="submit"
disabled={deleteAccountButtonLoading()} disabled={passwordChangeLoading() || !passwordsMatch()}
class={`${ class={`${
deleteAccountButtonLoading() passwordChangeLoading() || !passwordsMatch()
? "bg-red cursor-not-allowed brightness-75" ? "bg-blue cursor-not-allowed brightness-75"
: "bg-red hover:brightness-125 active:scale-90" : "bg-blue hover:brightness-125 active:scale-90"
} mx-auto mt-4 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`} } my-6 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
> >
{deleteAccountButtonLoading() {passwordChangeLoading() ? "Setting..." : "Set"}
? "Deleting..."
: "Delete Account"}
</button> </button>
<Show when={passwordDeletionError()}> <Show when={passwordError()}>
<div class="text-red mt-2 text-center text-sm"> <div class="text-red text-center text-sm">
Password did not match record {currentUser().hasPassword
? "Password did not match record"
: "Error setting password"}
</div> </div>
</Show> </Show>
</form>
<Show when={showPasswordSuccess()}>
<div class="text-green text-center text-sm">
Password {currentUser().hasPassword ? "changed" : "set"}{" "}
successfully!
</div>
</Show>
</div>
</form>
<hr class="mt-8 mb-8" />
{/* Delete Account Section */}
<div class="mx-auto max-w-2xl py-8">
<div class="bg-red w-full rounded-md px-6 pt-8 pb-4 shadow-md brightness-75">
<div class="pb-4 text-center text-xl font-semibold">
Delete Account
</div>
<div class="text-crust mb-4 text-center text-sm">
Warning: This will delete all account information and is
irreversible
</div>
<form onSubmit={deleteAccountTrigger}>
<div class="flex w-full justify-center">
<div class="input-group delete mx-4">
<input
ref={deleteAccountPasswordRef}
type="password"
required
disabled={deleteAccountButtonLoading()}
placeholder=" "
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
Enter Password
</label>
</div>
</div>
<button
type="submit"
disabled={deleteAccountButtonLoading()}
class={`${
deleteAccountButtonLoading()
? "bg-red cursor-not-allowed brightness-75"
: "bg-red hover:brightness-125 active:scale-90"
} mx-auto mt-4 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
>
{deleteAccountButtonLoading()
? "Deleting..."
: "Delete Account"}
</button>
<Show when={passwordDeletionError()}>
<div class="text-red mt-2 text-center text-sm">
Password did not match record
</div>
</Show>
</form>
</div>
</div> </div>
</div> </>
</> )}
)} </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 (
<div class="pt-20"> <>
<div class="mx-auto p-4 md:p-6 lg:p-12"> <Title>Account Deletion - Life and Lineage | Michael Freno</Title>
<div class="w-full justify-center text-text"> <Meta
<div class="text-xl"> name="description"
<em>What will happen</em>: 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="mx-auto p-4 md:p-6 lg:p-12">
<div class="text-text w-full justify-center">
<div class="text-xl">
<em>What will happen</em>:
</div>
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
cancellation of the account deletion. Once the grace period ends,
the account's entry in our central database will be completely
removed, and your individual database storing your remote saves will
also be deleted. No data related to the account is retained in any
way.
</div> </div>
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
cancellation of the account deletion. Once the grace period ends, the
account's entry in our central database will be completely removed,
and your individual database storing your remote saves will also be
deleted. No data related to the account is retained in any way.
</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,147 +22,155 @@ export default function DownloadsPage() {
}; };
return ( return (
<div class="bg-base min-h-screen pt-[15vh] pb-12"> <>
<div class="text-text text-center text-3xl tracking-widest"> <Title>Downloads | Michael Freno</Title>
Downloads <Meta
</div> name="description"
content="Download Life and Lineage, Shapes with Abigail, and Cork for macOS. Available on iOS, Android, and macOS."
/>
<div class="pt-12"> <div class="bg-base min-h-screen pt-[15vh] pb-12">
<div class="text-text text-center text-xl tracking-wide"> <div class="text-text text-center text-3xl tracking-widest">
Life and Lineage Downloads
<br />
</div>
<div class="flex justify-evenly md:mx-[25vw]">
<div class="flex w-1/3 flex-col">
<div class="text-center text-lg">Android (apk only)</div>
<button
onClick={() => download("lineage")}
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
>
Download APK
</button>
<div class="mt-2 text-center text-sm italic">
Note the android version is not well tested, and has performance
issues.
</div>
<div class="rule-around">Or</div>
<div class="mx-auto italic">(Coming soon)</div>
<button
onClick={joinBetaPrompt}
class="mx-auto transition-all duration-200 ease-out active:scale-95"
>
<img
src="/google-play-badge.png"
alt="google-play"
width={180}
height={60}
/>
</button>
</div>
<div class="flex flex-col">
<div class="text-center text-lg">iOS</div>
<A
class="my-auto transition-all duration-200 ease-out active:scale-95"
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
>
<DownloadOnAppStore size={50} />
</A>
</div>
</div>
</div>
<div class="pt-12">
<div class="text-center text-xl tracking-wide dark:text-white">
Shapes with Abigail!
<br />
(apk and iOS)
</div>
<div class="flex justify-evenly md:mx-[25vw]">
<div class="flex flex-col">
<div class="text-center text-lg">Android</div>
<button
onClick={() => download("shapes-with-abigail")}
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
>
Download APK
</button>
<div class="rule-around">Or</div>
<div class="mx-auto italic">(Coming soon)</div>
<button
onClick={joinBetaPrompt}
class="transition-all duration-200 ease-out active:scale-95"
>
<img
src="/google-play-badge.png"
alt="google-play"
width={180}
height={60}
/>
</button>
</div>
<div class="flex flex-col">
<div class="text-center text-lg">iOS</div>
<A
class="my-auto transition-all duration-200 ease-out active:scale-95"
href="https://apps.apple.com/us/app/shapes-with-abigail/id6474561117"
>
<DownloadOnAppStore size={50} />
</A>
</div>
</div> </div>
<div class="pt-12"> <div class="pt-12">
<div class="text-text text-center text-xl tracking-wide"> <div class="text-text text-center text-xl tracking-wide">
Cork Life and Lineage
<br /> <br />
(macOS 13 Ventura or later)
</div> </div>
<div class="flex justify-center"> <div class="flex justify-evenly md:mx-[25vw]">
<button <div class="flex w-1/3 flex-col">
onClick={() => download("cork")} <div class="text-center text-lg">Android (apk only)</div>
class="bg-blue my-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95" <button
> onClick={() => download("lineage")}
Download app class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
</button> >
</div> Download APK
<div class="text-center text-sm"> </button>
Just unzip and drag into 'Applications' folder <div class="mt-2 text-center text-sm italic">
Note the android version is not well tested, and has performance
issues.
</div>
<div class="rule-around">Or</div>
<div class="mx-auto italic">(Coming soon)</div>
<button
onClick={joinBetaPrompt}
class="mx-auto transition-all duration-200 ease-out active:scale-95"
>
<img
src="/google-play-badge.png"
alt="google-play"
width={180}
height={60}
/>
</button>
</div>
<div class="flex flex-col">
<div class="text-center text-lg">iOS</div>
<A
class="my-auto transition-all duration-200 ease-out active:scale-95"
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
>
<DownloadOnAppStore size={50} />
</A>
</div>
</div> </div>
</div> </div>
<ul class="icons flex justify-center gap-4 pt-24 pb-6"> <div class="pt-12">
<li> <div class="text-center text-xl tracking-wide dark:text-white">
<A Shapes with Abigail!
href="https://github.com/MikeFreno/" <br />
target="_blank" (apk and iOS)
rel="noreferrer" </div>
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
> <div class="flex justify-evenly md:mx-[25vw]">
<span class="m-auto block p-2"> <div class="flex flex-col">
<GitHub height={24} width={24} fill={undefined} /> <div class="text-center text-lg">Android</div>
</span> <button
</A> onClick={() => download("shapes-with-abigail")}
</li> class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
<li> >
<A Download APK
href="https://www.linkedin.com/in/michael-freno-176001256/" </button>
target="_blank" <div class="rule-around">Or</div>
rel="noreferrer" <div class="mx-auto italic">(Coming soon)</div>
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110" <button
> onClick={joinBetaPrompt}
<span class="m-auto block rounded-md p-2"> class="transition-all duration-200 ease-out active:scale-95"
<LinkedIn height={24} width={24} fill={undefined} /> >
</span> <img
</A> src="/google-play-badge.png"
</li> alt="google-play"
</ul> width={180}
height={60}
/>
</button>
</div>
<div class="flex flex-col">
<div class="text-center text-lg">iOS</div>
<A
class="my-auto transition-all duration-200 ease-out active:scale-95"
href="https://apps.apple.com/us/app/shapes-with-abigail/id6474561117"
>
<DownloadOnAppStore size={50} />
</A>
</div>
</div>
<div class="pt-12">
<div class="text-text text-center text-xl tracking-wide">
Cork
<br />
(macOS 13 Ventura or later)
</div>
<div class="flex justify-center">
<button
onClick={() => download("cork")}
class="bg-blue my-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
>
Download app
</button>
</div>
<div class="text-center text-sm">
Just unzip and drag into 'Applications' folder
</div>
</div>
<ul class="icons flex justify-center gap-4 pt-24 pb-6">
<li>
<A
href="https://github.com/MikeFreno/"
target="_blank"
rel="noreferrer"
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
>
<span class="m-auto block p-2">
<GitHub height={24} width={24} fill={undefined} />
</span>
</A>
</li>
<li>
<A
href="https://www.linkedin.com/in/michael-freno-176001256/"
target="_blank"
rel="noreferrer"
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
>
<span class="m-auto block rounded-md p-2">
<LinkedIn height={24} width={24} fill={undefined} />
</span>
</A>
</li>
</ul>
</div>
</div> </div>
</div> </>
); );
} }

View File

@@ -1,155 +1,164 @@
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 (
<main class="flex h-full flex-col gap-8 p-4 text-xl"> <>
<div class="flex-1"> <Title>Home | Michael Freno</Title>
<Typewriter speed={30} keepAlive={2000}> <Meta
<div class="text-4xl">Hey!</div> name="description"
</Typewriter> content="Michael Freno - Software Engineer based in Brooklyn, NY. Passionate about dev tooling, game development, and open source software."
<Typewriter speed={80} keepAlive={2000}> />
<div>
My name is <span class="text-green">Mike Freno</span>, I'm a{" "} <main class="flex h-full flex-col gap-8 p-4 text-xl">
<span class="text-blue">Software Engineer</span> based in{" "} <div class="flex-1">
<span class="text-yellow">Brooklyn, NY.</span> <Typewriter speed={30} keepAlive={2000}>
</div> <div class="text-4xl">Hey!</div>
</Typewriter> </Typewriter>
<Typewriter speed={100} keepAlive={2000}> <Typewriter speed={80} keepAlive={2000}>
I'm a passionate dev tooling, game, and open source software <div>
developer. Recently been working in the world of{" "} My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
<a <span class="text-blue">Software Engineer</span> based in{" "}
href="https://www.love2d.org" <span class="text-yellow">Brooklyn, NY.</span>
class="text-blue hover-underline-animation" </div>
> </Typewriter>
LÖVE <Typewriter speed={100} keepAlive={2000}>
</a>{" "} I'm a passionate dev tooling, game, and open source software
(an open source game engine for Lua). developer. Recently been working in the world of{" "}
</Typewriter> <a
<Typewriter speed={100} keepAlive={2000}> href="https://www.love2d.org"
You can see some of my work{" "} class="text-blue hover-underline-animation"
<a >
href="https://github.com/mikefreno" LÖVE
class="text-blue hover-underline-animation" </a>{" "}
> (an open source game engine for Lua).
here (github). </Typewriter>
</a> <Typewriter speed={100} keepAlive={2000}>
</Typewriter> You can see some of my work{" "}
<div class="pt-8 text-center"> <a
<div class="pb-4">Some of my recent projects:</div> href="https://github.com/mikefreno"
<div class="flex flex-col items-center gap-6 2xl:flex-row 2xl:items-start 2xl:justify-center"> class="text-blue hover-underline-animation"
{/* Life and Lineage */} >
<div class="border-surface0 flex w-full max-w-2xl flex-col gap-2 rounded-md border-2 p-4 text-center"> here (github).
<div>My mobile game:</div> </a>
<a </Typewriter>
class="text-blue hover-underline-animation" <div class="pt-8 text-center">
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442" <div class="pb-4">Some of my recent projects:</div>
> <div class="flex flex-col items-center gap-6 2xl:flex-row 2xl:items-start 2xl:justify-center">
Life and Lineage {/* Life and Lineage */}
</a> <div class="border-surface0 flex w-full max-w-2xl flex-col gap-2 rounded-md border-2 p-4 text-center">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div>My mobile game:</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg"> <a
<img class="text-blue hover-underline-animation"
src="/lineage-home.png" href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
alt="Life and Lineage Home" >
class="h-full w-full object-cover" Life and Lineage
/> </a>
</div> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="aspect-auto w-full overflow-hidden rounded-lg"> <div class="aspect-auto w-full overflow-hidden rounded-lg">
<video <img
src="/lineage-preview.mp4" src="/lineage-home.png"
class="h-full w-full object-cover" alt="Life and Lineage Home"
autoplay class="h-full w-full object-cover"
loop />
muted </div>
playsinline <div class="aspect-auto w-full overflow-hidden rounded-lg">
/> <video
</div> src="/lineage-preview.mp4"
<div class="aspect-auto w-full overflow-hidden rounded-lg"> class="h-full w-full object-cover"
<img autoplay
src="/lineage-shops.png" loop
alt="Life and Lineage Shops" muted
class="h-full w-full object-cover" playsinline
/> />
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<img
src="/lineage-shops.png"
alt="Life and Lineage Shops"
class="h-full w-full object-cover"
/>
</div>
</div> </div>
</div> </div>
</div>
{/* FlexLöve */} {/* FlexLöve */}
<div class="border-surface0 flex w-full max-w-md flex-col gap-2 rounded-md border-2 p-4 text-center"> <div class="border-surface0 flex w-full max-w-md flex-col gap-2 rounded-md border-2 p-4 text-center">
<div>My LÖVE UI library</div> <div>My LÖVE UI library</div>
<a <a
href="https://github.com/mikefreno/flexlove" href="https://github.com/mikefreno/flexlove"
class="text-blue hover-underline-animation" class="text-blue hover-underline-animation"
> >
FlexLöve FlexLöve
</a> </a>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="aspect-auto w-full overflow-hidden rounded-lg"> <div class="aspect-auto w-full overflow-hidden rounded-lg">
<video <video
src="/flexlove-scrollable.mp4" src="/flexlove-scrollable.mp4"
class="h-full w-full object-cover" class="h-full w-full object-cover"
autoplay autoplay
loop loop
muted muted
playsinline playsinline
/> />
</div> </div>
<div class="aspect-auto w-full overflow-hidden rounded-lg"> <div class="aspect-auto w-full overflow-hidden rounded-lg">
<video <video
src="/flexlove-input.mp4" src="/flexlove-input.mp4"
class="h-full w-full object-cover" class="h-full w-full object-cover"
autoplay autoplay
loop loop
muted muted
playsinline playsinline
/> />
</div> </div>
<div class="aspect-auto w-full overflow-hidden rounded-lg"> <div class="aspect-auto w-full overflow-hidden rounded-lg">
<video <video
src="/flexlove-slider.mp4" src="/flexlove-slider.mp4"
class="h-full w-full object-cover" class="h-full w-full object-cover"
autoplay autoplay
loop loop
muted muted
playsinline playsinline
/> />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> <div class="max-w-3/4 pt-8 md:max-w-1/2">
<div class="max-w-3/4 pt-8 md:max-w-1/2"> And if you love the color schemes of this site (which of course you
And if you love the color schemes of this site (which of course you do), you can see{" "}
do), you can see{" "} <a
<a href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua" class="text-blue hover-underline-animation"
class="text-blue hover-underline-animation" >
> here
here </a>{" "}
</a>{" "} - and also see the rest of my various dot files idk. There's a macos
- and also see the rest of my various dot files idk. There's a macos and arch linux rice in there if you're into that kinda thing and a
and arch linux rice in there if you're into that kinda thing and a home server setup too. Which I will write about soon.
home server setup too. Which I will write about soon.
</div>
</div>
<div class="flex flex-col items-end gap-4 pr-4">
<Typewriter speed={50} keepAlive={false}>
<div>
My Collection of
<br />
By-the-ways:
</div> </div>
</Typewriter> </div>
<Typewriter speed={50} keepAlive={false}>
<ul class="list-disc"> <div class="flex flex-col items-end gap-4 pr-4">
<li>I use Neovim</li> <Typewriter speed={50} keepAlive={false}>
<li>I use Arch Linux</li> <div>
<li>I use Rust</li> My Collection of
</ul> <br />
</Typewriter> By-the-ways:
</div> </div>
</main> </Typewriter>
<Typewriter speed={50} keepAlive={false}>
<ul class="list-disc">
<li>I use Neovim</li>
<li>I use Arch Linux</li>
<li>I use Rust</li>
</ul>
</Typewriter>
</div>
</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,324 +321,331 @@ export default function LoginPage() {
}; };
return ( return (
<div class="flex h-dvh flex-row justify-evenly"> <>
{/* Logo section - hidden on mobile */} <Title>Login | Michael Freno</Title>
{/* <div class="hidden md:flex"> <Meta
<div class="vertical-rule-around z-0 flex justify-center"> name="description"
<picture class="-mr-8"> content="Sign in to your account or register for a new account to access personalized features and manage your profile."
<source srcset="/WhiteLogo.png" media="(prefers-color-scheme: dark)" /> />
<img src="/BlackLogo.png" alt="logo" width={64} height={64} /> <div class="flex h-dvh flex-row justify-evenly">
</picture> {/* Logo section - hidden on mobile */}
</div> {/* <div class="hidden md:flex">
</div> */} <div class="vertical-rule-around z-0 flex justify-center">
<picture class="-mr-8">
<source srcset="/WhiteLogo.png" media="(prefers-color-scheme: dark)" />
<img src="/BlackLogo.png" alt="logo" width={64} height={64} />
</picture>
</div>
</div> */}
{/* Main content */} {/* Main content */}
<div class="pt-24 md:pt-48"> <div class="pt-24 md:pt-48">
{/* Error message */} {/* Error message */}
<div class="absolute -mt-12 text-center text-3xl text-red-400 italic"> <div class="absolute -mt-12 text-center text-3xl text-red-400 italic">
<Show when={error() === "passwordMismatch"}> <Show when={error() === "passwordMismatch"}>
Passwords did not match! Passwords did not match!
</Show> </Show>
<Show when={error() === "duplicate"}>Email Already Exists!</Show> <Show when={error() === "duplicate"}>Email Already Exists!</Show>
</div> </div>
{/* Title */} {/* Title */}
<div class="py-2 pl-6 text-2xl md:pl-0"> <div class="py-2 pl-6 text-2xl md:pl-0">
{register() ? "Register" : "Login"} {register() ? "Register" : "Login"}
</div> </div>
{/* Toggle Register/Login */} {/* Toggle Register/Login */}
<Show <Show
when={!register()} when={!register()}
fallback={ fallback={
<div class="py-4 text-center md:min-w-[475px]">
Already have an account?
<button
onClick={() => {
setRegister(false);
setUsePassword(false);
}}
class="text-blue pl-1 underline hover:brightness-125"
>
Click here to Login
</button>
</div>
}
>
<div class="py-4 text-center md:min-w-[475px]"> <div class="py-4 text-center md:min-w-[475px]">
Already have an account? Don't have an account yet?
<button <button
onClick={() => { onClick={() => {
setRegister(false); setRegister(true);
setUsePassword(false); setUsePassword(false);
}} }}
class="text-blue pl-1 underline hover:brightness-125" class="text-blue pl-1 underline hover:brightness-125"
> >
Click here to Login Click here to Register
</button> </button>
</div> </div>
}
>
<div class="py-4 text-center md:min-w-[475px]">
Don't have an account yet?
<button
onClick={() => {
setRegister(true);
setUsePassword(false);
}}
class="text-blue pl-1 underline hover:brightness-125"
>
Click here to Register
</button>
</div>
</Show>
{/* Form */}
<form onSubmit={formHandler} class="flex flex-col px-2 py-4">
{/* Email input */}
<div class="flex justify-center">
<div class="input-group mx-4">
<input
type="text"
required
ref={emailRef}
placeholder=" "
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Email</label>
</div>
</div>
{/* Password input - shown for login with password or registration */}
<Show when={usePassword() || register()}>
<div class="-mt-4 flex justify-center">
<div class="input-group mx-4 flex">
<input
type={showPasswordInput() ? "text" : "password"}
required
minLength={8}
ref={passwordRef}
onInput={register() ? handleNewPasswordChange : undefined}
onBlur={register() ? handlePasswordBlur : undefined}
placeholder=" "
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Password</label>
</div>
<button
onClick={() => {
setShowPasswordInput(!showPasswordInput());
passwordRef?.focus();
}}
class="absolute mt-14 ml-60"
type="button"
>
<Show
when={showPasswordInput()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white"
/>
</Show>
</button>
</div>
<div
class={`${
showPasswordLengthWarning() ? "" : "opacity-0 select-none"
} text-center text-red-500 transition-opacity duration-200 ease-in-out`}
>
Password too short! Min Length: 8
</div>
</Show> </Show>
{/* Password confirmation - shown only for registration */} {/* Form */}
<Show when={register()}> <form onSubmit={formHandler} class="flex flex-col px-2 py-4">
<div class="-mt-4 flex justify-center"> {/* Email input */}
<div class="flex justify-center">
<div class="input-group mx-4"> <div class="input-group mx-4">
<input <input
type={showPasswordConfInput() ? "text" : "password"} type="text"
required required
minLength={8} ref={emailRef}
ref={passwordConfRef}
onInput={handlePasswordConfChange}
placeholder=" " placeholder=" "
class="underlinedInput bg-transparent" class="underlinedInput bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
<label class="underlinedInputLabel">Confirm Password</label> <label class="underlinedInputLabel">Email</label>
</div> </div>
<button </div>
onClick={() => {
setShowPasswordConfInput(!showPasswordConfInput()); {/* Password input - shown for login with password or registration */}
passwordConfRef?.focus(); <Show when={usePassword() || register()}>
}} <div class="-mt-4 flex justify-center">
class="absolute mt-14 ml-60" <div class="input-group mx-4 flex">
type="button" <input
> type={showPasswordInput() ? "text" : "password"}
<Show required
when={showPasswordConfInput()} minLength={8}
fallback={ ref={passwordRef}
<EyeSlash onInput={register() ? handleNewPasswordChange : undefined}
onBlur={register() ? handlePasswordBlur : undefined}
placeholder=" "
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Password</label>
</div>
<button
onClick={() => {
setShowPasswordInput(!showPasswordInput());
passwordRef?.focus();
}}
class="absolute mt-14 ml-60"
type="button"
>
<Show
when={showPasswordInput()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white"
/>
}
>
<Eye
height={24} height={24}
width={24} width={24}
strokeWidth={1} strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white" class="stroke-zinc-900 dark:stroke-white"
/> />
} </Show>
> </button>
<Eye </div>
height={24} <div
width={24} class={`${
strokeWidth={1} showPasswordLengthWarning() ? "" : "opacity-0 select-none"
class="stroke-zinc-900 dark:stroke-white" } text-center text-red-500 transition-opacity duration-200 ease-in-out`}
>
Password too short! Min Length: 8
</div>
</Show>
{/* Password confirmation - shown only for registration */}
<Show when={register()}>
<div class="-mt-4 flex justify-center">
<div class="input-group mx-4">
<input
type={showPasswordConfInput() ? "text" : "password"}
required
minLength={8}
ref={passwordConfRef}
onInput={handlePasswordConfChange}
placeholder=" "
class="underlinedInput bg-transparent"
/> />
</Show> <span class="bar"></span>
</button> <label class="underlinedInputLabel">Confirm Password</label>
</div>
<button
onClick={() => {
setShowPasswordConfInput(!showPasswordConfInput());
passwordConfRef?.focus();
}}
class="absolute mt-14 ml-60"
type="button"
>
<Show
when={showPasswordConfInput()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-zinc-900 dark:stroke-white"
/>
</Show>
</button>
</div>
<div
class={`${
!passwordsMatch() &&
passwordLengthSufficient() &&
passwordConfRef &&
passwordConfRef.value.length >= 6
? ""
: "opacity-0 select-none"
} text-center text-red-500 transition-opacity duration-200 ease-in-out`}
>
Passwords do not match!
</div>
</Show>
{/* Remember Me checkbox */}
<div class="mx-auto flex pt-4">
<input type="checkbox" class="my-auto" ref={rememberMeRef} />
<div class="my-auto px-2 text-sm font-normal">Remember Me</div>
</div> </div>
{/* Error/Success messages */}
<div <div
class={`${ class={`${
!passwordsMatch() && showPasswordError()
passwordLengthSufficient() && ? "text-red-500"
passwordConfRef && : showPasswordSuccess()
passwordConfRef.value.length >= 6 ? "text-green-500"
? "" : "opacity-0 select-none"
: "opacity-0 select-none" } flex min-h-[16px] justify-center italic transition-opacity duration-300 ease-in-out`}
} text-center text-red-500 transition-opacity duration-200 ease-in-out`}
> >
Passwords do not match! <Show when={showPasswordError()}>
Credentials did not match any record
</Show>
<Show when={showPasswordSuccess()}>
Login Success! Redirecting...
</Show>
</div>
{/* Submit button or countdown timer */}
<div class="flex justify-center py-4">
<Show
when={!register() && !usePassword() && countDown() > 0}
fallback={
<button
type="submit"
disabled={loading()}
class={`${
loading()
? "bg-zinc-400"
: "bg-blue hover:brightness-125 active:scale-90"
} flex w-36 justify-center rounded py-3 text-white transition-all duration-300 ease-out`}
>
{register()
? "Sign Up"
: usePassword()
? "Sign In"
: "Get Link"}
</button>
}
>
<CountdownCircleTimer
duration={120}
initialRemainingTime={countDown()}
size={48}
strokeWidth={6}
colors="#60a5fa"
>
{renderTime}
</CountdownCircleTimer>
</Show>
{/* Toggle password/email link */}
<Show when={!register() && !usePassword()}>
<button
type="button"
onClick={() => setUsePassword(true)}
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
>
Use Password
</button>
</Show>
<Show when={usePassword()}>
<button
type="button"
onClick={() => setUsePassword(false)}
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
>
Use Email Link
</button>
</Show>
</div>
</form>
{/* Password reset link */}
<Show when={usePassword()}>
<div class="pb-4 text-center text-sm">
Trouble Logging In?{" "}
<A
class="text-blue underline underline-offset-4 hover:brightness-125"
href="/login/request-password-reset"
>
Reset Password
</A>
</div> </div>
</Show> </Show>
{/* Remember Me checkbox */} {/* Email sent confirmation */}
<div class="mx-auto flex pt-4">
<input type="checkbox" class="my-auto" ref={rememberMeRef} />
<div class="my-auto px-2 text-sm font-normal">Remember Me</div>
</div>
{/* Error/Success messages */}
<div <div
class={`${ class={`${
showPasswordError() emailSent() ? "" : "user-select opacity-0"
? "text-red-500" } text-green flex min-h-4 justify-center text-center italic transition-opacity duration-300 ease-in-out`}
: showPasswordSuccess()
? "text-green-500"
: "opacity-0 select-none"
} flex min-h-[16px] justify-center italic transition-opacity duration-300 ease-in-out`}
> >
<Show when={showPasswordError()}> <Show when={emailSent()}>Email Sent!</Show>
Credentials did not match any record
</Show>
<Show when={showPasswordSuccess()}>
Login Success! Redirecting...
</Show>
</div> </div>
{/* Submit button or countdown timer */} {/* Or divider */}
<div class="flex justify-center py-4"> <div class="rule-around text-center">Or</div>
<Show
when={!register() && !usePassword() && countDown() > 0} {/* OAuth buttons */}
fallback={ <div class="my-2 flex justify-center">
<button <div class="mx-auto mb-4 flex flex-col">
type="submit" {/* Google OAuth */}
disabled={loading()} <A
class={`${ href={`https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId}&redirect_uri=${domain}/api/auth/callback/google&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email`}
loading() class="my-4 flex w-80 flex-row justify-between rounded border border-zinc-800 bg-white px-4 py-2 text-black shadow-md transition-all duration-300 ease-out hover:bg-zinc-100 active:scale-95 dark:border dark:border-zinc-50 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700"
? "bg-zinc-400"
: "bg-blue hover:brightness-125 active:scale-90"
} flex w-36 justify-center rounded py-3 text-white transition-all duration-300 ease-out`}
>
{register()
? "Sign Up"
: usePassword()
? "Sign In"
: "Get Link"}
</button>
}
>
<CountdownCircleTimer
duration={120}
initialRemainingTime={countDown()}
size={48}
strokeWidth={6}
colors="#60a5fa"
> >
{renderTime} {register() ? "Register " : "Sign in "} with Google
</CountdownCircleTimer> <span class="my-auto">
</Show> <GoogleLogo height={24} width={24} />
</span>
</A>
{/* Toggle password/email link */} {/* GitHub OAuth */}
<Show when={!register() && !usePassword()}> <A
<button href={`https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${domain}/api/auth/callback/github&scope=user`}
type="button" class="my-4 flex w-80 flex-row justify-between rounded bg-zinc-600 px-4 py-2 text-white shadow-md transition-all duration-300 ease-out hover:bg-zinc-700 active:scale-95"
onClick={() => setUsePassword(true)}
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
> >
Use Password {register() ? "Register " : "Sign in "} with Github
</button> <span class="my-auto">
</Show> <GitHub height={24} width={24} fill="white" />
<Show when={usePassword()}> </span>
<button </A>
type="button" </div>
onClick={() => setUsePassword(false)}
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
>
Use Email Link
</button>
</Show>
</div>
</form>
{/* Password reset link */}
<Show when={usePassword()}>
<div class="pb-4 text-center text-sm">
Trouble Logging In?{" "}
<A
class="text-blue underline underline-offset-4 hover:brightness-125"
href="/login/request-password-reset"
>
Reset Password
</A>
</div>
</Show>
{/* Email sent confirmation */}
<div
class={`${
emailSent() ? "" : "user-select opacity-0"
} text-green flex min-h-4 justify-center text-center italic transition-opacity duration-300 ease-in-out`}
>
<Show when={emailSent()}>Email Sent!</Show>
</div>
{/* Or divider */}
<div class="rule-around text-center">Or</div>
{/* OAuth buttons */}
<div class="my-2 flex justify-center">
<div class="mx-auto mb-4 flex flex-col">
{/* Google OAuth */}
<A
href={`https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId}&redirect_uri=${domain}/api/auth/callback/google&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email`}
class="my-4 flex w-80 flex-row justify-between rounded border border-zinc-800 bg-white px-4 py-2 text-black shadow-md transition-all duration-300 ease-out hover:bg-zinc-100 active:scale-95 dark:border dark:border-zinc-50 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700"
>
{register() ? "Register " : "Sign in "} with Google
<span class="my-auto">
<GoogleLogo height={24} width={24} />
</span>
</A>
{/* GitHub OAuth */}
<A
href={`https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${domain}/api/auth/callback/github&scope=user`}
class="my-4 flex w-80 flex-row justify-between rounded bg-zinc-600 px-4 py-2 text-white shadow-md transition-all duration-300 ease-out hover:bg-zinc-700 active:scale-95"
>
{register() ? "Register " : "Sign in "} with Github
<span class="my-auto">
<GitHub height={24} width={24} fill="white" />
</span>
</A>
</div> </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,164 +161,179 @@ 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 (
<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"> <Title>Reset Password | Michael Freno</Title>
Set New Password <Meta
</div> name="description"
content="Set a new password for your account to regain access to your profile and personalized features."
<form />
onSubmit={(e) => setNewPasswordTrigger(e)} <div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
class="mt-4 flex w-full justify-center" <div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
> Set New Password
<div class="flex flex-col justify-center max-w-md w-full px-4">
{/* New Password Input */}
<div class="input-group mx-4 relative">
<input
ref={newPasswordRef}
name="newPassword"
type={showPasswordInput() ? "text" : "password"}
required
onInput={handleNewPasswordChange}
onBlur={handlePasswordBlur}
disabled={passwordChangeLoading()}
placeholder=" "
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">New Password</label>
<button
type="button"
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"
>
<Show when={showPasswordInput()} fallback={<Eye />}>
<EyeSlash />
</Show>
</button>
</div>
{/* Password Length Warning */}
<div
class={`${
showPasswordLengthWarning() ? "" : "select-none opacity-0"
} transition-opacity text-center text-red-500 text-sm duration-200 ease-in-out mt-2`}
>
Password too short! Min Length: 8
</div>
{/* Password Confirmation Input */}
<div class="input-group mx-4 mt-6 relative">
<input
ref={newPasswordConfRef}
name="newPasswordConf"
onInput={handlePasswordConfChange}
type={showPasswordConfInput() ? "text" : "password"}
required
disabled={passwordChangeLoading()}
placeholder=" "
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Password Confirmation</label>
<button
type="button"
onClick={() => setShowPasswordConfInput(!showPasswordConfInput())}
class="absolute right-0 top-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
>
<Show when={showPasswordConfInput()} fallback={<Eye />}>
<EyeSlash />
</Show>
</button>
</div>
{/* Password Mismatch Warning */}
<div
class={`${
!passwordsMatch() &&
passwordLengthSufficient() &&
newPasswordConfRef &&
newPasswordConfRef.value.length >= 6
? ""
: "select-none opacity-0"
} transition-opacity text-center text-red-500 text-sm duration-200 ease-in-out mt-2`}
>
Passwords do not match!
</div>
{/* Countdown Timer or Submit Button */}
<Show
when={countDown()}
fallback={
<button
type="submit"
disabled={passwordChangeLoading() || !passwordsMatch()}
class={`${
passwordChangeLoading() || !passwordsMatch()
? "bg-zinc-400 cursor-not-allowed"
: "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`}
>
{passwordChangeLoading() ? "Setting..." : "Set New Password"}
</button>
}
>
<div class="mx-auto pt-4">
<CountdownCircleTimer
isPlaying={countDown()}
duration={5}
size={200}
strokeWidth={12}
colors="#60a5fa"
onComplete={() => false}
>
{({ remainingTime }) => renderTime(remainingTime)}
</CountdownCircleTimer>
</div>
</Show>
</div> </div>
</form>
{/* Error Message */} <form
<Show when={error() && !showRequestNewEmail()}> onSubmit={(e) => setNewPasswordTrigger(e)}
<div class="flex justify-center mt-4"> class="mt-4 flex w-full justify-center"
<div class="text-red-500 text-sm italic">{error()}</div>
</div>
</Show>
{/* Token Expired Message */}
<div
class={`${
showRequestNewEmail() ? "" : "select-none opacity-0"
} text-red-500 italic transition-opacity flex justify-center duration-300 ease-in-out px-4`}
>
Token has expired, request a new one{" "}
<A
class="pl-1 text-blue-500 underline underline-offset-4 hover:text-blue-400"
href="/login/request-password-reset"
> >
here <div class="flex w-full max-w-md flex-col justify-center px-4">
</A> {/* New Password Input */}
</div> <div class="input-group relative mx-4">
<input
ref={newPasswordRef}
name="newPassword"
type={showPasswordInput() ? "text" : "password"}
required
onInput={handleNewPasswordChange}
onBlur={handlePasswordBlur}
disabled={passwordChangeLoading()}
placeholder=" "
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">New Password</label>
<button
type="button"
onClick={() => setShowPasswordInput(!showPasswordInput())}
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 />}>
<EyeSlash />
</Show>
</button>
</div>
{/* Back to Login Link */} {/* Password Length Warning */}
<Show when={!countDown()}> <div
<div class="flex justify-center mt-6"> class={`${
showPasswordLengthWarning() ? "" : "opacity-0 select-none"
} mt-2 text-center text-sm text-red-500 transition-opacity duration-200 ease-in-out`}
>
Password too short! Min Length: 8
</div>
{/* Password Confirmation Input */}
<div class="input-group relative mx-4 mt-6">
<input
ref={newPasswordConfRef}
name="newPasswordConf"
onInput={handlePasswordConfChange}
type={showPasswordConfInput() ? "text" : "password"}
required
disabled={passwordChangeLoading()}
placeholder=" "
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Password Confirmation</label>
<button
type="button"
onClick={() =>
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 />}>
<EyeSlash />
</Show>
</button>
</div>
{/* Password Mismatch Warning */}
<div
class={`${
!passwordsMatch() &&
passwordLengthSufficient() &&
newPasswordConfRef &&
newPasswordConfRef.value.length >= 6
? ""
: "opacity-0 select-none"
} mt-2 text-center text-sm text-red-500 transition-opacity duration-200 ease-in-out`}
>
Passwords do not match!
</div>
{/* Countdown Timer or Submit Button */}
<Show
when={countDown()}
fallback={
<button
type="submit"
disabled={passwordChangeLoading() || !passwordsMatch()}
class={`${
passwordChangeLoading() || !passwordsMatch()
? "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"
} 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"}
</button>
}
>
<div class="mx-auto pt-4">
<CountdownCircleTimer
isPlaying={countDown()}
duration={5}
size={200}
strokeWidth={12}
colors="#60a5fa"
onComplete={() => false}
>
{({ remainingTime }) => renderTime(remainingTime)}
</CountdownCircleTimer>
</div>
</Show>
</div>
</form>
{/* Error Message */}
<Show when={error() && !showRequestNewEmail()}>
<div class="mt-4 flex justify-center">
<div class="text-sm text-red-500 italic">{error()}</div>
</div>
</Show>
{/* Token Expired Message */}
<div
class={`${
showRequestNewEmail() ? "" : "opacity-0 select-none"
} flex justify-center px-4 text-red-500 italic transition-opacity duration-300 ease-in-out`}
>
Token has expired, request a new one{" "}
<A <A
href="/login" class="pl-1 text-blue-500 underline underline-offset-4 hover:text-blue-400"
class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 underline underline-offset-4 transition-colors" href="/login/request-password-reset"
> >
Back to Login here
</A> </A>
</div> </div>
</Show>
</div> {/* Back to Login Link */}
<Show when={!countDown()}>
<div class="mt-6 flex justify-center">
<A
href="/login"
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
</A>
</div>
</Show>
</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,90 +119,97 @@ export default function RequestPasswordResetPage() {
}; };
return ( return (
<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"> <Title>Request Password Reset | Michael Freno</Title>
Password Reset Request <Meta
</div> name="description"
content="Request a password reset link to regain access to your account. Enter your email to receive reset instructions."
<form />
onSubmit={(e) => requestPasswordResetTrigger(e)} <div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
class="mt-4 flex w-full justify-center" <div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
> Password Reset Request
<div class="flex flex-col justify-center">
{/* Email Input */}
<div class="input-group mx-4">
<input
ref={emailRef}
name="email"
type="text"
required
disabled={loading()}
placeholder=" "
class="underlinedInput w-full bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Enter Email</label>
</div>
{/* Countdown Timer or Submit Button */}
<Show
when={countDown() > 0}
fallback={
<button
type="submit"
disabled={loading()}
class={`${
loading()
? "bg-zinc-400"
: "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`}
>
{loading() ? "Sending..." : "Request Password Reset"}
</button>
}
>
<div class="mx-auto pt-4">
<CountdownCircleTimer
isPlaying={true}
duration={300}
initialRemainingTime={countDown()}
size={48}
strokeWidth={6}
colors="#60a5fa"
onComplete={() => false}
>
{renderTime}
</CountdownCircleTimer>
</div>
</Show>
</div> </div>
</form>
{/* Success Message */} <form
<div onSubmit={(e) => requestPasswordResetTrigger(e)}
class={`${ class="mt-4 flex w-full justify-center"
showSuccessMessage() ? "" : "select-none opacity-0"
} text-green-500 italic transition-opacity flex justify-center duration-300 ease-in-out`}
>
If email exists, you will receive an email shortly!
</div>
{/* Error Message */}
<Show when={error()}>
<div class="flex justify-center mt-4">
<div class="text-red-500 text-sm italic">{error()}</div>
</div>
</Show>
{/* Back to Login Link */}
<div class="flex justify-center mt-6">
<A
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"
> >
Back to Login <div class="flex flex-col justify-center">
</A> {/* Email Input */}
<div class="input-group mx-4">
<input
ref={emailRef}
name="email"
type="text"
required
disabled={loading()}
placeholder=" "
class="underlinedInput w-full bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Enter Email</label>
</div>
{/* Countdown Timer or Submit Button */}
<Show
when={countDown() > 0}
fallback={
<button
type="submit"
disabled={loading()}
class={`${
loading()
? "bg-zinc-400"
: "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700"
} 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"}
</button>
}
>
<div class="mx-auto pt-4">
<CountdownCircleTimer
isPlaying={true}
duration={300}
initialRemainingTime={countDown()}
size={48}
strokeWidth={6}
colors="#60a5fa"
onComplete={() => false}
>
{renderTime}
</CountdownCircleTimer>
</div>
</Show>
</div>
</form>
{/* Success Message */}
<div
class={`${
showSuccessMessage() ? "" : "opacity-0 select-none"
} flex justify-center text-green-500 italic transition-opacity duration-300 ease-in-out`}
>
If email exists, you will receive an email shortly!
</div>
{/* Error Message */}
<Show when={error()}>
<div class="mt-4 flex justify-center">
<div class="text-sm text-red-500 italic">{error()}</div>
</div>
</Show>
{/* Back to Login Link */}
<div class="mt-6 flex justify-center">
<A
href="/login"
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
</A>
</div>
</div> </div>
</div> </>
); );
} }

View File

@@ -1,46 +1,52 @@
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 (
<SimpleParallax> <>
<div class="flex flex-col items-center justify-center h-full text-white"> <Title>Life and Lineage | Michael Freno</Title>
<div> <Meta
<img name="description"
src="/LineageIcon.png" content="A dark fantasy adventure mobile game. Download Life and Lineage on the App Store and Google Play."
alt="Lineage App Icon" />
height={128} <SimpleParallax>
width={128} <div class="flex h-full flex-col items-center justify-center text-white">
class="object-cover object-center" <div>
/>
</div>
<h1 class="text-5xl font-bold mb-4 text-center">
Life and Lineage
</h1>
<p class="text-xl mb-8">A dark fantasy adventure</p>
<div class="flex space-x-4">
<a
class="my-auto transition-all duration-200 ease-out active:scale-95"
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
target="_blank"
rel="noopener noreferrer"
>
<DownloadOnAppStoreDark size={50} />
</a>
<A
href="/downloads"
class="transition-all duration-200 ease-out active:scale-95"
>
<img <img
src="/google-play-badge.png" src="/LineageIcon.png"
alt="google-play" alt="Lineage App Icon"
width={180} height={128}
height={60} width={128}
class="object-cover object-center"
/> />
</A> </div>
<h1 class="mb-4 text-center text-5xl font-bold">Life and Lineage</h1>
<p class="mb-8 text-xl">A dark fantasy adventure</p>
<div class="flex space-x-4">
<a
class="my-auto transition-all duration-200 ease-out active:scale-95"
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
target="_blank"
rel="noopener noreferrer"
>
<DownloadOnAppStoreDark size={50} />
</a>
<A
href="/downloads"
class="transition-all duration-200 ease-out active:scale-95"
>
<img
src="/google-play-badge.png"
alt="google-play"
width={180}
height={60}
/>
</A>
</div>
</div> </div>
</div> </SimpleParallax>
</SimpleParallax> </>
); );
} }

View File

@@ -1,99 +1,107 @@
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 (
<div class="min-h-screen px-[8vw] py-[10vh]"> <>
<div class="py-4 text-xl">Life and Lineage&apos;s Privacy Policy</div> <Title>Privacy Policy - Life and Lineage | Michael Freno</Title>
<div class="py-2">Last Updated: October 22, 2024</div> <Meta
<div class="py-2"> name="description"
Welcome to Life and Lineage (&apos;We&apos;, &apos;Us&apos;, content="Privacy policy for Life and Lineage mobile game, outlining data collection, usage, and user rights."
&apos;Our&apos;). Your privacy is important to us. This privacy policy />
will help you understand our policies and procedures related to the <div class="min-h-screen px-[8vw] py-[10vh]">
collection, use, and storage of personal information from our users. <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">
Welcome to Life and Lineage (&apos;We&apos;, &apos;Us&apos;,
&apos;Our&apos;). Your privacy is important to us. This privacy policy
will help you understand our policies and procedures related to the
collection, use, and storage of personal information from our users.
</div>
<ol>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">1.</span> Personal Information
</div>
<div class="pl-4">
<div class="pb-2">
<div class="-ml-6">(a) Collection of Personal Data:</div> Life
and Lineage collects and stores personal data only if users opt
to use the remote saving feature. The information collected
includes email address, and if using an OAuth provider - first
name, and last name. This information is used solely for the
purpose of providing and managing the remote saving feature. It
is and never will be shared with a third party.
</div>
<div class="pb-2">
<div class="-ml-6">(b) Data Removal:</div> Users can request the
removal of all information related to them by visiting{" "}
<A
href="/deletion/life-and-lineage"
class="text-blue hover-underline-animation"
>
this page
</A>{" "}
and filling out the provided form.
</div>
</div>
</div>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">2.</span> Third-Party Access
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Limited Third-Party Access:</div> We do not
share or sell user information to third parties. However, we do
utilize third-party services for crash reporting and performance
profiling. These services do not have access to personal user
information and only receive anonymized data related to app
performance and stability.
</div>
</div>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">3.</span> Security
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Data Protection:</div>Life and Lineage
takes appropriate measures to protect the personal information of
users who opt for the remote saving feature. We implement
industry-standard security protocols to prevent unauthorized
access, disclosure, alteration, or destruction of user data.
</div>
</div>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">4.</span> Changes to the Privacy Policy
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Updates:</div> We may update this privacy
policy periodically. Any changes to this privacy policy will be
posted on this page. We encourage users to review this policy
regularly to stay informed about how we protect their information.
</div>
</div>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">5.</span> Contact Us
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Reaching Out:</div> If there are any
questions or comments regarding this privacy policy, you can
contact us{" "}
<A href="/contact" class="text-blue hover-underline-animation">
here
</A>
.
</div>
</div>
</ol>
</div> </div>
<ol> </>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">1.</span> Personal Information
</div>
<div class="pl-4">
<div class="pb-2">
<div class="-ml-6">(a) Collection of Personal Data:</div> Life and
Lineage collects and stores personal data only if users opt to use
the remote saving feature. The information collected includes
email address, and if using an OAuth provider - first name, and
last name. This information is used solely for the purpose of
providing and managing the remote saving feature. It is and never
will be shared with a third party.
</div>
<div class="pb-2">
<div class="-ml-6">(b) Data Removal:</div> Users can request the
removal of all information related to them by visiting{" "}
<A
href="/deletion/life-and-lineage"
class="text-blue hover-underline-animation"
>
this page
</A>{" "}
and filling out the provided form.
</div>
</div>
</div>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">2.</span> Third-Party Access
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Limited Third-Party Access:</div> We do not
share or sell user information to third parties. However, we do
utilize third-party services for crash reporting and performance
profiling. These services do not have access to personal user
information and only receive anonymized data related to app
performance and stability.
</div>
</div>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">3.</span> Security
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Data Protection:</div>Life and Lineage takes
appropriate measures to protect the personal information of users
who opt for the remote saving feature. We implement
industry-standard security protocols to prevent unauthorized access,
disclosure, alteration, or destruction of user data.
</div>
</div>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">4.</span> Changes to the Privacy Policy
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Updates:</div> We may update this privacy
policy periodically. Any changes to this privacy policy will be
posted on this page. We encourage users to review this policy
regularly to stay informed about how we protect their information.
</div>
</div>
<div class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">5.</span> Contact Us
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Reaching Out:</div> If there are any
questions or comments regarding this privacy policy, you can contact
us{" "}
<A href="/contact" class="text-blue hover-underline-animation">
here
</A>
.
</div>
</div>
</ol>
</div>
); );
} }

View File

@@ -1,98 +1,103 @@
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 (
<div class="bg-zinc-100 dark:bg-zinc-900"> <>
<div class="min-h-screen px-[8vw] py-[8vh]"> <Title>Privacy Policy - Shapes with Abigail | Michael Freno</Title>
<div class="py-4 text-xl"> <Meta
Shapes with Abigail!&apos;s Privacy Policy name="description"
</div> content="Privacy policy for Shapes with Abigail app, explaining our commitment to child safety and non-collection of personal data."
<div class="py-2">Last Updated: December 21, 2023</div> />
<div class="py-2"> <div class="bg-zinc-100 dark:bg-zinc-900">
Welcome to Shapes with Abigail! (&apos;We&apos; , &apos;Us&apos;, <div class="min-h-screen px-[8vw] py-[8vh]">
&apos;Our&apos;). Your privacy is important to us. For that reason, <div class="py-4 text-xl">
our app, &quot;Shapes with Abigail!&quot; has been designed to Shapes with Abigail!&apos;s Privacy Policy
provide our users with a secure environment. This privacy policy </div>
will help you understand our policies and procedures related to the <div class="py-2">Last Updated: December 21, 2023</div>
non-collection, non-use, and non-storage of personal information
from our users.
</div>
<ol>
<div class="py-2"> <div class="py-2">
<div class="pb-2 text-lg"> Welcome to Shapes with Abigail! (&apos;We&apos; , &apos;Us&apos;,
<span class="-ml-4 pr-2">1.</span> Personal Information &apos;Our&apos;). Your privacy is important to us. For that reason,
</div> our app, &quot;Shapes with Abigail!&quot; has been designed to
<div class="pl-4"> provide our users with a secure environment. This privacy policy
<div class="pb-2"> will help you understand our policies and procedures related to the
<div class="-ml-6"> non-collection, non-use, and non-storage of personal information
(a) Non-Collection of Personal Data: from our users.
</div>{" "} </div>
Shapes with Abigail! does not collect nor store personal data. <ol>
We respect the privacy of our users, especially considering <div class="py-2">
the age of our users. We believe that no information, whether <div class="pb-2 text-lg">
private or personal, should be required for children to enjoy <span class="-ml-4 pr-2">1.</span> Personal Information
our fun and educational app. </div>
<div class="pl-4">
<div class="pb-2">
<div class="-ml-6">(a) Non-Collection of Personal Data:</div>{" "}
Shapes with Abigail! does not collect nor store personal data.
We respect the privacy of our users, especially considering
the age of our users. We believe that no information, whether
private or personal, should be required for children to enjoy
our fun and educational app.
</div>
</div> </div>
</div> </div>
</div>
<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">2.</span> Third-Party Access <span class="-ml-4 pr-2">2.</span> Third-Party Access
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) No Third-Party Access:</div> Since we do
not collect or store any user data, there is no possibility of
sharing or selling our users&apos; information to third parties.
Our priority is the safety and privacy of our users.
</div>
</div> </div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) No Third-Party Access:</div> Since we
do not collect or store any user data, there is no possibility
of sharing or selling our users&apos; information to third
parties. Our priority is the safety and privacy of our users.
</div>
</div>
<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">3.</span> Security <span class="-ml-4 pr-2">3.</span> Security
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Secure Environment:</div>Shapes with
Abigail! offers a secure and safe platform for children to play
and learn. Not requiring any personal data naturally enhances
security by eliminating potential risks related to data breaches
and misuse of information.
</div>
</div> </div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Secure Environment:</div>Shapes with
Abigail! offers a secure and safe platform for children to play
and learn. Not requiring any personal data naturally enhances
security by eliminating potential risks related to data breaches
and misuse of information.
</div>
</div>
<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 class="pb-2 pl-4">
<div class="-ml-6">(a) Updates:</div> We may update this privacy
policy periodically. Any changes to this privacy policy will be
posted on this page. However, since we do not collect any
personal data, these updates are likely to be insignificant.
</div>
</div> </div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Updates:</div> We may update this
privacy policy periodically. Any changes to this privacy policy
will be posted on this page. However, since we do not collect
any personal data, these updates are likely to be insignificant.
</div>
</div>
<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">5.</span> Contact Us <span class="-ml-4 pr-2">5.</span> Contact Us
</div>
<div class="pb-2 pl-4">
<div class="-ml-6">(a) Reaching Out:</div> If there are any
questions or comments regarding this privacy policy, you can
contact us{" "}
<A
href="/contact"
class="text-blue-400 underline-offset-4 hover:underline"
>
here
</A>
.
</div>
</div> </div>
<div class="pb-2 pl-4"> </ol>
<div class="-ml-6">(a) Reaching Out:</div> If there are any </div>
questions or comments regarding this privacy policy, you can
contact us{" "}
<A
href="/contact"
class="text-blue-400 underline-offset-4 hover:underline"
>
here
</A>
.
</div>
</div>
</ol>
</div> </div>
</div> </>
); );
} }

View File

@@ -1,16 +1,24 @@
import { Title } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
export default function Resume() { export default function Resume() {
return ( return (
<main class="flex h-screen w-full flex-col"> <>
<Title>Resume - Freno.dev</Title> <Title>Resume | Michael Freno</Title>
<div class="flex h-full w-full items-center justify-center"> <Meta
<iframe name="description"
src="/resume.pdf" content="View Michael Freno's resume - Software Engineer with expertise in full-stack development, game development, and open source."
class="h-full w-full border-0" />
title="Resume PDF"
/> <main class="flex h-screen w-full flex-col">
</div> <Title>Resume - Freno.dev</Title>
</main> <div class="flex h-full w-full items-center justify-center">
<iframe
src="/resume.pdf"
class="h-full w-full border-0"
title="Resume PDF"
/>
</div>
</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
@@ -26,144 +31,163 @@ export default function TestUtilsPage() {
const passwordMatchError = () => { const passwordMatchError = () => {
if (!passwordConf()) return undefined; if (!passwordConf()) return undefined;
return passwordsMatch(password(), passwordConf()) return passwordsMatch(password(), passwordConf())
? undefined ? undefined
: "Passwords do not match"; : "Passwords do not match";
}; };
const handleSubmit = async (e: Event) => { const handleSubmit = async (e: Event) => {
e.preventDefault(); e.preventDefault();
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 (
<main class="min-h-screen bg-gray-100 p-8"> <>
<div class="max-w-2xl mx-auto"> <Title>Utility Testing | Michael Freno</Title>
<div class="bg-white rounded-lg shadow-lg p-6 mb-6"> <Meta
<h1 class="text-3xl font-bold mb-2">Task 01 - Utility Testing</h1> name="description"
<p class="text-gray-600 mb-4"> content="Testing page for form components and validation utilities."
Testing shared utilities, types, and UI components />
</p> <main class="min-h-screen bg-gray-100 p-8">
</div> <div class="mx-auto max-w-2xl">
<div class="mb-6 rounded-lg bg-white p-6 shadow-lg">
<h1 class="mb-2 text-3xl font-bold">Task 01 - Utility Testing</h1>
<p class="mb-4 text-gray-600">
Testing shared utilities, types, and UI components
</p>
</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">
<Input
type="email"
label="Email Address"
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
error={emailError()}
helperText="Enter a valid email address"
required
/>
<Input <form onSubmit={handleSubmit} class="space-y-4">
type="password" <Input
label="Password" type="email"
value={password()} label="Email Address"
onInput={(e) => setPassword(e.currentTarget.value)} value={email()}
error={passwordError()} onInput={(e) => setEmail(e.currentTarget.value)}
helperText="Minimum 8 characters" error={emailError()}
required helperText="Enter a valid email address"
/> required
/>
<Input <Input
type="password" type="password"
label="Confirm Password" label="Password"
value={passwordConf()} value={password()}
onInput={(e) => setPasswordConf(e.currentTarget.value)} onInput={(e) => setPassword(e.currentTarget.value)}
error={passwordMatchError()} error={passwordError()}
required helperText="Minimum 8 characters"
/> required
/>
<div class="flex gap-2"> <Input
<Button type="password"
type="submit" label="Confirm Password"
variant="primary" value={passwordConf()}
loading={loading()} onInput={(e) => setPasswordConf(e.currentTarget.value)}
disabled={ error={passwordMatchError()}
!isValidEmail(email()) || required
!validatePassword(password()).isValid || />
!passwordsMatch(password(), passwordConf())
}
>
Submit
</Button>
<Button <div class="flex gap-2">
type="button" <Button
variant="secondary" type="submit"
onClick={() => { variant="primary"
setEmail(""); loading={loading()}
setPassword(""); disabled={
setPasswordConf(""); !isValidEmail(email()) ||
}} !validatePassword(password()).isValid ||
> !passwordsMatch(password(), passwordConf())
Reset }
</Button> >
Submit
</Button>
<Button <Button
type="button" type="button"
variant="danger" variant="secondary"
onClick={() => alert("Danger action!")} onClick={() => {
> setEmail("");
Delete setPassword("");
</Button> setPasswordConf("");
}}
>
Reset
</Button>
<Button <Button
type="button" type="button"
variant="ghost" variant="danger"
onClick={() => alert("Ghost action!")} onClick={() => alert("Danger action!")}
> >
Cancel Delete
</Button> </Button>
</div>
</form>
</div>
<div class="bg-white rounded-lg shadow p-6"> <Button
<h2 class="text-xl font-bold mb-4">Validation Status</h2> type="button"
variant="ghost"
<div class="space-y-2 text-sm"> onClick={() => alert("Ghost action!")}
<div class="flex items-center gap-2"> >
<span class={`w-3 h-3 rounded-full ${isValidEmail(email()) ? "bg-green-500" : "bg-gray-300"}`} /> Cancel
<span>Email Valid: {isValidEmail(email()) ? "✓" : "✗"}</span> </Button>
</div> </div>
</form>
</div>
<div class="flex items-center gap-2"> <div class="rounded-lg bg-white p-6 shadow">
<span class={`w-3 h-3 rounded-full ${validatePassword(password()).isValid ? "bg-green-500" : "bg-gray-300"}`} /> <h2 class="mb-4 text-xl font-bold">Validation Status</h2>
<span>Password Valid: {validatePassword(password()).isValid ? "✓" : "✗"}</span>
</div>
<div class="flex items-center gap-2"> <div class="space-y-2 text-sm">
<span class={`w-3 h-3 rounded-full ${passwordsMatch(password(), passwordConf()) ? "bg-green-500" : "bg-gray-300"}`} /> <div class="flex items-center gap-2">
<span>Passwords Match: {passwordsMatch(password(), passwordConf()) ? "✓" : "✗"}</span> <span
class={`h-3 w-3 rounded-full ${isValidEmail(email()) ? "bg-green-500" : "bg-gray-300"}`}
/>
<span>Email Valid: {isValidEmail(email()) ? "✓" : "✗"}</span>
</div>
<div class="flex items-center gap-2">
<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 class="flex items-center gap-2">
<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>
<li> Input component created</li> <li> Input component created</li>
<li> Button component created</li> <li> Button component created</li>
<li> Conversion patterns documented</li> <li> Conversion patterns documented</li>
<li> Build successful</li> <li> Build successful</li>
</ul> </ul>
</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,297 +933,318 @@ export default function TestPage() {
}; };
return ( return (
<Show <>
when={authState()?.privilegeLevel === "admin"} <Title>API Testing | Michael Freno</Title>
fallback={ <Meta
<div class="w-full pt-[30vh] text-center"> name="description"
<div class="text-text text-2xl">Unauthorized</div> content="tRPC API testing dashboard for developers to test endpoints and verify functionality."
<div class="text-subtext0 mt-4"> />
You must be an admin to access this page. <Show
when={authState()?.privilegeLevel === "admin"}
fallback={
<div class="w-full pt-[30vh] text-center">
<div class="text-text text-2xl">Unauthorized</div>
<div class="text-subtext0 mt-4">
You must be an admin to access this page.
</div>
</div> </div>
</div> }
} >
> <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">
<h1 class="mb-2 text-3xl font-bold">tRPC API Testing Dashboard</h1> tRPC API Testing Dashboard
<p class="text-text mb-4"> </h1>
Complete API coverage: Example, Auth, Database, User, Misc, and <p class="text-text mb-4">
Lineage routers Complete API coverage: Example, Auth, Database, User, Misc, and
</p> Lineage routers
<div class="border-lavender bg-mauve rounded border p-4">
<p class="text-base text-sm">
<strong>Quick Start:</strong> Expand any section below to test
endpoints. Public endpoints work immediately. Auth-required
endpoints need valid tokens.
</p> </p>
<div class="border-lavender bg-mauve rounded border p-4">
<p class="text-base text-sm">
<strong>Quick Start:</strong> Expand any section below to test
endpoints. Public endpoints work immediately. Auth-required
endpoints need valid tokens.
</p>
</div>
</div> </div>
</div>
<div class="space-y-4"> <div class="space-y-4">
<For each={routerSections}> <For each={routerSections}>
{(section) => { {(section) => {
const isExpanded = () => expandedSections().has(section.name); const isExpanded = () => expandedSections().has(section.name);
return ( return (
<div class="bg-surface0 rounded-lg shadow"> <div class="bg-surface0 rounded-lg shadow">
{/* Section Header */} {/* Section Header */}
<button <button
onClick={() => toggleSection(section.name)} onClick={() => toggleSection(section.name)}
class="flex w-full items-center justify-between px-6 py-4 transition" class="flex w-full items-center justify-between px-6 py-4 transition"
> >
<div class="text-left"> <div class="text-left">
<h2 class="text-xl font-bold">{section.name}</h2> <h2 class="text-xl font-bold">{section.name}</h2>
<p class="text-subtext0 text-sm"> <p class="text-subtext0 text-sm">
{section.description} {section.description}
</p> </p>
<p class="text-subtext1 mt-1 text-xs"> <p class="text-subtext1 mt-1 text-xs">
{section.endpoints.length} endpoint {section.endpoints.length} endpoint
{section.endpoints.length !== 1 ? "s" : ""} {section.endpoints.length !== 1 ? "s" : ""}
</p> </p>
</div> </div>
<div class="text-subtext1 text-2xl"> <div class="text-subtext1 text-2xl">
{isExpanded() ? "" : "+"} {isExpanded() ? "" : "+"}
</div> </div>
</button> </button>
{/* Section Content */} {/* Section Content */}
<Show when={isExpanded()}> <Show when={isExpanded()}>
<div class="border-base space-y-4 border-t p-6"> <div class="border-base space-y-4 border-t p-6">
<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 =
const displayInput = () => { endpoint.sampleInput !== undefined;
if (inputEdits()[key]) { const displayInput = () => {
return inputEdits()[key]; if (inputEdits()[key]) {
} return inputEdits()[key];
// Handle primitive values (string, number, boolean) }
if (typeof endpoint.sampleInput === "string") { // Handle primitive values (string, number, boolean)
return `"${endpoint.sampleInput}"`; if (typeof endpoint.sampleInput === "string") {
} return `"${endpoint.sampleInput}"`;
if ( }
typeof endpoint.sampleInput === "number" || if (
typeof endpoint.sampleInput === "boolean" typeof endpoint.sampleInput === "number" ||
) { typeof endpoint.sampleInput === "boolean"
return String(endpoint.sampleInput); ) {
} return String(endpoint.sampleInput);
// Handle objects and arrays }
return JSON.stringify( // Handle objects and arrays
endpoint.sampleInput, return JSON.stringify(
null, endpoint.sampleInput,
2 null,
); 2
}; );
};
return ( return (
<div class="bg-surface2 border-surface1 rounded-lg border p-4"> <div class="bg-surface2 border-surface1 rounded-lg border p-4">
{/* Endpoint Header */} {/* Endpoint Header */}
<div class="mb-3 flex items-start justify-between"> <div class="mb-3 flex items-start justify-between">
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<h3 class="text-subtext0 text-lg font-semibold"> <h3 class="text-subtext0 text-lg font-semibold">
{endpoint.name} {endpoint.name}
</h3> </h3>
<Show when={endpoint.requiresAuth}> <Show when={endpoint.requiresAuth}>
<span class="bg-surface1 text-yellow rounded px-2 py-1 text-xs"> <span class="bg-surface1 text-yellow rounded px-2 py-1 text-xs">
🔒 Auth Required 🔒 Auth Required
</span>
</Show>
<Show when={endpoint.requiresAdmin}>
<span class="bg-maroon rounded px-2 py-1 text-base text-xs">
👑 Admin Only
</span>
</Show>
</div>
<p class="mt-1 text-sm text-gray-600">
{endpoint.description}
</p>
<div class="mt-2 flex gap-2">
<code class="bg-surface0 rounded px-2 py-1 text-xs">
{key}
</code>
<span class="bg-blue text-text rounded px-2 py-1 text-xs">
{endpoint.method === "query"
? "GET"
: "POST"}
</span> </span>
</Show> </div>
<Show when={endpoint.requiresAdmin}>
<span class="bg-maroon rounded px-2 py-1 text-base text-xs">
👑 Admin Only
</span>
</Show>
</div>
<p class="mt-1 text-sm text-gray-600">
{endpoint.description}
</p>
<div class="mt-2 flex gap-2">
<code class="bg-surface0 rounded px-2 py-1 text-xs">
{key}
</code>
<span class="bg-blue text-text rounded px-2 py-1 text-xs">
{endpoint.method === "query"
? "GET"
: "POST"}
</span>
</div> </div>
<button
onClick={() => testEndpoint(endpoint)}
disabled={loading()[key]}
class="bg-green ml-4 rounded px-4 py-2 text-base font-semibold whitespace-nowrap transition hover:brightness-125 disabled:brightness-50"
>
{loading()[key] ? "Testing..." : "Test"}
</button>
</div> </div>
<button
onClick={() => testEndpoint(endpoint)} {/* Input Editor */}
disabled={loading()[key]} <Show when={hasInput}>
class="bg-green ml-4 rounded px-4 py-2 text-base font-semibold whitespace-nowrap transition hover:brightness-125 disabled:brightness-50" <div class="mb-3">
> <label class="text-text mb-1 block text-xs font-semibold">
{loading()[key] ? "Testing..." : "Test"} Request Body (edit JSON):
</button> </label>
<textarea
value={displayInput()}
onInput={(e) =>
updateInput(
key,
e.currentTarget.value
)
}
class="border-lavender bg-crust min-h-[100px] w-full rounded border p-2 font-mono text-xs"
spellcheck={false}
/>
</div>
</Show>
{/* Error Display */}
<Show when={errors()[key]}>
<div class="mb-3 rounded border border-red-200 bg-red-50 p-3">
<p class="text-sm font-semibold text-red-800">
Error:
</p>
<p class="font-mono text-sm text-red-600">
{errors()[key]}
</p>
</div>
</Show>
{/* Results Display */}
<Show when={results()[key]}>
<div class="rounded bg-gray-900 p-3">
<p class="mb-2 text-xs font-semibold text-green-400">
Response:
</p>
<pre class="max-h-60 overflow-auto text-xs text-green-400">
{JSON.stringify(
results()[key],
null,
2
)}
</pre>
</div>
</Show>
</div> </div>
);
}}
</For>
</div>
</Show>
</div>
);
}}
</For>
</div>
{/* Input Editor */} {/* Footer Instructions */}
<Show when={hasInput}> <div class="bg-overlay2 mt-6 rounded-lg p-6 shadow-lg">
<div class="mb-3"> <h2 class="text-crust mb-4 text-2xl font-bold">Testing Guide</h2>
<label class="text-text mb-1 block text-xs font-semibold">
Request Body (edit JSON):
</label>
<textarea
value={displayInput()}
onInput={(e) =>
updateInput(key, e.currentTarget.value)
}
class="border-lavender bg-crust min-h-[100px] w-full rounded border p-2 font-mono text-xs"
spellcheck={false}
/>
</div>
</Show>
{/* Error Display */} <div class="space-y-4 text-base">
<Show when={errors()[key]}> <div>
<div class="mb-3 rounded border border-red-200 bg-red-50 p-3"> <h3 class="mb-2 text-lg font-semibold">
<p class="text-sm font-semibold text-red-800"> 🟢 No Auth Required
Error: </h3>
</p> <ul class="ml-6 list-disc space-y-1 text-sm">
<p class="font-mono text-sm text-red-600"> <li>
{errors()[key]} <strong>Example Router</strong> - Hello endpoint
</p> </li>
</div> <li>
</Show> <strong>Lineage JSON Service</strong> - All 6 endpoints
work immediately
</li>
<li>
<strong>Database</strong> - All endpoints (comments,
posts, users, reactions, likes)
</li>
<li>
<strong>Misc</strong> - Downloads, S3 operations, password
utilities
</li>
<li>
<strong>Lineage Misc</strong> - Offline Secret, Get
Opponents
</li>
<li>
<strong>Lineage PvP</strong> - Get Opponents
</li>
</ul>
</div>
{/* Results Display */} <div>
<Show when={results()[key]}> <h3 class="mb-2 text-lg font-semibold">🟡 Auth Required</h3>
<div class="rounded bg-gray-900 p-3"> <p class="mb-2 text-sm">
<p class="mb-2 text-xs font-semibold text-green-400"> These need valid JWT tokens from login/registration:
Response: </p>
</p> <ul class="ml-6 list-disc space-y-1 text-sm">
<pre class="max-h-60 overflow-auto text-xs text-green-400"> <li>
{JSON.stringify(results()[key], null, 2)} <strong>Example Router</strong> - Get Profile
</pre> </li>
</div> <li>
</Show> <strong>User Router</strong> - All endpoints (profile
</div> updates, password, account deletion)
); </li>
}} <li>
</For> <strong>Lineage Auth</strong> - Email Login, Refresh Token
</div> </li>
</Show> <li>
</div> <strong>Lineage Database</strong> - Get Credentials,
); Deletion endpoints
}} </li>
</For> </ul>
</div> </div>
{/* Footer Instructions */} <div>
<div class="bg-overlay2 mt-6 rounded-lg p-6 shadow-lg"> <h3 class="mb-2 text-lg font-semibold">🔴 Admin Required</h3>
<h2 class="text-crust mb-4 text-2xl font-bold">Testing Guide</h2> <p class="mb-2 text-sm">
Maintenance endpoints require admin privileges (userIDToken
cookie with ADMIN_ID).
</p>
<ul class="ml-6 list-disc space-y-1 text-sm">
<li>
<strong>Example Router</strong> - Admin Dashboard
</li>
<li>
<strong>Lineage Maintenance</strong> - Find Loose
Databases, Cleanup Expired
</li>
</ul>
</div>
<div class="space-y-4 text-base"> <div>
<div> <h3 class="mb-2 text-lg font-semibold">
<h3 class="mb-2 text-lg font-semibold">🟢 No Auth Required</h3> 📝 Typical Workflows
<ul class="ml-6 list-disc space-y-1 text-sm"> </h3>
<li> <ol class="ml-6 list-decimal space-y-2 text-sm">
<strong>Example Router</strong> - Hello endpoint <li>
</li> <strong>Test public endpoints:</strong> Start with Example
<li> Hello, Lineage JSON Service, or Database queries
<strong>Lineage JSON Service</strong> - All 6 endpoints work </li>
immediately <li>
</li> <strong>OAuth flow:</strong> Use Auth router callbacks
<li> with OAuth codes from GitHub/Google
<strong>Database</strong> - All endpoints (comments, posts, </li>
users, reactions, likes) <li>
</li> <strong>Email auth flow:</strong> Register verify email
<li> login use JWT
<strong>Misc</strong> - Downloads, S3 operations, password </li>
utilities <li>
</li> <strong>Blog/Project management:</strong> Create posts
<li> add comments/likes upload images via S3
<strong>Lineage Misc</strong> - Offline Secret, Get </li>
Opponents <li>
</li> <strong>Lineage game data:</strong> Fetch JSON data
<li> register character find PvP opponents
<strong>Lineage PvP</strong> - Get Opponents </li>
</li> </ol>
</ul> </div>
</div>
<div> <div class="border-rosewater bg-rosewater mt-4 rounded border p-4">
<h3 class="mb-2 text-lg font-semibold">🟡 Auth Required</h3> <p class="text-crust text-sm">
<p class="mb-2 text-sm"> <strong>Note:</strong> Some endpoints require specific setup
These need valid JWT tokens from login/registration: (e.g., OAuth codes, existing database records, valid S3
</p> keys). Check the sample input to understand what data each
<ul class="ml-6 list-disc space-y-1 text-sm"> endpoint expects.
<li> </p>
<strong>Example Router</strong> - Get Profile </div>
</li>
<li>
<strong>User Router</strong> - All endpoints (profile
updates, password, account deletion)
</li>
<li>
<strong>Lineage Auth</strong> - Email Login, Refresh Token
</li>
<li>
<strong>Lineage Database</strong> - Get Credentials,
Deletion endpoints
</li>
</ul>
</div>
<div>
<h3 class="mb-2 text-lg font-semibold">🔴 Admin Required</h3>
<p class="mb-2 text-sm">
Maintenance endpoints require admin privileges (userIDToken
cookie with ADMIN_ID).
</p>
<ul class="ml-6 list-disc space-y-1 text-sm">
<li>
<strong>Example Router</strong> - Admin Dashboard
</li>
<li>
<strong>Lineage Maintenance</strong> - Find Loose Databases,
Cleanup Expired
</li>
</ul>
</div>
<div>
<h3 class="mb-2 text-lg font-semibold">📝 Typical Workflows</h3>
<ol class="ml-6 list-decimal space-y-2 text-sm">
<li>
<strong>Test public endpoints:</strong> Start with Example
Hello, Lineage JSON Service, or Database queries
</li>
<li>
<strong>OAuth flow:</strong> Use Auth router callbacks with
OAuth codes from GitHub/Google
</li>
<li>
<strong>Email auth flow:</strong> Register verify email
login use JWT
</li>
<li>
<strong>Blog/Project management:</strong> Create posts add
comments/likes upload images via S3
</li>
<li>
<strong>Lineage game data:</strong> Fetch JSON data
register character find PvP opponents
</li>
</ol>
</div>
<div class="border-rosewater bg-rosewater mt-4 rounded border p-4">
<p class="text-crust text-sm">
<strong>Note:</strong> Some endpoints require specific setup
(e.g., OAuth codes, existing database records, valid S3 keys).
Check the sample input to understand what data each endpoint
expects.
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </main>
</main> </Show>
</Show> </>
); );
} }