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 { useNavigate } from "@solidjs/router";
import { createEffect, createSignal, For } from "solid-js";
@@ -47,7 +47,11 @@ export default function Page_401() {
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} />
<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 */}

View File

@@ -1,4 +1,4 @@
import { Title } from "@solidjs/meta";
import { Title, Meta } from "@solidjs/meta";
import { HttpStatusCode } from "@solidjs/start";
import { useNavigate } from "@solidjs/router";
import { createEffect, createSignal, For } from "solid-js";
@@ -43,7 +43,11 @@ export default function NotFound() {
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} />
<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 */}

View File

@@ -1,10 +1,17 @@
import { Title } from "@solidjs/meta";
import { Title, Meta } from "@solidjs/meta";
export default function About() {
return (
<main>
<Title>About</Title>
<h1>About</h1>
</main>
<>
<Title>About | Michael Freno</Title>
<Meta
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 { Title, Meta } from "@solidjs/meta";
import { useNavigate, cache, redirect } from "@solidjs/router";
import { getEvent } from "vinxi/http";
import Eye from "~/components/icons/Eye";
@@ -453,389 +454,407 @@ export default function AccountPage() {
};
return (
<div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36">
<div class="pt-24">
<Show
when={!loading() && user()}
fallback={
<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>
<>
<Title>Account | Michael Freno</Title>
<Meta
name="description"
content="Manage your account settings, update profile information, and configure preferences."
/>
{/* Profile Image Section */}
<div class="mx-auto mb-8 flex max-w-md justify-center">
<div class="flex flex-col py-4">
<div class="mb-2 text-center text-lg font-semibold">
Profile Image
</div>
<div class="flex items-start">
<Dropzone
onDrop={handleImageDrop}
acceptedFiles="image/jpg, image/jpeg, image/png"
fileHolder={profileImageHolder()}
preSet={preSetHolder() || currentUser().image || null}
/>
<button
type="button"
onClick={removeImage}
class="z-20 -ml-6 h-fit rounded-full transition-all hover:brightness-125"
>
<XCircle
height={36}
width={36}
stroke="currentColor"
strokeWidth={1}
<div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36">
<div class="pt-24">
<Show
when={!loading() && user()}
fallback={
<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="mx-auto mb-8 flex max-w-md justify-center">
<div class="flex flex-col py-4">
<div class="mb-2 text-center text-lg font-semibold">
Profile Image
</div>
<div class="flex items-start">
<Dropzone
onDrop={handleImageDrop}
acceptedFiles="image/jpg, image/jpeg, image/png"
fileHolder={profileImageHolder()}
preSet={preSetHolder() || currentUser().image || null}
/>
</button>
</div>
<form onSubmit={setUserImage}>
<button
type="submit"
disabled={
profileImageSetLoading() || !profileImageStateChange()
}
class={`${
profileImageSetLoading() || !profileImageStateChange()
? "bg-blue cursor-not-allowed brightness-75"
: "bg-blue hover:brightness-125 active:scale-90"
} mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
>
{profileImageSetLoading() ? "Uploading..." : "Set Image"}
</button>
</form>
<Show when={showImageSuccess()}>
<div class="text-green mt-2 text-center text-sm">
Profile image updated!
<button
type="button"
onClick={removeImage}
class="z-20 -ml-6 h-fit rounded-full transition-all hover:brightness-125"
>
<XCircle
height={36}
width={36}
stroke="currentColor"
strokeWidth={1}
/>
</button>
</div>
</Show>
</div>
</div>
<hr class="mx-auto mb-8 max-w-4xl" />
{/* Email Section */}
<div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2">
<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">
Current email:
</div>
{currentUser().email ? (
<span>{currentUser().email}</span>
) : (
<span class="font-light italic underline underline-offset-4">
None Set
</span>
)}
</div>
<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>
)}
<form onSubmit={setUserImage}>
<button
type="submit"
disabled={
profileImageSetLoading() || !profileImageStateChange()
}
class={`${
profileImageSetLoading() || !profileImageStateChange()
? "bg-blue cursor-not-allowed brightness-75"
: "bg-blue hover:brightness-125 active:scale-90"
} mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
>
{profileImageSetLoading()
? "Uploading..."
: "Set Image"}
</button>
</form>
<Show when={showImageSuccess()}>
<div class="text-green mt-2 text-center text-sm">
Profile image updated!
</div>
</Show>
</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!
<hr class="mx-auto mb-8 max-w-4xl" />
{/* Email Section */}
<div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2">
<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">
Current email:
</div>
{currentUser().email ? (
<span>{currentUser().email}</span>
) : (
<span class="font-light italic underline underline-offset-4">
None Set
</span>
)}
</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"}
<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>
<Show when={currentUser().hasPassword}>
<div class="input-group relative mx-4 mb-6">
<form onSubmit={setEmailTrigger} class="mx-auto">
<div class="input-group mx-4">
<input
ref={oldPasswordRef}
type={showOldPasswordInput() ? "text" : "password"}
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>
<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()}
placeholder=" "
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Old Password</label>
<label class="underlinedInputLabel">New Password</label>
<button
type="button"
onClick={() =>
setShowOldPasswordInput(!showOldPasswordInput())
setShowPasswordInput(!showPasswordInput())
}
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
>
<Show when={showOldPasswordInput()} fallback={<Eye />}>
<Show when={showPasswordInput()} 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()}
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>
<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={deleteAccountButtonLoading()}
disabled={passwordChangeLoading() || !passwordsMatch()}
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`}
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`}
>
{deleteAccountButtonLoading()
? "Deleting..."
: "Delete Account"}
{passwordChangeLoading() ? "Setting..." : "Set"}
</button>
<Show when={passwordDeletionError()}>
<div class="text-red mt-2 text-center text-sm">
Password did not match record
<Show when={passwordError()}>
<div class="text-red text-center text-sm">
{currentUser().hasPassword
? "Password did not match record"
: "Error setting password"}
</div>
</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>
</>
)}
</Show>
</>
)}
</Show>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,6 +1,6 @@
import { Show, Suspense, For } from "solid-js";
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 { getRequestEvent } from "solid-js/web";
import SessionDependantLike from "~/components/blog/SessionDependantLike";
@@ -159,6 +159,13 @@ export default function PostPage() {
<Title>
{p().title.replaceAll("_", " ")} | Michael Freno
</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">
{/* Fixed banner image background */}

View File

@@ -1,6 +1,6 @@
import { Show, createSignal, createEffect, onCleanup } from "solid-js";
import { useNavigate, query } from "@solidjs/router";
import { Title } from "@solidjs/meta";
import { Title, Meta } from "@solidjs/meta";
import { createAsync } from "@solidjs/router";
import { getRequestEvent } from "solid-js/web";
import { api } from "~/lib/api";
@@ -195,6 +195,10 @@ export default function CreatePost() {
return (
<>
<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
when={authState()?.privilegeLevel === "admin"}

View File

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

View File

@@ -1,23 +1,32 @@
import { Title, Meta } from "@solidjs/meta";
import DeletionForm from "~/components/DeletionForm";
export default function LifeAndLinageDeletionForm() {
return (
<div class="pt-20">
<div class="mx-auto p-4 md:p-6 lg:p-12">
<div class="w-full justify-center text-text">
<div class="text-xl">
<em>What will happen</em>:
<>
<Title>Account Deletion - Life and Lineage | Michael Freno</Title>
<Meta
name="description"
content="Request account deletion for Life and Lineage. Remove all your data from our system with a 24-hour grace period."
/>
<div class="pt-20">
<div class="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>
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>
</>
);
}

View File

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

View File

@@ -1,155 +1,164 @@
import { Title, Meta } from "@solidjs/meta";
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
import { Typewriter } from "~/components/Typewriter";
export default function Home() {
return (
<main class="flex h-full flex-col gap-8 p-4 text-xl">
<div class="flex-1">
<Typewriter speed={30} keepAlive={2000}>
<div class="text-4xl">Hey!</div>
</Typewriter>
<Typewriter speed={80} keepAlive={2000}>
<div>
My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
<span class="text-blue">Software Engineer</span> based in{" "}
<span class="text-yellow">Brooklyn, NY.</span>
</div>
</Typewriter>
<Typewriter speed={100} keepAlive={2000}>
I'm a passionate dev tooling, game, and open source software
developer. Recently been working in the world of{" "}
<a
href="https://www.love2d.org"
class="text-blue hover-underline-animation"
>
LÖVE
</a>{" "}
(an open source game engine for Lua).
</Typewriter>
<Typewriter speed={100} keepAlive={2000}>
You can see some of my work{" "}
<a
href="https://github.com/mikefreno"
class="text-blue hover-underline-animation"
>
here (github).
</a>
</Typewriter>
<div class="pt-8 text-center">
<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 */}
<div class="border-surface0 flex w-full max-w-2xl flex-col gap-2 rounded-md border-2 p-4 text-center">
<div>My mobile game:</div>
<a
class="text-blue hover-underline-animation"
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
>
Life and Lineage
</a>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<img
src="/lineage-home.png"
alt="Life and Lineage Home"
class="h-full w-full object-cover"
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/lineage-preview.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
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"
/>
<>
<Title>Home | Michael Freno</Title>
<Meta
name="description"
content="Michael Freno - Software Engineer based in Brooklyn, NY. Passionate about dev tooling, game development, and open source software."
/>
<main class="flex h-full flex-col gap-8 p-4 text-xl">
<div class="flex-1">
<Typewriter speed={30} keepAlive={2000}>
<div class="text-4xl">Hey!</div>
</Typewriter>
<Typewriter speed={80} keepAlive={2000}>
<div>
My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
<span class="text-blue">Software Engineer</span> based in{" "}
<span class="text-yellow">Brooklyn, NY.</span>
</div>
</Typewriter>
<Typewriter speed={100} keepAlive={2000}>
I'm a passionate dev tooling, game, and open source software
developer. Recently been working in the world of{" "}
<a
href="https://www.love2d.org"
class="text-blue hover-underline-animation"
>
LÖVE
</a>{" "}
(an open source game engine for Lua).
</Typewriter>
<Typewriter speed={100} keepAlive={2000}>
You can see some of my work{" "}
<a
href="https://github.com/mikefreno"
class="text-blue hover-underline-animation"
>
here (github).
</a>
</Typewriter>
<div class="pt-8 text-center">
<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 */}
<div class="border-surface0 flex w-full max-w-2xl flex-col gap-2 rounded-md border-2 p-4 text-center">
<div>My mobile game:</div>
<a
class="text-blue hover-underline-animation"
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
>
Life and Lineage
</a>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<img
src="/lineage-home.png"
alt="Life and Lineage Home"
class="h-full w-full object-cover"
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/lineage-preview.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
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>
{/* 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>My LÖVE UI library</div>
<a
href="https://github.com/mikefreno/flexlove"
class="text-blue hover-underline-animation"
>
FlexLöve
</a>
<div class="flex flex-col gap-4">
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/flexlove-scrollable.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
playsinline
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/flexlove-input.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
playsinline
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/flexlove-slider.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
playsinline
/>
{/* 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>My LÖVE UI library</div>
<a
href="https://github.com/mikefreno/flexlove"
class="text-blue hover-underline-animation"
>
FlexLöve
</a>
<div class="flex flex-col gap-4">
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/flexlove-scrollable.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
playsinline
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/flexlove-input.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
playsinline
/>
</div>
<div class="aspect-auto w-full overflow-hidden rounded-lg">
<video
src="/flexlove-slider.mp4"
class="h-full w-full object-cover"
autoplay
loop
muted
playsinline
/>
</div>
</div>
</div>
</div>
</div>
</div>
<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
do), you can see{" "}
<a
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
class="text-blue hover-underline-animation"
>
here
</a>{" "}
- 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
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 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
do), you can see{" "}
<a
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
class="text-blue hover-underline-animation"
>
here
</a>{" "}
- 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
home server setup too. Which I will write about soon.
</div>
</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>
</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>
</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,
redirect
} from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import { getEvent } from "vinxi/http";
import GoogleLogo from "~/components/icons/GoogleLogo";
import GitHub from "~/components/icons/GitHub";
@@ -320,324 +321,331 @@ export default function LoginPage() {
};
return (
<div class="flex h-dvh flex-row justify-evenly">
{/* Logo section - hidden on mobile */}
{/* <div class="hidden md:flex">
<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> */}
<>
<Title>Login | Michael Freno</Title>
<Meta
name="description"
content="Sign in to your account or register for a new account to access personalized features and manage your profile."
/>
<div class="flex h-dvh flex-row justify-evenly">
{/* Logo section - hidden on mobile */}
{/* <div class="hidden md:flex">
<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 */}
<div class="pt-24 md:pt-48">
{/* Error message */}
<div class="absolute -mt-12 text-center text-3xl text-red-400 italic">
<Show when={error() === "passwordMismatch"}>
Passwords did not match!
</Show>
<Show when={error() === "duplicate"}>Email Already Exists!</Show>
</div>
{/* Main content */}
<div class="pt-24 md:pt-48">
{/* Error message */}
<div class="absolute -mt-12 text-center text-3xl text-red-400 italic">
<Show when={error() === "passwordMismatch"}>
Passwords did not match!
</Show>
<Show when={error() === "duplicate"}>Email Already Exists!</Show>
</div>
{/* Title */}
<div class="py-2 pl-6 text-2xl md:pl-0">
{register() ? "Register" : "Login"}
</div>
{/* Title */}
<div class="py-2 pl-6 text-2xl md:pl-0">
{register() ? "Register" : "Login"}
</div>
{/* Toggle Register/Login */}
<Show
when={!register()}
fallback={
{/* Toggle Register/Login */}
<Show
when={!register()}
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]">
Already have an account?
Don't have an account yet?
<button
onClick={() => {
setRegister(false);
setRegister(true);
setUsePassword(false);
}}
class="text-blue pl-1 underline hover:brightness-125"
>
Click here to Login
Click here to Register
</button>
</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>
{/* Password confirmation - shown only for registration */}
<Show when={register()}>
<div class="-mt-4 flex justify-center">
{/* 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={showPasswordConfInput() ? "text" : "password"}
type="text"
required
minLength={8}
ref={passwordConfRef}
onInput={handlePasswordConfChange}
ref={emailRef}
placeholder=" "
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Confirm Password</label>
<label class="underlinedInputLabel">Email</label>
</div>
<button
onClick={() => {
setShowPasswordConfInput(!showPasswordConfInput());
passwordConfRef?.focus();
}}
class="absolute mt-14 ml-60"
type="button"
>
<Show
when={showPasswordConfInput()}
fallback={
<EyeSlash
</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"
/>
}
>
<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>
{/* 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>
</button>
<span class="bar"></span>
<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>
{/* Error/Success messages */}
<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`}
showPasswordError()
? "text-red-500"
: showPasswordSuccess()
? "text-green-500"
: "opacity-0 select-none"
} flex min-h-[16px] justify-center italic transition-opacity duration-300 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>
</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>
{/* Error/Success messages */}
{/* Email sent confirmation */}
<div
class={`${
showPasswordError()
? "text-red-500"
: showPasswordSuccess()
? "text-green-500"
: "opacity-0 select-none"
} flex min-h-[16px] justify-center italic transition-opacity duration-300 ease-in-out`}
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={showPasswordError()}>
Credentials did not match any record
</Show>
<Show when={showPasswordSuccess()}>
Login Success! Redirecting...
</Show>
<Show when={emailSent()}>Email Sent!</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"
{/* 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"
>
{renderTime}
</CountdownCircleTimer>
</Show>
{register() ? "Register " : "Sign in "} with Google
<span class="my-auto">
<GoogleLogo height={24} width={24} />
</span>
</A>
{/* 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"
{/* 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"
>
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>
</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>
{register() ? "Register " : "Sign in "} with Github
<span class="my-auto">
<GitHub height={24} width={24} fill="white" />
</span>
</A>
</div>
</div>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,5 +1,6 @@
import { createSignal, createEffect, Show } from "solid-js";
import { A, useNavigate, useSearchParams } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import Eye from "~/components/icons/Eye";
import EyeSlash from "~/components/icons/EyeSlash";
@@ -13,8 +14,10 @@ export default function PasswordResetPage() {
const [passwordBlurred, setPasswordBlurred] = createSignal(false);
const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false);
const [passwordsMatch, setPasswordsMatch] = createSignal(false);
const [showPasswordLengthWarning, setShowPasswordLengthWarning] = createSignal(false);
const [passwordLengthSufficient, setPasswordLengthSufficient] = createSignal(false);
const [showPasswordLengthWarning, setShowPasswordLengthWarning] =
createSignal(false);
const [passwordLengthSufficient, setPasswordLengthSufficient] =
createSignal(false);
const [showRequestNewEmail, setShowRequestNewEmail] = createSignal(false);
const [countDown, setCountDown] = createSignal(false);
const [error, setError] = createSignal("");
@@ -70,8 +73,8 @@ export default function PasswordResetPage() {
body: JSON.stringify({
token: token,
newPassword,
newPasswordConfirmation: newPasswordConf,
}),
newPasswordConfirmation: newPasswordConf
})
});
const result = await response.json();
@@ -158,164 +161,179 @@ export default function PasswordResetPage() {
}
return (
<div class="timer text-center">
<div class="text-sm text-slate-700 dark:text-slate-300">Change Successful!</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 class="text-sm text-slate-700 dark:text-slate-300">
Change Successful!
</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>
);
};
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">
Set New Password
</div>
<form
onSubmit={(e) => setNewPasswordTrigger(e)}
class="mt-4 flex w-full justify-center"
>
<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>
<>
<Title>Reset Password | Michael Freno</Title>
<Meta
name="description"
content="Set a new password for your account to regain access to your profile and personalized features."
/>
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
<div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
Set New Password
</div>
</form>
{/* Error Message */}
<Show when={error() && !showRequestNewEmail()}>
<div class="flex justify-center mt-4">
<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"
<form
onSubmit={(e) => setNewPasswordTrigger(e)}
class="mt-4 flex w-full justify-center"
>
here
</A>
</div>
<div class="flex w-full max-w-md flex-col justify-center px-4">
{/* New Password Input */}
<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 */}
<Show when={!countDown()}>
<div class="flex justify-center mt-6">
{/* Password Length Warning */}
<div
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
href="/login"
class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 underline underline-offset-4 transition-colors"
class="pl-1 text-blue-500 underline underline-offset-4 hover:text-blue-400"
href="/login/request-password-reset"
>
Back to Login
here
</A>
</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 { A, useNavigate } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import { isValidEmail } from "~/lib/validation";
import { getClientCookie } from "~/lib/cookies.client";
@@ -37,7 +38,10 @@ export default function RequestPasswordResetPage() {
createEffect(() => {
const timer = getClientCookie("passwordResetRequested");
if (timer) {
timerInterval = setInterval(() => calcRemainder(timer), 1000) as unknown as number;
timerInterval = setInterval(
() => calcRemainder(timer),
1000
) as unknown as number;
onCleanup(() => {
if (timerInterval) {
clearInterval(timerInterval);
@@ -71,7 +75,7 @@ export default function RequestPasswordResetPage() {
const response = await fetch("/api/trpc/auth.requestPasswordReset", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email }),
body: JSON.stringify({ email })
});
const result = await response.json();
@@ -115,90 +119,97 @@ export default function RequestPasswordResetPage() {
};
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">
Password Reset Request
</div>
<form
onSubmit={(e) => requestPasswordResetTrigger(e)}
class="mt-4 flex w-full justify-center"
>
<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>
<>
<Title>Request Password Reset | Michael Freno</Title>
<Meta
name="description"
content="Request a password reset link to regain access to your account. Enter your email to receive reset instructions."
/>
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
<div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
Password Reset Request
</div>
</form>
{/* Success Message */}
<div
class={`${
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"
<form
onSubmit={(e) => requestPasswordResetTrigger(e)}
class="mt-4 flex w-full justify-center"
>
Back to Login
</A>
<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"
} 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>
</>
);
}

View File

@@ -1,46 +1,52 @@
import { A } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import SimpleParallax from "~/components/SimpleParallax";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
export default function LifeAndLineageMarketing() {
return (
<SimpleParallax>
<div class="flex flex-col items-center justify-center h-full text-white">
<div>
<img
src="/LineageIcon.png"
alt="Lineage App Icon"
height={128}
width={128}
class="object-cover object-center"
/>
</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"
>
<>
<Title>Life and Lineage | Michael Freno</Title>
<Meta
name="description"
content="A dark fantasy adventure mobile game. Download Life and Lineage on the App Store and Google Play."
/>
<SimpleParallax>
<div class="flex h-full flex-col items-center justify-center text-white">
<div>
<img
src="/google-play-badge.png"
alt="google-play"
width={180}
height={60}
src="/LineageIcon.png"
alt="Lineage App Icon"
height={128}
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>
</SimpleParallax>
</SimpleParallax>
</>
);
}

View File

@@ -1,99 +1,107 @@
import { A } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
export default function PrivacyPolicy() {
return (
<div class="min-h-screen px-[8vw] py-[10vh]">
<div class="py-4 text-xl">Life and Lineage&apos;s Privacy Policy</div>
<div class="py-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.
<>
<Title>Privacy Policy - Life and Lineage | Michael Freno</Title>
<Meta
name="description"
content="Privacy policy for Life and Lineage mobile game, outlining data collection, usage, and user rights."
/>
<div class="min-h-screen px-[8vw] py-[10vh]">
<div class="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>
<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 { Title, Meta } from "@solidjs/meta";
export default function PrivacyPolicy() {
return (
<div class="bg-zinc-100 dark:bg-zinc-900">
<div class="min-h-screen px-[8vw] py-[8vh]">
<div class="py-4 text-xl">
Shapes with Abigail!&apos;s Privacy Policy
</div>
<div class="py-2">Last Updated: December 21, 2023</div>
<div class="py-2">
Welcome to Shapes with Abigail! (&apos;We&apos; , &apos;Us&apos;,
&apos;Our&apos;). Your privacy is important to us. For that reason,
our app, &quot;Shapes with Abigail!&quot; has been designed to
provide our users with a secure environment. This privacy policy
will help you understand our policies and procedures related to the
non-collection, non-use, and non-storage of personal information
from our users.
</div>
<ol>
<>
<Title>Privacy Policy - Shapes with Abigail | Michael Freno</Title>
<Meta
name="description"
content="Privacy policy for Shapes with Abigail app, explaining our commitment to child safety and non-collection of personal data."
/>
<div class="bg-zinc-100 dark:bg-zinc-900">
<div class="min-h-screen px-[8vw] py-[8vh]">
<div class="py-4 text-xl">
Shapes with Abigail!&apos;s Privacy Policy
</div>
<div class="py-2">Last Updated: December 21, 2023</div>
<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) 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.
Welcome to Shapes with Abigail! (&apos;We&apos; , &apos;Us&apos;,
&apos;Our&apos;). Your privacy is important to us. For that reason,
our app, &quot;Shapes with Abigail!&quot; has been designed to
provide our users with a secure environment. This privacy policy
will help you understand our policies and procedures related to the
non-collection, non-use, and non-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) 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 class="py-2">
<div class="pb-2 text-lg">
<span class="-ml-4 pr-2">2.</span> Third-Party Access
<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) 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="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="pb-2 text-lg">
<span class="-ml-4 pr-2">3.</span> Security
<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) 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="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="pb-2 text-lg">
<span class="-ml-4 pr-2">4.</span> Changes to the Privacy
Policy
<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. However, since we do not collect any
personal data, these updates are likely to be insignificant.
</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="pb-2 text-lg">
<span class="-ml-4 pr-2">5.</span> Contact Us
<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-400 underline-offset-4 hover:underline"
>
here
</A>
.
</div>
</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>
</ol>
</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() {
return (
<main class="flex h-screen w-full flex-col">
<Title>Resume - Freno.dev</Title>
<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>
<>
<Title>Resume | Michael Freno</Title>
<Meta
name="description"
content="View Michael Freno's resume - Software Engineer with expertise in full-stack development, game development, and open source."
/>
<main class="flex h-screen w-full flex-col">
<Title>Resume - Freno.dev</Title>
<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 { Title, Meta } from "@solidjs/meta";
import Input from "~/components/ui/Input";
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
@@ -36,134 +41,153 @@ export default function TestUtilsPage() {
setLoading(true);
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
await new Promise((resolve) => setTimeout(resolve, 2000));
alert(`Form submitted!\nEmail: ${email()}\nPassword: ${password()}`);
setLoading(false);
};
return (
<main class="min-h-screen bg-gray-100 p-8">
<div class="max-w-2xl mx-auto">
<div class="bg-white rounded-lg shadow-lg p-6 mb-6">
<h1 class="text-3xl font-bold mb-2">Task 01 - Utility Testing</h1>
<p class="text-gray-600 mb-4">
Testing shared utilities, types, and UI components
</p>
</div>
<>
<Title>Utility Testing | Michael Freno</Title>
<Meta
name="description"
content="Testing page for form components and validation utilities."
/>
<main class="min-h-screen bg-gray-100 p-8">
<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">
<h2 class="text-xl font-bold mb-4">Form Components & Validation</h2>
<div class="mb-6 rounded-lg bg-white p-6 shadow">
<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
/>
<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
type="password"
label="Password"
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
error={passwordError()}
helperText="Minimum 8 characters"
required
/>
<Input
type="password"
label="Password"
value={password()}
onInput={(e) => setPassword(e.currentTarget.value)}
error={passwordError()}
helperText="Minimum 8 characters"
required
/>
<Input
type="password"
label="Confirm Password"
value={passwordConf()}
onInput={(e) => setPasswordConf(e.currentTarget.value)}
error={passwordMatchError()}
required
/>
<Input
type="password"
label="Confirm Password"
value={passwordConf()}
onInput={(e) => setPasswordConf(e.currentTarget.value)}
error={passwordMatchError()}
required
/>
<div class="flex gap-2">
<Button
type="submit"
variant="primary"
loading={loading()}
disabled={
!isValidEmail(email()) ||
!validatePassword(password()).isValid ||
!passwordsMatch(password(), passwordConf())
}
>
Submit
</Button>
<div class="flex gap-2">
<Button
type="submit"
variant="primary"
loading={loading()}
disabled={
!isValidEmail(email()) ||
!validatePassword(password()).isValid ||
!passwordsMatch(password(), passwordConf())
}
>
Submit
</Button>
<Button
type="button"
variant="secondary"
onClick={() => {
setEmail("");
setPassword("");
setPasswordConf("");
}}
>
Reset
</Button>
<Button
type="button"
variant="secondary"
onClick={() => {
setEmail("");
setPassword("");
setPasswordConf("");
}}
>
Reset
</Button>
<Button
type="button"
variant="danger"
onClick={() => alert("Danger action!")}
>
Delete
</Button>
<Button
type="button"
variant="danger"
onClick={() => alert("Danger action!")}
>
Delete
</Button>
<Button
type="button"
variant="ghost"
onClick={() => alert("Ghost action!")}
>
Cancel
</Button>
</div>
</form>
</div>
<Button
type="button"
variant="ghost"
onClick={() => alert("Ghost action!")}
>
Cancel
</Button>
</div>
</form>
</div>
<div class="bg-white rounded-lg shadow p-6">
<h2 class="text-xl font-bold mb-4">Validation Status</h2>
<div class="rounded-lg bg-white p-6 shadow">
<h2 class="mb-4 text-xl font-bold">Validation Status</h2>
<div class="space-y-2 text-sm">
<div class="flex items-center gap-2">
<span class={`w-3 h-3 rounded-full ${isValidEmail(email()) ? "bg-green-500" : "bg-gray-300"}`} />
<span>Email Valid: {isValidEmail(email()) ? "✓" : "✗"}</span>
</div>
<div class="space-y-2 text-sm">
<div class="flex items-center gap-2">
<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={`w-3 h-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 ${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={`w-3 h-3 rounded-full ${passwordsMatch(password(), passwordConf()) ? "bg-green-500" : "bg-gray-300"}`} />
<span>Passwords Match: {passwordsMatch(password(), passwordConf()) ? "✓" : "✗"}</span>
<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 class="bg-blue-50 border border-blue-200 rounded p-4 mt-6">
<h3 class="font-bold text-blue-800 mb-2"> Task 01 Complete</h3>
<ul class="text-sm text-blue-700 space-y-1">
<li> User types created</li>
<li> Cookie utilities created</li>
<li> Validation helpers created</li>
<li> Input component created</li>
<li> Button component created</li>
<li> Conversion patterns documented</li>
<li> Build successful</li>
</ul>
<div class="mt-6 rounded border border-blue-200 bg-blue-50 p-4">
<h3 class="mb-2 font-bold text-blue-800"> Task 01 Complete</h3>
<ul class="space-y-1 text-sm text-blue-700">
<li> User types created</li>
<li> Cookie utilities created</li>
<li> Validation helpers created</li>
<li> Input component created</li>
<li> Button component created</li>
<li> Conversion patterns documented</li>
<li> Build successful</li>
</ul>
</div>
</div>
</div>
</main>
</main>
</>
);
}

View File

@@ -1,5 +1,6 @@
import { createSignal, For, Show } from "solid-js";
import { query, createAsync } from "@solidjs/router";
import { Title, Meta } from "@solidjs/meta";
import { getRequestEvent } from "solid-js/web";
import { api } from "~/lib/api";
@@ -932,297 +933,318 @@ export default function TestPage() {
};
return (
<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.
<>
<Title>API Testing | Michael Freno</Title>
<Meta
name="description"
content="tRPC API testing dashboard for developers to test endpoints and verify functionality."
/>
<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>
}
>
<main class="min-h-screen p-8">
<div class="mx-auto max-w-6xl">
<div class="bg-surface0 mb-6 rounded-lg p-6 shadow-lg">
<h1 class="mb-2 text-3xl font-bold">tRPC API Testing Dashboard</h1>
<p class="text-text mb-4">
Complete API coverage: Example, Auth, Database, User, Misc, and
Lineage routers
</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.
}
>
<main class="min-h-screen p-8">
<div class="mx-auto max-w-6xl">
<div class="bg-surface0 mb-6 rounded-lg p-6 shadow-lg">
<h1 class="mb-2 text-3xl font-bold">
tRPC API Testing Dashboard
</h1>
<p class="text-text mb-4">
Complete API coverage: Example, Auth, Database, User, Misc, and
Lineage routers
</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 class="space-y-4">
<For each={routerSections}>
{(section) => {
const isExpanded = () => expandedSections().has(section.name);
<div class="space-y-4">
<For each={routerSections}>
{(section) => {
const isExpanded = () => expandedSections().has(section.name);
return (
<div class="bg-surface0 rounded-lg shadow">
{/* Section Header */}
<button
onClick={() => toggleSection(section.name)}
class="flex w-full items-center justify-between px-6 py-4 transition"
>
<div class="text-left">
<h2 class="text-xl font-bold">{section.name}</h2>
<p class="text-subtext0 text-sm">
{section.description}
</p>
<p class="text-subtext1 mt-1 text-xs">
{section.endpoints.length} endpoint
{section.endpoints.length !== 1 ? "s" : ""}
</p>
</div>
<div class="text-subtext1 text-2xl">
{isExpanded() ? "" : "+"}
</div>
</button>
return (
<div class="bg-surface0 rounded-lg shadow">
{/* Section Header */}
<button
onClick={() => toggleSection(section.name)}
class="flex w-full items-center justify-between px-6 py-4 transition"
>
<div class="text-left">
<h2 class="text-xl font-bold">{section.name}</h2>
<p class="text-subtext0 text-sm">
{section.description}
</p>
<p class="text-subtext1 mt-1 text-xs">
{section.endpoints.length} endpoint
{section.endpoints.length !== 1 ? "s" : ""}
</p>
</div>
<div class="text-subtext1 text-2xl">
{isExpanded() ? "" : "+"}
</div>
</button>
{/* Section Content */}
<Show when={isExpanded()}>
<div class="border-base space-y-4 border-t p-6">
<For each={section.endpoints}>
{(endpoint) => {
const key = `${endpoint.router}.${endpoint.procedure}`;
const hasInput = endpoint.sampleInput !== undefined;
const displayInput = () => {
if (inputEdits()[key]) {
return inputEdits()[key];
}
// Handle primitive values (string, number, boolean)
if (typeof endpoint.sampleInput === "string") {
return `"${endpoint.sampleInput}"`;
}
if (
typeof endpoint.sampleInput === "number" ||
typeof endpoint.sampleInput === "boolean"
) {
return String(endpoint.sampleInput);
}
// Handle objects and arrays
return JSON.stringify(
endpoint.sampleInput,
null,
2
);
};
{/* Section Content */}
<Show when={isExpanded()}>
<div class="border-base space-y-4 border-t p-6">
<For each={section.endpoints}>
{(endpoint) => {
const key = `${endpoint.router}.${endpoint.procedure}`;
const hasInput =
endpoint.sampleInput !== undefined;
const displayInput = () => {
if (inputEdits()[key]) {
return inputEdits()[key];
}
// Handle primitive values (string, number, boolean)
if (typeof endpoint.sampleInput === "string") {
return `"${endpoint.sampleInput}"`;
}
if (
typeof endpoint.sampleInput === "number" ||
typeof endpoint.sampleInput === "boolean"
) {
return String(endpoint.sampleInput);
}
// Handle objects and arrays
return JSON.stringify(
endpoint.sampleInput,
null,
2
);
};
return (
<div class="bg-surface2 border-surface1 rounded-lg border p-4">
{/* Endpoint Header */}
<div class="mb-3 flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2">
<h3 class="text-subtext0 text-lg font-semibold">
{endpoint.name}
</h3>
<Show when={endpoint.requiresAuth}>
<span class="bg-surface1 text-yellow rounded px-2 py-1 text-xs">
🔒 Auth Required
return (
<div class="bg-surface2 border-surface1 rounded-lg border p-4">
{/* Endpoint Header */}
<div class="mb-3 flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2">
<h3 class="text-subtext0 text-lg font-semibold">
{endpoint.name}
</h3>
<Show when={endpoint.requiresAuth}>
<span class="bg-surface1 text-yellow rounded px-2 py-1 text-xs">
🔒 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>
</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>
</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>
<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>
{/* Input Editor */}
<Show when={hasInput}>
<div class="mb-3">
<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 */}
<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>
);
}}
</For>
</div>
</Show>
</div>
);
}}
</For>
</div>
{/* Input Editor */}
<Show when={hasInput}>
<div class="mb-3">
<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>
{/* Footer Instructions */}
<div class="bg-overlay2 mt-6 rounded-lg p-6 shadow-lg">
<h2 class="text-crust mb-4 text-2xl font-bold">Testing Guide</h2>
{/* 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>
<div class="space-y-4 text-base">
<div>
<h3 class="mb-2 text-lg font-semibold">
🟢 No Auth Required
</h3>
<ul class="ml-6 list-disc space-y-1 text-sm">
<li>
<strong>Example Router</strong> - Hello endpoint
</li>
<li>
<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 */}
<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>
);
}}
</For>
</div>
</Show>
</div>
);
}}
</For>
</div>
<div>
<h3 class="mb-2 text-lg font-semibold">🟡 Auth Required</h3>
<p class="mb-2 text-sm">
These need valid JWT tokens from login/registration:
</p>
<ul class="ml-6 list-disc space-y-1 text-sm">
<li>
<strong>Example Router</strong> - Get Profile
</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>
{/* Footer Instructions */}
<div class="bg-overlay2 mt-6 rounded-lg p-6 shadow-lg">
<h2 class="text-crust mb-4 text-2xl font-bold">Testing Guide</h2>
<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 class="space-y-4 text-base">
<div>
<h3 class="mb-2 text-lg font-semibold">🟢 No Auth Required</h3>
<ul class="ml-6 list-disc space-y-1 text-sm">
<li>
<strong>Example Router</strong> - Hello endpoint
</li>
<li>
<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>
<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>
<h3 class="mb-2 text-lg font-semibold">🟡 Auth Required</h3>
<p class="mb-2 text-sm">
These need valid JWT tokens from login/registration:
</p>
<ul class="ml-6 list-disc space-y-1 text-sm">
<li>
<strong>Example Router</strong> - Get Profile
</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 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>
</main>
</Show>
</main>
</Show>
</>
);
}