metadata and titles
This commit is contained in:
@@ -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 */}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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's Privacy Policy</div>
|
||||
<div class="py-2">Last Updated: October 22, 2024</div>
|
||||
<div class="py-2">
|
||||
Welcome to Life and Lineage ('We', 'Us',
|
||||
'Our'). 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's Privacy Policy</div>
|
||||
<div class="py-2">Last Updated: October 22, 2024</div>
|
||||
<div class="py-2">
|
||||
Welcome to Life and Lineage ('We', 'Us',
|
||||
'Our'). 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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!'s Privacy Policy
|
||||
</div>
|
||||
<div class="py-2">Last Updated: December 21, 2023</div>
|
||||
<div class="py-2">
|
||||
Welcome to Shapes with Abigail! ('We' , 'Us',
|
||||
'Our'). Your privacy is important to us. For that reason,
|
||||
our app, "Shapes with Abigail!" 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!'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! ('We' , 'Us',
|
||||
'Our'). Your privacy is important to us. For that reason,
|
||||
our app, "Shapes with Abigail!" 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' 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' 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user