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 { HttpStatusCode } from "@solidjs/start";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { createEffect, createSignal, For } from "solid-js";
|
import { createEffect, createSignal, For } from "solid-js";
|
||||||
@@ -47,7 +47,11 @@ export default function Page_401() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>401 - Unauthorized</Title>
|
<Title>401 Unauthorized | Michael Freno</Title>
|
||||||
|
<Meta
|
||||||
|
name="description"
|
||||||
|
content="401 - Unauthorized access. Please log in to access this page."
|
||||||
|
/>
|
||||||
<HttpStatusCode code={401} />
|
<HttpStatusCode code={401} />
|
||||||
<div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-slate-900 via-amber-950/20 to-slate-900 dark:from-black dark:via-amber-950/30 dark:to-black">
|
<div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-slate-900 via-amber-950/20 to-slate-900 dark:from-black dark:via-amber-950/30 dark:to-black">
|
||||||
{/* Animated particle background */}
|
{/* Animated particle background */}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Title } from "@solidjs/meta";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { HttpStatusCode } from "@solidjs/start";
|
import { HttpStatusCode } from "@solidjs/start";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { createEffect, createSignal, For } from "solid-js";
|
import { createEffect, createSignal, For } from "solid-js";
|
||||||
@@ -43,7 +43,11 @@ export default function NotFound() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>404 - Not Found</Title>
|
<Title>404 Not Found | Michael Freno</Title>
|
||||||
|
<Meta
|
||||||
|
name="description"
|
||||||
|
content="404 - Page not found. The page you're looking for doesn't exist."
|
||||||
|
/>
|
||||||
<HttpStatusCode code={404} />
|
<HttpStatusCode code={404} />
|
||||||
<div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 dark:from-black dark:via-slate-900 dark:to-black">
|
<div class="relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 dark:from-black dark:via-slate-900 dark:to-black">
|
||||||
{/* Animated particle background */}
|
{/* Animated particle background */}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { Title } from "@solidjs/meta";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
return (
|
return (
|
||||||
<main>
|
<>
|
||||||
<Title>About</Title>
|
<Title>About | Michael Freno</Title>
|
||||||
<h1>About</h1>
|
<Meta
|
||||||
</main>
|
name="description"
|
||||||
|
content="Learn more about Michael Freno - Software Engineer, game developer, and open source contributor."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<h1>About</h1>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createSignal, createEffect, Show, onMount } from "solid-js";
|
import { createSignal, createEffect, Show, onMount } from "solid-js";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { useNavigate, cache, redirect } from "@solidjs/router";
|
import { useNavigate, cache, redirect } from "@solidjs/router";
|
||||||
import { getEvent } from "vinxi/http";
|
import { getEvent } from "vinxi/http";
|
||||||
import Eye from "~/components/icons/Eye";
|
import Eye from "~/components/icons/Eye";
|
||||||
@@ -453,389 +454,407 @@ export default function AccountPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36">
|
<>
|
||||||
<div class="pt-24">
|
<Title>Account | Michael Freno</Title>
|
||||||
<Show
|
<Meta
|
||||||
when={!loading() && user()}
|
name="description"
|
||||||
fallback={
|
content="Manage your account settings, update profile information, and configure preferences."
|
||||||
<div class="mt-[35vh] flex w-full justify-center">
|
/>
|
||||||
<div class="text-text text-xl">Loading...</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(currentUser) => (
|
|
||||||
<>
|
|
||||||
<div class="text-text mb-8 text-center text-3xl font-bold">
|
|
||||||
Account Settings
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Profile Image Section */}
|
<div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36">
|
||||||
<div class="mx-auto mb-8 flex max-w-md justify-center">
|
<div class="pt-24">
|
||||||
<div class="flex flex-col py-4">
|
<Show
|
||||||
<div class="mb-2 text-center text-lg font-semibold">
|
when={!loading() && user()}
|
||||||
Profile Image
|
fallback={
|
||||||
</div>
|
<div class="mt-[35vh] flex w-full justify-center">
|
||||||
<div class="flex items-start">
|
<div class="text-text text-xl">Loading...</div>
|
||||||
<Dropzone
|
</div>
|
||||||
onDrop={handleImageDrop}
|
}
|
||||||
acceptedFiles="image/jpg, image/jpeg, image/png"
|
>
|
||||||
fileHolder={profileImageHolder()}
|
{(currentUser) => (
|
||||||
preSet={preSetHolder() || currentUser().image || null}
|
<>
|
||||||
/>
|
<div class="text-text mb-8 text-center text-3xl font-bold">
|
||||||
<button
|
Account Settings
|
||||||
type="button"
|
</div>
|
||||||
onClick={removeImage}
|
|
||||||
class="z-20 -ml-6 h-fit rounded-full transition-all hover:brightness-125"
|
{/* Profile Image Section */}
|
||||||
>
|
<div class="mx-auto mb-8 flex max-w-md justify-center">
|
||||||
<XCircle
|
<div class="flex flex-col py-4">
|
||||||
height={36}
|
<div class="mb-2 text-center text-lg font-semibold">
|
||||||
width={36}
|
Profile Image
|
||||||
stroke="currentColor"
|
</div>
|
||||||
strokeWidth={1}
|
<div class="flex items-start">
|
||||||
|
<Dropzone
|
||||||
|
onDrop={handleImageDrop}
|
||||||
|
acceptedFiles="image/jpg, image/jpeg, image/png"
|
||||||
|
fileHolder={profileImageHolder()}
|
||||||
|
preSet={preSetHolder() || currentUser().image || null}
|
||||||
/>
|
/>
|
||||||
</button>
|
<button
|
||||||
</div>
|
type="button"
|
||||||
<form onSubmit={setUserImage}>
|
onClick={removeImage}
|
||||||
<button
|
class="z-20 -ml-6 h-fit rounded-full transition-all hover:brightness-125"
|
||||||
type="submit"
|
>
|
||||||
disabled={
|
<XCircle
|
||||||
profileImageSetLoading() || !profileImageStateChange()
|
height={36}
|
||||||
}
|
width={36}
|
||||||
class={`${
|
stroke="currentColor"
|
||||||
profileImageSetLoading() || !profileImageStateChange()
|
strokeWidth={1}
|
||||||
? "bg-blue cursor-not-allowed brightness-75"
|
/>
|
||||||
: "bg-blue hover:brightness-125 active:scale-90"
|
</button>
|
||||||
} mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
|
||||||
>
|
|
||||||
{profileImageSetLoading() ? "Uploading..." : "Set Image"}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<Show when={showImageSuccess()}>
|
|
||||||
<div class="text-green mt-2 text-center text-sm">
|
|
||||||
Profile image updated!
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<form onSubmit={setUserImage}>
|
||||||
</div>
|
<button
|
||||||
</div>
|
type="submit"
|
||||||
|
disabled={
|
||||||
<hr class="mx-auto mb-8 max-w-4xl" />
|
profileImageSetLoading() || !profileImageStateChange()
|
||||||
|
}
|
||||||
{/* Email Section */}
|
class={`${
|
||||||
<div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2">
|
profileImageSetLoading() || !profileImageStateChange()
|
||||||
<div class="flex items-center justify-center text-lg md:justify-normal">
|
? "bg-blue cursor-not-allowed brightness-75"
|
||||||
<div class="flex flex-col lg:flex-row">
|
: "bg-blue hover:brightness-125 active:scale-90"
|
||||||
<div class="pr-1 font-semibold whitespace-nowrap">
|
} mt-2 flex w-full justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
Current email:
|
>
|
||||||
</div>
|
{profileImageSetLoading()
|
||||||
{currentUser().email ? (
|
? "Uploading..."
|
||||||
<span>{currentUser().email}</span>
|
: "Set Image"}
|
||||||
) : (
|
</button>
|
||||||
<span class="font-light italic underline underline-offset-4">
|
</form>
|
||||||
None Set
|
<Show when={showImageSuccess()}>
|
||||||
</span>
|
<div class="text-green mt-2 text-center text-sm">
|
||||||
)}
|
Profile image updated!
|
||||||
</div>
|
</div>
|
||||||
<Show
|
</Show>
|
||||||
when={currentUser().email && !currentUser().emailVerified}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
onClick={sendEmailVerification}
|
|
||||||
class="text-red ml-2 text-sm underline transition-all hover:brightness-125"
|
|
||||||
>
|
|
||||||
Verify Email
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={setEmailTrigger} class="mx-auto">
|
|
||||||
<div class="input-group mx-4">
|
|
||||||
<input
|
|
||||||
ref={emailRef}
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
disabled={
|
|
||||||
emailButtonLoading() ||
|
|
||||||
(currentUser().email !== null &&
|
|
||||||
!currentUser().emailVerified)
|
|
||||||
}
|
|
||||||
placeholder=" "
|
|
||||||
class="underlinedInput bg-transparent"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Set New Email</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={
|
|
||||||
emailButtonLoading() ||
|
|
||||||
(currentUser().email !== null &&
|
|
||||||
!currentUser().emailVerified)
|
|
||||||
}
|
|
||||||
class={`${
|
|
||||||
emailButtonLoading() ||
|
|
||||||
(currentUser().email !== null &&
|
|
||||||
!currentUser().emailVerified)
|
|
||||||
? "bg-blue cursor-not-allowed brightness-75"
|
|
||||||
: "bg-blue hover:brightness-125 active:scale-90"
|
|
||||||
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
|
||||||
>
|
|
||||||
{emailButtonLoading() ? "Submitting..." : "Submit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Show when={showEmailSuccess()}>
|
|
||||||
<div class="text-green mt-2 text-center text-sm">
|
|
||||||
Email updated!
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Display Name Section */}
|
|
||||||
<div class="flex items-center justify-center text-lg md:justify-normal">
|
|
||||||
<div class="flex flex-col lg:flex-row">
|
|
||||||
<div class="pr-1 font-semibold whitespace-nowrap">
|
|
||||||
Display Name:
|
|
||||||
</div>
|
|
||||||
{currentUser().displayName ? (
|
|
||||||
<span>{currentUser().displayName}</span>
|
|
||||||
) : (
|
|
||||||
<span class="font-light italic underline underline-offset-4">
|
|
||||||
None Set
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={setDisplayNameTrigger} class="mx-auto">
|
<hr class="mx-auto mb-8 max-w-4xl" />
|
||||||
<div class="input-group mx-4">
|
|
||||||
<input
|
{/* Email Section */}
|
||||||
ref={displayNameRef}
|
<div class="mx-auto flex max-w-4xl flex-col gap-6 md:grid md:grid-cols-2">
|
||||||
type="text"
|
<div class="flex items-center justify-center text-lg md:justify-normal">
|
||||||
required
|
<div class="flex flex-col lg:flex-row">
|
||||||
disabled={displayNameButtonLoading()}
|
<div class="pr-1 font-semibold whitespace-nowrap">
|
||||||
placeholder=" "
|
Current email:
|
||||||
class="underlinedInput bg-transparent"
|
</div>
|
||||||
/>
|
{currentUser().email ? (
|
||||||
<span class="bar"></span>
|
<span>{currentUser().email}</span>
|
||||||
<label class="underlinedInputLabel">
|
) : (
|
||||||
Set {currentUser().displayName ? "New " : ""}Display Name
|
<span class="font-light italic underline underline-offset-4">
|
||||||
</label>
|
None Set
|
||||||
</div>
|
</span>
|
||||||
<div class="flex justify-end">
|
)}
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={displayNameButtonLoading()}
|
|
||||||
class={`${
|
|
||||||
displayNameButtonLoading()
|
|
||||||
? "bg-blue cursor-not-allowed brightness-75"
|
|
||||||
: "bg-blue hover:brightness-125 active:scale-90"
|
|
||||||
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
|
||||||
>
|
|
||||||
{displayNameButtonLoading() ? "Submitting..." : "Submit"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Show when={showDisplayNameSuccess()}>
|
|
||||||
<div class="text-green mt-2 text-center text-sm">
|
|
||||||
Display name updated!
|
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
<Show
|
||||||
</form>
|
when={currentUser().email && !currentUser().emailVerified}
|
||||||
</div>
|
>
|
||||||
|
<button
|
||||||
{/* Password Change/Set Section */}
|
onClick={sendEmailVerification}
|
||||||
<form
|
class="text-red ml-2 text-sm underline transition-all hover:brightness-125"
|
||||||
onSubmit={handlePasswordSubmit}
|
>
|
||||||
class="mt-8 flex w-full justify-center"
|
Verify Email
|
||||||
>
|
</button>
|
||||||
<div class="flex w-full max-w-md flex-col justify-center">
|
</Show>
|
||||||
<div class="mb-4 text-center text-xl font-semibold">
|
|
||||||
{currentUser().hasPassword
|
|
||||||
? "Change Password"
|
|
||||||
: "Set Password"}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when={currentUser().hasPassword}>
|
<form onSubmit={setEmailTrigger} class="mx-auto">
|
||||||
<div class="input-group relative mx-4 mb-6">
|
<div class="input-group mx-4">
|
||||||
<input
|
<input
|
||||||
ref={oldPasswordRef}
|
ref={emailRef}
|
||||||
type={showOldPasswordInput() ? "text" : "password"}
|
type="email"
|
||||||
required
|
required
|
||||||
|
disabled={
|
||||||
|
emailButtonLoading() ||
|
||||||
|
(currentUser().email !== null &&
|
||||||
|
!currentUser().emailVerified)
|
||||||
|
}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput bg-transparent"
|
||||||
|
/>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<label class="underlinedInputLabel">Set New Email</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={
|
||||||
|
emailButtonLoading() ||
|
||||||
|
(currentUser().email !== null &&
|
||||||
|
!currentUser().emailVerified)
|
||||||
|
}
|
||||||
|
class={`${
|
||||||
|
emailButtonLoading() ||
|
||||||
|
(currentUser().email !== null &&
|
||||||
|
!currentUser().emailVerified)
|
||||||
|
? "bg-blue cursor-not-allowed brightness-75"
|
||||||
|
: "bg-blue hover:brightness-125 active:scale-90"
|
||||||
|
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
|
>
|
||||||
|
{emailButtonLoading() ? "Submitting..." : "Submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Show when={showEmailSuccess()}>
|
||||||
|
<div class="text-green mt-2 text-center text-sm">
|
||||||
|
Email updated!
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Display Name Section */}
|
||||||
|
<div class="flex items-center justify-center text-lg md:justify-normal">
|
||||||
|
<div class="flex flex-col lg:flex-row">
|
||||||
|
<div class="pr-1 font-semibold whitespace-nowrap">
|
||||||
|
Display Name:
|
||||||
|
</div>
|
||||||
|
{currentUser().displayName ? (
|
||||||
|
<span>{currentUser().displayName}</span>
|
||||||
|
) : (
|
||||||
|
<span class="font-light italic underline underline-offset-4">
|
||||||
|
None Set
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={setDisplayNameTrigger} class="mx-auto">
|
||||||
|
<div class="input-group mx-4">
|
||||||
|
<input
|
||||||
|
ref={displayNameRef}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
disabled={displayNameButtonLoading()}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput bg-transparent"
|
||||||
|
/>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<label class="underlinedInputLabel">
|
||||||
|
Set {currentUser().displayName ? "New " : ""}Display
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={displayNameButtonLoading()}
|
||||||
|
class={`${
|
||||||
|
displayNameButtonLoading()
|
||||||
|
? "bg-blue cursor-not-allowed brightness-75"
|
||||||
|
: "bg-blue hover:brightness-125 active:scale-90"
|
||||||
|
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
|
>
|
||||||
|
{displayNameButtonLoading()
|
||||||
|
? "Submitting..."
|
||||||
|
: "Submit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Show when={showDisplayNameSuccess()}>
|
||||||
|
<div class="text-green mt-2 text-center text-sm">
|
||||||
|
Display name updated!
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password Change/Set Section */}
|
||||||
|
<form
|
||||||
|
onSubmit={handlePasswordSubmit}
|
||||||
|
class="mt-8 flex w-full justify-center"
|
||||||
|
>
|
||||||
|
<div class="flex w-full max-w-md flex-col justify-center">
|
||||||
|
<div class="mb-4 text-center text-xl font-semibold">
|
||||||
|
{currentUser().hasPassword
|
||||||
|
? "Change Password"
|
||||||
|
: "Set Password"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={currentUser().hasPassword}>
|
||||||
|
<div class="input-group relative mx-4 mb-6">
|
||||||
|
<input
|
||||||
|
ref={oldPasswordRef}
|
||||||
|
type={showOldPasswordInput() ? "text" : "password"}
|
||||||
|
required
|
||||||
|
disabled={passwordChangeLoading()}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput w-full bg-transparent pr-10"
|
||||||
|
/>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<label class="underlinedInputLabel">Old Password</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setShowOldPasswordInput(!showOldPasswordInput())
|
||||||
|
}
|
||||||
|
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={showOldPasswordInput()}
|
||||||
|
fallback={<Eye />}
|
||||||
|
>
|
||||||
|
<EyeSlash />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="input-group relative mx-4 mb-2">
|
||||||
|
<input
|
||||||
|
ref={newPasswordRef}
|
||||||
|
type={showPasswordInput() ? "text" : "password"}
|
||||||
|
required
|
||||||
|
onInput={handleNewPasswordChange}
|
||||||
|
onBlur={handlePasswordBlur}
|
||||||
disabled={passwordChangeLoading()}
|
disabled={passwordChangeLoading()}
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
class="underlinedInput w-full bg-transparent pr-10"
|
class="underlinedInput w-full bg-transparent pr-10"
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
<span class="bar"></span>
|
||||||
<label class="underlinedInputLabel">Old Password</label>
|
<label class="underlinedInputLabel">New Password</label>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setShowOldPasswordInput(!showOldPasswordInput())
|
setShowPasswordInput(!showPasswordInput())
|
||||||
}
|
}
|
||||||
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
||||||
>
|
>
|
||||||
<Show when={showOldPasswordInput()} fallback={<Eye />}>
|
<Show when={showPasswordInput()} fallback={<Eye />}>
|
||||||
<EyeSlash />
|
<EyeSlash />
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div class="input-group relative mx-4 mb-2">
|
<Show when={showPasswordLengthWarning()}>
|
||||||
<input
|
<div class="text-red mb-4 text-center text-sm">
|
||||||
ref={newPasswordRef}
|
Password too short! Min Length: 8
|
||||||
type={showPasswordInput() ? "text" : "password"}
|
|
||||||
required
|
|
||||||
onInput={handleNewPasswordChange}
|
|
||||||
onBlur={handlePasswordBlur}
|
|
||||||
disabled={passwordChangeLoading()}
|
|
||||||
placeholder=" "
|
|
||||||
class="underlinedInput w-full bg-transparent pr-10"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">New Password</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPasswordInput(!showPasswordInput())}
|
|
||||||
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
|
||||||
>
|
|
||||||
<Show when={showPasswordInput()} fallback={<Eye />}>
|
|
||||||
<EyeSlash />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show when={showPasswordLengthWarning()}>
|
|
||||||
<div class="text-red mb-4 text-center text-sm">
|
|
||||||
Password too short! Min Length: 8
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<div class="input-group relative mx-4 mb-2">
|
|
||||||
<input
|
|
||||||
ref={newPasswordConfRef}
|
|
||||||
type={showPasswordConfInput() ? "text" : "password"}
|
|
||||||
required
|
|
||||||
onInput={handlePasswordConfChange}
|
|
||||||
disabled={passwordChangeLoading()}
|
|
||||||
placeholder=" "
|
|
||||||
class="underlinedInput w-full bg-transparent pr-10"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">
|
|
||||||
Password Confirmation
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
setShowPasswordConfInput(!showPasswordConfInput())
|
|
||||||
}
|
|
||||||
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
|
||||||
>
|
|
||||||
<Show when={showPasswordConfInput()} fallback={<Eye />}>
|
|
||||||
<EyeSlash />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Show
|
|
||||||
when={
|
|
||||||
!passwordsMatch() &&
|
|
||||||
passwordLengthSufficient() &&
|
|
||||||
newPasswordConfRef &&
|
|
||||||
newPasswordConfRef.value.length >= 6
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="text-red mb-4 text-center text-sm">
|
|
||||||
Passwords do not match!
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={passwordChangeLoading() || !passwordsMatch()}
|
|
||||||
class={`${
|
|
||||||
passwordChangeLoading() || !passwordsMatch()
|
|
||||||
? "bg-blue cursor-not-allowed brightness-75"
|
|
||||||
: "bg-blue hover:brightness-125 active:scale-90"
|
|
||||||
} my-6 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
|
||||||
>
|
|
||||||
{passwordChangeLoading() ? "Setting..." : "Set"}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<Show when={passwordError()}>
|
|
||||||
<div class="text-red text-center text-sm">
|
|
||||||
{currentUser().hasPassword
|
|
||||||
? "Password did not match record"
|
|
||||||
: "Error setting password"}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<Show when={showPasswordSuccess()}>
|
|
||||||
<div class="text-green text-center text-sm">
|
|
||||||
Password {currentUser().hasPassword ? "changed" : "set"}{" "}
|
|
||||||
successfully!
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<hr class="mt-8 mb-8" />
|
|
||||||
|
|
||||||
{/* Delete Account Section */}
|
|
||||||
<div class="mx-auto max-w-2xl py-8">
|
|
||||||
<div class="bg-red w-full rounded-md px-6 pt-8 pb-4 shadow-md brightness-75">
|
|
||||||
<div class="pb-4 text-center text-xl font-semibold">
|
|
||||||
Delete Account
|
|
||||||
</div>
|
|
||||||
<div class="text-crust mb-4 text-center text-sm">
|
|
||||||
Warning: This will delete all account information and is
|
|
||||||
irreversible
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={deleteAccountTrigger}>
|
|
||||||
<div class="flex w-full justify-center">
|
|
||||||
<div class="input-group delete mx-4">
|
|
||||||
<input
|
|
||||||
ref={deleteAccountPasswordRef}
|
|
||||||
type="password"
|
|
||||||
required
|
|
||||||
disabled={deleteAccountButtonLoading()}
|
|
||||||
placeholder=" "
|
|
||||||
class="underlinedInput bg-transparent"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">
|
|
||||||
Enter Password
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div class="input-group relative mx-4 mb-2">
|
||||||
|
<input
|
||||||
|
ref={newPasswordConfRef}
|
||||||
|
type={showPasswordConfInput() ? "text" : "password"}
|
||||||
|
required
|
||||||
|
onInput={handlePasswordConfChange}
|
||||||
|
disabled={passwordChangeLoading()}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput w-full bg-transparent pr-10"
|
||||||
|
/>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<label class="underlinedInputLabel">
|
||||||
|
Password Confirmation
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setShowPasswordConfInput(!showPasswordConfInput())
|
||||||
|
}
|
||||||
|
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
|
||||||
|
>
|
||||||
|
<Show when={showPasswordConfInput()} fallback={<Eye />}>
|
||||||
|
<EyeSlash />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={
|
||||||
|
!passwordsMatch() &&
|
||||||
|
passwordLengthSufficient() &&
|
||||||
|
newPasswordConfRef &&
|
||||||
|
newPasswordConfRef.value.length >= 6
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="text-red mb-4 text-center text-sm">
|
||||||
|
Passwords do not match!
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={deleteAccountButtonLoading()}
|
disabled={passwordChangeLoading() || !passwordsMatch()}
|
||||||
class={`${
|
class={`${
|
||||||
deleteAccountButtonLoading()
|
passwordChangeLoading() || !passwordsMatch()
|
||||||
? "bg-red cursor-not-allowed brightness-75"
|
? "bg-blue cursor-not-allowed brightness-75"
|
||||||
: "bg-red hover:brightness-125 active:scale-90"
|
: "bg-blue hover:brightness-125 active:scale-90"
|
||||||
} mx-auto mt-4 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
} my-6 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
>
|
>
|
||||||
{deleteAccountButtonLoading()
|
{passwordChangeLoading() ? "Setting..." : "Set"}
|
||||||
? "Deleting..."
|
|
||||||
: "Delete Account"}
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Show when={passwordDeletionError()}>
|
<Show when={passwordError()}>
|
||||||
<div class="text-red mt-2 text-center text-sm">
|
<div class="text-red text-center text-sm">
|
||||||
Password did not match record
|
{currentUser().hasPassword
|
||||||
|
? "Password did not match record"
|
||||||
|
: "Error setting password"}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</form>
|
|
||||||
|
<Show when={showPasswordSuccess()}>
|
||||||
|
<div class="text-green text-center text-sm">
|
||||||
|
Password {currentUser().hasPassword ? "changed" : "set"}{" "}
|
||||||
|
successfully!
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr class="mt-8 mb-8" />
|
||||||
|
|
||||||
|
{/* Delete Account Section */}
|
||||||
|
<div class="mx-auto max-w-2xl py-8">
|
||||||
|
<div class="bg-red w-full rounded-md px-6 pt-8 pb-4 shadow-md brightness-75">
|
||||||
|
<div class="pb-4 text-center text-xl font-semibold">
|
||||||
|
Delete Account
|
||||||
|
</div>
|
||||||
|
<div class="text-crust mb-4 text-center text-sm">
|
||||||
|
Warning: This will delete all account information and is
|
||||||
|
irreversible
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={deleteAccountTrigger}>
|
||||||
|
<div class="flex w-full justify-center">
|
||||||
|
<div class="input-group delete mx-4">
|
||||||
|
<input
|
||||||
|
ref={deleteAccountPasswordRef}
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
disabled={deleteAccountButtonLoading()}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput bg-transparent"
|
||||||
|
/>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<label class="underlinedInputLabel">
|
||||||
|
Enter Password
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={deleteAccountButtonLoading()}
|
||||||
|
class={`${
|
||||||
|
deleteAccountButtonLoading()
|
||||||
|
? "bg-red cursor-not-allowed brightness-75"
|
||||||
|
: "bg-red hover:brightness-125 active:scale-90"
|
||||||
|
} mx-auto mt-4 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||||
|
>
|
||||||
|
{deleteAccountButtonLoading()
|
||||||
|
? "Deleting..."
|
||||||
|
: "Delete Account"}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Show when={passwordDeletionError()}>
|
||||||
|
<div class="text-red mt-2 text-center text-sm">
|
||||||
|
Password did not match record
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</Show>
|
||||||
</Show>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Show, Suspense, For } from "solid-js";
|
import { Show, Suspense, For } from "solid-js";
|
||||||
import { useParams, A, Navigate, query } from "@solidjs/router";
|
import { useParams, A, Navigate, query } from "@solidjs/router";
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import SessionDependantLike from "~/components/blog/SessionDependantLike";
|
import SessionDependantLike from "~/components/blog/SessionDependantLike";
|
||||||
@@ -159,6 +159,13 @@ export default function PostPage() {
|
|||||||
<Title>
|
<Title>
|
||||||
{p().title.replaceAll("_", " ")} | Michael Freno
|
{p().title.replaceAll("_", " ")} | Michael Freno
|
||||||
</Title>
|
</Title>
|
||||||
|
<Meta
|
||||||
|
name="description"
|
||||||
|
content={
|
||||||
|
p().subtitle ||
|
||||||
|
`Read ${p().title.replaceAll("_", " ")} by Michael Freno on the freno.me blog.`
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="relative overflow-x-hidden">
|
<div class="relative overflow-x-hidden">
|
||||||
{/* Fixed banner image background */}
|
{/* Fixed banner image background */}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Show, createSignal, createEffect, onCleanup } from "solid-js";
|
import { Show, createSignal, createEffect, onCleanup } from "solid-js";
|
||||||
import { useNavigate, query } from "@solidjs/router";
|
import { useNavigate, query } from "@solidjs/router";
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
@@ -195,6 +195,10 @@ export default function CreatePost() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Create Blog Post | Michael Freno</Title>
|
<Title>Create Blog Post | Michael Freno</Title>
|
||||||
|
<Meta
|
||||||
|
name="description"
|
||||||
|
content="Create a new blog post with rich text editing, image uploads, and tag management."
|
||||||
|
/>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={authState()?.privilegeLevel === "admin"}
|
when={authState()?.privilegeLevel === "admin"}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Show, createSignal, createEffect, onCleanup } from "solid-js";
|
import { Show, createSignal, createEffect, onCleanup } from "solid-js";
|
||||||
import { useParams, useNavigate, query } from "@solidjs/router";
|
import { useParams, useNavigate, query } from "@solidjs/router";
|
||||||
import { Title } from "@solidjs/meta";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { createAsync } from "@solidjs/router";
|
import { createAsync } from "@solidjs/router";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
@@ -241,6 +241,10 @@ export default function EditPost() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Edit Post | Michael Freno</Title>
|
<Title>Edit Post | Michael Freno</Title>
|
||||||
|
<Meta
|
||||||
|
name="description"
|
||||||
|
content="Edit your blog post with rich text editing, image management, and tag updates."
|
||||||
|
/>
|
||||||
|
|
||||||
<Show
|
<Show
|
||||||
when={data()?.privilegeLevel === "admin"}
|
when={data()?.privilegeLevel === "admin"}
|
||||||
|
|||||||
@@ -1,23 +1,32 @@
|
|||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import DeletionForm from "~/components/DeletionForm";
|
import DeletionForm from "~/components/DeletionForm";
|
||||||
|
|
||||||
export default function LifeAndLinageDeletionForm() {
|
export default function LifeAndLinageDeletionForm() {
|
||||||
return (
|
return (
|
||||||
<div class="pt-20">
|
<>
|
||||||
<div class="mx-auto p-4 md:p-6 lg:p-12">
|
<Title>Account Deletion - Life and Lineage | Michael Freno</Title>
|
||||||
<div class="w-full justify-center text-text">
|
<Meta
|
||||||
<div class="text-xl">
|
name="description"
|
||||||
<em>What will happen</em>:
|
content="Request account deletion for Life and Lineage. Remove all your data from our system with a 24-hour grace period."
|
||||||
|
/>
|
||||||
|
<div class="pt-20">
|
||||||
|
<div class="mx-auto p-4 md:p-6 lg:p-12">
|
||||||
|
<div class="text-text w-full justify-center">
|
||||||
|
<div class="text-xl">
|
||||||
|
<em>What will happen</em>:
|
||||||
|
</div>
|
||||||
|
Once you send, if a match to the email provided is found in our
|
||||||
|
system, a 24hr grace period is started where you can request a
|
||||||
|
cancellation of the account deletion. Once the grace period ends,
|
||||||
|
the account's entry in our central database will be completely
|
||||||
|
removed, and your individual database storing your remote saves will
|
||||||
|
also be deleted. No data related to the account is retained in any
|
||||||
|
way.
|
||||||
</div>
|
</div>
|
||||||
Once you send, if a match to the email provided is found in our
|
|
||||||
system, a 24hr grace period is started where you can request a
|
|
||||||
cancellation of the account deletion. Once the grace period ends, the
|
|
||||||
account's entry in our central database will be completely removed,
|
|
||||||
and your individual database storing your remote saves will also be
|
|
||||||
deleted. No data related to the account is retained in any way.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DeletionForm />
|
<DeletionForm />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { A } from "@solidjs/router";
|
import { A } from "@solidjs/router";
|
||||||
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
||||||
import GitHub from "~/components/icons/GitHub";
|
import GitHub from "~/components/icons/GitHub";
|
||||||
@@ -21,147 +22,155 @@ export default function DownloadsPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bg-base min-h-screen pt-[15vh] pb-12">
|
<>
|
||||||
<div class="text-text text-center text-3xl tracking-widest">
|
<Title>Downloads | Michael Freno</Title>
|
||||||
Downloads
|
<Meta
|
||||||
</div>
|
name="description"
|
||||||
|
content="Download Life and Lineage, Shapes with Abigail, and Cork for macOS. Available on iOS, Android, and macOS."
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="pt-12">
|
<div class="bg-base min-h-screen pt-[15vh] pb-12">
|
||||||
<div class="text-text text-center text-xl tracking-wide">
|
<div class="text-text text-center text-3xl tracking-widest">
|
||||||
Life and Lineage
|
Downloads
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-evenly md:mx-[25vw]">
|
|
||||||
<div class="flex w-1/3 flex-col">
|
|
||||||
<div class="text-center text-lg">Android (apk only)</div>
|
|
||||||
<button
|
|
||||||
onClick={() => download("lineage")}
|
|
||||||
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
|
||||||
>
|
|
||||||
Download APK
|
|
||||||
</button>
|
|
||||||
<div class="mt-2 text-center text-sm italic">
|
|
||||||
Note the android version is not well tested, and has performance
|
|
||||||
issues.
|
|
||||||
</div>
|
|
||||||
<div class="rule-around">Or</div>
|
|
||||||
|
|
||||||
<div class="mx-auto italic">(Coming soon)</div>
|
|
||||||
<button
|
|
||||||
onClick={joinBetaPrompt}
|
|
||||||
class="mx-auto transition-all duration-200 ease-out active:scale-95"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/google-play-badge.png"
|
|
||||||
alt="google-play"
|
|
||||||
width={180}
|
|
||||||
height={60}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="text-center text-lg">iOS</div>
|
|
||||||
<A
|
|
||||||
class="my-auto transition-all duration-200 ease-out active:scale-95"
|
|
||||||
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
|
|
||||||
>
|
|
||||||
<DownloadOnAppStore size={50} />
|
|
||||||
</A>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-12">
|
|
||||||
<div class="text-center text-xl tracking-wide dark:text-white">
|
|
||||||
Shapes with Abigail!
|
|
||||||
<br />
|
|
||||||
(apk and iOS)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-evenly md:mx-[25vw]">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="text-center text-lg">Android</div>
|
|
||||||
<button
|
|
||||||
onClick={() => download("shapes-with-abigail")}
|
|
||||||
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
|
||||||
>
|
|
||||||
Download APK
|
|
||||||
</button>
|
|
||||||
<div class="rule-around">Or</div>
|
|
||||||
<div class="mx-auto italic">(Coming soon)</div>
|
|
||||||
<button
|
|
||||||
onClick={joinBetaPrompt}
|
|
||||||
class="transition-all duration-200 ease-out active:scale-95"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src="/google-play-badge.png"
|
|
||||||
alt="google-play"
|
|
||||||
width={180}
|
|
||||||
height={60}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<div class="text-center text-lg">iOS</div>
|
|
||||||
<A
|
|
||||||
class="my-auto transition-all duration-200 ease-out active:scale-95"
|
|
||||||
href="https://apps.apple.com/us/app/shapes-with-abigail/id6474561117"
|
|
||||||
>
|
|
||||||
<DownloadOnAppStore size={50} />
|
|
||||||
</A>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-12">
|
<div class="pt-12">
|
||||||
<div class="text-text text-center text-xl tracking-wide">
|
<div class="text-text text-center text-xl tracking-wide">
|
||||||
Cork
|
Life and Lineage
|
||||||
<br />
|
<br />
|
||||||
(macOS 13 Ventura or later)
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-evenly md:mx-[25vw]">
|
||||||
<button
|
<div class="flex w-1/3 flex-col">
|
||||||
onClick={() => download("cork")}
|
<div class="text-center text-lg">Android (apk only)</div>
|
||||||
class="bg-blue my-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
<button
|
||||||
>
|
onClick={() => download("lineage")}
|
||||||
Download app
|
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
||||||
</button>
|
>
|
||||||
</div>
|
Download APK
|
||||||
<div class="text-center text-sm">
|
</button>
|
||||||
Just unzip and drag into 'Applications' folder
|
<div class="mt-2 text-center text-sm italic">
|
||||||
|
Note the android version is not well tested, and has performance
|
||||||
|
issues.
|
||||||
|
</div>
|
||||||
|
<div class="rule-around">Or</div>
|
||||||
|
|
||||||
|
<div class="mx-auto italic">(Coming soon)</div>
|
||||||
|
<button
|
||||||
|
onClick={joinBetaPrompt}
|
||||||
|
class="mx-auto transition-all duration-200 ease-out active:scale-95"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/google-play-badge.png"
|
||||||
|
alt="google-play"
|
||||||
|
width={180}
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="text-center text-lg">iOS</div>
|
||||||
|
<A
|
||||||
|
class="my-auto transition-all duration-200 ease-out active:scale-95"
|
||||||
|
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
|
||||||
|
>
|
||||||
|
<DownloadOnAppStore size={50} />
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="icons flex justify-center gap-4 pt-24 pb-6">
|
<div class="pt-12">
|
||||||
<li>
|
<div class="text-center text-xl tracking-wide dark:text-white">
|
||||||
<A
|
Shapes with Abigail!
|
||||||
href="https://github.com/MikeFreno/"
|
<br />
|
||||||
target="_blank"
|
(apk and iOS)
|
||||||
rel="noreferrer"
|
</div>
|
||||||
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
|
|
||||||
>
|
<div class="flex justify-evenly md:mx-[25vw]">
|
||||||
<span class="m-auto block p-2">
|
<div class="flex flex-col">
|
||||||
<GitHub height={24} width={24} fill={undefined} />
|
<div class="text-center text-lg">Android</div>
|
||||||
</span>
|
<button
|
||||||
</A>
|
onClick={() => download("shapes-with-abigail")}
|
||||||
</li>
|
class="bg-blue mt-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
||||||
<li>
|
>
|
||||||
<A
|
Download APK
|
||||||
href="https://www.linkedin.com/in/michael-freno-176001256/"
|
</button>
|
||||||
target="_blank"
|
<div class="rule-around">Or</div>
|
||||||
rel="noreferrer"
|
<div class="mx-auto italic">(Coming soon)</div>
|
||||||
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
|
<button
|
||||||
>
|
onClick={joinBetaPrompt}
|
||||||
<span class="m-auto block rounded-md p-2">
|
class="transition-all duration-200 ease-out active:scale-95"
|
||||||
<LinkedIn height={24} width={24} fill={undefined} />
|
>
|
||||||
</span>
|
<img
|
||||||
</A>
|
src="/google-play-badge.png"
|
||||||
</li>
|
alt="google-play"
|
||||||
</ul>
|
width={180}
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="text-center text-lg">iOS</div>
|
||||||
|
<A
|
||||||
|
class="my-auto transition-all duration-200 ease-out active:scale-95"
|
||||||
|
href="https://apps.apple.com/us/app/shapes-with-abigail/id6474561117"
|
||||||
|
>
|
||||||
|
<DownloadOnAppStore size={50} />
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pt-12">
|
||||||
|
<div class="text-text text-center text-xl tracking-wide">
|
||||||
|
Cork
|
||||||
|
<br />
|
||||||
|
(macOS 13 Ventura or later)
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => download("cork")}
|
||||||
|
class="bg-blue my-2 rounded-md px-4 py-2 text-base shadow-lg transition-all duration-200 ease-out hover:brightness-125 active:scale-95"
|
||||||
|
>
|
||||||
|
Download app
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-center text-sm">
|
||||||
|
Just unzip and drag into 'Applications' folder
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="icons flex justify-center gap-4 pt-24 pb-6">
|
||||||
|
<li>
|
||||||
|
<A
|
||||||
|
href="https://github.com/MikeFreno/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
|
||||||
|
>
|
||||||
|
<span class="m-auto block p-2">
|
||||||
|
<GitHub height={24} width={24} fill={undefined} />
|
||||||
|
</span>
|
||||||
|
</A>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<A
|
||||||
|
href="https://www.linkedin.com/in/michael-freno-176001256/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="shaker border-text inline-block rounded-full border transition-transform hover:scale-110"
|
||||||
|
>
|
||||||
|
<span class="m-auto block rounded-md p-2">
|
||||||
|
<LinkedIn height={24} width={24} fill={undefined} />
|
||||||
|
</span>
|
||||||
|
</A>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,155 +1,164 @@
|
|||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
|
||||||
import { Typewriter } from "~/components/Typewriter";
|
import { Typewriter } from "~/components/Typewriter";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main class="flex h-full flex-col gap-8 p-4 text-xl">
|
<>
|
||||||
<div class="flex-1">
|
<Title>Home | Michael Freno</Title>
|
||||||
<Typewriter speed={30} keepAlive={2000}>
|
<Meta
|
||||||
<div class="text-4xl">Hey!</div>
|
name="description"
|
||||||
</Typewriter>
|
content="Michael Freno - Software Engineer based in Brooklyn, NY. Passionate about dev tooling, game development, and open source software."
|
||||||
<Typewriter speed={80} keepAlive={2000}>
|
/>
|
||||||
<div>
|
|
||||||
My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
|
<main class="flex h-full flex-col gap-8 p-4 text-xl">
|
||||||
<span class="text-blue">Software Engineer</span> based in{" "}
|
<div class="flex-1">
|
||||||
<span class="text-yellow">Brooklyn, NY.</span>
|
<Typewriter speed={30} keepAlive={2000}>
|
||||||
</div>
|
<div class="text-4xl">Hey!</div>
|
||||||
</Typewriter>
|
</Typewriter>
|
||||||
<Typewriter speed={100} keepAlive={2000}>
|
<Typewriter speed={80} keepAlive={2000}>
|
||||||
I'm a passionate dev tooling, game, and open source software
|
<div>
|
||||||
developer. Recently been working in the world of{" "}
|
My name is <span class="text-green">Mike Freno</span>, I'm a{" "}
|
||||||
<a
|
<span class="text-blue">Software Engineer</span> based in{" "}
|
||||||
href="https://www.love2d.org"
|
<span class="text-yellow">Brooklyn, NY.</span>
|
||||||
class="text-blue hover-underline-animation"
|
</div>
|
||||||
>
|
</Typewriter>
|
||||||
LÖVE
|
<Typewriter speed={100} keepAlive={2000}>
|
||||||
</a>{" "}
|
I'm a passionate dev tooling, game, and open source software
|
||||||
(an open source game engine for Lua).
|
developer. Recently been working in the world of{" "}
|
||||||
</Typewriter>
|
<a
|
||||||
<Typewriter speed={100} keepAlive={2000}>
|
href="https://www.love2d.org"
|
||||||
You can see some of my work{" "}
|
class="text-blue hover-underline-animation"
|
||||||
<a
|
>
|
||||||
href="https://github.com/mikefreno"
|
LÖVE
|
||||||
class="text-blue hover-underline-animation"
|
</a>{" "}
|
||||||
>
|
(an open source game engine for Lua).
|
||||||
here (github).
|
</Typewriter>
|
||||||
</a>
|
<Typewriter speed={100} keepAlive={2000}>
|
||||||
</Typewriter>
|
You can see some of my work{" "}
|
||||||
<div class="pt-8 text-center">
|
<a
|
||||||
<div class="pb-4">Some of my recent projects:</div>
|
href="https://github.com/mikefreno"
|
||||||
<div class="flex flex-col items-center gap-6 2xl:flex-row 2xl:items-start 2xl:justify-center">
|
class="text-blue hover-underline-animation"
|
||||||
{/* Life and Lineage */}
|
>
|
||||||
<div class="border-surface0 flex w-full max-w-2xl flex-col gap-2 rounded-md border-2 p-4 text-center">
|
here (github).
|
||||||
<div>My mobile game:</div>
|
</a>
|
||||||
<a
|
</Typewriter>
|
||||||
class="text-blue hover-underline-animation"
|
<div class="pt-8 text-center">
|
||||||
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
|
<div class="pb-4">Some of my recent projects:</div>
|
||||||
>
|
<div class="flex flex-col items-center gap-6 2xl:flex-row 2xl:items-start 2xl:justify-center">
|
||||||
Life and Lineage
|
{/* Life and Lineage */}
|
||||||
</a>
|
<div class="border-surface0 flex w-full max-w-2xl flex-col gap-2 rounded-md border-2 p-4 text-center">
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
<div>My mobile game:</div>
|
||||||
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
<a
|
||||||
<img
|
class="text-blue hover-underline-animation"
|
||||||
src="/lineage-home.png"
|
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
|
||||||
alt="Life and Lineage Home"
|
>
|
||||||
class="h-full w-full object-cover"
|
Life and Lineage
|
||||||
/>
|
</a>
|
||||||
</div>
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||||
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
||||||
<video
|
<img
|
||||||
src="/lineage-preview.mp4"
|
src="/lineage-home.png"
|
||||||
class="h-full w-full object-cover"
|
alt="Life and Lineage Home"
|
||||||
autoplay
|
class="h-full w-full object-cover"
|
||||||
loop
|
/>
|
||||||
muted
|
</div>
|
||||||
playsinline
|
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
||||||
/>
|
<video
|
||||||
</div>
|
src="/lineage-preview.mp4"
|
||||||
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
class="h-full w-full object-cover"
|
||||||
<img
|
autoplay
|
||||||
src="/lineage-shops.png"
|
loop
|
||||||
alt="Life and Lineage Shops"
|
muted
|
||||||
class="h-full w-full object-cover"
|
playsinline
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
||||||
|
<img
|
||||||
|
src="/lineage-shops.png"
|
||||||
|
alt="Life and Lineage Shops"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* FlexLöve */}
|
{/* FlexLöve */}
|
||||||
<div class="border-surface0 flex w-full max-w-md flex-col gap-2 rounded-md border-2 p-4 text-center">
|
<div class="border-surface0 flex w-full max-w-md flex-col gap-2 rounded-md border-2 p-4 text-center">
|
||||||
<div>My LÖVE UI library</div>
|
<div>My LÖVE UI library</div>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/mikefreno/flexlove"
|
href="https://github.com/mikefreno/flexlove"
|
||||||
class="text-blue hover-underline-animation"
|
class="text-blue hover-underline-animation"
|
||||||
>
|
>
|
||||||
FlexLöve
|
FlexLöve
|
||||||
</a>
|
</a>
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
||||||
<video
|
<video
|
||||||
src="/flexlove-scrollable.mp4"
|
src="/flexlove-scrollable.mp4"
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
autoplay
|
autoplay
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
playsinline
|
playsinline
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
||||||
<video
|
<video
|
||||||
src="/flexlove-input.mp4"
|
src="/flexlove-input.mp4"
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
autoplay
|
autoplay
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
playsinline
|
playsinline
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
<div class="aspect-auto w-full overflow-hidden rounded-lg">
|
||||||
<video
|
<video
|
||||||
src="/flexlove-slider.mp4"
|
src="/flexlove-slider.mp4"
|
||||||
class="h-full w-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
autoplay
|
autoplay
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
playsinline
|
playsinline
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="max-w-3/4 pt-8 md:max-w-1/2">
|
||||||
<div class="max-w-3/4 pt-8 md:max-w-1/2">
|
And if you love the color schemes of this site (which of course you
|
||||||
And if you love the color schemes of this site (which of course you
|
do), you can see{" "}
|
||||||
do), you can see{" "}
|
<a
|
||||||
<a
|
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
|
||||||
href="https://github.com/mikefreno/dots/blob/master/mac/nvim/lua/colors.lua"
|
class="text-blue hover-underline-animation"
|
||||||
class="text-blue hover-underline-animation"
|
>
|
||||||
>
|
here
|
||||||
here
|
</a>{" "}
|
||||||
</a>{" "}
|
- and also see the rest of my various dot files idk. There's a macos
|
||||||
- and also see the rest of my various dot files idk. There's a macos
|
and arch linux rice in there if you're into that kinda thing and a
|
||||||
and arch linux rice in there if you're into that kinda thing and a
|
home server setup too. Which I will write about soon™.
|
||||||
home server setup too. Which I will write about soon™.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col items-end gap-4 pr-4">
|
|
||||||
<Typewriter speed={50} keepAlive={false}>
|
|
||||||
<div>
|
|
||||||
My Collection of
|
|
||||||
<br />
|
|
||||||
By-the-ways:
|
|
||||||
</div>
|
</div>
|
||||||
</Typewriter>
|
</div>
|
||||||
<Typewriter speed={50} keepAlive={false}>
|
|
||||||
<ul class="list-disc">
|
<div class="flex flex-col items-end gap-4 pr-4">
|
||||||
<li>I use Neovim</li>
|
<Typewriter speed={50} keepAlive={false}>
|
||||||
<li>I use Arch Linux</li>
|
<div>
|
||||||
<li>I use Rust</li>
|
My Collection of
|
||||||
</ul>
|
<br />
|
||||||
</Typewriter>
|
By-the-ways:
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</Typewriter>
|
||||||
|
<Typewriter speed={50} keepAlive={false}>
|
||||||
|
<ul class="list-disc">
|
||||||
|
<li>I use Neovim</li>
|
||||||
|
<li>I use Arch Linux</li>
|
||||||
|
<li>I use Rust</li>
|
||||||
|
</ul>
|
||||||
|
</Typewriter>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
cache,
|
cache,
|
||||||
redirect
|
redirect
|
||||||
} from "@solidjs/router";
|
} from "@solidjs/router";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { getEvent } from "vinxi/http";
|
import { getEvent } from "vinxi/http";
|
||||||
import GoogleLogo from "~/components/icons/GoogleLogo";
|
import GoogleLogo from "~/components/icons/GoogleLogo";
|
||||||
import GitHub from "~/components/icons/GitHub";
|
import GitHub from "~/components/icons/GitHub";
|
||||||
@@ -320,324 +321,331 @@ export default function LoginPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex h-dvh flex-row justify-evenly">
|
<>
|
||||||
{/* Logo section - hidden on mobile */}
|
<Title>Login | Michael Freno</Title>
|
||||||
{/* <div class="hidden md:flex">
|
<Meta
|
||||||
<div class="vertical-rule-around z-0 flex justify-center">
|
name="description"
|
||||||
<picture class="-mr-8">
|
content="Sign in to your account or register for a new account to access personalized features and manage your profile."
|
||||||
<source srcset="/WhiteLogo.png" media="(prefers-color-scheme: dark)" />
|
/>
|
||||||
<img src="/BlackLogo.png" alt="logo" width={64} height={64} />
|
<div class="flex h-dvh flex-row justify-evenly">
|
||||||
</picture>
|
{/* Logo section - hidden on mobile */}
|
||||||
</div>
|
{/* <div class="hidden md:flex">
|
||||||
</div> */}
|
<div class="vertical-rule-around z-0 flex justify-center">
|
||||||
|
<picture class="-mr-8">
|
||||||
|
<source srcset="/WhiteLogo.png" media="(prefers-color-scheme: dark)" />
|
||||||
|
<img src="/BlackLogo.png" alt="logo" width={64} height={64} />
|
||||||
|
</picture>
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
{/* Main content */}
|
{/* Main content */}
|
||||||
<div class="pt-24 md:pt-48">
|
<div class="pt-24 md:pt-48">
|
||||||
{/* Error message */}
|
{/* Error message */}
|
||||||
<div class="absolute -mt-12 text-center text-3xl text-red-400 italic">
|
<div class="absolute -mt-12 text-center text-3xl text-red-400 italic">
|
||||||
<Show when={error() === "passwordMismatch"}>
|
<Show when={error() === "passwordMismatch"}>
|
||||||
Passwords did not match!
|
Passwords did not match!
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={error() === "duplicate"}>Email Already Exists!</Show>
|
<Show when={error() === "duplicate"}>Email Already Exists!</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Title */}
|
{/* Title */}
|
||||||
<div class="py-2 pl-6 text-2xl md:pl-0">
|
<div class="py-2 pl-6 text-2xl md:pl-0">
|
||||||
{register() ? "Register" : "Login"}
|
{register() ? "Register" : "Login"}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Toggle Register/Login */}
|
{/* Toggle Register/Login */}
|
||||||
<Show
|
<Show
|
||||||
when={!register()}
|
when={!register()}
|
||||||
fallback={
|
fallback={
|
||||||
|
<div class="py-4 text-center md:min-w-[475px]">
|
||||||
|
Already have an account?
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setRegister(false);
|
||||||
|
setUsePassword(false);
|
||||||
|
}}
|
||||||
|
class="text-blue pl-1 underline hover:brightness-125"
|
||||||
|
>
|
||||||
|
Click here to Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<div class="py-4 text-center md:min-w-[475px]">
|
<div class="py-4 text-center md:min-w-[475px]">
|
||||||
Already have an account?
|
Don't have an account yet?
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setRegister(false);
|
setRegister(true);
|
||||||
setUsePassword(false);
|
setUsePassword(false);
|
||||||
}}
|
}}
|
||||||
class="text-blue pl-1 underline hover:brightness-125"
|
class="text-blue pl-1 underline hover:brightness-125"
|
||||||
>
|
>
|
||||||
Click here to Login
|
Click here to Register
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="py-4 text-center md:min-w-[475px]">
|
|
||||||
Don't have an account yet?
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setRegister(true);
|
|
||||||
setUsePassword(false);
|
|
||||||
}}
|
|
||||||
class="text-blue pl-1 underline hover:brightness-125"
|
|
||||||
>
|
|
||||||
Click here to Register
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<form onSubmit={formHandler} class="flex flex-col px-2 py-4">
|
|
||||||
{/* Email input */}
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div class="input-group mx-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
ref={emailRef}
|
|
||||||
placeholder=" "
|
|
||||||
class="underlinedInput bg-transparent"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Email</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password input - shown for login with password or registration */}
|
|
||||||
<Show when={usePassword() || register()}>
|
|
||||||
<div class="-mt-4 flex justify-center">
|
|
||||||
<div class="input-group mx-4 flex">
|
|
||||||
<input
|
|
||||||
type={showPasswordInput() ? "text" : "password"}
|
|
||||||
required
|
|
||||||
minLength={8}
|
|
||||||
ref={passwordRef}
|
|
||||||
onInput={register() ? handleNewPasswordChange : undefined}
|
|
||||||
onBlur={register() ? handlePasswordBlur : undefined}
|
|
||||||
placeholder=" "
|
|
||||||
class="underlinedInput bg-transparent"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Password</label>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setShowPasswordInput(!showPasswordInput());
|
|
||||||
passwordRef?.focus();
|
|
||||||
}}
|
|
||||||
class="absolute mt-14 ml-60"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Show
|
|
||||||
when={showPasswordInput()}
|
|
||||||
fallback={
|
|
||||||
<EyeSlash
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-zinc-900 dark:stroke-white"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Eye
|
|
||||||
height={24}
|
|
||||||
width={24}
|
|
||||||
strokeWidth={1}
|
|
||||||
class="stroke-zinc-900 dark:stroke-white"
|
|
||||||
/>
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class={`${
|
|
||||||
showPasswordLengthWarning() ? "" : "opacity-0 select-none"
|
|
||||||
} text-center text-red-500 transition-opacity duration-200 ease-in-out`}
|
|
||||||
>
|
|
||||||
Password too short! Min Length: 8
|
|
||||||
</div>
|
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Password confirmation - shown only for registration */}
|
{/* Form */}
|
||||||
<Show when={register()}>
|
<form onSubmit={formHandler} class="flex flex-col px-2 py-4">
|
||||||
<div class="-mt-4 flex justify-center">
|
{/* Email input */}
|
||||||
|
<div class="flex justify-center">
|
||||||
<div class="input-group mx-4">
|
<div class="input-group mx-4">
|
||||||
<input
|
<input
|
||||||
type={showPasswordConfInput() ? "text" : "password"}
|
type="text"
|
||||||
required
|
required
|
||||||
minLength={8}
|
ref={emailRef}
|
||||||
ref={passwordConfRef}
|
|
||||||
onInput={handlePasswordConfChange}
|
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
class="underlinedInput bg-transparent"
|
class="underlinedInput bg-transparent"
|
||||||
/>
|
/>
|
||||||
<span class="bar"></span>
|
<span class="bar"></span>
|
||||||
<label class="underlinedInputLabel">Confirm Password</label>
|
<label class="underlinedInputLabel">Email</label>
|
||||||
</div>
|
</div>
|
||||||
<button
|
</div>
|
||||||
onClick={() => {
|
|
||||||
setShowPasswordConfInput(!showPasswordConfInput());
|
{/* Password input - shown for login with password or registration */}
|
||||||
passwordConfRef?.focus();
|
<Show when={usePassword() || register()}>
|
||||||
}}
|
<div class="-mt-4 flex justify-center">
|
||||||
class="absolute mt-14 ml-60"
|
<div class="input-group mx-4 flex">
|
||||||
type="button"
|
<input
|
||||||
>
|
type={showPasswordInput() ? "text" : "password"}
|
||||||
<Show
|
required
|
||||||
when={showPasswordConfInput()}
|
minLength={8}
|
||||||
fallback={
|
ref={passwordRef}
|
||||||
<EyeSlash
|
onInput={register() ? handleNewPasswordChange : undefined}
|
||||||
|
onBlur={register() ? handlePasswordBlur : undefined}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput bg-transparent"
|
||||||
|
/>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<label class="underlinedInputLabel">Password</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowPasswordInput(!showPasswordInput());
|
||||||
|
passwordRef?.focus();
|
||||||
|
}}
|
||||||
|
class="absolute mt-14 ml-60"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={showPasswordInput()}
|
||||||
|
fallback={
|
||||||
|
<EyeSlash
|
||||||
|
height={24}
|
||||||
|
width={24}
|
||||||
|
strokeWidth={1}
|
||||||
|
class="stroke-zinc-900 dark:stroke-white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Eye
|
||||||
height={24}
|
height={24}
|
||||||
width={24}
|
width={24}
|
||||||
strokeWidth={1}
|
strokeWidth={1}
|
||||||
class="stroke-zinc-900 dark:stroke-white"
|
class="stroke-zinc-900 dark:stroke-white"
|
||||||
/>
|
/>
|
||||||
}
|
</Show>
|
||||||
>
|
</button>
|
||||||
<Eye
|
</div>
|
||||||
height={24}
|
<div
|
||||||
width={24}
|
class={`${
|
||||||
strokeWidth={1}
|
showPasswordLengthWarning() ? "" : "opacity-0 select-none"
|
||||||
class="stroke-zinc-900 dark:stroke-white"
|
} text-center text-red-500 transition-opacity duration-200 ease-in-out`}
|
||||||
|
>
|
||||||
|
Password too short! Min Length: 8
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Password confirmation - shown only for registration */}
|
||||||
|
<Show when={register()}>
|
||||||
|
<div class="-mt-4 flex justify-center">
|
||||||
|
<div class="input-group mx-4">
|
||||||
|
<input
|
||||||
|
type={showPasswordConfInput() ? "text" : "password"}
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
ref={passwordConfRef}
|
||||||
|
onInput={handlePasswordConfChange}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput bg-transparent"
|
||||||
/>
|
/>
|
||||||
</Show>
|
<span class="bar"></span>
|
||||||
</button>
|
<label class="underlinedInputLabel">Confirm Password</label>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowPasswordConfInput(!showPasswordConfInput());
|
||||||
|
passwordConfRef?.focus();
|
||||||
|
}}
|
||||||
|
class="absolute mt-14 ml-60"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={showPasswordConfInput()}
|
||||||
|
fallback={
|
||||||
|
<EyeSlash
|
||||||
|
height={24}
|
||||||
|
width={24}
|
||||||
|
strokeWidth={1}
|
||||||
|
class="stroke-zinc-900 dark:stroke-white"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Eye
|
||||||
|
height={24}
|
||||||
|
width={24}
|
||||||
|
strokeWidth={1}
|
||||||
|
class="stroke-zinc-900 dark:stroke-white"
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`${
|
||||||
|
!passwordsMatch() &&
|
||||||
|
passwordLengthSufficient() &&
|
||||||
|
passwordConfRef &&
|
||||||
|
passwordConfRef.value.length >= 6
|
||||||
|
? ""
|
||||||
|
: "opacity-0 select-none"
|
||||||
|
} text-center text-red-500 transition-opacity duration-200 ease-in-out`}
|
||||||
|
>
|
||||||
|
Passwords do not match!
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Remember Me checkbox */}
|
||||||
|
<div class="mx-auto flex pt-4">
|
||||||
|
<input type="checkbox" class="my-auto" ref={rememberMeRef} />
|
||||||
|
<div class="my-auto px-2 text-sm font-normal">Remember Me</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Error/Success messages */}
|
||||||
<div
|
<div
|
||||||
class={`${
|
class={`${
|
||||||
!passwordsMatch() &&
|
showPasswordError()
|
||||||
passwordLengthSufficient() &&
|
? "text-red-500"
|
||||||
passwordConfRef &&
|
: showPasswordSuccess()
|
||||||
passwordConfRef.value.length >= 6
|
? "text-green-500"
|
||||||
? ""
|
: "opacity-0 select-none"
|
||||||
: "opacity-0 select-none"
|
} flex min-h-[16px] justify-center italic transition-opacity duration-300 ease-in-out`}
|
||||||
} text-center text-red-500 transition-opacity duration-200 ease-in-out`}
|
|
||||||
>
|
>
|
||||||
Passwords do not match!
|
<Show when={showPasswordError()}>
|
||||||
|
Credentials did not match any record
|
||||||
|
</Show>
|
||||||
|
<Show when={showPasswordSuccess()}>
|
||||||
|
Login Success! Redirecting...
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit button or countdown timer */}
|
||||||
|
<div class="flex justify-center py-4">
|
||||||
|
<Show
|
||||||
|
when={!register() && !usePassword() && countDown() > 0}
|
||||||
|
fallback={
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading()}
|
||||||
|
class={`${
|
||||||
|
loading()
|
||||||
|
? "bg-zinc-400"
|
||||||
|
: "bg-blue hover:brightness-125 active:scale-90"
|
||||||
|
} flex w-36 justify-center rounded py-3 text-white transition-all duration-300 ease-out`}
|
||||||
|
>
|
||||||
|
{register()
|
||||||
|
? "Sign Up"
|
||||||
|
: usePassword()
|
||||||
|
? "Sign In"
|
||||||
|
: "Get Link"}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CountdownCircleTimer
|
||||||
|
duration={120}
|
||||||
|
initialRemainingTime={countDown()}
|
||||||
|
size={48}
|
||||||
|
strokeWidth={6}
|
||||||
|
colors="#60a5fa"
|
||||||
|
>
|
||||||
|
{renderTime}
|
||||||
|
</CountdownCircleTimer>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Toggle password/email link */}
|
||||||
|
<Show when={!register() && !usePassword()}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setUsePassword(true)}
|
||||||
|
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
|
||||||
|
>
|
||||||
|
Use Password
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<Show when={usePassword()}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setUsePassword(false)}
|
||||||
|
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
|
||||||
|
>
|
||||||
|
Use Email Link
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Password reset link */}
|
||||||
|
<Show when={usePassword()}>
|
||||||
|
<div class="pb-4 text-center text-sm">
|
||||||
|
Trouble Logging In?{" "}
|
||||||
|
<A
|
||||||
|
class="text-blue underline underline-offset-4 hover:brightness-125"
|
||||||
|
href="/login/request-password-reset"
|
||||||
|
>
|
||||||
|
Reset Password
|
||||||
|
</A>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Remember Me checkbox */}
|
{/* Email sent confirmation */}
|
||||||
<div class="mx-auto flex pt-4">
|
|
||||||
<input type="checkbox" class="my-auto" ref={rememberMeRef} />
|
|
||||||
<div class="my-auto px-2 text-sm font-normal">Remember Me</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error/Success messages */}
|
|
||||||
<div
|
<div
|
||||||
class={`${
|
class={`${
|
||||||
showPasswordError()
|
emailSent() ? "" : "user-select opacity-0"
|
||||||
? "text-red-500"
|
} text-green flex min-h-4 justify-center text-center italic transition-opacity duration-300 ease-in-out`}
|
||||||
: showPasswordSuccess()
|
|
||||||
? "text-green-500"
|
|
||||||
: "opacity-0 select-none"
|
|
||||||
} flex min-h-[16px] justify-center italic transition-opacity duration-300 ease-in-out`}
|
|
||||||
>
|
>
|
||||||
<Show when={showPasswordError()}>
|
<Show when={emailSent()}>Email Sent!</Show>
|
||||||
Credentials did not match any record
|
|
||||||
</Show>
|
|
||||||
<Show when={showPasswordSuccess()}>
|
|
||||||
Login Success! Redirecting...
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit button or countdown timer */}
|
{/* Or divider */}
|
||||||
<div class="flex justify-center py-4">
|
<div class="rule-around text-center">Or</div>
|
||||||
<Show
|
|
||||||
when={!register() && !usePassword() && countDown() > 0}
|
{/* OAuth buttons */}
|
||||||
fallback={
|
<div class="my-2 flex justify-center">
|
||||||
<button
|
<div class="mx-auto mb-4 flex flex-col">
|
||||||
type="submit"
|
{/* Google OAuth */}
|
||||||
disabled={loading()}
|
<A
|
||||||
class={`${
|
href={`https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId}&redirect_uri=${domain}/api/auth/callback/google&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email`}
|
||||||
loading()
|
class="my-4 flex w-80 flex-row justify-between rounded border border-zinc-800 bg-white px-4 py-2 text-black shadow-md transition-all duration-300 ease-out hover:bg-zinc-100 active:scale-95 dark:border dark:border-zinc-50 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700"
|
||||||
? "bg-zinc-400"
|
|
||||||
: "bg-blue hover:brightness-125 active:scale-90"
|
|
||||||
} flex w-36 justify-center rounded py-3 text-white transition-all duration-300 ease-out`}
|
|
||||||
>
|
|
||||||
{register()
|
|
||||||
? "Sign Up"
|
|
||||||
: usePassword()
|
|
||||||
? "Sign In"
|
|
||||||
: "Get Link"}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CountdownCircleTimer
|
|
||||||
duration={120}
|
|
||||||
initialRemainingTime={countDown()}
|
|
||||||
size={48}
|
|
||||||
strokeWidth={6}
|
|
||||||
colors="#60a5fa"
|
|
||||||
>
|
>
|
||||||
{renderTime}
|
{register() ? "Register " : "Sign in "} with Google
|
||||||
</CountdownCircleTimer>
|
<span class="my-auto">
|
||||||
</Show>
|
<GoogleLogo height={24} width={24} />
|
||||||
|
</span>
|
||||||
|
</A>
|
||||||
|
|
||||||
{/* Toggle password/email link */}
|
{/* GitHub OAuth */}
|
||||||
<Show when={!register() && !usePassword()}>
|
<A
|
||||||
<button
|
href={`https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${domain}/api/auth/callback/github&scope=user`}
|
||||||
type="button"
|
class="my-4 flex w-80 flex-row justify-between rounded bg-zinc-600 px-4 py-2 text-white shadow-md transition-all duration-300 ease-out hover:bg-zinc-700 active:scale-95"
|
||||||
onClick={() => setUsePassword(true)}
|
|
||||||
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
|
|
||||||
>
|
>
|
||||||
Use Password
|
{register() ? "Register " : "Sign in "} with Github
|
||||||
</button>
|
<span class="my-auto">
|
||||||
</Show>
|
<GitHub height={24} width={24} fill="white" />
|
||||||
<Show when={usePassword()}>
|
</span>
|
||||||
<button
|
</A>
|
||||||
type="button"
|
</div>
|
||||||
onClick={() => setUsePassword(false)}
|
|
||||||
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
|
|
||||||
>
|
|
||||||
Use Email Link
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Password reset link */}
|
|
||||||
<Show when={usePassword()}>
|
|
||||||
<div class="pb-4 text-center text-sm">
|
|
||||||
Trouble Logging In?{" "}
|
|
||||||
<A
|
|
||||||
class="text-blue underline underline-offset-4 hover:brightness-125"
|
|
||||||
href="/login/request-password-reset"
|
|
||||||
>
|
|
||||||
Reset Password
|
|
||||||
</A>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Email sent confirmation */}
|
|
||||||
<div
|
|
||||||
class={`${
|
|
||||||
emailSent() ? "" : "user-select opacity-0"
|
|
||||||
} text-green flex min-h-4 justify-center text-center italic transition-opacity duration-300 ease-in-out`}
|
|
||||||
>
|
|
||||||
<Show when={emailSent()}>Email Sent!</Show>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Or divider */}
|
|
||||||
<div class="rule-around text-center">Or</div>
|
|
||||||
|
|
||||||
{/* OAuth buttons */}
|
|
||||||
<div class="my-2 flex justify-center">
|
|
||||||
<div class="mx-auto mb-4 flex flex-col">
|
|
||||||
{/* Google OAuth */}
|
|
||||||
<A
|
|
||||||
href={`https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleClientId}&redirect_uri=${domain}/api/auth/callback/google&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email`}
|
|
||||||
class="my-4 flex w-80 flex-row justify-between rounded border border-zinc-800 bg-white px-4 py-2 text-black shadow-md transition-all duration-300 ease-out hover:bg-zinc-100 active:scale-95 dark:border dark:border-zinc-50 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700"
|
|
||||||
>
|
|
||||||
{register() ? "Register " : "Sign in "} with Google
|
|
||||||
<span class="my-auto">
|
|
||||||
<GoogleLogo height={24} width={24} />
|
|
||||||
</span>
|
|
||||||
</A>
|
|
||||||
|
|
||||||
{/* GitHub OAuth */}
|
|
||||||
<A
|
|
||||||
href={`https://github.com/login/oauth/authorize?client_id=${githubClientId}&redirect_uri=${domain}/api/auth/callback/github&scope=user`}
|
|
||||||
class="my-4 flex w-80 flex-row justify-between rounded bg-zinc-600 px-4 py-2 text-white shadow-md transition-all duration-300 ease-out hover:bg-zinc-700 active:scale-95"
|
|
||||||
>
|
|
||||||
{register() ? "Register " : "Sign in "} with Github
|
|
||||||
<span class="my-auto">
|
|
||||||
<GitHub height={24} width={24} fill="white" />
|
|
||||||
</span>
|
|
||||||
</A>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createSignal, createEffect, Show } from "solid-js";
|
import { createSignal, createEffect, Show } from "solid-js";
|
||||||
import { A, useNavigate, useSearchParams } from "@solidjs/router";
|
import { A, useNavigate, useSearchParams } from "@solidjs/router";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
||||||
import Eye from "~/components/icons/Eye";
|
import Eye from "~/components/icons/Eye";
|
||||||
import EyeSlash from "~/components/icons/EyeSlash";
|
import EyeSlash from "~/components/icons/EyeSlash";
|
||||||
@@ -13,8 +14,10 @@ export default function PasswordResetPage() {
|
|||||||
const [passwordBlurred, setPasswordBlurred] = createSignal(false);
|
const [passwordBlurred, setPasswordBlurred] = createSignal(false);
|
||||||
const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false);
|
const [passwordChangeLoading, setPasswordChangeLoading] = createSignal(false);
|
||||||
const [passwordsMatch, setPasswordsMatch] = createSignal(false);
|
const [passwordsMatch, setPasswordsMatch] = createSignal(false);
|
||||||
const [showPasswordLengthWarning, setShowPasswordLengthWarning] = createSignal(false);
|
const [showPasswordLengthWarning, setShowPasswordLengthWarning] =
|
||||||
const [passwordLengthSufficient, setPasswordLengthSufficient] = createSignal(false);
|
createSignal(false);
|
||||||
|
const [passwordLengthSufficient, setPasswordLengthSufficient] =
|
||||||
|
createSignal(false);
|
||||||
const [showRequestNewEmail, setShowRequestNewEmail] = createSignal(false);
|
const [showRequestNewEmail, setShowRequestNewEmail] = createSignal(false);
|
||||||
const [countDown, setCountDown] = createSignal(false);
|
const [countDown, setCountDown] = createSignal(false);
|
||||||
const [error, setError] = createSignal("");
|
const [error, setError] = createSignal("");
|
||||||
@@ -70,8 +73,8 @@ export default function PasswordResetPage() {
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
token: token,
|
token: token,
|
||||||
newPassword,
|
newPassword,
|
||||||
newPasswordConfirmation: newPasswordConf,
|
newPasswordConfirmation: newPasswordConf
|
||||||
}),
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -158,164 +161,179 @@ export default function PasswordResetPage() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div class="timer text-center">
|
<div class="timer text-center">
|
||||||
<div class="text-sm text-slate-700 dark:text-slate-300">Change Successful!</div>
|
<div class="text-sm text-slate-700 dark:text-slate-300">
|
||||||
<div class="value py-1 text-3xl text-blue-500 dark:text-blue-400">{timeRemaining}</div>
|
Change Successful!
|
||||||
<div class="text-sm text-slate-700 dark:text-slate-300">Redirecting...</div>
|
</div>
|
||||||
|
<div class="value py-1 text-3xl text-blue-500 dark:text-blue-400">
|
||||||
|
{timeRemaining}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-700 dark:text-slate-300">
|
||||||
|
Redirecting...
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
|
<>
|
||||||
<div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
|
<Title>Reset Password | Michael Freno</Title>
|
||||||
Set New Password
|
<Meta
|
||||||
</div>
|
name="description"
|
||||||
|
content="Set a new password for your account to regain access to your profile and personalized features."
|
||||||
<form
|
/>
|
||||||
onSubmit={(e) => setNewPasswordTrigger(e)}
|
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
|
||||||
class="mt-4 flex w-full justify-center"
|
<div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
|
||||||
>
|
Set New Password
|
||||||
<div class="flex flex-col justify-center max-w-md w-full px-4">
|
|
||||||
{/* New Password Input */}
|
|
||||||
<div class="input-group mx-4 relative">
|
|
||||||
<input
|
|
||||||
ref={newPasswordRef}
|
|
||||||
name="newPassword"
|
|
||||||
type={showPasswordInput() ? "text" : "password"}
|
|
||||||
required
|
|
||||||
onInput={handleNewPasswordChange}
|
|
||||||
onBlur={handlePasswordBlur}
|
|
||||||
disabled={passwordChangeLoading()}
|
|
||||||
placeholder=" "
|
|
||||||
class="underlinedInput w-full bg-transparent pr-10"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">New Password</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPasswordInput(!showPasswordInput())}
|
|
||||||
class="absolute right-0 top-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<Show when={showPasswordInput()} fallback={<Eye />}>
|
|
||||||
<EyeSlash />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password Length Warning */}
|
|
||||||
<div
|
|
||||||
class={`${
|
|
||||||
showPasswordLengthWarning() ? "" : "select-none opacity-0"
|
|
||||||
} transition-opacity text-center text-red-500 text-sm duration-200 ease-in-out mt-2`}
|
|
||||||
>
|
|
||||||
Password too short! Min Length: 8
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password Confirmation Input */}
|
|
||||||
<div class="input-group mx-4 mt-6 relative">
|
|
||||||
<input
|
|
||||||
ref={newPasswordConfRef}
|
|
||||||
name="newPasswordConf"
|
|
||||||
onInput={handlePasswordConfChange}
|
|
||||||
type={showPasswordConfInput() ? "text" : "password"}
|
|
||||||
required
|
|
||||||
disabled={passwordChangeLoading()}
|
|
||||||
placeholder=" "
|
|
||||||
class="underlinedInput w-full bg-transparent pr-10"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Password Confirmation</label>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setShowPasswordConfInput(!showPasswordConfInput())}
|
|
||||||
class="absolute right-0 top-2 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
|
|
||||||
>
|
|
||||||
<Show when={showPasswordConfInput()} fallback={<Eye />}>
|
|
||||||
<EyeSlash />
|
|
||||||
</Show>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Password Mismatch Warning */}
|
|
||||||
<div
|
|
||||||
class={`${
|
|
||||||
!passwordsMatch() &&
|
|
||||||
passwordLengthSufficient() &&
|
|
||||||
newPasswordConfRef &&
|
|
||||||
newPasswordConfRef.value.length >= 6
|
|
||||||
? ""
|
|
||||||
: "select-none opacity-0"
|
|
||||||
} transition-opacity text-center text-red-500 text-sm duration-200 ease-in-out mt-2`}
|
|
||||||
>
|
|
||||||
Passwords do not match!
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Countdown Timer or Submit Button */}
|
|
||||||
<Show
|
|
||||||
when={countDown()}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={passwordChangeLoading() || !passwordsMatch()}
|
|
||||||
class={`${
|
|
||||||
passwordChangeLoading() || !passwordsMatch()
|
|
||||||
? "bg-zinc-400 cursor-not-allowed"
|
|
||||||
: "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700"
|
|
||||||
} flex justify-center rounded transition-all duration-300 ease-out my-6 px-4 py-2 text-white font-medium`}
|
|
||||||
>
|
|
||||||
{passwordChangeLoading() ? "Setting..." : "Set New Password"}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="mx-auto pt-4">
|
|
||||||
<CountdownCircleTimer
|
|
||||||
isPlaying={countDown()}
|
|
||||||
duration={5}
|
|
||||||
size={200}
|
|
||||||
strokeWidth={12}
|
|
||||||
colors="#60a5fa"
|
|
||||||
onComplete={() => false}
|
|
||||||
>
|
|
||||||
{({ remainingTime }) => renderTime(remainingTime)}
|
|
||||||
</CountdownCircleTimer>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Error Message */}
|
<form
|
||||||
<Show when={error() && !showRequestNewEmail()}>
|
onSubmit={(e) => setNewPasswordTrigger(e)}
|
||||||
<div class="flex justify-center mt-4">
|
class="mt-4 flex w-full justify-center"
|
||||||
<div class="text-red-500 text-sm italic">{error()}</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Token Expired Message */}
|
|
||||||
<div
|
|
||||||
class={`${
|
|
||||||
showRequestNewEmail() ? "" : "select-none opacity-0"
|
|
||||||
} text-red-500 italic transition-opacity flex justify-center duration-300 ease-in-out px-4`}
|
|
||||||
>
|
|
||||||
Token has expired, request a new one{" "}
|
|
||||||
<A
|
|
||||||
class="pl-1 text-blue-500 underline underline-offset-4 hover:text-blue-400"
|
|
||||||
href="/login/request-password-reset"
|
|
||||||
>
|
>
|
||||||
here
|
<div class="flex w-full max-w-md flex-col justify-center px-4">
|
||||||
</A>
|
{/* New Password Input */}
|
||||||
</div>
|
<div class="input-group relative mx-4">
|
||||||
|
<input
|
||||||
|
ref={newPasswordRef}
|
||||||
|
name="newPassword"
|
||||||
|
type={showPasswordInput() ? "text" : "password"}
|
||||||
|
required
|
||||||
|
onInput={handleNewPasswordChange}
|
||||||
|
onBlur={handlePasswordBlur}
|
||||||
|
disabled={passwordChangeLoading()}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput w-full bg-transparent pr-10"
|
||||||
|
/>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<label class="underlinedInputLabel">New Password</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowPasswordInput(!showPasswordInput())}
|
||||||
|
class="absolute top-2 right-0 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
|
||||||
|
>
|
||||||
|
<Show when={showPasswordInput()} fallback={<Eye />}>
|
||||||
|
<EyeSlash />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Back to Login Link */}
|
{/* Password Length Warning */}
|
||||||
<Show when={!countDown()}>
|
<div
|
||||||
<div class="flex justify-center mt-6">
|
class={`${
|
||||||
|
showPasswordLengthWarning() ? "" : "opacity-0 select-none"
|
||||||
|
} mt-2 text-center text-sm text-red-500 transition-opacity duration-200 ease-in-out`}
|
||||||
|
>
|
||||||
|
Password too short! Min Length: 8
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password Confirmation Input */}
|
||||||
|
<div class="input-group relative mx-4 mt-6">
|
||||||
|
<input
|
||||||
|
ref={newPasswordConfRef}
|
||||||
|
name="newPasswordConf"
|
||||||
|
onInput={handlePasswordConfChange}
|
||||||
|
type={showPasswordConfInput() ? "text" : "password"}
|
||||||
|
required
|
||||||
|
disabled={passwordChangeLoading()}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput w-full bg-transparent pr-10"
|
||||||
|
/>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<label class="underlinedInputLabel">Password Confirmation</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setShowPasswordConfInput(!showPasswordConfInput())
|
||||||
|
}
|
||||||
|
class="absolute top-2 right-0 text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200"
|
||||||
|
>
|
||||||
|
<Show when={showPasswordConfInput()} fallback={<Eye />}>
|
||||||
|
<EyeSlash />
|
||||||
|
</Show>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Password Mismatch Warning */}
|
||||||
|
<div
|
||||||
|
class={`${
|
||||||
|
!passwordsMatch() &&
|
||||||
|
passwordLengthSufficient() &&
|
||||||
|
newPasswordConfRef &&
|
||||||
|
newPasswordConfRef.value.length >= 6
|
||||||
|
? ""
|
||||||
|
: "opacity-0 select-none"
|
||||||
|
} mt-2 text-center text-sm text-red-500 transition-opacity duration-200 ease-in-out`}
|
||||||
|
>
|
||||||
|
Passwords do not match!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Countdown Timer or Submit Button */}
|
||||||
|
<Show
|
||||||
|
when={countDown()}
|
||||||
|
fallback={
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={passwordChangeLoading() || !passwordsMatch()}
|
||||||
|
class={`${
|
||||||
|
passwordChangeLoading() || !passwordsMatch()
|
||||||
|
? "cursor-not-allowed bg-zinc-400"
|
||||||
|
: "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||||
|
} my-6 flex justify-center rounded px-4 py-2 font-medium text-white transition-all duration-300 ease-out`}
|
||||||
|
>
|
||||||
|
{passwordChangeLoading() ? "Setting..." : "Set New Password"}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="mx-auto pt-4">
|
||||||
|
<CountdownCircleTimer
|
||||||
|
isPlaying={countDown()}
|
||||||
|
duration={5}
|
||||||
|
size={200}
|
||||||
|
strokeWidth={12}
|
||||||
|
colors="#60a5fa"
|
||||||
|
onComplete={() => false}
|
||||||
|
>
|
||||||
|
{({ remainingTime }) => renderTime(remainingTime)}
|
||||||
|
</CountdownCircleTimer>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
<Show when={error() && !showRequestNewEmail()}>
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<div class="text-sm text-red-500 italic">{error()}</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Token Expired Message */}
|
||||||
|
<div
|
||||||
|
class={`${
|
||||||
|
showRequestNewEmail() ? "" : "opacity-0 select-none"
|
||||||
|
} flex justify-center px-4 text-red-500 italic transition-opacity duration-300 ease-in-out`}
|
||||||
|
>
|
||||||
|
Token has expired, request a new one{" "}
|
||||||
<A
|
<A
|
||||||
href="/login"
|
class="pl-1 text-blue-500 underline underline-offset-4 hover:text-blue-400"
|
||||||
class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 underline underline-offset-4 transition-colors"
|
href="/login/request-password-reset"
|
||||||
>
|
>
|
||||||
Back to Login
|
here
|
||||||
</A>
|
</A>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
|
||||||
</div>
|
{/* Back to Login Link */}
|
||||||
|
<Show when={!countDown()}>
|
||||||
|
<div class="mt-6 flex justify-center">
|
||||||
|
<A
|
||||||
|
href="/login"
|
||||||
|
class="text-blue-500 underline underline-offset-4 transition-colors hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
Back to Login
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createSignal, createEffect, onCleanup, Show } from "solid-js";
|
import { createSignal, createEffect, onCleanup, Show } from "solid-js";
|
||||||
import { A, useNavigate } from "@solidjs/router";
|
import { A, useNavigate } from "@solidjs/router";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
import CountdownCircleTimer from "~/components/CountdownCircleTimer";
|
||||||
import { isValidEmail } from "~/lib/validation";
|
import { isValidEmail } from "~/lib/validation";
|
||||||
import { getClientCookie } from "~/lib/cookies.client";
|
import { getClientCookie } from "~/lib/cookies.client";
|
||||||
@@ -37,7 +38,10 @@ export default function RequestPasswordResetPage() {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const timer = getClientCookie("passwordResetRequested");
|
const timer = getClientCookie("passwordResetRequested");
|
||||||
if (timer) {
|
if (timer) {
|
||||||
timerInterval = setInterval(() => calcRemainder(timer), 1000) as unknown as number;
|
timerInterval = setInterval(
|
||||||
|
() => calcRemainder(timer),
|
||||||
|
1000
|
||||||
|
) as unknown as number;
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
if (timerInterval) {
|
if (timerInterval) {
|
||||||
clearInterval(timerInterval);
|
clearInterval(timerInterval);
|
||||||
@@ -71,7 +75,7 @@ export default function RequestPasswordResetPage() {
|
|||||||
const response = await fetch("/api/trpc/auth.requestPasswordReset", {
|
const response = await fetch("/api/trpc/auth.requestPasswordReset", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ email }),
|
body: JSON.stringify({ email })
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -115,90 +119,97 @@ export default function RequestPasswordResetPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
|
<>
|
||||||
<div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
|
<Title>Request Password Reset | Michael Freno</Title>
|
||||||
Password Reset Request
|
<Meta
|
||||||
</div>
|
name="description"
|
||||||
|
content="Request a password reset link to regain access to your account. Enter your email to receive reset instructions."
|
||||||
<form
|
/>
|
||||||
onSubmit={(e) => requestPasswordResetTrigger(e)}
|
<div class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-900 dark:to-slate-800">
|
||||||
class="mt-4 flex w-full justify-center"
|
<div class="pt-24 text-center text-xl font-semibold text-slate-800 dark:text-slate-100">
|
||||||
>
|
Password Reset Request
|
||||||
<div class="flex flex-col justify-center">
|
|
||||||
{/* Email Input */}
|
|
||||||
<div class="input-group mx-4">
|
|
||||||
<input
|
|
||||||
ref={emailRef}
|
|
||||||
name="email"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
disabled={loading()}
|
|
||||||
placeholder=" "
|
|
||||||
class="underlinedInput w-full bg-transparent"
|
|
||||||
/>
|
|
||||||
<span class="bar"></span>
|
|
||||||
<label class="underlinedInputLabel">Enter Email</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Countdown Timer or Submit Button */}
|
|
||||||
<Show
|
|
||||||
when={countDown() > 0}
|
|
||||||
fallback={
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={loading()}
|
|
||||||
class={`${
|
|
||||||
loading()
|
|
||||||
? "bg-zinc-400"
|
|
||||||
: "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700"
|
|
||||||
} flex justify-center rounded transition-all duration-300 ease-out my-6 px-4 py-2 text-white font-medium`}
|
|
||||||
>
|
|
||||||
{loading() ? "Sending..." : "Request Password Reset"}
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="mx-auto pt-4">
|
|
||||||
<CountdownCircleTimer
|
|
||||||
isPlaying={true}
|
|
||||||
duration={300}
|
|
||||||
initialRemainingTime={countDown()}
|
|
||||||
size={48}
|
|
||||||
strokeWidth={6}
|
|
||||||
colors="#60a5fa"
|
|
||||||
onComplete={() => false}
|
|
||||||
>
|
|
||||||
{renderTime}
|
|
||||||
</CountdownCircleTimer>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
{/* Success Message */}
|
<form
|
||||||
<div
|
onSubmit={(e) => requestPasswordResetTrigger(e)}
|
||||||
class={`${
|
class="mt-4 flex w-full justify-center"
|
||||||
showSuccessMessage() ? "" : "select-none opacity-0"
|
|
||||||
} text-green-500 italic transition-opacity flex justify-center duration-300 ease-in-out`}
|
|
||||||
>
|
|
||||||
If email exists, you will receive an email shortly!
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Error Message */}
|
|
||||||
<Show when={error()}>
|
|
||||||
<div class="flex justify-center mt-4">
|
|
||||||
<div class="text-red-500 text-sm italic">{error()}</div>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Back to Login Link */}
|
|
||||||
<div class="flex justify-center mt-6">
|
|
||||||
<A
|
|
||||||
href="/login"
|
|
||||||
class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 underline underline-offset-4 transition-colors"
|
|
||||||
>
|
>
|
||||||
Back to Login
|
<div class="flex flex-col justify-center">
|
||||||
</A>
|
{/* Email Input */}
|
||||||
|
<div class="input-group mx-4">
|
||||||
|
<input
|
||||||
|
ref={emailRef}
|
||||||
|
name="email"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
disabled={loading()}
|
||||||
|
placeholder=" "
|
||||||
|
class="underlinedInput w-full bg-transparent"
|
||||||
|
/>
|
||||||
|
<span class="bar"></span>
|
||||||
|
<label class="underlinedInputLabel">Enter Email</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Countdown Timer or Submit Button */}
|
||||||
|
<Show
|
||||||
|
when={countDown() > 0}
|
||||||
|
fallback={
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading()}
|
||||||
|
class={`${
|
||||||
|
loading()
|
||||||
|
? "bg-zinc-400"
|
||||||
|
: "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700"
|
||||||
|
} my-6 flex justify-center rounded px-4 py-2 font-medium text-white transition-all duration-300 ease-out`}
|
||||||
|
>
|
||||||
|
{loading() ? "Sending..." : "Request Password Reset"}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="mx-auto pt-4">
|
||||||
|
<CountdownCircleTimer
|
||||||
|
isPlaying={true}
|
||||||
|
duration={300}
|
||||||
|
initialRemainingTime={countDown()}
|
||||||
|
size={48}
|
||||||
|
strokeWidth={6}
|
||||||
|
colors="#60a5fa"
|
||||||
|
onComplete={() => false}
|
||||||
|
>
|
||||||
|
{renderTime}
|
||||||
|
</CountdownCircleTimer>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Success Message */}
|
||||||
|
<div
|
||||||
|
class={`${
|
||||||
|
showSuccessMessage() ? "" : "opacity-0 select-none"
|
||||||
|
} flex justify-center text-green-500 italic transition-opacity duration-300 ease-in-out`}
|
||||||
|
>
|
||||||
|
If email exists, you will receive an email shortly!
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
<Show when={error()}>
|
||||||
|
<div class="mt-4 flex justify-center">
|
||||||
|
<div class="text-sm text-red-500 italic">{error()}</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Back to Login Link */}
|
||||||
|
<div class="mt-6 flex justify-center">
|
||||||
|
<A
|
||||||
|
href="/login"
|
||||||
|
class="text-blue-500 underline underline-offset-4 transition-colors hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
|
||||||
|
>
|
||||||
|
Back to Login
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,52 @@
|
|||||||
import { A } from "@solidjs/router";
|
import { A } from "@solidjs/router";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import SimpleParallax from "~/components/SimpleParallax";
|
import SimpleParallax from "~/components/SimpleParallax";
|
||||||
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
|
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
|
||||||
|
|
||||||
export default function LifeAndLineageMarketing() {
|
export default function LifeAndLineageMarketing() {
|
||||||
return (
|
return (
|
||||||
<SimpleParallax>
|
<>
|
||||||
<div class="flex flex-col items-center justify-center h-full text-white">
|
<Title>Life and Lineage | Michael Freno</Title>
|
||||||
<div>
|
<Meta
|
||||||
<img
|
name="description"
|
||||||
src="/LineageIcon.png"
|
content="A dark fantasy adventure mobile game. Download Life and Lineage on the App Store and Google Play."
|
||||||
alt="Lineage App Icon"
|
/>
|
||||||
height={128}
|
<SimpleParallax>
|
||||||
width={128}
|
<div class="flex h-full flex-col items-center justify-center text-white">
|
||||||
class="object-cover object-center"
|
<div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-5xl font-bold mb-4 text-center">
|
|
||||||
Life and Lineage
|
|
||||||
</h1>
|
|
||||||
<p class="text-xl mb-8">A dark fantasy adventure</p>
|
|
||||||
<div class="flex space-x-4">
|
|
||||||
<a
|
|
||||||
class="my-auto transition-all duration-200 ease-out active:scale-95"
|
|
||||||
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<DownloadOnAppStoreDark size={50} />
|
|
||||||
</a>
|
|
||||||
<A
|
|
||||||
href="/downloads"
|
|
||||||
class="transition-all duration-200 ease-out active:scale-95"
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
src="/google-play-badge.png"
|
src="/LineageIcon.png"
|
||||||
alt="google-play"
|
alt="Lineage App Icon"
|
||||||
width={180}
|
height={128}
|
||||||
height={60}
|
width={128}
|
||||||
|
class="object-cover object-center"
|
||||||
/>
|
/>
|
||||||
</A>
|
</div>
|
||||||
|
<h1 class="mb-4 text-center text-5xl font-bold">Life and Lineage</h1>
|
||||||
|
<p class="mb-8 text-xl">A dark fantasy adventure</p>
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<a
|
||||||
|
class="my-auto transition-all duration-200 ease-out active:scale-95"
|
||||||
|
href="https://apps.apple.com/us/app/life-and-lineage/id6737252442"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<DownloadOnAppStoreDark size={50} />
|
||||||
|
</a>
|
||||||
|
<A
|
||||||
|
href="/downloads"
|
||||||
|
class="transition-all duration-200 ease-out active:scale-95"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/google-play-badge.png"
|
||||||
|
alt="google-play"
|
||||||
|
width={180}
|
||||||
|
height={60}
|
||||||
|
/>
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</SimpleParallax>
|
||||||
</SimpleParallax>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,99 +1,107 @@
|
|||||||
import { A } from "@solidjs/router";
|
import { A } from "@solidjs/router";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
return (
|
return (
|
||||||
<div class="min-h-screen px-[8vw] py-[10vh]">
|
<>
|
||||||
<div class="py-4 text-xl">Life and Lineage's Privacy Policy</div>
|
<Title>Privacy Policy - Life and Lineage | Michael Freno</Title>
|
||||||
<div class="py-2">Last Updated: October 22, 2024</div>
|
<Meta
|
||||||
<div class="py-2">
|
name="description"
|
||||||
Welcome to Life and Lineage ('We', 'Us',
|
content="Privacy policy for Life and Lineage mobile game, outlining data collection, usage, and user rights."
|
||||||
'Our'). Your privacy is important to us. This privacy policy
|
/>
|
||||||
will help you understand our policies and procedures related to the
|
<div class="min-h-screen px-[8vw] py-[10vh]">
|
||||||
collection, use, and storage of personal information from our users.
|
<div class="py-4 text-xl">Life and Lineage'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>
|
</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 { A } from "@solidjs/router";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
export default function PrivacyPolicy() {
|
||||||
return (
|
return (
|
||||||
<div class="bg-zinc-100 dark:bg-zinc-900">
|
<>
|
||||||
<div class="min-h-screen px-[8vw] py-[8vh]">
|
<Title>Privacy Policy - Shapes with Abigail | Michael Freno</Title>
|
||||||
<div class="py-4 text-xl">
|
<Meta
|
||||||
Shapes with Abigail!'s Privacy Policy
|
name="description"
|
||||||
</div>
|
content="Privacy policy for Shapes with Abigail app, explaining our commitment to child safety and non-collection of personal data."
|
||||||
<div class="py-2">Last Updated: December 21, 2023</div>
|
/>
|
||||||
<div class="py-2">
|
<div class="bg-zinc-100 dark:bg-zinc-900">
|
||||||
Welcome to Shapes with Abigail! ('We' , 'Us',
|
<div class="min-h-screen px-[8vw] py-[8vh]">
|
||||||
'Our'). Your privacy is important to us. For that reason,
|
<div class="py-4 text-xl">
|
||||||
our app, "Shapes with Abigail!" has been designed to
|
Shapes with Abigail!'s Privacy Policy
|
||||||
provide our users with a secure environment. This privacy policy
|
</div>
|
||||||
will help you understand our policies and procedures related to the
|
<div class="py-2">Last Updated: December 21, 2023</div>
|
||||||
non-collection, non-use, and non-storage of personal information
|
|
||||||
from our users.
|
|
||||||
</div>
|
|
||||||
<ol>
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
Welcome to Shapes with Abigail! ('We' , 'Us',
|
||||||
<span class="-ml-4 pr-2">1.</span> Personal Information
|
'Our'). Your privacy is important to us. For that reason,
|
||||||
</div>
|
our app, "Shapes with Abigail!" has been designed to
|
||||||
<div class="pl-4">
|
provide our users with a secure environment. This privacy policy
|
||||||
<div class="pb-2">
|
will help you understand our policies and procedures related to the
|
||||||
<div class="-ml-6">
|
non-collection, non-use, and non-storage of personal information
|
||||||
(a) Non-Collection of Personal Data:
|
from our users.
|
||||||
</div>{" "}
|
</div>
|
||||||
Shapes with Abigail! does not collect nor store personal data.
|
<ol>
|
||||||
We respect the privacy of our users, especially considering
|
<div class="py-2">
|
||||||
the age of our users. We believe that no information, whether
|
<div class="pb-2 text-lg">
|
||||||
private or personal, should be required for children to enjoy
|
<span class="-ml-4 pr-2">1.</span> Personal Information
|
||||||
our fun and educational app.
|
</div>
|
||||||
|
<div class="pl-4">
|
||||||
|
<div class="pb-2">
|
||||||
|
<div class="-ml-6">(a) Non-Collection of Personal Data:</div>{" "}
|
||||||
|
Shapes with Abigail! does not collect nor store personal data.
|
||||||
|
We respect the privacy of our users, especially considering
|
||||||
|
the age of our users. We believe that no information, whether
|
||||||
|
private or personal, should be required for children to enjoy
|
||||||
|
our fun and educational app.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
<div class="pb-2 text-lg">
|
||||||
<span class="-ml-4 pr-2">2.</span> Third-Party Access
|
<span class="-ml-4 pr-2">2.</span> Third-Party Access
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
<div class="-ml-6">(a) No Third-Party Access:</div> Since we do
|
||||||
|
not collect or store any user data, there is no possibility of
|
||||||
|
sharing or selling our users' information to third parties.
|
||||||
|
Our priority is the safety and privacy of our users.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pb-2 pl-4">
|
|
||||||
<div class="-ml-6">(a) No Third-Party Access:</div> Since we
|
|
||||||
do not collect or store any user data, there is no possibility
|
|
||||||
of sharing or selling our users' information to third
|
|
||||||
parties. Our priority is the safety and privacy of our users.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
<div class="pb-2 text-lg">
|
||||||
<span class="-ml-4 pr-2">3.</span> Security
|
<span class="-ml-4 pr-2">3.</span> Security
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
<div class="-ml-6">(a) Secure Environment:</div>Shapes with
|
||||||
|
Abigail! offers a secure and safe platform for children to play
|
||||||
|
and learn. Not requiring any personal data naturally enhances
|
||||||
|
security by eliminating potential risks related to data breaches
|
||||||
|
and misuse of information.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pb-2 pl-4">
|
|
||||||
<div class="-ml-6">(a) Secure Environment:</div>Shapes with
|
|
||||||
Abigail! offers a secure and safe platform for children to play
|
|
||||||
and learn. Not requiring any personal data naturally enhances
|
|
||||||
security by eliminating potential risks related to data breaches
|
|
||||||
and misuse of information.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
<div class="pb-2 text-lg">
|
||||||
<span class="-ml-4 pr-2">4.</span> Changes to the Privacy
|
<span class="-ml-4 pr-2">4.</span> Changes to the Privacy Policy
|
||||||
Policy
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
<div class="-ml-6">(a) Updates:</div> We may update this privacy
|
||||||
|
policy periodically. Any changes to this privacy policy will be
|
||||||
|
posted on this page. However, since we do not collect any
|
||||||
|
personal data, these updates are likely to be insignificant.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pb-2 pl-4">
|
|
||||||
<div class="-ml-6">(a) Updates:</div> We may update this
|
|
||||||
privacy policy periodically. Any changes to this privacy policy
|
|
||||||
will be posted on this page. However, since we do not collect
|
|
||||||
any personal data, these updates are likely to be insignificant.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<div class="pb-2 text-lg">
|
<div class="pb-2 text-lg">
|
||||||
<span class="-ml-4 pr-2">5.</span> Contact Us
|
<span class="-ml-4 pr-2">5.</span> Contact Us
|
||||||
|
</div>
|
||||||
|
<div class="pb-2 pl-4">
|
||||||
|
<div class="-ml-6">(a) Reaching Out:</div> If there are any
|
||||||
|
questions or comments regarding this privacy policy, you can
|
||||||
|
contact us{" "}
|
||||||
|
<A
|
||||||
|
href="/contact"
|
||||||
|
class="text-blue-400 underline-offset-4 hover:underline"
|
||||||
|
>
|
||||||
|
here
|
||||||
|
</A>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pb-2 pl-4">
|
</ol>
|
||||||
<div class="-ml-6">(a) Reaching Out:</div> If there are any
|
</div>
|
||||||
questions or comments regarding this privacy policy, you can
|
|
||||||
contact us{" "}
|
|
||||||
<A
|
|
||||||
href="/contact"
|
|
||||||
class="text-blue-400 underline-offset-4 hover:underline"
|
|
||||||
>
|
|
||||||
here
|
|
||||||
</A>
|
|
||||||
.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import { Title } from "@solidjs/meta";
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
|
|
||||||
export default function Resume() {
|
export default function Resume() {
|
||||||
return (
|
return (
|
||||||
<main class="flex h-screen w-full flex-col">
|
<>
|
||||||
<Title>Resume - Freno.dev</Title>
|
<Title>Resume | Michael Freno</Title>
|
||||||
<div class="flex h-full w-full items-center justify-center">
|
<Meta
|
||||||
<iframe
|
name="description"
|
||||||
src="/resume.pdf"
|
content="View Michael Freno's resume - Software Engineer with expertise in full-stack development, game development, and open source."
|
||||||
class="h-full w-full border-0"
|
/>
|
||||||
title="Resume PDF"
|
|
||||||
/>
|
<main class="flex h-screen w-full flex-col">
|
||||||
</div>
|
<Title>Resume - Freno.dev</Title>
|
||||||
</main>
|
<div class="flex h-full w-full items-center justify-center">
|
||||||
|
<iframe
|
||||||
|
src="/resume.pdf"
|
||||||
|
class="h-full w-full border-0"
|
||||||
|
title="Resume PDF"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import Input from "~/components/ui/Input";
|
import Input from "~/components/ui/Input";
|
||||||
import Button from "~/components/ui/Button";
|
import Button from "~/components/ui/Button";
|
||||||
import { isValidEmail, validatePassword, passwordsMatch } from "~/lib/validation";
|
import {
|
||||||
|
isValidEmail,
|
||||||
|
validatePassword,
|
||||||
|
passwordsMatch
|
||||||
|
} from "~/lib/validation";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test page to validate Task 01 components and utilities
|
* Test page to validate Task 01 components and utilities
|
||||||
@@ -36,134 +41,153 @@ export default function TestUtilsPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
// Simulate API call
|
// Simulate API call
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
alert(`Form submitted!\nEmail: ${email()}\nPassword: ${password()}`);
|
alert(`Form submitted!\nEmail: ${email()}\nPassword: ${password()}`);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main class="min-h-screen bg-gray-100 p-8">
|
<>
|
||||||
<div class="max-w-2xl mx-auto">
|
<Title>Utility Testing | Michael Freno</Title>
|
||||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-6">
|
<Meta
|
||||||
<h1 class="text-3xl font-bold mb-2">Task 01 - Utility Testing</h1>
|
name="description"
|
||||||
<p class="text-gray-600 mb-4">
|
content="Testing page for form components and validation utilities."
|
||||||
Testing shared utilities, types, and UI components
|
/>
|
||||||
</p>
|
<main class="min-h-screen bg-gray-100 p-8">
|
||||||
</div>
|
<div class="mx-auto max-w-2xl">
|
||||||
|
<div class="mb-6 rounded-lg bg-white p-6 shadow-lg">
|
||||||
|
<h1 class="mb-2 text-3xl font-bold">Task 01 - Utility Testing</h1>
|
||||||
|
<p class="mb-4 text-gray-600">
|
||||||
|
Testing shared utilities, types, and UI components
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
<div class="mb-6 rounded-lg bg-white p-6 shadow">
|
||||||
<h2 class="text-xl font-bold mb-4">Form Components & Validation</h2>
|
<h2 class="mb-4 text-xl font-bold">Form Components & Validation</h2>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} class="space-y-4">
|
<form onSubmit={handleSubmit} class="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
label="Email Address"
|
label="Email Address"
|
||||||
value={email()}
|
value={email()}
|
||||||
onInput={(e) => setEmail(e.currentTarget.value)}
|
onInput={(e) => setEmail(e.currentTarget.value)}
|
||||||
error={emailError()}
|
error={emailError()}
|
||||||
helperText="Enter a valid email address"
|
helperText="Enter a valid email address"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
value={password()}
|
value={password()}
|
||||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||||
error={passwordError()}
|
error={passwordError()}
|
||||||
helperText="Minimum 8 characters"
|
helperText="Minimum 8 characters"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
label="Confirm Password"
|
label="Confirm Password"
|
||||||
value={passwordConf()}
|
value={passwordConf()}
|
||||||
onInput={(e) => setPasswordConf(e.currentTarget.value)}
|
onInput={(e) => setPasswordConf(e.currentTarget.value)}
|
||||||
error={passwordMatchError()}
|
error={passwordMatchError()}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
loading={loading()}
|
loading={loading()}
|
||||||
disabled={
|
disabled={
|
||||||
!isValidEmail(email()) ||
|
!isValidEmail(email()) ||
|
||||||
!validatePassword(password()).isValid ||
|
!validatePassword(password()).isValid ||
|
||||||
!passwordsMatch(password(), passwordConf())
|
!passwordsMatch(password(), passwordConf())
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEmail("");
|
setEmail("");
|
||||||
setPassword("");
|
setPassword("");
|
||||||
setPasswordConf("");
|
setPasswordConf("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
onClick={() => alert("Danger action!")}
|
onClick={() => alert("Danger action!")}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => alert("Ghost action!")}
|
onClick={() => alert("Ghost action!")}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
<div class="rounded-lg bg-white p-6 shadow">
|
||||||
<h2 class="text-xl font-bold mb-4">Validation Status</h2>
|
<h2 class="mb-4 text-xl font-bold">Validation Status</h2>
|
||||||
|
|
||||||
<div class="space-y-2 text-sm">
|
<div class="space-y-2 text-sm">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class={`w-3 h-3 rounded-full ${isValidEmail(email()) ? "bg-green-500" : "bg-gray-300"}`} />
|
<span
|
||||||
<span>Email Valid: {isValidEmail(email()) ? "✓" : "✗"}</span>
|
class={`h-3 w-3 rounded-full ${isValidEmail(email()) ? "bg-green-500" : "bg-gray-300"}`}
|
||||||
</div>
|
/>
|
||||||
|
<span>Email Valid: {isValidEmail(email()) ? "✓" : "✗"}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class={`w-3 h-3 rounded-full ${validatePassword(password()).isValid ? "bg-green-500" : "bg-gray-300"}`} />
|
<span
|
||||||
<span>Password Valid: {validatePassword(password()).isValid ? "✓" : "✗"}</span>
|
class={`h-3 w-3 rounded-full ${validatePassword(password()).isValid ? "bg-green-500" : "bg-gray-300"}`}
|
||||||
</div>
|
/>
|
||||||
|
<span>
|
||||||
|
Password Valid:{" "}
|
||||||
|
{validatePassword(password()).isValid ? "✓" : "✗"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span class={`w-3 h-3 rounded-full ${passwordsMatch(password(), passwordConf()) ? "bg-green-500" : "bg-gray-300"}`} />
|
<span
|
||||||
<span>Passwords Match: {passwordsMatch(password(), passwordConf()) ? "✓" : "✗"}</span>
|
class={`h-3 w-3 rounded-full ${passwordsMatch(password(), passwordConf()) ? "bg-green-500" : "bg-gray-300"}`}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
Passwords Match:{" "}
|
||||||
|
{passwordsMatch(password(), passwordConf()) ? "✓" : "✗"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-blue-50 border border-blue-200 rounded p-4 mt-6">
|
<div class="mt-6 rounded border border-blue-200 bg-blue-50 p-4">
|
||||||
<h3 class="font-bold text-blue-800 mb-2">✅ Task 01 Complete</h3>
|
<h3 class="mb-2 font-bold text-blue-800">✅ Task 01 Complete</h3>
|
||||||
<ul class="text-sm text-blue-700 space-y-1">
|
<ul class="space-y-1 text-sm text-blue-700">
|
||||||
<li>✓ User types created</li>
|
<li>✓ User types created</li>
|
||||||
<li>✓ Cookie utilities created</li>
|
<li>✓ Cookie utilities created</li>
|
||||||
<li>✓ Validation helpers created</li>
|
<li>✓ Validation helpers created</li>
|
||||||
<li>✓ Input component created</li>
|
<li>✓ Input component created</li>
|
||||||
<li>✓ Button component created</li>
|
<li>✓ Button component created</li>
|
||||||
<li>✓ Conversion patterns documented</li>
|
<li>✓ Conversion patterns documented</li>
|
||||||
<li>✓ Build successful</li>
|
<li>✓ Build successful</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</main>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createSignal, For, Show } from "solid-js";
|
import { createSignal, For, Show } from "solid-js";
|
||||||
import { query, createAsync } from "@solidjs/router";
|
import { query, createAsync } from "@solidjs/router";
|
||||||
|
import { Title, Meta } from "@solidjs/meta";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
import { getRequestEvent } from "solid-js/web";
|
||||||
import { api } from "~/lib/api";
|
import { api } from "~/lib/api";
|
||||||
|
|
||||||
@@ -932,297 +933,318 @@ export default function TestPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show
|
<>
|
||||||
when={authState()?.privilegeLevel === "admin"}
|
<Title>API Testing | Michael Freno</Title>
|
||||||
fallback={
|
<Meta
|
||||||
<div class="w-full pt-[30vh] text-center">
|
name="description"
|
||||||
<div class="text-text text-2xl">Unauthorized</div>
|
content="tRPC API testing dashboard for developers to test endpoints and verify functionality."
|
||||||
<div class="text-subtext0 mt-4">
|
/>
|
||||||
You must be an admin to access this page.
|
<Show
|
||||||
|
when={authState()?.privilegeLevel === "admin"}
|
||||||
|
fallback={
|
||||||
|
<div class="w-full pt-[30vh] text-center">
|
||||||
|
<div class="text-text text-2xl">Unauthorized</div>
|
||||||
|
<div class="text-subtext0 mt-4">
|
||||||
|
You must be an admin to access this page.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
}
|
||||||
}
|
>
|
||||||
>
|
<main class="min-h-screen p-8">
|
||||||
<main class="min-h-screen p-8">
|
<div class="mx-auto max-w-6xl">
|
||||||
<div class="mx-auto max-w-6xl">
|
<div class="bg-surface0 mb-6 rounded-lg p-6 shadow-lg">
|
||||||
<div class="bg-surface0 mb-6 rounded-lg p-6 shadow-lg">
|
<h1 class="mb-2 text-3xl font-bold">
|
||||||
<h1 class="mb-2 text-3xl font-bold">tRPC API Testing Dashboard</h1>
|
tRPC API Testing Dashboard
|
||||||
<p class="text-text mb-4">
|
</h1>
|
||||||
Complete API coverage: Example, Auth, Database, User, Misc, and
|
<p class="text-text mb-4">
|
||||||
Lineage routers
|
Complete API coverage: Example, Auth, Database, User, Misc, and
|
||||||
</p>
|
Lineage routers
|
||||||
|
|
||||||
<div class="border-lavender bg-mauve rounded border p-4">
|
|
||||||
<p class="text-base text-sm">
|
|
||||||
<strong>Quick Start:</strong> Expand any section below to test
|
|
||||||
endpoints. Public endpoints work immediately. Auth-required
|
|
||||||
endpoints need valid tokens.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="border-lavender bg-mauve rounded border p-4">
|
||||||
|
<p class="text-base text-sm">
|
||||||
|
<strong>Quick Start:</strong> Expand any section below to test
|
||||||
|
endpoints. Public endpoints work immediately. Auth-required
|
||||||
|
endpoints need valid tokens.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<For each={routerSections}>
|
<For each={routerSections}>
|
||||||
{(section) => {
|
{(section) => {
|
||||||
const isExpanded = () => expandedSections().has(section.name);
|
const isExpanded = () => expandedSections().has(section.name);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bg-surface0 rounded-lg shadow">
|
<div class="bg-surface0 rounded-lg shadow">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleSection(section.name)}
|
onClick={() => toggleSection(section.name)}
|
||||||
class="flex w-full items-center justify-between px-6 py-4 transition"
|
class="flex w-full items-center justify-between px-6 py-4 transition"
|
||||||
>
|
>
|
||||||
<div class="text-left">
|
<div class="text-left">
|
||||||
<h2 class="text-xl font-bold">{section.name}</h2>
|
<h2 class="text-xl font-bold">{section.name}</h2>
|
||||||
<p class="text-subtext0 text-sm">
|
<p class="text-subtext0 text-sm">
|
||||||
{section.description}
|
{section.description}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-subtext1 mt-1 text-xs">
|
<p class="text-subtext1 mt-1 text-xs">
|
||||||
{section.endpoints.length} endpoint
|
{section.endpoints.length} endpoint
|
||||||
{section.endpoints.length !== 1 ? "s" : ""}
|
{section.endpoints.length !== 1 ? "s" : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-subtext1 text-2xl">
|
<div class="text-subtext1 text-2xl">
|
||||||
{isExpanded() ? "−" : "+"}
|
{isExpanded() ? "−" : "+"}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Section Content */}
|
{/* Section Content */}
|
||||||
<Show when={isExpanded()}>
|
<Show when={isExpanded()}>
|
||||||
<div class="border-base space-y-4 border-t p-6">
|
<div class="border-base space-y-4 border-t p-6">
|
||||||
<For each={section.endpoints}>
|
<For each={section.endpoints}>
|
||||||
{(endpoint) => {
|
{(endpoint) => {
|
||||||
const key = `${endpoint.router}.${endpoint.procedure}`;
|
const key = `${endpoint.router}.${endpoint.procedure}`;
|
||||||
const hasInput = endpoint.sampleInput !== undefined;
|
const hasInput =
|
||||||
const displayInput = () => {
|
endpoint.sampleInput !== undefined;
|
||||||
if (inputEdits()[key]) {
|
const displayInput = () => {
|
||||||
return inputEdits()[key];
|
if (inputEdits()[key]) {
|
||||||
}
|
return inputEdits()[key];
|
||||||
// Handle primitive values (string, number, boolean)
|
}
|
||||||
if (typeof endpoint.sampleInput === "string") {
|
// Handle primitive values (string, number, boolean)
|
||||||
return `"${endpoint.sampleInput}"`;
|
if (typeof endpoint.sampleInput === "string") {
|
||||||
}
|
return `"${endpoint.sampleInput}"`;
|
||||||
if (
|
}
|
||||||
typeof endpoint.sampleInput === "number" ||
|
if (
|
||||||
typeof endpoint.sampleInput === "boolean"
|
typeof endpoint.sampleInput === "number" ||
|
||||||
) {
|
typeof endpoint.sampleInput === "boolean"
|
||||||
return String(endpoint.sampleInput);
|
) {
|
||||||
}
|
return String(endpoint.sampleInput);
|
||||||
// Handle objects and arrays
|
}
|
||||||
return JSON.stringify(
|
// Handle objects and arrays
|
||||||
endpoint.sampleInput,
|
return JSON.stringify(
|
||||||
null,
|
endpoint.sampleInput,
|
||||||
2
|
null,
|
||||||
);
|
2
|
||||||
};
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="bg-surface2 border-surface1 rounded-lg border p-4">
|
<div class="bg-surface2 border-surface1 rounded-lg border p-4">
|
||||||
{/* Endpoint Header */}
|
{/* Endpoint Header */}
|
||||||
<div class="mb-3 flex items-start justify-between">
|
<div class="mb-3 flex items-start justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<h3 class="text-subtext0 text-lg font-semibold">
|
<h3 class="text-subtext0 text-lg font-semibold">
|
||||||
{endpoint.name}
|
{endpoint.name}
|
||||||
</h3>
|
</h3>
|
||||||
<Show when={endpoint.requiresAuth}>
|
<Show when={endpoint.requiresAuth}>
|
||||||
<span class="bg-surface1 text-yellow rounded px-2 py-1 text-xs">
|
<span class="bg-surface1 text-yellow rounded px-2 py-1 text-xs">
|
||||||
🔒 Auth Required
|
🔒 Auth Required
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
<Show when={endpoint.requiresAdmin}>
|
||||||
|
<span class="bg-maroon rounded px-2 py-1 text-base text-xs">
|
||||||
|
👑 Admin Only
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1 text-sm text-gray-600">
|
||||||
|
{endpoint.description}
|
||||||
|
</p>
|
||||||
|
<div class="mt-2 flex gap-2">
|
||||||
|
<code class="bg-surface0 rounded px-2 py-1 text-xs">
|
||||||
|
{key}
|
||||||
|
</code>
|
||||||
|
<span class="bg-blue text-text rounded px-2 py-1 text-xs">
|
||||||
|
{endpoint.method === "query"
|
||||||
|
? "GET"
|
||||||
|
: "POST"}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</div>
|
||||||
<Show when={endpoint.requiresAdmin}>
|
|
||||||
<span class="bg-maroon rounded px-2 py-1 text-base text-xs">
|
|
||||||
👑 Admin Only
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
<p class="mt-1 text-sm text-gray-600">
|
|
||||||
{endpoint.description}
|
|
||||||
</p>
|
|
||||||
<div class="mt-2 flex gap-2">
|
|
||||||
<code class="bg-surface0 rounded px-2 py-1 text-xs">
|
|
||||||
{key}
|
|
||||||
</code>
|
|
||||||
<span class="bg-blue text-text rounded px-2 py-1 text-xs">
|
|
||||||
{endpoint.method === "query"
|
|
||||||
? "GET"
|
|
||||||
: "POST"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => testEndpoint(endpoint)}
|
||||||
|
disabled={loading()[key]}
|
||||||
|
class="bg-green ml-4 rounded px-4 py-2 text-base font-semibold whitespace-nowrap transition hover:brightness-125 disabled:brightness-50"
|
||||||
|
>
|
||||||
|
{loading()[key] ? "Testing..." : "Test"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => testEndpoint(endpoint)}
|
{/* Input Editor */}
|
||||||
disabled={loading()[key]}
|
<Show when={hasInput}>
|
||||||
class="bg-green ml-4 rounded px-4 py-2 text-base font-semibold whitespace-nowrap transition hover:brightness-125 disabled:brightness-50"
|
<div class="mb-3">
|
||||||
>
|
<label class="text-text mb-1 block text-xs font-semibold">
|
||||||
{loading()[key] ? "Testing..." : "Test"}
|
Request Body (edit JSON):
|
||||||
</button>
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={displayInput()}
|
||||||
|
onInput={(e) =>
|
||||||
|
updateInput(
|
||||||
|
key,
|
||||||
|
e.currentTarget.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
class="border-lavender bg-crust min-h-[100px] w-full rounded border p-2 font-mono text-xs"
|
||||||
|
spellcheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Error Display */}
|
||||||
|
<Show when={errors()[key]}>
|
||||||
|
<div class="mb-3 rounded border border-red-200 bg-red-50 p-3">
|
||||||
|
<p class="text-sm font-semibold text-red-800">
|
||||||
|
Error:
|
||||||
|
</p>
|
||||||
|
<p class="font-mono text-sm text-red-600">
|
||||||
|
{errors()[key]}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
{/* Results Display */}
|
||||||
|
<Show when={results()[key]}>
|
||||||
|
<div class="rounded bg-gray-900 p-3">
|
||||||
|
<p class="mb-2 text-xs font-semibold text-green-400">
|
||||||
|
✓ Response:
|
||||||
|
</p>
|
||||||
|
<pre class="max-h-60 overflow-auto text-xs text-green-400">
|
||||||
|
{JSON.stringify(
|
||||||
|
results()[key],
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Input Editor */}
|
{/* Footer Instructions */}
|
||||||
<Show when={hasInput}>
|
<div class="bg-overlay2 mt-6 rounded-lg p-6 shadow-lg">
|
||||||
<div class="mb-3">
|
<h2 class="text-crust mb-4 text-2xl font-bold">Testing Guide</h2>
|
||||||
<label class="text-text mb-1 block text-xs font-semibold">
|
|
||||||
Request Body (edit JSON):
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
value={displayInput()}
|
|
||||||
onInput={(e) =>
|
|
||||||
updateInput(key, e.currentTarget.value)
|
|
||||||
}
|
|
||||||
class="border-lavender bg-crust min-h-[100px] w-full rounded border p-2 font-mono text-xs"
|
|
||||||
spellcheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
{/* Error Display */}
|
<div class="space-y-4 text-base">
|
||||||
<Show when={errors()[key]}>
|
<div>
|
||||||
<div class="mb-3 rounded border border-red-200 bg-red-50 p-3">
|
<h3 class="mb-2 text-lg font-semibold">
|
||||||
<p class="text-sm font-semibold text-red-800">
|
🟢 No Auth Required
|
||||||
Error:
|
</h3>
|
||||||
</p>
|
<ul class="ml-6 list-disc space-y-1 text-sm">
|
||||||
<p class="font-mono text-sm text-red-600">
|
<li>
|
||||||
{errors()[key]}
|
<strong>Example Router</strong> - Hello endpoint
|
||||||
</p>
|
</li>
|
||||||
</div>
|
<li>
|
||||||
</Show>
|
<strong>Lineage JSON Service</strong> - All 6 endpoints
|
||||||
|
work immediately
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Database</strong> - All endpoints (comments,
|
||||||
|
posts, users, reactions, likes)
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Misc</strong> - Downloads, S3 operations, password
|
||||||
|
utilities
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Lineage Misc</strong> - Offline Secret, Get
|
||||||
|
Opponents
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Lineage PvP</strong> - Get Opponents
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Results Display */}
|
<div>
|
||||||
<Show when={results()[key]}>
|
<h3 class="mb-2 text-lg font-semibold">🟡 Auth Required</h3>
|
||||||
<div class="rounded bg-gray-900 p-3">
|
<p class="mb-2 text-sm">
|
||||||
<p class="mb-2 text-xs font-semibold text-green-400">
|
These need valid JWT tokens from login/registration:
|
||||||
✓ Response:
|
</p>
|
||||||
</p>
|
<ul class="ml-6 list-disc space-y-1 text-sm">
|
||||||
<pre class="max-h-60 overflow-auto text-xs text-green-400">
|
<li>
|
||||||
{JSON.stringify(results()[key], null, 2)}
|
<strong>Example Router</strong> - Get Profile
|
||||||
</pre>
|
</li>
|
||||||
</div>
|
<li>
|
||||||
</Show>
|
<strong>User Router</strong> - All endpoints (profile
|
||||||
</div>
|
updates, password, account deletion)
|
||||||
);
|
</li>
|
||||||
}}
|
<li>
|
||||||
</For>
|
<strong>Lineage Auth</strong> - Email Login, Refresh Token
|
||||||
</div>
|
</li>
|
||||||
</Show>
|
<li>
|
||||||
</div>
|
<strong>Lineage Database</strong> - Get Credentials,
|
||||||
);
|
Deletion endpoints
|
||||||
}}
|
</li>
|
||||||
</For>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Instructions */}
|
<div>
|
||||||
<div class="bg-overlay2 mt-6 rounded-lg p-6 shadow-lg">
|
<h3 class="mb-2 text-lg font-semibold">🔴 Admin Required</h3>
|
||||||
<h2 class="text-crust mb-4 text-2xl font-bold">Testing Guide</h2>
|
<p class="mb-2 text-sm">
|
||||||
|
Maintenance endpoints require admin privileges (userIDToken
|
||||||
|
cookie with ADMIN_ID).
|
||||||
|
</p>
|
||||||
|
<ul class="ml-6 list-disc space-y-1 text-sm">
|
||||||
|
<li>
|
||||||
|
<strong>Example Router</strong> - Admin Dashboard
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Lineage Maintenance</strong> - Find Loose
|
||||||
|
Databases, Cleanup Expired
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4 text-base">
|
<div>
|
||||||
<div>
|
<h3 class="mb-2 text-lg font-semibold">
|
||||||
<h3 class="mb-2 text-lg font-semibold">🟢 No Auth Required</h3>
|
📝 Typical Workflows
|
||||||
<ul class="ml-6 list-disc space-y-1 text-sm">
|
</h3>
|
||||||
<li>
|
<ol class="ml-6 list-decimal space-y-2 text-sm">
|
||||||
<strong>Example Router</strong> - Hello endpoint
|
<li>
|
||||||
</li>
|
<strong>Test public endpoints:</strong> Start with Example
|
||||||
<li>
|
Hello, Lineage JSON Service, or Database queries
|
||||||
<strong>Lineage JSON Service</strong> - All 6 endpoints work
|
</li>
|
||||||
immediately
|
<li>
|
||||||
</li>
|
<strong>OAuth flow:</strong> Use Auth router callbacks
|
||||||
<li>
|
with OAuth codes from GitHub/Google
|
||||||
<strong>Database</strong> - All endpoints (comments, posts,
|
</li>
|
||||||
users, reactions, likes)
|
<li>
|
||||||
</li>
|
<strong>Email auth flow:</strong> Register → verify email
|
||||||
<li>
|
→ login → use JWT
|
||||||
<strong>Misc</strong> - Downloads, S3 operations, password
|
</li>
|
||||||
utilities
|
<li>
|
||||||
</li>
|
<strong>Blog/Project management:</strong> Create posts →
|
||||||
<li>
|
add comments/likes → upload images via S3
|
||||||
<strong>Lineage Misc</strong> - Offline Secret, Get
|
</li>
|
||||||
Opponents
|
<li>
|
||||||
</li>
|
<strong>Lineage game data:</strong> Fetch JSON data →
|
||||||
<li>
|
register character → find PvP opponents
|
||||||
<strong>Lineage PvP</strong> - Get Opponents
|
</li>
|
||||||
</li>
|
</ol>
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div class="border-rosewater bg-rosewater mt-4 rounded border p-4">
|
||||||
<h3 class="mb-2 text-lg font-semibold">🟡 Auth Required</h3>
|
<p class="text-crust text-sm">
|
||||||
<p class="mb-2 text-sm">
|
<strong>Note:</strong> Some endpoints require specific setup
|
||||||
These need valid JWT tokens from login/registration:
|
(e.g., OAuth codes, existing database records, valid S3
|
||||||
</p>
|
keys). Check the sample input to understand what data each
|
||||||
<ul class="ml-6 list-disc space-y-1 text-sm">
|
endpoint expects.
|
||||||
<li>
|
</p>
|
||||||
<strong>Example Router</strong> - Get Profile
|
</div>
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>User Router</strong> - All endpoints (profile
|
|
||||||
updates, password, account deletion)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Lineage Auth</strong> - Email Login, Refresh Token
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Lineage Database</strong> - Get Credentials,
|
|
||||||
Deletion endpoints
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 class="mb-2 text-lg font-semibold">🔴 Admin Required</h3>
|
|
||||||
<p class="mb-2 text-sm">
|
|
||||||
Maintenance endpoints require admin privileges (userIDToken
|
|
||||||
cookie with ADMIN_ID).
|
|
||||||
</p>
|
|
||||||
<ul class="ml-6 list-disc space-y-1 text-sm">
|
|
||||||
<li>
|
|
||||||
<strong>Example Router</strong> - Admin Dashboard
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Lineage Maintenance</strong> - Find Loose Databases,
|
|
||||||
Cleanup Expired
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 class="mb-2 text-lg font-semibold">📝 Typical Workflows</h3>
|
|
||||||
<ol class="ml-6 list-decimal space-y-2 text-sm">
|
|
||||||
<li>
|
|
||||||
<strong>Test public endpoints:</strong> Start with Example
|
|
||||||
Hello, Lineage JSON Service, or Database queries
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>OAuth flow:</strong> Use Auth router callbacks with
|
|
||||||
OAuth codes from GitHub/Google
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Email auth flow:</strong> Register → verify email →
|
|
||||||
login → use JWT
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Blog/Project management:</strong> Create posts → add
|
|
||||||
comments/likes → upload images via S3
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Lineage game data:</strong> Fetch JSON data →
|
|
||||||
register character → find PvP opponents
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="border-rosewater bg-rosewater mt-4 rounded border p-4">
|
|
||||||
<p class="text-crust text-sm">
|
|
||||||
<strong>Note:</strong> Some endpoints require specific setup
|
|
||||||
(e.g., OAuth codes, existing database records, valid S3 keys).
|
|
||||||
Check the sample input to understand what data each endpoint
|
|
||||||
expects.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</main>
|
</Show>
|
||||||
</Show>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user