meta: task ref cleanup
This commit is contained in:
@@ -89,7 +89,7 @@ function getGtActivityPromise(): Promise<ContributionDay[]> {
|
||||
.catch(() => []));
|
||||
}
|
||||
|
||||
// ── Subdomain nav rendering (task 04) ─────────────────────────────────────
|
||||
// ── Subdomain nav rendering ──────────────────────────────────────────────
|
||||
//
|
||||
// The main site retains its bespoke LeftBar / RightBarContent rendering
|
||||
// unchanged (Recent Posts, auth-aware Account/Login/SignOut, admin links,
|
||||
@@ -578,170 +578,170 @@ function MainLeftBarContent() {
|
||||
<div class="text-text flex flex-1 flex-col px-4 pb-4 text-xl font-bold">
|
||||
<div class="flex flex-col py-8">
|
||||
<span class="text-lg font-semibold">Recent Posts</span>
|
||||
<div class="flex max-h-[50dvh] flex-col gap-3 pt-4">
|
||||
<Show
|
||||
when={recentPosts()}
|
||||
fallback={
|
||||
<For each={[1, 2, 3]}>
|
||||
{() => (
|
||||
<div class="flex w-52 flex-col">
|
||||
<div class="relative overflow-hidden">
|
||||
<SkeletonBox class="float-right ml-2 h-12 w-16" />
|
||||
<div class="flex flex-col">
|
||||
<SkeletonText class="h-6 w-full" />
|
||||
<SkeletonText class="mt-1.5 h-6 w-2/3" />
|
||||
<div class="flex max-h-[50dvh] flex-col gap-3 pt-4">
|
||||
<Show
|
||||
when={recentPosts()}
|
||||
fallback={
|
||||
<For each={[1, 2, 3]}>
|
||||
{() => (
|
||||
<div class="flex w-52 flex-col">
|
||||
<div class="relative overflow-hidden">
|
||||
<SkeletonBox class="float-right ml-2 h-12 w-16" />
|
||||
<div class="flex flex-col">
|
||||
<SkeletonText class="h-6 w-full" />
|
||||
<SkeletonText class="mt-1.5 h-6 w-2/3" />
|
||||
</div>
|
||||
</div>
|
||||
<SkeletonText class="mt-1.5 h-6 w-40" />
|
||||
<SkeletonText class="mt-1.5 h-4 w-1/2" />
|
||||
</div>
|
||||
<SkeletonText class="mt-1.5 h-6 w-40" />
|
||||
<SkeletonText class="mt-1.5 h-4 w-1/2" />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
}
|
||||
>
|
||||
<For each={recentPosts()}>
|
||||
{(post) => (
|
||||
<a
|
||||
href={`/blog/${post.title}`}
|
||||
onClick={handleLinkClick}
|
||||
class="hover:text-subtext0 block w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105 hover:font-bold"
|
||||
>
|
||||
<Typewriter class="flex flex-col" keepAlive={false}>
|
||||
<div class="relative overflow-hidden">
|
||||
<img
|
||||
src={getThumbnailUrl(post.banner_photo)}
|
||||
alt="post-cover"
|
||||
class="float-right mb-1 ml-2 h-12 w-16 rounded object-cover"
|
||||
onError={(e) => {
|
||||
const img = e.currentTarget;
|
||||
if (
|
||||
img.src !==
|
||||
(post.banner_photo || "/blueprint.jpg")
|
||||
) {
|
||||
img.src = post.banner_photo || "/blueprint.jpg";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span class="inline wrap-break-word hyphens-auto">
|
||||
{insertSoftHyphens(post.title.replace(/_/g, " "))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span class="text-subtext0 clear-both text-sm">
|
||||
{new Date(post.date).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric"
|
||||
})}
|
||||
</span>
|
||||
</Typewriter>
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
}
|
||||
>
|
||||
<For each={recentPosts()}>
|
||||
{(post) => (
|
||||
<a
|
||||
href={`/blog/${post.title}`}
|
||||
onClick={handleLinkClick}
|
||||
class="hover:text-subtext0 block w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-105 hover:font-bold"
|
||||
>
|
||||
<Typewriter class="flex flex-col" keepAlive={false}>
|
||||
<div class="relative overflow-hidden">
|
||||
<img
|
||||
src={getThumbnailUrl(post.banner_photo)}
|
||||
alt="post-cover"
|
||||
class="float-right mb-1 ml-2 h-12 w-16 rounded object-cover"
|
||||
onError={(e) => {
|
||||
const img = e.currentTarget;
|
||||
if (
|
||||
img.src !==
|
||||
(post.banner_photo || "/blueprint.jpg")
|
||||
) {
|
||||
img.src = post.banner_photo || "/blueprint.jpg";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span class="inline wrap-break-word hyphens-auto">
|
||||
{insertSoftHyphens(post.title.replace(/_/g, " "))}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span class="text-subtext0 clear-both text-sm">
|
||||
{new Date(post.date).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric"
|
||||
})}
|
||||
</span>
|
||||
</Typewriter>
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto">
|
||||
<Typewriter keepAlive={false}>
|
||||
<ul class="flex flex-col gap-4 pt-6">
|
||||
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||
<a href="/" onClick={handleLinkClick}>
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||
<a href="/blog" onClick={handleLinkClick}>
|
||||
Blog
|
||||
</a>
|
||||
</li>
|
||||
<Show when={isMounted() && isAdmin()}>
|
||||
<div class="mt-auto">
|
||||
<Typewriter keepAlive={false}>
|
||||
<ul class="flex flex-col gap-4 pt-6">
|
||||
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||
<a href="/analytics" onClick={handleLinkClick}>
|
||||
Analytics
|
||||
<a href="/" onClick={handleLinkClick}>
|
||||
Home
|
||||
</a>
|
||||
</li>
|
||||
</Show>
|
||||
<li
|
||||
class="hover:text-subtext0 w-fit cursor-pointer transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
|
||||
onClick={() => {
|
||||
navigate("/account");
|
||||
handleLinkClick();
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={isMounted() && isAuthenticated()}
|
||||
fallback={
|
||||
<a href="/login" onClick={handleLinkClick}>
|
||||
Login
|
||||
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||
<a href="/blog" onClick={handleLinkClick}>
|
||||
Blog
|
||||
</a>
|
||||
</li>
|
||||
<Show when={isMounted() && isAdmin()}>
|
||||
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||
<a href="/analytics" onClick={handleLinkClick}>
|
||||
Analytics
|
||||
</a>
|
||||
}
|
||||
>
|
||||
<A href="/account" onClick={handleLinkClick}>
|
||||
Account
|
||||
<Show when={email()}>
|
||||
<span class="text-subtext0 text-sm font-normal">
|
||||
{" "}
|
||||
({email()})
|
||||
</span>
|
||||
</Show>
|
||||
</A>
|
||||
</li>
|
||||
</Show>
|
||||
</li>
|
||||
<Show when={isMounted() && isAuthenticated()}>
|
||||
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
disabled={signOutLoading()}
|
||||
class="text-left disabled:opacity-50"
|
||||
<li
|
||||
class="hover:text-subtext0 w-fit cursor-pointer transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
|
||||
onClick={() => {
|
||||
navigate("/account");
|
||||
handleLinkClick();
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={isMounted() && isAuthenticated()}
|
||||
fallback={
|
||||
<a href="/login" onClick={handleLinkClick}>
|
||||
Login
|
||||
</a>
|
||||
}
|
||||
>
|
||||
{signOutLoading() ? "Signing Out..." : "Sign Out"}
|
||||
</button>
|
||||
<A href="/account" onClick={handleLinkClick}>
|
||||
Account
|
||||
<Show when={email()}>
|
||||
<span class="text-subtext0 text-sm font-normal">
|
||||
{" "}
|
||||
({email()})
|
||||
</span>
|
||||
</Show>
|
||||
</A>
|
||||
</Show>
|
||||
</li>
|
||||
</Show>
|
||||
</ul>
|
||||
</Typewriter>
|
||||
<Show when={isMounted() && isAuthenticated()}>
|
||||
<li class="hover:text-subtext0 w-fit transition-transform duration-200 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold">
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
disabled={signOutLoading()}
|
||||
class="text-left disabled:opacity-50"
|
||||
>
|
||||
{signOutLoading() ? "Signing Out..." : "Sign Out"}
|
||||
</button>
|
||||
</li>
|
||||
</Show>
|
||||
</ul>
|
||||
</Typewriter>
|
||||
|
||||
<ul class="pt-4 pb-6">
|
||||
<li
|
||||
class="hover:text-subtext0 w-fit transition-all duration-500 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
|
||||
classList={{
|
||||
"opacity-0 pointer-events-none": !getLostVisible(),
|
||||
"opacity-100": getLostVisible()
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
const lostUrls = [
|
||||
"/dev/null",
|
||||
"/segfault",
|
||||
"/void",
|
||||
"/404",
|
||||
"/lost-and-still-lost"
|
||||
];
|
||||
const randomUrl =
|
||||
lostUrls[Math.floor(Math.random() * lostUrls.length)];
|
||||
navigate(randomUrl);
|
||||
handleLinkClick();
|
||||
<ul class="pt-4 pb-6">
|
||||
<li
|
||||
class="hover:text-subtext0 w-fit transition-all duration-500 ease-in-out hover:-translate-y-0.5 hover:scale-110 hover:font-bold"
|
||||
classList={{
|
||||
"opacity-0 pointer-events-none": !getLostVisible(),
|
||||
"opacity-100": getLostVisible()
|
||||
}}
|
||||
class="text-left font-mono transition-opacity duration-75"
|
||||
style={{ "will-change": "contents" }}
|
||||
>
|
||||
{getLostText()}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<button
|
||||
onClick={() => {
|
||||
const lostUrls = [
|
||||
"/dev/null",
|
||||
"/segfault",
|
||||
"/void",
|
||||
"/404",
|
||||
"/lost-and-still-lost"
|
||||
];
|
||||
const randomUrl =
|
||||
lostUrls[Math.floor(Math.random() * lostUrls.length)];
|
||||
navigate(randomUrl);
|
||||
handleLinkClick();
|
||||
}}
|
||||
class="text-left font-mono transition-opacity duration-75"
|
||||
style={{ "will-change": "contents" }}
|
||||
>
|
||||
{getLostText()}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<hr class="border-overlay0 -mx-4 my-auto" />
|
||||
<div class="my-auto">
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
<hr class="border-overlay0 -mx-4 my-auto" />
|
||||
<div class="my-auto">
|
||||
<DarkModeToggle />
|
||||
</div>
|
||||
|
||||
<div class="border-overlay0 -mx-4 border-t pt-8 md:hidden">
|
||||
<RightBarContent />
|
||||
<div class="border-overlay0 -mx-4 border-t pt-8 md:hidden">
|
||||
<RightBarContent />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -828,9 +828,7 @@ export function LeftBar() {
|
||||
// ("bars render appropriately styled per site — brand color hint from
|
||||
// SITE_CONFIG"). Main keeps the existing neutral styling.
|
||||
const accentBorder = () =>
|
||||
site().id === "main"
|
||||
? undefined
|
||||
: { "border-color": site().brandColor };
|
||||
site().id === "main" ? undefined : { "border-color": site().brandColor };
|
||||
|
||||
return (
|
||||
<nav
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
createSignal,
|
||||
onMount,
|
||||
createEffect,
|
||||
Show,
|
||||
type JSX
|
||||
} from "solid-js";
|
||||
import { createSignal, onMount, createEffect, Show, type JSX } from "solid-js";
|
||||
import { useSearchParams, query, createAsync } from "@solidjs/router";
|
||||
import { action, redirect } from "@solidjs/router";
|
||||
import { PageHead } from "~/components/PageHead";
|
||||
@@ -43,7 +37,7 @@ import {
|
||||
} from "~/lib/contact-config";
|
||||
|
||||
/**
|
||||
* Shared, site-aware contact form (task 09 — per-subdomain contact pages).
|
||||
* Shared, site-aware contact form — per-subdomain contact pages.
|
||||
*
|
||||
* Extracted verbatim-in-spirit from the legacy `src/routes/contact.tsx` so the
|
||||
* main-site contact flow (`freno.me/contact`) keeps its exact Turnstile +
|
||||
@@ -57,10 +51,10 @@ import {
|
||||
* `CONTACT_CONTEXT[site().id]` (subjectPrefix, recipientLabel, heading,
|
||||
* PageHead title + description). Props override the defaults.
|
||||
* - Emits `<PageHead>` so every per-subdomain `/contact` route gets
|
||||
* site-aware title / canonical / OG tags for free (task 02).
|
||||
* site-aware title / canonical / OG tags for free.
|
||||
* - The Turnstile site key (`VITE_TURNSTILE_SITE_KEY`) is shared across all
|
||||
* subdomains — ensure it is configured for `*.freno.me` in the Cloudflare
|
||||
* Turnstile dashboard (see task notes).
|
||||
* Turnstile dashboard.
|
||||
*
|
||||
* Email routing:
|
||||
* - JS path: `api.misc.sendContactRequest.mutate({ …, subjectPrefix })` — the
|
||||
@@ -247,7 +241,8 @@ export function ContactForm(props: ContactFormProps) {
|
||||
const ctx = () => getContactContext(site().id);
|
||||
|
||||
// Effective values — props override the site-context defaults.
|
||||
const effectiveSubjectPrefix = () => props.subjectPrefix ?? ctx().subjectPrefix;
|
||||
const effectiveSubjectPrefix = () =>
|
||||
props.subjectPrefix ?? ctx().subjectPrefix;
|
||||
const effectiveRecipientLabel = () =>
|
||||
props.recipientLabel ?? ctx().recipientLabel;
|
||||
const effectiveHeading = () => props.heading ?? ctx().heading;
|
||||
@@ -283,7 +278,7 @@ export function ContactForm(props: ContactFormProps) {
|
||||
|
||||
// Load Cloudflare Turnstile script with explicit rendering.
|
||||
// The site key is shared across all subdomains — ensure it is configured
|
||||
// for `*.freno.me` in the Cloudflare Turnstile dashboard (task notes).
|
||||
// for `*.freno.me` in the Cloudflare Turnstile dashboard.
|
||||
const script = document.createElement("script");
|
||||
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
|
||||
script.async = true;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getClientCookie } from "~/lib/cookies.client";
|
||||
/**
|
||||
* Product discriminator forwarded to the generalized
|
||||
* `misc.sendDeletionRequestEmail` mutation so the email copy + cooldown
|
||||
* cookie are product-appropriate (task 11).
|
||||
* cookie are product-appropriate.
|
||||
*/
|
||||
export type DeletionProduct = "lineage" | "nessa";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Unit tests for `PageHead` site-aware metadata derivation (task 02).
|
||||
* Unit tests for `PageHead` site-aware metadata derivation.
|
||||
*
|
||||
* `resolvePageHeadMeta` is a pure function over (props, site, pathname), so
|
||||
* these tests mirror the acceptance matrix without a DOM / SolidJS router.
|
||||
@@ -28,21 +28,13 @@ describe("resolvePageHeadMeta — title suffix per site", () => {
|
||||
|
||||
for (const { id, suffix } of cases) {
|
||||
it(`${id} → title is "${BASE_PROPS.title}${suffix}"`, () => {
|
||||
const meta = resolvePageHeadMeta(
|
||||
BASE_PROPS,
|
||||
SITE_CONFIG[id],
|
||||
"/blog"
|
||||
);
|
||||
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG[id], "/blog");
|
||||
expect(meta.title).toBe(`${BASE_PROPS.title}${suffix}`);
|
||||
});
|
||||
}
|
||||
|
||||
it("main produces 'Home | Michael Freno' for the homepage", () => {
|
||||
const meta = resolvePageHeadMeta(
|
||||
{ title: "Home" },
|
||||
SITE_CONFIG.main,
|
||||
"/"
|
||||
);
|
||||
const meta = resolvePageHeadMeta({ title: "Home" }, SITE_CONFIG.main, "/");
|
||||
expect(meta.title).toBe("Home | Michael Freno");
|
||||
});
|
||||
});
|
||||
@@ -64,11 +56,7 @@ describe("resolvePageHeadMeta — canonical URL derivation", () => {
|
||||
});
|
||||
|
||||
it("nessa /contact → https://nessa.freno.me/contact", () => {
|
||||
const meta = resolvePageHeadMeta(
|
||||
BASE_PROPS,
|
||||
SITE_CONFIG.nessa,
|
||||
"/contact"
|
||||
);
|
||||
const meta = resolvePageHeadMeta(BASE_PROPS, SITE_CONFIG.nessa, "/contact");
|
||||
expect(meta.canonical).toBe("https://nessa.freno.me/contact");
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export {
|
||||
/**
|
||||
* PageHead component for consistent page metadata across the application.
|
||||
*
|
||||
* Site-aware (task 02): reads `useSite()` for the per-site title suffix,
|
||||
* Site-aware: reads `useSite()` for the per-site title suffix,
|
||||
* canonical domain, and default OpenGraph image, so the same component
|
||||
* renders `" | Michael Freno"` / `" | Nessa"` / … depending on the active
|
||||
* subdomain. Canonical URLs are auto-derived from the site domain + the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Pure metadata derivation for `PageHead` (task 02).
|
||||
* Pure metadata derivation for `PageHead`.
|
||||
*
|
||||
* Intentionally imports NOTHING from solid-js / @solidjs/router / @solidjs/meta
|
||||
* so it can be unit-tested in `bun:test` without spinning up the SolidJS
|
||||
|
||||
Reference in New Issue
Block a user