good enough
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
25
src/routes/api/auth/signout.ts
Normal file
25
src/routes/api/auth/signout.ts
Normal 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: "/"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user