good enough

This commit is contained in:
Michael Freno
2025-12-22 16:44:43 -05:00
parent ad2cde6afc
commit 11fad35288
9 changed files with 332 additions and 208 deletions

View File

@@ -309,7 +309,10 @@ export function LeftBar() {
return ( return (
<nav <nav
id="navigation"
tabindex="-1"
ref={ref} ref={ref}
aria-label="Main navigation"
class="border-r-overlay2 bg-base fixed z-50 h-dvh w-min border-r-2 transition-transform duration-500 ease-out md:max-w-[20%]" class="border-r-overlay2 bg-base fixed z-50 h-dvh w-min border-r-2 transition-transform duration-500 ease-out md:max-w-[20%]"
classList={{ classList={{
"-translate-x-full": !leftBarVisible(), "-translate-x-full": !leftBarVisible(),
@@ -512,8 +515,9 @@ export function RightBar() {
}); });
return ( return (
<nav <aside
ref={ref} ref={ref}
aria-label="Links and activity"
class="border-l-overlay2 bg-base fixed right-0 z-50 hidden h-dvh w-fit border-l-2 transition-transform duration-500 ease-out md:block md:max-w-[20%]" class="border-l-overlay2 bg-base fixed right-0 z-50 hidden h-dvh w-fit border-l-2 transition-transform duration-500 ease-out md:block md:max-w-[20%]"
classList={{ classList={{
"translate-x-full": !rightBarVisible(), "translate-x-full": !rightBarVisible(),
@@ -528,6 +532,6 @@ export function RightBar() {
}} }}
> >
<RightBarContent /> <RightBarContent />
</nav> </aside>
); );
} }

View File

@@ -39,7 +39,7 @@ export default function Card(props: CardProps) {
</div> </div>
</Show> </Show>
<img <img
src={props.post.banner_photo ? props.post.banner_photo : "/bitcoin.jpg"} src={props.post.banner_photo ?? ""}
alt={props.post.title.replaceAll("_", " ") + " banner"} alt={props.post.title.replaceAll("_", " ") + " banner"}
class="h-full w-full object-cover" class="h-full w-full object-cover"
/> />

View File

@@ -1,6 +1,6 @@
import { createSignal, Show, onMount } from "solid-js"; import { createSignal, Show, createEffect } from "solid-js";
import { Title, Meta } from "@solidjs/meta"; import { Title, Meta } from "@solidjs/meta";
import { useNavigate, redirect, query } 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 Eye from "~/components/icons/Eye";
import EyeSlash from "~/components/icons/EyeSlash"; import EyeSlash from "~/components/icons/EyeSlash";
@@ -23,29 +23,56 @@ type UserProfile = {
hasPassword: boolean; hasPassword: boolean;
}; };
const checkAuth = query(async () => { const getUserProfile = query(async (): Promise<UserProfile | null> => {
"use server"; "use server";
const { checkAuthStatus } = await import("~/server/utils"); const { getUserID, ConnectionFactory } = await import("~/server/utils");
const event = getEvent()!; const event = getEvent()!;
const { isAuthenticated } = await checkAuthStatus(event);
if (!isAuthenticated) { const userId = await getUserID(event);
if (!userId) {
throw redirect("/login"); throw redirect("/login");
} }
return { isAuthenticated }; const conn = ConnectionFactory();
}, "accountAuthCheck"); try {
const res = await conn.execute({
sql: "SELECT * FROM User WHERE id = ?",
args: [userId]
});
if (res.rows.length === 0) {
throw redirect("/login");
}
const user = res.rows[0] as any;
// Transform to UserProfile type
return {
id: user.id,
email: user.email || null,
emailVerified: Boolean(user.emailVerified),
displayName: user.displayName || null,
image: user.image || null,
provider: user.provider || null,
hasPassword: Boolean(user.password)
};
} catch (err) {
console.error("Failed to fetch user profile:", err);
throw redirect("/login");
}
}, "accountUserProfile");
export const route = { export const route = {
load: () => checkAuth() load: () => getUserProfile()
}; };
export default function AccountPage() { export default function AccountPage() {
const navigate = useNavigate(); const navigate = useNavigate();
// User data const userData = createAsync(() => getUserProfile(), { deferStream: true });
// Local user state for client-side updates
const [user, setUser] = createSignal<UserProfile | null>(null); const [user, setUser] = createSignal<UserProfile | null>(null);
const [loading, setLoading] = createSignal(true);
// Form loading states // Form loading states
const [emailButtonLoading, setEmailButtonLoading] = createSignal(false); const [emailButtonLoading, setEmailButtonLoading] = createSignal(false);
@@ -98,27 +125,14 @@ export default function AccountPage() {
let displayNameRef: HTMLInputElement | undefined; let displayNameRef: HTMLInputElement | undefined;
let deleteAccountPasswordRef: HTMLInputElement | undefined; let deleteAccountPasswordRef: HTMLInputElement | undefined;
// Fetch user profile on mount // Helper to get current user (from SSR data or local state)
onMount(async () => { const currentUser = () => user() || userData();
try {
const response = await fetch("/api/trpc/user.getProfile", {
method: "GET"
});
if (response.ok) { // Initialize preSetHolder when userData loads
const result = await response.json(); createEffect(() => {
if (result.result?.data) { const userProfile = userData();
setUser(result.result.data); if (userProfile?.image && !preSetHolder()) {
// Set preset holder if user has existing image setPreSetHolder(userProfile.image);
if (result.result.data.image) {
setPreSetHolder(result.result.data.image);
}
}
}
} catch (err) {
console.error("Failed to fetch user profile:", err);
} finally {
setLoading(false);
} }
}); });
@@ -152,8 +166,8 @@ export default function AccountPage() {
setProfileImageSetLoading(true); setProfileImageSetLoading(true);
setShowImageSuccess(false); setShowImageSuccess(false);
const currentUser = user(); const userProfile = currentUser();
if (!currentUser) { if (!userProfile) {
setProfileImageSetLoading(false); setProfileImageSetLoading(false);
return; return;
} }
@@ -161,17 +175,15 @@ export default function AccountPage() {
try { try {
let imageUrl = ""; let imageUrl = "";
// Upload new image if one was selected
if (profileImage()) { if (profileImage()) {
const imageKey = await AddImageToS3( const imageKey = await AddImageToS3(
profileImage()!, profileImage()!,
currentUser.id, userProfile.id,
"user" "user"
); );
imageUrl = imageKey || ""; imageUrl = imageKey || "";
} }
// Update user profile image
const response = await fetch("/api/trpc/user.updateProfileImage", { const response = await fetch("/api/trpc/user.updateProfileImage", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -264,10 +276,10 @@ export default function AccountPage() {
// Password change/set handler // Password change/set handler
const handlePasswordSubmit = async (e: Event) => { const handlePasswordSubmit = async (e: Event) => {
e.preventDefault(); e.preventDefault();
const currentUser = user(); const userProfile = currentUser();
if (!currentUser) return; if (!userProfile) return;
if (currentUser.hasPassword) { if (userProfile.hasPassword) {
// Change password (requires old password) // Change password (requires old password)
if (!oldPasswordRef || !newPasswordRef || !newPasswordConfRef) return; if (!oldPasswordRef || !newPasswordRef || !newPasswordConfRef) return;
@@ -399,14 +411,14 @@ export default function AccountPage() {
// Resend email verification // Resend email verification
const sendEmailVerification = async () => { const sendEmailVerification = async () => {
const currentUser = user(); const userProfile = currentUser();
if (!currentUser?.email) return; if (!userProfile?.email) return;
try { try {
await fetch("/api/trpc/auth.resendEmailVerification", { await fetch("/api/trpc/auth.resendEmailVerification", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: currentUser.email }) body: JSON.stringify({ email: userProfile.email })
}); });
alert("Verification email sent!"); alert("Verification email sent!");
} catch (err) { } catch (err) {
@@ -496,9 +508,20 @@ export default function AccountPage() {
/> />
<div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36"> <div class="bg-base mx-8 min-h-screen md:mx-24 lg:mx-36">
<noscript>
<div class="bg-yellow mx-auto mt-8 max-w-2xl rounded-lg px-6 py-4 text-center text-black">
<strong> JavaScript Required for Account Updates</strong>
<p class="mt-2 text-sm">
You can view your account information below, but JavaScript is
required to update your profile, change settings, or delete your
account.
</p>
</div>
</noscript>
<div class="pt-24"> <div class="pt-24">
<Show when={!loading() && user()} fallback={<TerminalSplash />}> <Show when={currentUser()} fallback={<TerminalSplash />}>
{(currentUser) => ( {(userProfile) => (
<> <>
<div class="text-text mb-8 text-center text-3xl font-bold"> <div class="text-text mb-8 text-center text-3xl font-bold">
Account Settings Account Settings
@@ -512,18 +535,18 @@ export default function AccountPage() {
</div> </div>
<div class="flex items-center justify-center gap-3"> <div class="flex items-center justify-center gap-3">
<span <span
class={getProviderInfo(currentUser().provider).color} class={getProviderInfo(userProfile().provider).color}
> >
{getProviderInfo(currentUser().provider).icon} {getProviderInfo(userProfile().provider).icon}
</span> </span>
<span class="text-lg font-semibold"> <span class="text-lg font-semibold">
{getProviderInfo(currentUser().provider).name} Account {getProviderInfo(userProfile().provider).name} Account
</span> </span>
</div> </div>
<Show <Show
when={ when={
currentUser().provider !== "email" && userProfile().provider !== "email" &&
!currentUser().email !userProfile().email
} }
> >
<div class="mt-3 rounded bg-yellow-500/10 px-3 py-2 text-center text-sm text-yellow-600 dark:text-yellow-400"> <div class="mt-3 rounded bg-yellow-500/10 px-3 py-2 text-center text-sm text-yellow-600 dark:text-yellow-400">
@@ -532,8 +555,8 @@ export default function AccountPage() {
</Show> </Show>
<Show <Show
when={ when={
currentUser().provider !== "email" && userProfile().provider !== "email" &&
!currentUser().hasPassword !userProfile().hasPassword
} }
> >
<div class="mt-3 rounded bg-blue-500/10 px-3 py-2 text-center text-sm text-blue-600 dark:text-blue-400"> <div class="mt-3 rounded bg-blue-500/10 px-3 py-2 text-center text-sm text-blue-600 dark:text-blue-400">
@@ -551,12 +574,17 @@ export default function AccountPage() {
<div class="mb-2 text-center text-lg font-semibold"> <div class="mb-2 text-center text-lg font-semibold">
Profile Image Profile Image
</div> </div>
<div class="flex items-start"> <noscript>
<div class="text-subtext0 mb-4 text-center text-sm">
JavaScript is required to update profile images
</div>
</noscript>
<div class="flex items-start justify-center">
<Dropzone <Dropzone
onDrop={handleImageDrop} onDrop={handleImageDrop}
acceptedFiles="image/jpg, image/jpeg, image/png" acceptedFiles="image/jpg, image/jpeg, image/png"
fileHolder={profileImageHolder()} fileHolder={profileImageHolder()}
preSet={preSetHolder() || currentUser().image || null} preSet={preSetHolder() || userProfile().image || null}
/> />
<button <button
type="button" type="button"
@@ -603,22 +631,22 @@ export default function AccountPage() {
<div class="flex items-center justify-center text-lg md:justify-normal"> <div class="flex items-center justify-center text-lg md:justify-normal">
<div class="flex flex-col lg:flex-row"> <div class="flex flex-col lg:flex-row">
<div class="pr-1 font-semibold whitespace-nowrap"> <div class="pr-1 font-semibold whitespace-nowrap">
{currentUser().provider === "email" {userProfile().provider === "email"
? "Email:" ? "Email:"
: "Linked Email:"} : "Linked Email:"}
</div> </div>
{currentUser().email ? ( {userProfile().email ? (
<span>{currentUser().email}</span> <span>{userProfile().email}</span>
) : ( ) : (
<span class="font-light italic underline underline-offset-4"> <span class="font-light italic underline underline-offset-4">
{currentUser().provider === "email" {userProfile().provider === "email"
? "None Set" ? "None Set"
: "Not Linked"} : "Not Linked"}
</span> </span>
)} )}
</div> </div>
<Show <Show
when={currentUser().email && !currentUser().emailVerified} when={userProfile().email && !userProfile().emailVerified}
> >
<button <button
onClick={sendEmailVerification} onClick={sendEmailVerification}
@@ -630,6 +658,11 @@ export default function AccountPage() {
</div> </div>
<form onSubmit={setEmailTrigger} class="mx-auto"> <form onSubmit={setEmailTrigger} class="mx-auto">
<noscript>
<div class="text-subtext0 mb-2 px-4 text-center text-xs">
JavaScript required to update email
</div>
</noscript>
<div class="input-group mx-4"> <div class="input-group mx-4">
<input <input
ref={emailRef} ref={emailRef}
@@ -637,21 +670,22 @@ export default function AccountPage() {
required required
disabled={ disabled={
emailButtonLoading() || emailButtonLoading() ||
(currentUser().email !== null && (userProfile().email !== null &&
!currentUser().emailVerified) !userProfile().emailVerified)
} }
placeholder=" " placeholder=" "
title="Please enter a valid email address"
class="underlinedInput bg-transparent" class="underlinedInput bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
<label class="underlinedInputLabel"> <label class="underlinedInputLabel">
{currentUser().email ? "Update Email" : "Add Email"} {userProfile().email ? "Update Email" : "Add Email"}
</label> </label>
</div> </div>
<Show <Show
when={ when={
currentUser().provider !== "email" && userProfile().provider !== "email" &&
!currentUser().email !userProfile().email
} }
> >
<div class="text-subtext0 mt-1 px-4 text-xs"> <div class="text-subtext0 mt-1 px-4 text-xs">
@@ -663,13 +697,13 @@ export default function AccountPage() {
type="submit" type="submit"
disabled={ disabled={
emailButtonLoading() || emailButtonLoading() ||
(currentUser().email !== null && (userProfile().email !== null &&
!currentUser().emailVerified) !userProfile().emailVerified)
} }
class={`${ class={`${
emailButtonLoading() || emailButtonLoading() ||
(currentUser().email !== null && (userProfile().email !== null &&
!currentUser().emailVerified) !userProfile().emailVerified)
? "bg-blue cursor-not-allowed brightness-75" ? "bg-blue cursor-not-allowed brightness-75"
: "bg-blue hover:brightness-125 active:scale-90" : "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`} } mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
@@ -690,8 +724,8 @@ export default function AccountPage() {
<div class="pr-1 font-semibold whitespace-nowrap"> <div class="pr-1 font-semibold whitespace-nowrap">
Display Name: Display Name:
</div> </div>
{currentUser().displayName ? ( {userProfile().displayName ? (
<span>{currentUser().displayName}</span> <span>{userProfile().displayName}</span>
) : ( ) : (
<span class="font-light italic underline underline-offset-4"> <span class="font-light italic underline underline-offset-4">
None Set None Set
@@ -701,6 +735,11 @@ export default function AccountPage() {
</div> </div>
<form onSubmit={setDisplayNameTrigger} class="mx-auto"> <form onSubmit={setDisplayNameTrigger} class="mx-auto">
<noscript>
<div class="text-subtext0 mb-2 px-4 text-center text-xs">
JavaScript required to update display name
</div>
</noscript>
<div class="input-group mx-4"> <div class="input-group mx-4">
<input <input
ref={displayNameRef} ref={displayNameRef}
@@ -708,11 +747,12 @@ export default function AccountPage() {
required required
disabled={displayNameButtonLoading()} disabled={displayNameButtonLoading()}
placeholder=" " placeholder=" "
title="Please enter your display name"
class="underlinedInput bg-transparent" class="underlinedInput bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
<label class="underlinedInputLabel"> <label class="underlinedInputLabel">
Set {currentUser().displayName ? "New " : ""}Display Set {userProfile().displayName ? "New " : ""}Display
Name Name
</label> </label>
</div> </div>
@@ -746,28 +786,36 @@ export default function AccountPage() {
> >
<div class="flex w-full max-w-md flex-col justify-center"> <div class="flex w-full max-w-md flex-col justify-center">
<div class="mb-2 text-center text-xl font-semibold"> <div class="mb-2 text-center text-xl font-semibold">
{currentUser().hasPassword {userProfile().hasPassword
? "Change Password" ? "Change Password"
: "Add Password"} : "Add Password"}
</div> </div>
<Show when={!currentUser().hasPassword}> <noscript>
<div class="text-subtext0 mb-4 text-center text-sm"> <div class="text-subtext0 mb-4 text-center text-sm">
{currentUser().provider === "email" JavaScript required to{" "}
{userProfile().hasPassword ? "change" : "add"} password
</div>
</noscript>
<Show when={!userProfile().hasPassword}>
<div class="text-subtext0 mb-4 text-center text-sm">
{userProfile().provider === "email"
? "Set a password to enable password login" ? "Set a password to enable password login"
: "Add a password to enable email/password login alongside your " + : "Add a password to enable email/password login alongside your " +
getProviderInfo(currentUser().provider).name + getProviderInfo(userProfile().provider).name +
" login"} " login"}
</div> </div>
</Show> </Show>
<Show when={currentUser().hasPassword}> <Show when={userProfile().hasPassword}>
<div class="input-group relative mx-4 mb-6"> <div class="input-group relative mx-4 mb-6">
<input <input
ref={oldPasswordRef} ref={oldPasswordRef}
type={showOldPasswordInput() ? "text" : "password"} type={showOldPasswordInput() ? "text" : "password"}
required required
minlength="8"
disabled={passwordChangeLoading()} disabled={passwordChangeLoading()}
placeholder=" " placeholder=" "
title="Password must be at least 8 characters"
class="underlinedInput w-full bg-transparent pr-10" class="underlinedInput w-full bg-transparent pr-10"
/> />
<span class="bar"></span> <span class="bar"></span>
@@ -794,10 +842,12 @@ export default function AccountPage() {
ref={newPasswordRef} ref={newPasswordRef}
type={showPasswordInput() ? "text" : "password"} type={showPasswordInput() ? "text" : "password"}
required required
minlength="8"
onInput={handleNewPasswordChange} onInput={handleNewPasswordChange}
onBlur={handlePasswordBlur} onBlur={handlePasswordBlur}
disabled={passwordChangeLoading()} disabled={passwordChangeLoading()}
placeholder=" " placeholder=" "
title="Password must be at least 8 characters"
class="underlinedInput w-full bg-transparent pr-10" class="underlinedInput w-full bg-transparent pr-10"
/> />
<span class="bar"></span> <span class="bar"></span>
@@ -826,9 +876,11 @@ export default function AccountPage() {
ref={newPasswordConfRef} ref={newPasswordConfRef}
type={showPasswordConfInput() ? "text" : "password"} type={showPasswordConfInput() ? "text" : "password"}
required required
minlength="8"
onInput={handlePasswordConfChange} onInput={handlePasswordConfChange}
disabled={passwordChangeLoading()} disabled={passwordChangeLoading()}
placeholder=" " placeholder=" "
title="Password must be at least 8 characters"
class="underlinedInput w-full bg-transparent pr-10" class="underlinedInput w-full bg-transparent pr-10"
/> />
<span class="bar"></span> <span class="bar"></span>
@@ -875,7 +927,7 @@ export default function AccountPage() {
<Show when={passwordError()}> <Show when={passwordError()}>
<div class="text-red text-center text-sm"> <div class="text-red text-center text-sm">
{currentUser().hasPassword {userProfile().hasPassword
? "Password did not match record" ? "Password did not match record"
: "Error setting password"} : "Error setting password"}
</div> </div>
@@ -883,7 +935,7 @@ export default function AccountPage() {
<Show when={showPasswordSuccess()}> <Show when={showPasswordSuccess()}>
<div class="text-green text-center text-sm"> <div class="text-green text-center text-sm">
Password {currentUser().hasPassword ? "changed" : "set"}{" "} Password {userProfile().hasPassword ? "changed" : "set"}{" "}
successfully! successfully!
</div> </div>
</Show> </Show>
@@ -892,6 +944,20 @@ export default function AccountPage() {
<hr class="mt-8 mb-8" /> <hr class="mt-8 mb-8" />
{/* Sign Out Section */}
<div class="mx-auto max-w-md py-4">
<form method="post" action="/api/auth/signout">
<button
type="submit"
class="bg-overlay0 hover:bg-overlay1 w-full rounded px-4 py-2 transition-all"
>
Sign Out
</button>
</form>
</div>
<hr class="mt-8 mb-8" />
{/* Delete Account Section */} {/* Delete Account Section */}
<div class="mx-auto max-w-2xl py-8"> <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="bg-red w-full rounded-md px-6 pt-8 pb-4 shadow-md brightness-75">
@@ -903,12 +969,18 @@ export default function AccountPage() {
irreversible irreversible
</div> </div>
<noscript>
<div class="text-crust mb-4 text-center text-sm font-semibold">
JavaScript is required to delete your account
</div>
</noscript>
<Show <Show
when={currentUser().hasPassword} when={userProfile().hasPassword}
fallback={ fallback={
<div class="flex flex-col items-center"> <div class="flex flex-col items-center">
<div class="text-crust mb-4 text-center text-sm"> <div class="text-crust mb-4 text-center text-sm">
Your {getProviderInfo(currentUser().provider).name}{" "} Your {getProviderInfo(userProfile().provider).name}{" "}
account doesn't have a password. To delete your account doesn't have a password. To delete your
account, please set a password first, then return account, please set a password first, then return
here to proceed with deletion. here to proceed with deletion.
@@ -931,8 +1003,10 @@ export default function AccountPage() {
ref={deleteAccountPasswordRef} ref={deleteAccountPasswordRef}
type="password" type="password"
required required
minlength="8"
disabled={deleteAccountButtonLoading()} disabled={deleteAccountButtonLoading()}
placeholder=" " placeholder=" "
title="Enter your password to confirm account deletion"
class="underlinedInput bg-transparent" class="underlinedInput bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>

View File

@@ -0,0 +1,25 @@
import type { APIEvent } from "@solidjs/start/server";
import { getCookie, getEvent, setCookie } from "vinxi/http";
export async function POST() {
"use server";
const event = getEvent()!;
// Clear the userIDToken cookie (the actual session cookie)
setCookie(event, "userIDToken", "", {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: 0, // Expire immediately
expires: new Date(0) // Set expiry to past date
});
// Redirect to home page
return new Response(null, {
status: 302,
headers: {
Location: "/"
}
});
}

View File

@@ -312,10 +312,9 @@ export default function PostPage() {
</div> </div>
{/* Spacer to push content down */} {/* Spacer to push content down */}
<div class="-mt-[10vh] h-80 sm:h-96 md:h-[50vh]" /> <div class="z-10: pt-80 sm:pt-96 md:pt-[50vh]">
{/* Content that slides over the fixed image */} {/* Content that slides over the fixed image */}
<div class="bg-base relative z-40 pb-24"> <div class="bg-base relative pb-24">
<div class="top-4 flex w-full flex-col justify-center md:absolute md:flex-row md:justify-between"> <div class="top-4 flex w-full flex-col justify-center md:absolute md:flex-row md:justify-between">
<div class=""> <div class="">
<div class="flex justify-center italic md:justify-start md:pl-24"> <div class="flex justify-center italic md:justify-start md:pl-24">
@@ -402,6 +401,7 @@ export default function PostPage() {
</div> </div>
</div> </div>
</div> </div>
</div>
</> </>
); );
}} }}

View File

@@ -215,6 +215,7 @@ export default function ContactPage() {
name="name" name="name"
value={user()?.displayName ?? ""} value={user()?.displayName ?? ""}
placeholder=" " placeholder=" "
title="Please enter your name"
class="underlinedInput w-full bg-transparent" class="underlinedInput w-full bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
@@ -227,6 +228,7 @@ export default function ContactPage() {
name="email" name="email"
value={user()?.email ?? ""} value={user()?.email ?? ""}
placeholder=" " placeholder=" "
title="Please enter a valid email address"
class="underlinedInput w-full bg-transparent" class="underlinedInput w-full bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
@@ -239,6 +241,7 @@ export default function ContactPage() {
required required
name="message" name="message"
placeholder=" " placeholder=" "
title="Please enter your message"
class="underlinedInput w-full bg-transparent" class="underlinedInput w-full bg-transparent"
rows={4} rows={4}
/> />

View File

@@ -38,11 +38,13 @@ export default function LoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
// Derive state directly from URL parameters (no signals needed)
const register = () => searchParams.mode === "register";
const usePassword = () => searchParams.auth === "password";
// State management // State management
const [register, setRegister] = createSignal(false);
const [error, setError] = createSignal(""); const [error, setError] = createSignal("");
const [loading, setLoading] = createSignal(false); const [loading, setLoading] = createSignal(false);
const [usePassword, setUsePassword] = createSignal(false);
const [countDown, setCountDown] = createSignal(0); const [countDown, setCountDown] = createSignal(0);
const [emailSent, setEmailSent] = createSignal(false); const [emailSent, setEmailSent] = createSignal(false);
const [showPasswordError, setShowPasswordError] = createSignal(false); const [showPasswordError, setShowPasswordError] = createSignal(false);
@@ -370,29 +372,23 @@ export default function LoginPage() {
fallback={ fallback={
<div class="py-4 text-center md:min-w-118.75"> <div class="py-4 text-center md:min-w-118.75">
Already have an account? Already have an account?
<button <A
onClick={() => { href="/login"
setRegister(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 Login
</button> </A>
</div> </div>
} }
> >
<div class="py-4 text-center md:min-w-118.75"> <div class="py-4 text-center md:min-w-118.75">
Don't have an account yet? Don't have an account yet?
<button <A
onClick={() => { href="/login?mode=register"
setRegister(true);
setUsePassword(false);
}}
class="text-blue pl-1 underline hover:brightness-125" class="text-blue pl-1 underline hover:brightness-125"
> >
Click here to Register Click here to Register
</button> </A>
</div> </div>
</Show> </Show>
@@ -406,6 +402,7 @@ export default function LoginPage() {
required required
ref={emailRef} ref={emailRef}
placeholder=" " placeholder=" "
title="Please enter a valid email address"
class="underlinedInput bg-transparent" class="underlinedInput bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
@@ -425,6 +422,7 @@ export default function LoginPage() {
onInput={register() ? handleNewPasswordChange : undefined} onInput={register() ? handleNewPasswordChange : undefined}
onBlur={register() ? handlePasswordBlur : undefined} onBlur={register() ? handlePasswordBlur : undefined}
placeholder=" " placeholder=" "
title="Password must be at least 8 characters"
class="underlinedInput bg-transparent" class="underlinedInput bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
@@ -478,6 +476,7 @@ export default function LoginPage() {
ref={passwordConfRef} ref={passwordConfRef}
onInput={handlePasswordConfChange} onInput={handlePasswordConfChange}
placeholder=" " placeholder=" "
title="Password must be at least 8 characters and match the password above"
class="underlinedInput bg-transparent" class="underlinedInput bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>
@@ -584,22 +583,20 @@ export default function LoginPage() {
{/* Toggle password/email link */} {/* Toggle password/email link */}
<Show when={!register() && !usePassword()}> <Show when={!register() && !usePassword()}>
<button <A
type="button" href="/login?auth=password"
onClick={() => setUsePassword(true)}
class="hover-underline-animation my-auto ml-2 px-2 text-sm" class="hover-underline-animation my-auto ml-2 px-2 text-sm"
> >
Use Password Use Password
</button> </A>
</Show> </Show>
<Show when={usePassword()}> <Show when={usePassword()}>
<button <A
type="button" href="/login"
onClick={() => setUsePassword(false)}
class="hover-underline-animation my-auto ml-2 px-2 text-sm" class="hover-underline-animation my-auto ml-2 px-2 text-sm"
> >
Use Email Link Use Email Link
</button> </A>
</Show> </Show>
</div> </div>
</form> </form>

View File

@@ -144,6 +144,7 @@ export default function RequestPasswordResetPage() {
required required
disabled={loading()} disabled={loading()}
placeholder=" " placeholder=" "
title="Please enter a valid email address"
class="underlinedInput w-full bg-transparent" class="underlinedInput w-full bg-transparent"
/> />
<span class="bar"></span> <span class="bar"></span>

View File

@@ -63,28 +63,38 @@ function processBlockConditionals(
html: string, html: string,
context: ConditionalContext context: ConditionalContext
): string { ): string {
// Regex to match conditional blocks // More flexible regex that handles attributes in any order
// Matches: <div class="conditional-block" data-condition-type="..." data-condition-value="..." data-show-when="...">...</div> // Match div with class="conditional-block" and capture the full tag
const conditionalRegex = const divRegex =
/<div\s+[^>]*class="[^"]*conditional-block[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/div>/gi; /<div\s+([^>]*class="[^"]*conditional-block[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi;
let processedHtml = html; let processedHtml = html;
let match: RegExpExecArray | null; let match: RegExpExecArray | null;
// Reset regex lastIndex // Reset regex lastIndex
conditionalRegex.lastIndex = 0; divRegex.lastIndex = 0;
// Collect all matches first to avoid regex state issues // Collect all matches first to avoid regex state issues
const matches: ConditionalBlock[] = []; const matches: ConditionalBlock[] = [];
while ((match = conditionalRegex.exec(html)) !== null) { while ((match = divRegex.exec(html)) !== null) {
const attributes = match[1];
const content = match[2];
// Extract individual attributes
const typeMatch = /data-condition-type="([^"]+)"/.exec(attributes);
const valueMatch = /data-condition-value="([^"]+)"/.exec(attributes);
const showWhenMatch = /data-show-when="(true|false)"/.exec(attributes);
if (typeMatch && valueMatch && showWhenMatch) {
matches.push({ matches.push({
fullMatch: match[0], fullMatch: match[0],
conditionType: match[1], conditionType: typeMatch[1],
conditionValue: match[2], conditionValue: valueMatch[1],
showWhen: match[3], showWhen: showWhenMatch[1],
content: match[4] content: content
}); });
} }
}
// Process each conditional block // Process each conditional block
for (const block of matches) { for (const block of matches) {
@@ -120,28 +130,38 @@ function processInlineConditionals(
html: string, html: string,
context: ConditionalContext context: ConditionalContext
): string { ): string {
// Regex to match inline conditionals // More flexible regex that handles attributes in any order
// Matches: <span class="conditional-inline" data-condition-type="..." data-condition-value="..." data-show-when="...">...</span> // Match span with class="conditional-inline" and capture the full tag
const inlineRegex = const spanRegex =
/<span\s+[^>]*class="[^"]*conditional-inline[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/span>/gi; /<span\s+([^>]*class="[^"]*conditional-inline[^"]*"[^>]*)>([\s\S]*?)<\/span>/gi;
let processedHtml = html; let processedHtml = html;
let match: RegExpExecArray | null; let match: RegExpExecArray | null;
// Reset regex lastIndex // Reset regex lastIndex
inlineRegex.lastIndex = 0; spanRegex.lastIndex = 0;
// Collect all matches first // Collect all matches first
const matches: ConditionalBlock[] = []; const matches: ConditionalBlock[] = [];
while ((match = inlineRegex.exec(html)) !== null) { while ((match = spanRegex.exec(html)) !== null) {
const attributes = match[1];
const content = match[2];
// Extract individual attributes
const typeMatch = /data-condition-type="([^"]+)"/.exec(attributes);
const valueMatch = /data-condition-value="([^"]+)"/.exec(attributes);
const showWhenMatch = /data-show-when="(true|false)"/.exec(attributes);
if (typeMatch && valueMatch && showWhenMatch) {
matches.push({ matches.push({
fullMatch: match[0], fullMatch: match[0],
conditionType: match[1], conditionType: typeMatch[1],
conditionValue: match[2], conditionValue: valueMatch[1],
showWhen: match[3], showWhen: showWhenMatch[1],
content: match[4] content: content
}); });
} }
}
// Process each inline conditional // Process each inline conditional
for (const inline of matches) { for (const inline of matches) {