good enough
This commit is contained in:
@@ -309,7 +309,10 @@ export function LeftBar() {
|
||||
|
||||
return (
|
||||
<nav
|
||||
id="navigation"
|
||||
tabindex="-1"
|
||||
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%]"
|
||||
classList={{
|
||||
"-translate-x-full": !leftBarVisible(),
|
||||
@@ -512,8 +515,9 @@ export function RightBar() {
|
||||
});
|
||||
|
||||
return (
|
||||
<nav
|
||||
<aside
|
||||
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%]"
|
||||
classList={{
|
||||
"translate-x-full": !rightBarVisible(),
|
||||
@@ -528,6 +532,6 @@ export function RightBar() {
|
||||
}}
|
||||
>
|
||||
<RightBarContent />
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export default function Card(props: CardProps) {
|
||||
</div>
|
||||
</Show>
|
||||
<img
|
||||
src={props.post.banner_photo ? props.post.banner_photo : "/bitcoin.jpg"}
|
||||
src={props.post.banner_photo ?? ""}
|
||||
alt={props.post.title.replaceAll("_", " ") + " banner"}
|
||||
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 { useNavigate, redirect, query } from "@solidjs/router";
|
||||
import { useNavigate, redirect, query, createAsync } from "@solidjs/router";
|
||||
import { getEvent } from "vinxi/http";
|
||||
import Eye from "~/components/icons/Eye";
|
||||
import EyeSlash from "~/components/icons/EyeSlash";
|
||||
@@ -23,29 +23,56 @@ type UserProfile = {
|
||||
hasPassword: boolean;
|
||||
};
|
||||
|
||||
const checkAuth = query(async () => {
|
||||
const getUserProfile = query(async (): Promise<UserProfile | null> => {
|
||||
"use server";
|
||||
const { checkAuthStatus } = await import("~/server/utils");
|
||||
const { getUserID, ConnectionFactory } = await import("~/server/utils");
|
||||
const event = getEvent()!;
|
||||
const { isAuthenticated } = await checkAuthStatus(event);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
const userId = await getUserID(event);
|
||||
if (!userId) {
|
||||
throw redirect("/login");
|
||||
}
|
||||
|
||||
return { isAuthenticated };
|
||||
}, "accountAuthCheck");
|
||||
const conn = ConnectionFactory();
|
||||
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 = {
|
||||
load: () => checkAuth()
|
||||
load: () => getUserProfile()
|
||||
};
|
||||
|
||||
export default function AccountPage() {
|
||||
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 [loading, setLoading] = createSignal(true);
|
||||
|
||||
// Form loading states
|
||||
const [emailButtonLoading, setEmailButtonLoading] = createSignal(false);
|
||||
@@ -98,27 +125,14 @@ export default function AccountPage() {
|
||||
let displayNameRef: HTMLInputElement | undefined;
|
||||
let deleteAccountPasswordRef: HTMLInputElement | undefined;
|
||||
|
||||
// Fetch user profile on mount
|
||||
onMount(async () => {
|
||||
try {
|
||||
const response = await fetch("/api/trpc/user.getProfile", {
|
||||
method: "GET"
|
||||
});
|
||||
// Helper to get current user (from SSR data or local state)
|
||||
const currentUser = () => user() || userData();
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
if (result.result?.data) {
|
||||
setUser(result.result.data);
|
||||
// Set preset holder if user has existing image
|
||||
if (result.result.data.image) {
|
||||
setPreSetHolder(result.result.data.image);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch user profile:", err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
// Initialize preSetHolder when userData loads
|
||||
createEffect(() => {
|
||||
const userProfile = userData();
|
||||
if (userProfile?.image && !preSetHolder()) {
|
||||
setPreSetHolder(userProfile.image);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -152,8 +166,8 @@ export default function AccountPage() {
|
||||
setProfileImageSetLoading(true);
|
||||
setShowImageSuccess(false);
|
||||
|
||||
const currentUser = user();
|
||||
if (!currentUser) {
|
||||
const userProfile = currentUser();
|
||||
if (!userProfile) {
|
||||
setProfileImageSetLoading(false);
|
||||
return;
|
||||
}
|
||||
@@ -161,17 +175,15 @@ export default function AccountPage() {
|
||||
try {
|
||||
let imageUrl = "";
|
||||
|
||||
// Upload new image if one was selected
|
||||
if (profileImage()) {
|
||||
const imageKey = await AddImageToS3(
|
||||
profileImage()!,
|
||||
currentUser.id,
|
||||
userProfile.id,
|
||||
"user"
|
||||
);
|
||||
imageUrl = imageKey || "";
|
||||
}
|
||||
|
||||
// Update user profile image
|
||||
const response = await fetch("/api/trpc/user.updateProfileImage", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -264,10 +276,10 @@ export default function AccountPage() {
|
||||
// Password change/set handler
|
||||
const handlePasswordSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
const currentUser = user();
|
||||
if (!currentUser) return;
|
||||
const userProfile = currentUser();
|
||||
if (!userProfile) return;
|
||||
|
||||
if (currentUser.hasPassword) {
|
||||
if (userProfile.hasPassword) {
|
||||
// Change password (requires old password)
|
||||
if (!oldPasswordRef || !newPasswordRef || !newPasswordConfRef) return;
|
||||
|
||||
@@ -399,14 +411,14 @@ export default function AccountPage() {
|
||||
|
||||
// Resend email verification
|
||||
const sendEmailVerification = async () => {
|
||||
const currentUser = user();
|
||||
if (!currentUser?.email) return;
|
||||
const userProfile = currentUser();
|
||||
if (!userProfile?.email) return;
|
||||
|
||||
try {
|
||||
await fetch("/api/trpc/auth.resendEmailVerification", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email: currentUser.email })
|
||||
body: JSON.stringify({ email: userProfile.email })
|
||||
});
|
||||
alert("Verification email sent!");
|
||||
} 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">
|
||||
<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">
|
||||
<Show when={!loading() && user()} fallback={<TerminalSplash />}>
|
||||
{(currentUser) => (
|
||||
<Show when={currentUser()} fallback={<TerminalSplash />}>
|
||||
{(userProfile) => (
|
||||
<>
|
||||
<div class="text-text mb-8 text-center text-3xl font-bold">
|
||||
Account Settings
|
||||
@@ -512,18 +535,18 @@ export default function AccountPage() {
|
||||
</div>
|
||||
<div class="flex items-center justify-center gap-3">
|
||||
<span
|
||||
class={getProviderInfo(currentUser().provider).color}
|
||||
class={getProviderInfo(userProfile().provider).color}
|
||||
>
|
||||
{getProviderInfo(currentUser().provider).icon}
|
||||
{getProviderInfo(userProfile().provider).icon}
|
||||
</span>
|
||||
<span class="text-lg font-semibold">
|
||||
{getProviderInfo(currentUser().provider).name} Account
|
||||
{getProviderInfo(userProfile().provider).name} Account
|
||||
</span>
|
||||
</div>
|
||||
<Show
|
||||
when={
|
||||
currentUser().provider !== "email" &&
|
||||
!currentUser().email
|
||||
userProfile().provider !== "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">
|
||||
@@ -532,8 +555,8 @@ export default function AccountPage() {
|
||||
</Show>
|
||||
<Show
|
||||
when={
|
||||
currentUser().provider !== "email" &&
|
||||
!currentUser().hasPassword
|
||||
userProfile().provider !== "email" &&
|
||||
!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">
|
||||
@@ -551,12 +574,17 @@ export default function AccountPage() {
|
||||
<div class="mb-2 text-center text-lg font-semibold">
|
||||
Profile Image
|
||||
</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
|
||||
onDrop={handleImageDrop}
|
||||
acceptedFiles="image/jpg, image/jpeg, image/png"
|
||||
fileHolder={profileImageHolder()}
|
||||
preSet={preSetHolder() || currentUser().image || null}
|
||||
preSet={preSetHolder() || userProfile().image || null}
|
||||
/>
|
||||
<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 flex-col lg:flex-row">
|
||||
<div class="pr-1 font-semibold whitespace-nowrap">
|
||||
{currentUser().provider === "email"
|
||||
{userProfile().provider === "email"
|
||||
? "Email:"
|
||||
: "Linked Email:"}
|
||||
</div>
|
||||
{currentUser().email ? (
|
||||
<span>{currentUser().email}</span>
|
||||
{userProfile().email ? (
|
||||
<span>{userProfile().email}</span>
|
||||
) : (
|
||||
<span class="font-light italic underline underline-offset-4">
|
||||
{currentUser().provider === "email"
|
||||
{userProfile().provider === "email"
|
||||
? "None Set"
|
||||
: "Not Linked"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Show
|
||||
when={currentUser().email && !currentUser().emailVerified}
|
||||
when={userProfile().email && !userProfile().emailVerified}
|
||||
>
|
||||
<button
|
||||
onClick={sendEmailVerification}
|
||||
@@ -630,6 +658,11 @@ export default function AccountPage() {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<input
|
||||
ref={emailRef}
|
||||
@@ -637,21 +670,22 @@ export default function AccountPage() {
|
||||
required
|
||||
disabled={
|
||||
emailButtonLoading() ||
|
||||
(currentUser().email !== null &&
|
||||
!currentUser().emailVerified)
|
||||
(userProfile().email !== null &&
|
||||
!userProfile().emailVerified)
|
||||
}
|
||||
placeholder=" "
|
||||
title="Please enter a valid email address"
|
||||
class="underlinedInput bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
<label class="underlinedInputLabel">
|
||||
{currentUser().email ? "Update Email" : "Add Email"}
|
||||
{userProfile().email ? "Update Email" : "Add Email"}
|
||||
</label>
|
||||
</div>
|
||||
<Show
|
||||
when={
|
||||
currentUser().provider !== "email" &&
|
||||
!currentUser().email
|
||||
userProfile().provider !== "email" &&
|
||||
!userProfile().email
|
||||
}
|
||||
>
|
||||
<div class="text-subtext0 mt-1 px-4 text-xs">
|
||||
@@ -663,13 +697,13 @@ export default function AccountPage() {
|
||||
type="submit"
|
||||
disabled={
|
||||
emailButtonLoading() ||
|
||||
(currentUser().email !== null &&
|
||||
!currentUser().emailVerified)
|
||||
(userProfile().email !== null &&
|
||||
!userProfile().emailVerified)
|
||||
}
|
||||
class={`${
|
||||
emailButtonLoading() ||
|
||||
(currentUser().email !== null &&
|
||||
!currentUser().emailVerified)
|
||||
(userProfile().email !== null &&
|
||||
!userProfile().emailVerified)
|
||||
? "bg-blue cursor-not-allowed brightness-75"
|
||||
: "bg-blue hover:brightness-125 active:scale-90"
|
||||
} mt-2 flex justify-center rounded px-4 py-2 text-base transition-all duration-300 ease-out`}
|
||||
@@ -690,8 +724,8 @@ export default function AccountPage() {
|
||||
<div class="pr-1 font-semibold whitespace-nowrap">
|
||||
Display Name:
|
||||
</div>
|
||||
{currentUser().displayName ? (
|
||||
<span>{currentUser().displayName}</span>
|
||||
{userProfile().displayName ? (
|
||||
<span>{userProfile().displayName}</span>
|
||||
) : (
|
||||
<span class="font-light italic underline underline-offset-4">
|
||||
None Set
|
||||
@@ -701,6 +735,11 @@ export default function AccountPage() {
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<input
|
||||
ref={displayNameRef}
|
||||
@@ -708,11 +747,12 @@ export default function AccountPage() {
|
||||
required
|
||||
disabled={displayNameButtonLoading()}
|
||||
placeholder=" "
|
||||
title="Please enter your display name"
|
||||
class="underlinedInput bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
<label class="underlinedInputLabel">
|
||||
Set {currentUser().displayName ? "New " : ""}Display
|
||||
Set {userProfile().displayName ? "New " : ""}Display
|
||||
Name
|
||||
</label>
|
||||
</div>
|
||||
@@ -746,28 +786,36 @@ export default function AccountPage() {
|
||||
>
|
||||
<div class="flex w-full max-w-md flex-col justify-center">
|
||||
<div class="mb-2 text-center text-xl font-semibold">
|
||||
{currentUser().hasPassword
|
||||
{userProfile().hasPassword
|
||||
? "Change Password"
|
||||
: "Add Password"}
|
||||
</div>
|
||||
<Show when={!currentUser().hasPassword}>
|
||||
<noscript>
|
||||
<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"
|
||||
: "Add a password to enable email/password login alongside your " +
|
||||
getProviderInfo(currentUser().provider).name +
|
||||
getProviderInfo(userProfile().provider).name +
|
||||
" login"}
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={currentUser().hasPassword}>
|
||||
<Show when={userProfile().hasPassword}>
|
||||
<div class="input-group relative mx-4 mb-6">
|
||||
<input
|
||||
ref={oldPasswordRef}
|
||||
type={showOldPasswordInput() ? "text" : "password"}
|
||||
required
|
||||
minlength="8"
|
||||
disabled={passwordChangeLoading()}
|
||||
placeholder=" "
|
||||
title="Password must be at least 8 characters"
|
||||
class="underlinedInput w-full bg-transparent pr-10"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
@@ -794,10 +842,12 @@ export default function AccountPage() {
|
||||
ref={newPasswordRef}
|
||||
type={showPasswordInput() ? "text" : "password"}
|
||||
required
|
||||
minlength="8"
|
||||
onInput={handleNewPasswordChange}
|
||||
onBlur={handlePasswordBlur}
|
||||
disabled={passwordChangeLoading()}
|
||||
placeholder=" "
|
||||
title="Password must be at least 8 characters"
|
||||
class="underlinedInput w-full bg-transparent pr-10"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
@@ -826,9 +876,11 @@ export default function AccountPage() {
|
||||
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>
|
||||
@@ -875,7 +927,7 @@ export default function AccountPage() {
|
||||
|
||||
<Show when={passwordError()}>
|
||||
<div class="text-red text-center text-sm">
|
||||
{currentUser().hasPassword
|
||||
{userProfile().hasPassword
|
||||
? "Password did not match record"
|
||||
: "Error setting password"}
|
||||
</div>
|
||||
@@ -883,7 +935,7 @@ export default function AccountPage() {
|
||||
|
||||
<Show when={showPasswordSuccess()}>
|
||||
<div class="text-green text-center text-sm">
|
||||
Password {currentUser().hasPassword ? "changed" : "set"}{" "}
|
||||
Password {userProfile().hasPassword ? "changed" : "set"}{" "}
|
||||
successfully!
|
||||
</div>
|
||||
</Show>
|
||||
@@ -892,6 +944,20 @@ export default function AccountPage() {
|
||||
|
||||
<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 */}
|
||||
<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">
|
||||
@@ -903,12 +969,18 @@ export default function AccountPage() {
|
||||
irreversible
|
||||
</div>
|
||||
|
||||
<noscript>
|
||||
<div class="text-crust mb-4 text-center text-sm font-semibold">
|
||||
JavaScript is required to delete your account
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<Show
|
||||
when={currentUser().hasPassword}
|
||||
when={userProfile().hasPassword}
|
||||
fallback={
|
||||
<div class="flex flex-col items-center">
|
||||
<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, please set a password first, then return
|
||||
here to proceed with deletion.
|
||||
@@ -931,8 +1003,10 @@ export default function AccountPage() {
|
||||
ref={deleteAccountPasswordRef}
|
||||
type="password"
|
||||
required
|
||||
minlength="8"
|
||||
disabled={deleteAccountButtonLoading()}
|
||||
placeholder=" "
|
||||
title="Enter your password to confirm account deletion"
|
||||
class="underlinedInput bg-transparent"
|
||||
/>
|
||||
<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,93 +312,93 @@ export default function PostPage() {
|
||||
</div>
|
||||
|
||||
{/* Spacer to push content down */}
|
||||
<div class="-mt-[10vh] h-80 sm:h-96 md:h-[50vh]" />
|
||||
|
||||
{/* Content that slides over the fixed image */}
|
||||
<div class="bg-base relative z-40 pb-24">
|
||||
<div class="top-4 flex w-full flex-col justify-center md:absolute md:flex-row md:justify-between">
|
||||
<div class="">
|
||||
<div class="flex justify-center italic md:justify-start md:pl-24">
|
||||
<div>
|
||||
Written {new Date(p().date).toDateString()}
|
||||
<br />
|
||||
By Michael Freno
|
||||
<div class="z-10: pt-80 sm:pt-96 md:pt-[50vh]">
|
||||
{/* Content that slides over the fixed image */}
|
||||
<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="">
|
||||
<div class="flex justify-center italic md:justify-start md:pl-24">
|
||||
<div>
|
||||
Written {new Date(p().date).toDateString()}
|
||||
<br />
|
||||
By Michael Freno
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex max-w-105 flex-wrap justify-center italic md:justify-start md:pl-24">
|
||||
<For each={postData.tags as any[]}>
|
||||
{(tag) => (
|
||||
<div class="group relative m-1 h-fit w-fit rounded-xl bg-purple-600 px-2 py-1 text-sm">
|
||||
<div class="text-white">{tag.value}</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex max-w-105 flex-wrap justify-center italic md:justify-start md:pl-24">
|
||||
<For each={postData.tags as any[]}>
|
||||
{(tag) => (
|
||||
<div class="group relative m-1 h-fit w-fit rounded-xl bg-purple-600 px-2 py-1 text-sm">
|
||||
<div class="text-white">{tag.value}</div>
|
||||
|
||||
<div class="flex flex-row justify-center pt-4 md:pt-0 md:pr-8">
|
||||
<a href="#comments" class="mx-2">
|
||||
<div class="tooltip flex flex-col">
|
||||
<div class="mx-auto hover:brightness-125">
|
||||
<CommentIcon
|
||||
strokeWidth={1}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-text my-auto pt-0.5 pl-2 text-sm">
|
||||
{postData.comments.length}{" "}
|
||||
{postData.comments.length === 1
|
||||
? "Comment"
|
||||
: "Comments"}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="flex flex-row justify-center pt-4 md:pt-0 md:pr-8">
|
||||
<a href="#comments" class="mx-2">
|
||||
<div class="tooltip flex flex-col">
|
||||
<div class="mx-auto hover:brightness-125">
|
||||
<CommentIcon
|
||||
strokeWidth={1}
|
||||
height={32}
|
||||
width={32}
|
||||
/>
|
||||
</div>
|
||||
<div class="text-text my-auto pt-0.5 pl-2 text-sm">
|
||||
{postData.comments.length}{" "}
|
||||
{postData.comments.length === 1
|
||||
? "Comment"
|
||||
: "Comments"}
|
||||
</div>
|
||||
<div class="mx-2">
|
||||
<SessionDependantLike
|
||||
currentUserID={postData.userID}
|
||||
privilegeLevel={postData.privilegeLevel}
|
||||
likes={postData.likes as any[]}
|
||||
projectID={p().id}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="mx-2">
|
||||
<SessionDependantLike
|
||||
currentUserID={postData.userID}
|
||||
privilegeLevel={postData.privilegeLevel}
|
||||
likes={postData.likes as any[]}
|
||||
projectID={p().id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Post body */}
|
||||
<PostBodyClient
|
||||
body={p().body}
|
||||
hasCodeBlock={hasCodeBlock(p().body)}
|
||||
/>
|
||||
|
||||
<Show when={postData.privilegeLevel === "admin"}>
|
||||
<div class="flex justify-center">
|
||||
<A
|
||||
class="border-blue bg-blue z-100 h-fit rounded border px-4 py-2 text-base shadow-md transition-all duration-300 ease-in-out hover:brightness-125 active:scale-90"
|
||||
href={`/blog/edit/${p().id}`}
|
||||
>
|
||||
Edit
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Comments section */}
|
||||
<div
|
||||
id="comments"
|
||||
class="mx-4 pt-12 pb-12 md:mx-8 lg:mx-12"
|
||||
>
|
||||
<CommentSectionWrapper
|
||||
privilegeLevel={postData.privilegeLevel}
|
||||
allComments={postData.comments as Comment[]}
|
||||
topLevelComments={
|
||||
postData.topLevelComments as Comment[]
|
||||
}
|
||||
id={p().id}
|
||||
reactionMap={reactionMap}
|
||||
currentUserID={postData.userID || ""}
|
||||
userCommentMap={userCommentMap}
|
||||
{/* Post body */}
|
||||
<PostBodyClient
|
||||
body={p().body}
|
||||
hasCodeBlock={hasCodeBlock(p().body)}
|
||||
/>
|
||||
|
||||
<Show when={postData.privilegeLevel === "admin"}>
|
||||
<div class="flex justify-center">
|
||||
<A
|
||||
class="border-blue bg-blue z-100 h-fit rounded border px-4 py-2 text-base shadow-md transition-all duration-300 ease-in-out hover:brightness-125 active:scale-90"
|
||||
href={`/blog/edit/${p().id}`}
|
||||
>
|
||||
Edit
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Comments section */}
|
||||
<div
|
||||
id="comments"
|
||||
class="mx-4 pt-12 pb-12 md:mx-8 lg:mx-12"
|
||||
>
|
||||
<CommentSectionWrapper
|
||||
privilegeLevel={postData.privilegeLevel}
|
||||
allComments={postData.comments as Comment[]}
|
||||
topLevelComments={
|
||||
postData.topLevelComments as Comment[]
|
||||
}
|
||||
id={p().id}
|
||||
reactionMap={reactionMap}
|
||||
currentUserID={postData.userID || ""}
|
||||
userCommentMap={userCommentMap}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -215,6 +215,7 @@ export default function ContactPage() {
|
||||
name="name"
|
||||
value={user()?.displayName ?? ""}
|
||||
placeholder=" "
|
||||
title="Please enter your name"
|
||||
class="underlinedInput w-full bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
@@ -227,6 +228,7 @@ export default function ContactPage() {
|
||||
name="email"
|
||||
value={user()?.email ?? ""}
|
||||
placeholder=" "
|
||||
title="Please enter a valid email address"
|
||||
class="underlinedInput w-full bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
@@ -239,6 +241,7 @@ export default function ContactPage() {
|
||||
required
|
||||
name="message"
|
||||
placeholder=" "
|
||||
title="Please enter your message"
|
||||
class="underlinedInput w-full bg-transparent"
|
||||
rows={4}
|
||||
/>
|
||||
|
||||
@@ -38,11 +38,13 @@ export default function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
// Derive state directly from URL parameters (no signals needed)
|
||||
const register = () => searchParams.mode === "register";
|
||||
const usePassword = () => searchParams.auth === "password";
|
||||
|
||||
// State management
|
||||
const [register, setRegister] = createSignal(false);
|
||||
const [error, setError] = createSignal("");
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [usePassword, setUsePassword] = createSignal(false);
|
||||
const [countDown, setCountDown] = createSignal(0);
|
||||
const [emailSent, setEmailSent] = createSignal(false);
|
||||
const [showPasswordError, setShowPasswordError] = createSignal(false);
|
||||
@@ -370,29 +372,23 @@ export default function LoginPage() {
|
||||
fallback={
|
||||
<div class="py-4 text-center md:min-w-118.75">
|
||||
Already have an account?
|
||||
<button
|
||||
onClick={() => {
|
||||
setRegister(false);
|
||||
setUsePassword(false);
|
||||
}}
|
||||
<A
|
||||
href="/login"
|
||||
class="text-blue pl-1 underline hover:brightness-125"
|
||||
>
|
||||
Click here to Login
|
||||
</button>
|
||||
</A>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div class="py-4 text-center md:min-w-118.75">
|
||||
Don't have an account yet?
|
||||
<button
|
||||
onClick={() => {
|
||||
setRegister(true);
|
||||
setUsePassword(false);
|
||||
}}
|
||||
<A
|
||||
href="/login?mode=register"
|
||||
class="text-blue pl-1 underline hover:brightness-125"
|
||||
>
|
||||
Click here to Register
|
||||
</button>
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -406,6 +402,7 @@ export default function LoginPage() {
|
||||
required
|
||||
ref={emailRef}
|
||||
placeholder=" "
|
||||
title="Please enter a valid email address"
|
||||
class="underlinedInput bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
@@ -425,6 +422,7 @@ export default function LoginPage() {
|
||||
onInput={register() ? handleNewPasswordChange : undefined}
|
||||
onBlur={register() ? handlePasswordBlur : undefined}
|
||||
placeholder=" "
|
||||
title="Password must be at least 8 characters"
|
||||
class="underlinedInput bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
@@ -478,6 +476,7 @@ export default function LoginPage() {
|
||||
ref={passwordConfRef}
|
||||
onInput={handlePasswordConfChange}
|
||||
placeholder=" "
|
||||
title="Password must be at least 8 characters and match the password above"
|
||||
class="underlinedInput bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
@@ -584,22 +583,20 @@ export default function LoginPage() {
|
||||
|
||||
{/* Toggle password/email link */}
|
||||
<Show when={!register() && !usePassword()}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUsePassword(true)}
|
||||
<A
|
||||
href="/login?auth=password"
|
||||
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
|
||||
>
|
||||
Use Password
|
||||
</button>
|
||||
</A>
|
||||
</Show>
|
||||
<Show when={usePassword()}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUsePassword(false)}
|
||||
<A
|
||||
href="/login"
|
||||
class="hover-underline-animation my-auto ml-2 px-2 text-sm"
|
||||
>
|
||||
Use Email Link
|
||||
</button>
|
||||
</A>
|
||||
</Show>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -144,6 +144,7 @@ export default function RequestPasswordResetPage() {
|
||||
required
|
||||
disabled={loading()}
|
||||
placeholder=" "
|
||||
title="Please enter a valid email address"
|
||||
class="underlinedInput w-full bg-transparent"
|
||||
/>
|
||||
<span class="bar"></span>
|
||||
|
||||
@@ -63,27 +63,37 @@ function processBlockConditionals(
|
||||
html: string,
|
||||
context: ConditionalContext
|
||||
): string {
|
||||
// Regex to match conditional blocks
|
||||
// Matches: <div class="conditional-block" data-condition-type="..." data-condition-value="..." data-show-when="...">...</div>
|
||||
const conditionalRegex =
|
||||
/<div\s+[^>]*class="[^"]*conditional-block[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/div>/gi;
|
||||
// More flexible regex that handles attributes in any order
|
||||
// Match div with class="conditional-block" and capture the full tag
|
||||
const divRegex =
|
||||
/<div\s+([^>]*class="[^"]*conditional-block[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi;
|
||||
|
||||
let processedHtml = html;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
// Reset regex lastIndex
|
||||
conditionalRegex.lastIndex = 0;
|
||||
divRegex.lastIndex = 0;
|
||||
|
||||
// Collect all matches first to avoid regex state issues
|
||||
const matches: ConditionalBlock[] = [];
|
||||
while ((match = conditionalRegex.exec(html)) !== null) {
|
||||
matches.push({
|
||||
fullMatch: match[0],
|
||||
conditionType: match[1],
|
||||
conditionValue: match[2],
|
||||
showWhen: match[3],
|
||||
content: match[4]
|
||||
});
|
||||
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({
|
||||
fullMatch: match[0],
|
||||
conditionType: typeMatch[1],
|
||||
conditionValue: valueMatch[1],
|
||||
showWhen: showWhenMatch[1],
|
||||
content: content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process each conditional block
|
||||
@@ -120,27 +130,37 @@ function processInlineConditionals(
|
||||
html: string,
|
||||
context: ConditionalContext
|
||||
): string {
|
||||
// Regex to match inline conditionals
|
||||
// Matches: <span class="conditional-inline" data-condition-type="..." data-condition-value="..." data-show-when="...">...</span>
|
||||
const inlineRegex =
|
||||
/<span\s+[^>]*class="[^"]*conditional-inline[^"]*"[^>]*data-condition-type="([^"]+)"[^>]*data-condition-value="([^"]+)"[^>]*data-show-when="(true|false)"[^>]*>([\s\S]*?)<\/span>/gi;
|
||||
// More flexible regex that handles attributes in any order
|
||||
// Match span with class="conditional-inline" and capture the full tag
|
||||
const spanRegex =
|
||||
/<span\s+([^>]*class="[^"]*conditional-inline[^"]*"[^>]*)>([\s\S]*?)<\/span>/gi;
|
||||
|
||||
let processedHtml = html;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
// Reset regex lastIndex
|
||||
inlineRegex.lastIndex = 0;
|
||||
spanRegex.lastIndex = 0;
|
||||
|
||||
// Collect all matches first
|
||||
const matches: ConditionalBlock[] = [];
|
||||
while ((match = inlineRegex.exec(html)) !== null) {
|
||||
matches.push({
|
||||
fullMatch: match[0],
|
||||
conditionType: match[1],
|
||||
conditionValue: match[2],
|
||||
showWhen: match[3],
|
||||
content: match[4]
|
||||
});
|
||||
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({
|
||||
fullMatch: match[0],
|
||||
conditionType: typeMatch[1],
|
||||
conditionValue: valueMatch[1],
|
||||
showWhen: showWhenMatch[1],
|
||||
content: content
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process each inline conditional
|
||||
|
||||
Reference in New Issue
Block a user