UI consolidation

This commit is contained in:
Michael Freno
2026-01-06 10:16:38 -05:00
parent c468b442c8
commit 609932a55b
13 changed files with 383 additions and 517 deletions

View File

@@ -0,0 +1,49 @@
import { Title, Meta, Link } from "@solidjs/meta";
export interface PageHeadProps {
title: string;
description?: string;
ogImage?: string;
ogTitle?: string;
ogDescription?: string;
canonical?: string;
}
/**
* PageHead component for consistent page metadata across the application.
* Automatically appends " | Michael Freno" to the title.
*
* @example
* ```tsx
* <PageHead
* title="Blog"
* description="Technical blog posts about web development"
* ogImage="https://example.com/og-image.jpg"
* />
* ```
*/
export default function PageHead(props: PageHeadProps) {
const fullTitle = () => `${props.title} | Michael Freno`;
return (
<>
<Title>{fullTitle()}</Title>
{props.description && (
<Meta name="description" content={props.description} />
)}
{props.canonical && <Link rel="canonical" href={props.canonical} />}
{/* Open Graph / Social Media Tags */}
{(props.ogTitle || props.title) && (
<Meta property="og:title" content={props.ogTitle || props.title} />
)}
{(props.ogDescription || props.description) && (
<Meta
property="og:description"
content={props.ogDescription || props.description}
/>
)}
{props.ogImage && <Meta property="og:image" content={props.ogImage} />}
</>
);
}

View File

@@ -8,6 +8,7 @@ import TagMaker from "~/components/blog/TagMaker";
import AddAttachmentSection from "~/components/blog/AddAttachmentSection"; import AddAttachmentSection from "~/components/blog/AddAttachmentSection";
import XCircle from "~/components/icons/XCircle"; import XCircle from "~/components/icons/XCircle";
import AddImageToS3 from "~/lib/s3upload"; import AddImageToS3 from "~/lib/s3upload";
import Input from "~/components/ui/Input";
interface PostFormProps { interface PostFormProps {
mode: "create" | "edit"; mode: "create" | "edit";
@@ -435,32 +436,26 @@ export default function PostForm(props: PostFormProps) {
<form onSubmit={handleSubmit} class="w-full max-w-full px-4"> <form onSubmit={handleSubmit} class="w-full max-w-full px-4">
<div class="mx-auto w-full md:w-3/4 xl:w-1/2"> <div class="mx-auto w-full md:w-3/4 xl:w-1/2">
{/* Title */} {/* Title */}
<div class="input-group mx-4"> <Input
<input type="text"
type="text" value={title()}
value={title()} onInput={(e) => setTitle(e.currentTarget.value)}
onInput={(e) => setTitle(e.currentTarget.value)} name="title"
name="title" label="Title"
placeholder=" " containerClass="input-group mx-4"
class="underlinedInput w-full bg-transparent" class="w-full"
/> />
<span class="bar"></span>
<label class="underlinedInputLabel">Title</label>
</div>
{/* Subtitle */} {/* Subtitle */}
<div class="input-group mx-4"> <Input
<input type="text"
type="text" value={subtitle()}
value={subtitle()} onInput={(e) => setSubtitle(e.currentTarget.value)}
onInput={(e) => setSubtitle(e.currentTarget.value)} name="subtitle"
name="subtitle" label="Subtitle"
placeholder=" " containerClass="input-group mx-4"
class="underlinedInput w-full bg-transparent" class="w-full"
/> />
<span class="bar"></span>
<label class="underlinedInputLabel">Subtitle</label>
</div>
{/* Banner */} {/* Banner */}
<div class="pt-8 text-center text-xl">Banner</div> <div class="pt-8 text-center text-xl">Banner</div>

View File

@@ -1,4 +1,5 @@
import { JSX, splitProps, Show } from "solid-js"; import { JSX, splitProps, Show } from "solid-js";
import LoadingSpinner from "~/components/LoadingSpinner";
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> { export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "danger" | "ghost"; variant?: "primary" | "secondary" | "danger" | "ghost";
@@ -22,18 +23,28 @@ export default function Button(props: ButtonProps) {
const size = () => local.size || "md"; const size = () => local.size || "md";
const baseClasses = const baseClasses =
"flex justify-center items-center rounded font-semibold transition-all duration-300 ease-out disabled:opacity-50 disabled:cursor-not-allowed"; "flex justify-center items-center rounded transition-all duration-300 ease-out";
const variantClasses = () => { const variantClasses = () => {
const isDisabledOrLoading = local.disabled || local.loading;
switch (variant()) { switch (variant()) {
case "primary": case "primary":
return "bg-blue-400 hover:bg-blue-500 active:scale-90 dark:bg-blue-600 dark:hover:bg-blue-700 text-white shadow-lg shadow-blue-300 dark:shadow-blue-700"; return isDisabledOrLoading
? "bg-blue cursor-not-allowed brightness-75"
: "bg-blue hover:brightness-125 active:scale-90";
case "secondary": case "secondary":
return "bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-900 dark:text-white"; return isDisabledOrLoading
? "bg-surface0 cursor-not-allowed brightness-75"
: "bg-surface0 hover:brightness-125 active:scale-90";
case "danger": case "danger":
return "bg-red-500 hover:bg-red-600 active:scale-90 text-white shadow-lg shadow-red-300 dark:shadow-red-700"; return isDisabledOrLoading
? "bg-red cursor-not-allowed brightness-75"
: "bg-red hover:brightness-125 active:scale-90";
case "ghost": case "ghost":
return "bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300"; return isDisabledOrLoading
? "cursor-not-allowed opacity-50"
: "hover:brightness-125 active:scale-90";
default: default:
return ""; return "";
} }
@@ -61,27 +72,7 @@ export default function Button(props: ButtonProps) {
class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`} class={`${baseClasses} ${variantClasses()} ${sizeClasses()} ${widthClass()} ${local.class || ""}`}
> >
<Show when={local.loading} fallback={local.children}> <Show when={local.loading} fallback={local.children}>
<svg <LoadingSpinner height={24} width={24} />
class="mr-2 h-5 w-5 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Loading...
</Show> </Show>
</button> </button>
); );

View File

@@ -4,6 +4,8 @@ export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement> {
label?: string; label?: string;
error?: string; error?: string;
helperText?: string; helperText?: string;
containerClass?: string;
ref?: HTMLInputElement | ((el: HTMLInputElement) => void);
} }
export default function Input(props: InputProps) { export default function Input(props: InputProps) {
@@ -11,13 +13,16 @@ export default function Input(props: InputProps) {
"label", "label",
"error", "error",
"helperText", "helperText",
"class" "class",
"containerClass",
"ref"
]); ]);
return ( return (
<div class="input-group"> <div class={local.containerClass || "input-group"}>
<input <input
{...others} {...others}
ref={local.ref}
placeholder=" " placeholder=" "
class={`underlinedInput bg-transparent ${local.class || ""}`} class={`underlinedInput bg-transparent ${local.class || ""}`}
aria-invalid={!!local.error} aria-invalid={!!local.error}

View File

@@ -0,0 +1,65 @@
import { JSX, splitProps, createSignal, Show } from "solid-js";
import Input, { InputProps } from "./Input";
import Eye from "~/components/icons/Eye";
import EyeSlash from "~/components/icons/EyeSlash";
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
export interface PasswordInputProps extends Omit<InputProps, "type"> {
showStrength?: boolean;
defaultVisible?: boolean;
passwordValue?: string;
}
export default function PasswordInput(props: PasswordInputProps) {
const [local, inputProps] = splitProps(props, [
"showStrength",
"defaultVisible",
"passwordValue",
"class",
"containerClass"
]);
const [showPassword, setShowPassword] = createSignal(
local.defaultVisible || false
);
return (
<>
<div class={local.containerClass || "input-group relative mx-4 mb-2"}>
<Input
{...inputProps}
type={showPassword() ? "text" : "password"}
class={`w-full pr-10 ${local.class || ""}`}
containerClass=""
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword())}
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
aria-label={showPassword() ? "Hide password" : "Show password"}
>
<Show
when={showPassword()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
}
>
<Eye height={24} width={24} strokeWidth={1} class="stroke-text" />
</Show>
</button>
</div>
{local.showStrength && local.passwordValue !== undefined && (
<div class="px-4 pt-1">
<PasswordStrengthMeter password={local.passwordValue} />
</div>
)}
</>
);
}

View File

@@ -72,3 +72,32 @@ export function insertSoftHyphens(
}) })
.join(" "); .join(" ");
} }
export function glitchText(
originalText: string,
setter: (text: string) => void,
glitchInterval: number = 300,
glitchLength: number = 80
) {
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`";
const interval = setInterval(() => {
if (Math.random() > 0.9) {
let glitched = "";
for (let i = 0; i < originalText.length; i++) {
if (Math.random() > 0.8) {
glitched +=
glitchChars[Math.floor(Math.random() * glitchChars.length)];
} else {
glitched += originalText[i];
}
}
setter(glitched);
setTimeout(() => {
setter(originalText);
}, glitchLength);
}
}, glitchInterval);
return interval;
}

View File

@@ -2,8 +2,6 @@ import { createSignal, Show, createEffect } from "solid-js";
import { Title, Meta } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
import { useNavigate, redirect, query, createAsync } from "@solidjs/router"; import { useNavigate, redirect, query, createAsync } from "@solidjs/router";
import { getEvent } from "vinxi/http"; import { getEvent } from "vinxi/http";
import Eye from "~/components/icons/Eye";
import EyeSlash from "~/components/icons/EyeSlash";
import XCircle from "~/components/icons/XCircle"; import XCircle from "~/components/icons/XCircle";
import GoogleLogo from "~/components/icons/GoogleLogo"; import GoogleLogo from "~/components/icons/GoogleLogo";
import GitHub from "~/components/icons/GitHub"; import GitHub from "~/components/icons/GitHub";
@@ -14,6 +12,9 @@ import { validatePassword, isValidEmail } from "~/lib/validation";
import { TerminalSplash } from "~/components/TerminalSplash"; import { TerminalSplash } from "~/components/TerminalSplash";
import { VALIDATION_CONFIG } from "~/config"; import { VALIDATION_CONFIG } from "~/config";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import Input from "~/components/ui/Input";
import PasswordInput from "~/components/ui/PasswordInput";
import Button from "~/components/ui/Button";
import type { UserProfile } from "~/types/user"; import type { UserProfile } from "~/types/user";
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter"; import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
@@ -87,10 +88,6 @@ export default function AccountPage() {
const [passwordDeletionError, setPasswordDeletionError] = createSignal(false); const [passwordDeletionError, setPasswordDeletionError] = createSignal(false);
const [newPassword, setNewPassword] = createSignal(""); const [newPassword, setNewPassword] = createSignal("");
const [showOldPasswordInput, setShowOldPasswordInput] = createSignal(false);
const [showPasswordInput, setShowPasswordInput] = createSignal(false);
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
const [showImageSuccess, setShowImageSuccess] = createSignal(false); const [showImageSuccess, setShowImageSuccess] = createSignal(false);
const [showEmailSuccess, setShowEmailSuccess] = createSignal(false); const [showEmailSuccess, setShowEmailSuccess] = createSignal(false);
const [showDisplayNameSuccess, setShowDisplayNameSuccess] = const [showDisplayNameSuccess, setShowDisplayNameSuccess] =
@@ -662,25 +659,19 @@ export default function AccountPage() {
JavaScript required to update email JavaScript required to update email
</div> </div>
</noscript> </noscript>
<div class="input-group mx-4"> <Input
<input ref={emailRef}
ref={emailRef} type="email"
type="email" required
required disabled={
disabled={ emailButtonLoading() ||
emailButtonLoading() || (userProfile().email !== null &&
(userProfile().email !== null && !userProfile().emailVerified)
!userProfile().emailVerified) }
} title="Please enter a valid email address"
placeholder=" " label={userProfile().email ? "Update Email" : "Add Email"}
title="Please enter a valid email address" containerClass="input-group mx-4"
class="underlinedInput bg-transparent" />
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
{userProfile().email ? "Update Email" : "Add Email"}
</label>
</div>
<Show <Show
when={ when={
userProfile().provider !== "email" && userProfile().provider !== "email" &&
@@ -739,22 +730,15 @@ export default function AccountPage() {
JavaScript required to update display name JavaScript required to update display name
</div> </div>
</noscript> </noscript>
<div class="input-group mx-4"> <Input
<input ref={displayNameRef}
ref={displayNameRef} type="text"
type="text" required
required disabled={displayNameButtonLoading()}
disabled={displayNameButtonLoading()} title="Please enter your display name"
placeholder=" " label={`Set ${userProfile().displayName ? "New " : ""}Display Name`}
title="Please enter your display name" containerClass="input-group mx-4"
class="underlinedInput bg-transparent" />
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
Set {userProfile().displayName ? "New " : ""}Display
Name
</label>
</div>
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
type="submit" type="submit"
@@ -806,136 +790,40 @@ export default function AccountPage() {
</Show> </Show>
<Show when={userProfile().hasPassword}> <Show when={userProfile().hasPassword}>
<div class="input-group relative mx-4 mb-6"> <PasswordInput
<input ref={oldPasswordRef}
ref={oldPasswordRef} required
type={showOldPasswordInput() ? "text" : "password"} minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH}
required disabled={passwordChangeLoading()}
minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH} title="Password must be at least 8 characters"
disabled={passwordChangeLoading()} label="Old Password"
placeholder=" " containerClass="input-group relative mx-4 mb-6"
title="Password must be at least 8 characters" />
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={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
</Show>
</button>
</div>
</Show> </Show>
<div class="input-group relative mx-4 mb-2"> <PasswordInput
<input ref={newPasswordRef}
ref={newPasswordRef} required
type={showPasswordInput() ? "text" : "password"} minlength="8"
required onInput={handleNewPasswordChange}
minlength="8" onBlur={handlePasswordBlur}
onInput={handleNewPasswordChange} disabled={passwordChangeLoading()}
onBlur={handlePasswordBlur} title="Password must be at least 8 characters"
disabled={passwordChangeLoading()} label="New Password"
placeholder=" " showStrength
title="Password must be at least 8 characters" passwordValue={newPassword()}
class="underlinedInput w-full bg-transparent pr-10" containerClass="input-group relative mx-4 mb-2"
/> />
<span class="bar"></span> <PasswordInput
<label class="underlinedInputLabel">New Password</label> ref={newPasswordConfRef}
<div class="pt-1"> required
<PasswordStrengthMeter password={newPassword()} /> minlength="8"
</div> onInput={handlePasswordConfChange}
<button disabled={passwordChangeLoading()}
type="button" title="Password must be at least 8 characters"
onClick={() => label="New Password Confirmation"
setShowPasswordInput(!showPasswordInput()) containerClass="input-group relative mx-4 mb-2"
} />
class="text-subtext0 absolute top-2 right-0 transition-all hover:brightness-125"
>
<Show
when={showPasswordInput()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
</Show>
</button>
</div>
<div class="input-group relative mx-4 mb-2">
<input
ref={newPasswordConfRef}
type={showPasswordConfInput() ? "text" : "password"}
required
minlength="8"
onInput={handlePasswordConfChange}
disabled={passwordChangeLoading()}
placeholder=" "
title="Password must be at least 8 characters"
class="underlinedInput w-full bg-transparent pr-10"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
New 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={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
</Show>
</button>
</div>
<Show <Show
when={ when={
@@ -1039,22 +927,15 @@ export default function AccountPage() {
> >
<form onSubmit={deleteAccountTrigger}> <form onSubmit={deleteAccountTrigger}>
<div class="flex w-full justify-center"> <div class="flex w-full justify-center">
<div class="input-group delete mx-4"> <PasswordInput
<input ref={deleteAccountPasswordRef}
ref={deleteAccountPasswordRef} required
type="password" minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH}
required disabled={deleteAccountButtonLoading()}
minlength={VALIDATION_CONFIG.MIN_PASSWORD_LENGTH} title="Enter your password to confirm account deletion"
disabled={deleteAccountButtonLoading()} label="Enter Password"
placeholder=" " containerClass="input-group delete mx-4"
title="Enter your password to confirm account deletion" />
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
Enter Password
</label>
</div>
</div> </div>
<button <button

View File

@@ -852,3 +852,6 @@ pre {
scrollbar-color: var(--color-text) transparent; scrollbar-color: var(--color-text) transparent;
} }
} }
img {
max-height: 50vh !important;
}

View File

@@ -14,6 +14,7 @@ import { getClientCookie, setClientCookie } from "~/lib/cookies.client";
import CountdownCircleTimer from "~/components/CountdownCircleTimer"; import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import LoadingSpinner from "~/components/LoadingSpinner"; import LoadingSpinner from "~/components/LoadingSpinner";
import RevealDropDown from "~/components/RevealDropDown"; import RevealDropDown from "~/components/RevealDropDown";
import Input from "~/components/ui/Input";
import type { UserProfile } from "~/types/user"; import type { UserProfile } from "~/types/user";
import { getCookie, setCookie } from "vinxi/http"; import { getCookie, setCookie } from "vinxi/http";
import { z } from "zod"; import { z } from "zod";
@@ -379,32 +380,26 @@ export default function ContactPage() {
> >
<div class="flex w-full flex-col justify-evenly"> <div class="flex w-full flex-col justify-evenly">
<div class="mx-auto w-full justify-evenly md:flex md:flex-row"> <div class="mx-auto w-full justify-evenly md:flex md:flex-row">
<div class="input-group md:mx-4"> <Input
<input type="text"
type="text" required
required name="name"
name="name" value={user()?.displayName ?? ""}
value={user()?.displayName ?? ""} title="Please enter your name"
placeholder=" " label="Name"
title="Please enter your name" containerClass="input-group md:mx-4"
class="underlinedInput w-full bg-transparent" class="w-full"
/> />
<span class="bar"></span> <Input
<label class="underlinedInputLabel">Name</label> type="email"
</div> required
<div class="input-group md:mx-4"> name="email"
<input value={user()?.email ?? ""}
type="email" title="Please enter a valid email address"
required label="Email"
name="email" containerClass="input-group md:mx-4"
value={user()?.email ?? ""} class="w-full"
placeholder=" " />
title="Please enter a valid email address"
class="underlinedInput w-full bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Email</label>
</div>
</div> </div>
<div class="mx-auto w-full pt-6 md:pt-12"> <div class="mx-auto w-full pt-6 md:pt-12">
<div class="textarea-group"> <div class="textarea-group">

View File

@@ -2,9 +2,29 @@ import { Title, Meta } from "@solidjs/meta";
import { A } from "@solidjs/router"; import { A } from "@solidjs/router";
import { createSignal, onMount, onCleanup } from "solid-js"; import { createSignal, onMount, onCleanup } from "solid-js";
import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore"; import DownloadOnAppStore from "~/components/icons/DownloadOnAppStore";
import { glitchText } from "~/lib/client-utils";
const DownloadButton = ({
onClick,
children
}: {
onClick: () => void;
children: Element | string;
}) => {
return (
<button
onClick={onClick}
class="bg-green hover:bg-green/90 cursor-pointer rounded-md px-6 py-3 font-mono text-base font-semibold shadow-lg transition-all duration-200 ease-out hover:scale-105 active:scale-95"
>
{children}
</button>
);
};
export default function DownloadsPage() { export default function DownloadsPage() {
const [glitchText, setGlitchText] = createSignal("$ downloads"); const [LaLText, setLaLText] = createSignal("Life and Lineage");
const [SwAText, setSwAText] = createSignal("Shapes with Abigail!");
const [corkText, setCorkText] = createSignal("Cork");
const download = (assetName: string) => { const download = (assetName: string) => {
fetch(`/api/downloads/public/${assetName}`) fetch(`/api/downloads/public/${assetName}`)
@@ -17,30 +37,14 @@ export default function DownloadsPage() {
}; };
onMount(() => { onMount(() => {
const originalText = "$ downloads"; const lalInterval = glitchText(LaLText(), setLaLText);
const glitchChars = "!@#$%^&*()_+-=[]{}|;':\",./<>?~`"; const swaInterval = glitchText(SwAText(), setSwAText);
const corkInterval = glitchText(corkText(), setCorkText);
const glitchInterval = setInterval(() => {
if (Math.random() > 0.9) {
let glitched = "";
for (let i = 0; i < originalText.length; i++) {
if (Math.random() > 0.8) {
glitched +=
glitchChars[Math.floor(Math.random() * glitchChars.length)];
} else {
glitched += originalText[i];
}
}
setGlitchText(glitched);
setTimeout(() => {
setGlitchText(originalText);
}, 80);
}
}, 300);
onCleanup(() => { onCleanup(() => {
clearInterval(glitchInterval); clearInterval(lalInterval);
clearInterval(swaInterval);
clearInterval(corkInterval);
}); });
}); });
@@ -65,18 +69,11 @@ export default function DownloadsPage() {
</div> </div>
<div class="relative z-10"> <div class="relative z-10">
<div class="text-text mb-12 font-mono text-3xl tracking-wider">
<span class="text-green">freno@downloads</span>
<span class="text-subtext1">:</span>
<span class="text-blue">~</span>
<span class="text-subtext1 ml-2">{glitchText()}</span>
</div>
<div class="mx-auto max-w-5xl space-y-16"> <div class="mx-auto max-w-5xl space-y-16">
{/* Life and Lineage */} {/* Life and Lineage */}
<div class="border-overlay0 rounded-lg border p-6 md:p-8"> <div class="border-overlay0 rounded-lg border p-6 md:p-8">
<h2 class="text-text mb-6 font-mono text-2xl"> <h2 class="text-text mb-6 font-mono text-2xl">
<span class="text-yellow">{">"}</span> Life and Lineage <span class="text-yellow">{">"}</span> {LaLText()}
</h2> </h2>
<div class="flex flex-col gap-8 md:flex-row md:justify-around"> <div class="flex flex-col gap-8 md:flex-row md:justify-around">
@@ -84,12 +81,9 @@ export default function DownloadsPage() {
<span class="text-subtext0 font-mono text-sm"> <span class="text-subtext0 font-mono text-sm">
platform: android platform: android
</span> </span>
<button <DownloadButton onClick={() => download("lineage")}>
onClick={() => download("lineage")}
class="bg-green hover:bg-green/90 rounded-md px-6 py-3 font-mono text-base font-semibold shadow-lg transition-all duration-200 ease-out hover:scale-105 active:scale-95"
>
download.apk download.apk
</button> </DownloadButton>
<span class="text-subtext1 max-w-xs text-center text-xs italic"> <span class="text-subtext1 max-w-xs text-center text-xs italic">
# android build not optimized # android build not optimized
</span> </span>
@@ -112,7 +106,7 @@ export default function DownloadsPage() {
{/* Shapes with Abigail */} {/* Shapes with Abigail */}
<div class="border-overlay0 rounded-lg border p-6 md:p-8"> <div class="border-overlay0 rounded-lg border p-6 md:p-8">
<h2 class="text-text mb-6 font-mono text-2xl"> <h2 class="text-text mb-6 font-mono text-2xl">
<span class="text-yellow">{">"}</span> Shapes with Abigail! <span class="text-yellow">{">"}</span> {SwAText()}
</h2> </h2>
<div class="flex flex-col gap-8 md:flex-row md:justify-around"> <div class="flex flex-col gap-8 md:flex-row md:justify-around">
@@ -120,12 +114,11 @@ export default function DownloadsPage() {
<span class="text-subtext0 font-mono text-sm"> <span class="text-subtext0 font-mono text-sm">
platform: android platform: android
</span> </span>
<button <DownloadButton
onClick={() => download("shapes-with-abigail")} onClick={() => download("shapes-with-abigail")}
class="bg-green hover:bg-green/90 rounded-md px-6 py-3 font-mono text-base font-semibold shadow-lg transition-all duration-200 ease-out hover:scale-105 active:scale-95"
> >
download.apk download.apk
</button> </DownloadButton>
</div> </div>
<div class="flex flex-col items-center gap-3"> <div class="flex flex-col items-center gap-3">
@@ -145,19 +138,16 @@ export default function DownloadsPage() {
{/* Cork */} {/* Cork */}
<div class="border-overlay0 rounded-lg border p-6 md:p-8"> <div class="border-overlay0 rounded-lg border p-6 md:p-8">
<h2 class="text-text mb-6 font-mono text-2xl"> <h2 class="text-text mb-6 font-mono text-2xl">
<span class="text-yellow">{">"}</span> Cork <span class="text-yellow">{">"}</span> {corkText()}
</h2> </h2>
<div class="flex flex-col items-center gap-3"> <div class="flex flex-col items-center gap-3">
<span class="text-subtext0 font-mono text-sm"> <span class="text-subtext0 font-mono text-sm">
platform: macOS (13+) platform: macOS (13+)
</span> </span>
<button <DownloadButton onClick={() => download("cork")}>
onClick={() => download("cork")}
class="bg-green hover:bg-green/90 rounded-md px-6 py-3 font-mono text-base font-semibold shadow-lg transition-all duration-200 ease-out hover:scale-105 active:scale-95"
>
download.zip download.zip
</button> </DownloadButton>
<span class="text-subtext1 text-xs"> <span class="text-subtext1 text-xs">
# unzip drag to /Applications # unzip drag to /Applications
</span> </span>

View File

@@ -10,14 +10,14 @@ 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";
import Eye from "~/components/icons/Eye";
import EyeSlash from "~/components/icons/EyeSlash";
import CountdownCircleTimer from "~/components/CountdownCircleTimer"; import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter"; import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
import { isValidEmail, validatePassword } from "~/lib/validation"; import { isValidEmail, validatePassword } from "~/lib/validation";
import { getClientCookie } from "~/lib/cookies.client"; import { getClientCookie } from "~/lib/cookies.client";
import { env } from "~/env/client"; import { env } from "~/env/client";
import { VALIDATION_CONFIG, COUNTDOWN_CONFIG } from "~/config"; import { VALIDATION_CONFIG, COUNTDOWN_CONFIG } from "~/config";
import Input from "~/components/ui/Input";
import PasswordInput from "~/components/ui/PasswordInput";
const checkAuth = query(async () => { const checkAuth = query(async () => {
"use server"; "use server";
@@ -49,8 +49,6 @@ export default function LoginPage() {
const [emailSent, setEmailSent] = createSignal(false); const [emailSent, setEmailSent] = createSignal(false);
const [showPasswordError, setShowPasswordError] = createSignal(false); const [showPasswordError, setShowPasswordError] = createSignal(false);
const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false); const [showPasswordSuccess, setShowPasswordSuccess] = createSignal(false);
const [showPasswordInput, setShowPasswordInput] = createSignal(false);
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
const [passwordsMatch, setPasswordsMatch] = createSignal(false); const [passwordsMatch, setPasswordsMatch] = createSignal(false);
const [password, setPassword] = createSignal(""); const [password, setPassword] = createSignal("");
const [passwordConf, setPasswordConf] = createSignal(""); const [passwordConf, setPasswordConf] = createSignal("");
@@ -402,116 +400,47 @@ export default function LoginPage() {
<form onSubmit={formHandler} class="flex flex-col px-2 py-4"> <form onSubmit={formHandler} class="flex flex-col px-2 py-4">
<div class="flex justify-center"> <div class="flex justify-center">
<div class="input-group mx-4"> <Input
<input type="email"
type="email" required
required ref={emailRef}
ref={emailRef} title="Please enter a valid email address"
placeholder=" " label="Email"
title="Please enter a valid email address" containerClass="input-group mx-4"
class="underlinedInput bg-transparent" />
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Email</label>
</div>
</div> </div>
<Show when={usePassword() || register()}> <Show when={usePassword() || register()}>
<div class="-mt-4 flex justify-center"> <div class="-mt-4 flex justify-center">
<div class="input-group mx-4 flex"> <PasswordInput
<input required
type={showPasswordInput() ? "text" : "password"} minLength={8}
required ref={passwordRef}
minLength={8} onInput={register() ? handlePasswordChange : undefined}
ref={passwordRef} title="Password must be at least 8 characters"
onInput={register() ? handlePasswordChange : undefined} label="Password"
placeholder=" " containerClass="input-group mx-4 flex"
title="Password must be at least 8 characters" showStrength={register()}
class="underlinedInput bg-transparent" passwordValue={register() ? password() : undefined}
/> />
<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-text"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
</Show>
</button>
</div>
</Show>
<Show when={register()}>
<div class="mx-auto flex justify-center px-4 py-2">
<PasswordStrengthMeter password={password()} />
</div> </div>
</Show> </Show>
<Show when={register()}> <Show when={register()}>
<div class="flex justify-center"> <div class="flex justify-center">
<div class="input-group mx-4"> <PasswordInput
<input required
type={showPasswordConfInput() ? "text" : "password"} minLength={8}
required ref={passwordConfRef}
minLength={8} onInput={handlePasswordConfChange}
ref={passwordConfRef} title="Password must be at least 8 characters and match the password above"
onInput={handlePasswordConfChange} label="Confirm Password"
placeholder=" " containerClass="input-group mx-4"
title="Password must be at least 8 characters and match the password above" />
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">Confirm Password</label>
</div>
<button
onClick={() => {
setShowPasswordConfInput(!showPasswordConfInput());
passwordConfRef?.focus();
}}
class="absolute mt-14 ml-60"
type="button"
>
<Show
when={showPasswordConfInput()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
</Show>
</button>
</div> </div>
</Show>
<Show when={register()}>
<div <div
class={`${ class={`${
!passwordsMatch() && !passwordsMatch() &&

View File

@@ -2,11 +2,11 @@ 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 { Title, Meta } from "@solidjs/meta";
import CountdownCircleTimer from "~/components/CountdownCircleTimer"; import CountdownCircleTimer from "~/components/CountdownCircleTimer";
import Eye from "~/components/icons/Eye";
import EyeSlash from "~/components/icons/EyeSlash";
import { validatePassword } from "~/lib/validation"; import { validatePassword } from "~/lib/validation";
import { api } from "~/lib/api"; import { api } from "~/lib/api";
import { VALIDATION_CONFIG, COUNTDOWN_CONFIG } from "~/config"; import { VALIDATION_CONFIG, COUNTDOWN_CONFIG } from "~/config";
import PasswordInput from "~/components/ui/PasswordInput";
import PasswordStrengthMeter from "~/components/PasswordStrengthMeter";
export default function PasswordResetPage() { export default function PasswordResetPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -22,8 +22,7 @@ export default function PasswordResetPage() {
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("");
const [showPasswordInput, setShowPasswordInput] = createSignal(false); const [newPassword, setNewPassword] = createSignal("");
const [showPasswordConfInput, setShowPasswordConfInput] = createSignal(false);
let newPasswordRef: HTMLInputElement | undefined; let newPasswordRef: HTMLInputElement | undefined;
let newPasswordConfRef: HTMLInputElement | undefined; let newPasswordConfRef: HTMLInputElement | undefined;
@@ -121,6 +120,7 @@ export default function PasswordResetPage() {
const handleNewPasswordChange = (e: Event) => { const handleNewPasswordChange = (e: Event) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
setNewPassword(target.value);
checkPasswordLength(target.value); checkPasswordLength(target.value);
if (newPasswordConfRef) { if (newPasswordConfRef) {
checkForMatch(target.value, newPasswordConfRef.value); checkForMatch(target.value, newPasswordConfRef.value);
@@ -169,49 +169,19 @@ export default function PasswordResetPage() {
> >
<div class="flex w-full max-w-md flex-col justify-center px-4"> <div class="flex w-full max-w-md flex-col justify-center px-4">
<div class="flex justify-center"> <div class="flex justify-center">
<div class="input-group mx-4 flex"> <PasswordInput
<input ref={newPasswordRef}
ref={newPasswordRef} name="newPassword"
name="newPassword" required
type={showPasswordInput() ? "text" : "password"} autofocus
required onInput={handleNewPasswordChange}
autofocus onBlur={handlePasswordBlur}
onInput={handleNewPasswordChange} disabled={passwordChangeLoading()}
onBlur={handlePasswordBlur} label="New Password"
disabled={passwordChangeLoading()} containerClass="input-group mx-4 flex"
placeholder=" " showStrength
class="underlinedInput bg-transparent" passwordValue={newPassword()}
/> />
<span class="bar"></span>
<label class="underlinedInputLabel">New Password</label>
</div>
<button
type="button"
onClick={() => {
setShowPasswordInput(!showPasswordInput());
newPasswordRef?.focus();
}}
class="absolute mt-14 ml-60"
>
<Show
when={showPasswordInput()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
</Show>
</button>
</div> </div>
<div <div
@@ -224,49 +194,15 @@ export default function PasswordResetPage() {
</div> </div>
<div class="-mt-4 flex justify-center"> <div class="-mt-4 flex justify-center">
<div class="input-group mx-4 flex"> <PasswordInput
<input ref={newPasswordConfRef}
ref={newPasswordConfRef} name="newPasswordConf"
name="newPasswordConf" onInput={handlePasswordConfChange}
onInput={handlePasswordConfChange} required
type={showPasswordConfInput() ? "text" : "password"} disabled={passwordChangeLoading()}
required label="Password Confirmation"
disabled={passwordChangeLoading()} containerClass="input-group mx-4 flex"
placeholder=" " />
class="underlinedInput bg-transparent"
/>
<span class="bar"></span>
<label class="underlinedInputLabel">
Password Confirmation
</label>
</div>
<button
type="button"
onClick={() => {
setShowPasswordConfInput(!showPasswordConfInput());
newPasswordConfRef?.focus();
}}
class="absolute mt-14 ml-60"
>
<Show
when={showPasswordConfInput()}
fallback={
<EyeSlash
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
}
>
<Eye
height={24}
width={24}
strokeWidth={1}
class="stroke-text"
/>
</Show>
</button>
</div> </div>
<div <div

View File

@@ -5,6 +5,7 @@ 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";
import { COUNTDOWN_CONFIG } from "~/config"; import { COUNTDOWN_CONFIG } from "~/config";
import Input from "~/components/ui/Input";
export default function RequestPasswordResetPage() { export default function RequestPasswordResetPage() {
const navigate = useNavigate(); const navigate = useNavigate();
@@ -136,20 +137,17 @@ export default function RequestPasswordResetPage() {
class="mt-4 flex w-full justify-center" class="mt-4 flex w-full justify-center"
> >
<div class="flex flex-col justify-center"> <div class="flex flex-col justify-center">
<div class="input-group mx-4"> <Input
<input ref={emailRef}
ref={emailRef} name="email"
name="email" type="email"
type="email" required
required disabled={loading()}
disabled={loading()} title="Please enter a valid email address"
placeholder=" " label="Enter Email"
title="Please enter a valid email address" containerClass="input-group mx-4"
class="underlinedInput w-full bg-transparent" class="w-full"
/> />
<span class="bar"></span>
<label class="underlinedInputLabel">Enter Email</label>
</div>
<Show <Show
when={countDown() > 0} when={countDown() > 0}