Improve page load times and rendering

This commit is contained in:
adilallo
2026-05-26 06:59:52 -06:00
parent 6b45a2e5d0
commit 3be188a3cc
29 changed files with 467 additions and 176 deletions
+9 -1
View File
@@ -24,7 +24,15 @@ const Avatar = memo<AvatarProps>(
return (
/* eslint-disable-next-line @next/next/no-img-element -- avatar image from URL */
<img src={src} alt={alt} className={baseStyles} {...props} />
<img
src={src}
alt={alt}
className={baseStyles}
loading="eager"
decoding="async"
fetchPriority="high"
{...props}
/>
);
},
);
@@ -1,8 +1,10 @@
"use client";
import Image from "next/image";
import { memo } from "react";
import { caseStudyVisualPath, getAssetPath } from "../../../../lib/assetUtils";
import type { ComponentType, SVGProps } from "react";
import MutualAidArt from "../../../../public/assets/case-study/case-study-mutual-aid.svg";
import FoodNotBombsArt from "../../../../public/assets/case-study/case-study-food-not-bombs.svg";
import BoulderCountyStreetMedicsArt from "../../../../public/assets/case-study/case-study-boulder-county-street-medics.svg";
import type { CaseStudyProps } from "./CaseStudy.types";
const SURFACE_CLASS: Record<CaseStudyProps["surface"], string> = {
@@ -11,11 +13,23 @@ const SURFACE_CLASS: Record<CaseStudyProps["surface"], string> = {
rose: "bg-[var(--color-surface-invert-brand-red)]",
};
/** Default art per tile: Figma-exported SVG composites (305×305 incl. rounded bg). */
const SURFACE_ART: Record<CaseStudyProps["surface"], string> = {
lavender: getAssetPath(caseStudyVisualPath("lavender")),
neutral: getAssetPath(caseStudyVisualPath("neutral")),
rose: getAssetPath(caseStudyVisualPath("rose")),
/**
* Inline SVGR components avoid the network round-trip the prior `next/image`
* version required, so the illustration paints with the colored tile shell.
*/
const SURFACE_ART: Record<
CaseStudyProps["surface"],
ComponentType<SVGProps<SVGSVGElement>>
> = {
lavender: MutualAidArt,
neutral: FoodNotBombsArt,
rose: BoulderCountyStreetMedicsArt,
};
const SURFACE_ART_DATA_KEY: Record<CaseStudyProps["surface"], string> = {
lavender: "case-study-mutual-aid",
neutral: "case-study-food-not-bombs",
rose: "case-study-boulder-county-street-medics",
};
/** Figma: ~23px corner (“Card / CaseStudy” shells). */
@@ -27,6 +41,7 @@ function CaseStudyView({
visual,
className = "",
}: CaseStudyProps) {
const Art = SURFACE_ART[surface];
return (
<div
data-figma-node="21993-32352"
@@ -35,14 +50,13 @@ function CaseStudyView({
{visual ? (
<div className="flex size-full items-center justify-center p-2">{visual}</div>
) : (
<Image
src={SURFACE_ART[surface]}
alt={imageAlt}
<Art
role="img"
aria-label={imageAlt}
data-case-study-art={SURFACE_ART_DATA_KEY[surface]}
width={305}
height={305}
unoptimized
className="pointer-events-none size-full select-none object-contain object-center"
draggable={false}
/>
)}
</div>
@@ -0,0 +1,27 @@
"use client";
import { memo } from "react";
import { usePathname } from "next/navigation";
import { isChromelessNavigationPath } from "../../../lib/navigationChromelessPath";
import TopWithPathname from "./Top/TopWithPathname";
/**
* Marketing-only navigation. Skips the server-side `getNavAuthSignedIn()` call
* so marketing pages can render statically (no `force-dynamic`); `TopWithPathname`
* fetches `/api/auth/session` on mount and updates the header from "Log in" to
* "Profile" when the user is signed in. Brief mismatch is acceptable here —
* `(app)` / `(admin)` keep the server-rendered nav.
*/
const MarketingNavigation = memo(() => {
const pathname = usePathname();
if (isChromelessNavigationPath(pathname)) {
return null;
}
return <TopWithPathname initialSignedIn={false} />;
});
MarketingNavigation.displayName = "MarketingNavigation";
export default MarketingNavigation;
@@ -13,7 +13,7 @@ import Button from "../../buttons/Button";
import AvatarContainer from "../../asset/AvatarContainer";
import Avatar from "../../asset/Avatar";
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
import { prepareFreshCreateFlowEntry } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
import { TopView } from "./Top.view";
import type { TopProps, NavSize } from "./Top.types";
@@ -51,15 +51,14 @@ const TopContainer = memo<TopProps>(
/**
* `Top` is hidden on `/create` routes by ConditionalNavigationClient, so
* this button is always clicked from outside the wizard. Clears anonymous
* `localStorage` and, when backend sync is on, deletes the server draft
* so signed-in users get the same fresh start as guests (see
* {@link prepareFreshCreateFlowEntry}).
* `localStorage` synchronously and, when backend sync is on, fires the
* server `DELETE /api/drafts/me` in the background. `SignedInDraftHydration`
* reads the `create:fresh-entry-pending` sentinel and waits before fetching
* (see {@link prepareFreshCreateFlowEntrySync}).
*/
const handleCreateRuleClick = useCallback(() => {
void (async () => {
await prepareFreshCreateFlowEntry();
router.push("/create");
})();
prepareFreshCreateFlowEntrySync();
router.push("/create/informational");
}, [router]);
// Schema markup for site navigation
@@ -21,7 +21,6 @@ const QuoteBlockContainer = memo<QuoteBlockProps>(
onError,
}) => {
const [imageError, setImageError] = useState(false);
const [imageLoading, setImageLoading] = useState(true);
// Variant configurations
const variants: Record<string, VariantConfig> = {
@@ -97,16 +96,13 @@ const QuoteBlockContainer = memo<QuoteBlockProps>(
const quoteId = `${baseId}-content`;
const authorId = `${baseId}-author`;
// Error handling functions
const handleImageError = (error: unknown) => {
logger.warn(
`QuoteBlock: Failed to load avatar image for ${author}:`,
error,
);
setImageError(true);
setImageLoading(false);
// Call error callback if provided
if (onError) {
onError({
type: "image_load_error",
@@ -118,11 +114,6 @@ const QuoteBlockContainer = memo<QuoteBlockProps>(
}
};
const handleImageLoad = () => {
setImageLoading(false);
setImageError(false);
};
// Validate required props
if (variantProp === "statement") {
if (!quote?.trim() || !quoteSecondary?.trim()) {
@@ -166,9 +157,7 @@ const QuoteBlockContainer = memo<QuoteBlockProps>(
authorId={authorId}
config={config}
imageError={imageError}
imageLoading={imageLoading}
currentAvatarSrc={currentAvatarSrc}
onImageLoad={handleImageLoad}
onImageError={handleImageError}
/>
);
@@ -52,8 +52,6 @@ export interface QuoteBlockViewProps {
authorId: string;
config: VariantConfig;
imageError: boolean;
imageLoading: boolean;
currentAvatarSrc: string;
onImageLoad: () => void;
onImageError: (_error: unknown) => void;
}
@@ -17,9 +17,7 @@ function QuoteBlockView({
authorId,
config,
imageError,
imageLoading,
currentAvatarSrc,
onImageLoad,
onImageError,
}: QuoteBlockViewProps) {
const t = useTranslation("quoteBlock");
@@ -89,7 +87,6 @@ function QuoteBlockView({
<div className={`flex flex-col ${config.gap} relative z-10`}>
<div className={`flex flex-col ${config.avatarGap}`}>
{/* Avatar with error handling */}
<div className="relative">
{!imageError ? (
<Image
@@ -97,26 +94,12 @@ function QuoteBlockView({
alt={avatarAlt}
width={64}
height={64}
className={`filter sepia ${
config.avatar
} transition-opacity duration-300 ${
imageLoading ? "opacity-0" : "opacity-100"
}`}
loading="lazy"
className={`filter sepia ${config.avatar}`}
loading="eager"
priority
onError={onImageError}
onLoad={onImageLoad}
/>
) : null}
{/* Loading state */}
{imageLoading && !imageError && (
<div
className={`absolute inset-0 bg-gray-200 animate-pulse rounded-full ${config.avatar}`}
/>
)}
{/* Error state - show initials */}
{imageError && (
) : (
<div
className={`flex items-center justify-center bg-gray-300 rounded-full ${config.avatar} text-gray-600 font-bold`}
>
@@ -8,7 +8,7 @@ import { memo, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useTranslation } from "../../../contexts/MessagesContext";
import { logger } from "../../../../lib/logger";
import { prepareFreshCreateFlowEntry } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
import { prepareFreshCreateFlowEntrySync } from "../../../(app)/create/utils/prepareFreshCreateFlowEntry";
import {
fetchTemplates,
isTemplatesFetchAborted,
@@ -99,10 +99,8 @@ const RuleStackContainer = memo<RuleStackProps>(
logger.debug(`${slug} template clicked`);
// Marketing home “Popular templates”: same fresh start as Top “Create rule”
// (local + server draft when sync) so stale state cannot break template apply.
void (async () => {
await prepareFreshCreateFlowEntry();
router.push(`/create/review-template/${encodeURIComponent(slug)}`);
})();
prepareFreshCreateFlowEntrySync();
router.push(`/create/review-template/${encodeURIComponent(slug)}`);
};
return (