QA pass: layout, create-flow, and About books #68
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Figma: Content page Template body ornaments (19003:23305).
|
||||
* Hidden below md (640). From md, shapes sit in the side gutters and the
|
||||
* page frame clips anything that bleeds past the viewport.
|
||||
*
|
||||
* - 19003:23575 / 19000:23008 — right
|
||||
* - 19003:23576 / 19000:23009 — left
|
||||
*/
|
||||
import {
|
||||
ASSETS,
|
||||
getAssetPath,
|
||||
howItWorksOrnamentLeftPath,
|
||||
howItWorksOrnamentRightPath,
|
||||
} from "../../../lib/assetUtils";
|
||||
|
||||
type ContentTemplateDecorativeShapesVariant = "guide" | "article";
|
||||
|
||||
type ContentTemplateDecorativeShapesProps = {
|
||||
variant: ContentTemplateDecorativeShapesVariant;
|
||||
};
|
||||
|
||||
const RIGHT_GUIDE_CLASS =
|
||||
"pointer-events-none absolute z-0 hidden aspect-[155/250] md:block left-[87.19%] right-0 top-[266px] lg:left-[84.86%] lg:top-[255px]";
|
||||
const LEFT_GUIDE_CLASS =
|
||||
"pointer-events-none absolute z-0 hidden aspect-[119/136] md:block left-0 right-[88.38%] top-[811px]";
|
||||
const RIGHT_ARTICLE_CLASS =
|
||||
"pointer-events-none absolute z-0 hidden aspect-square md:block left-[87.19%] right-[-8.13%] top-[266px] lg:left-[84.86%] lg:right-[-9.28%] lg:top-[255px]";
|
||||
const LEFT_ARTICLE_CLASS =
|
||||
"pointer-events-none absolute z-0 hidden aspect-square md:block left-[-1.66%] right-[88.38%] top-[811px]";
|
||||
|
||||
export default function ContentTemplateDecorativeShapes({
|
||||
variant,
|
||||
}: ContentTemplateDecorativeShapesProps) {
|
||||
const isArticle = variant === "article";
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className={isArticle ? RIGHT_ARTICLE_CLASS : RIGHT_GUIDE_CLASS}
|
||||
data-node-id="19003:23575"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={
|
||||
isArticle
|
||||
? getAssetPath(ASSETS.CONTENT_SHAPE_1)
|
||||
: getAssetPath(howItWorksOrnamentRightPath())
|
||||
}
|
||||
alt=""
|
||||
className="pointer-events-none size-full max-w-none object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className={isArticle ? LEFT_ARTICLE_CLASS : LEFT_GUIDE_CLASS}
|
||||
data-node-id="19003:23576"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={
|
||||
isArticle
|
||||
? getAssetPath(ASSETS.CONTENT_SHAPE_2)
|
||||
: getAssetPath(howItWorksOrnamentLeftPath())
|
||||
}
|
||||
alt=""
|
||||
className="pointer-events-none size-full max-w-none object-contain"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { logger } from "../../../../lib/logger";
|
||||
import ContentBanner from "../../../components/sections/ContentBanner";
|
||||
import AskOrganizer from "../../../components/sections/AskOrganizer";
|
||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
||||
import ContentTemplateDecorativeShapes from "../../_components/ContentTemplateDecorativeShapes";
|
||||
import "../blog.css";
|
||||
|
||||
// Code split RelatedArticles - blog-specific, below the fold
|
||||
@@ -205,49 +205,15 @@ export default async function BlogPostPage({ params }: PageProps) {
|
||||
className="relative min-h-screen overflow-x-clip"
|
||||
style={{ backgroundColor }}
|
||||
>
|
||||
{/* Content Banner */}
|
||||
<ContentBanner post={post} />
|
||||
|
||||
{/* Decorative Shapes */}
|
||||
{/* Right Side Shape (3/4 up the page) */}
|
||||
<div
|
||||
className="hidden md:block absolute top-1/4 right-0 pointer-events-none z-10"
|
||||
style={{ transform: "translateX(40%)" }}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.CONTENT_SHAPE_1)}
|
||||
alt=""
|
||||
className="w-auto h-auto max-w-none"
|
||||
style={{
|
||||
width: "clamp(120px, 15vw, 200px)",
|
||||
height: "auto",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative w-full overflow-x-clip">
|
||||
<ContentTemplateDecorativeShapes variant="article" />
|
||||
|
||||
{/* Left Side Shape (3/4 down the page) */}
|
||||
<div
|
||||
className="hidden md:block absolute top-1/2 left-0 pointer-events-none z-10"
|
||||
style={{ transform: "translateX(-10%)" }}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getAssetPath(ASSETS.CONTENT_SHAPE_2)}
|
||||
alt=""
|
||||
className="w-auto h-auto max-w-none"
|
||||
style={{
|
||||
width: "clamp(100px, 12vw, 180px)",
|
||||
height: "auto",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Main Content — Figma Content page Template (19003:23305) article body instances */}
|
||||
<article
|
||||
data-node-id="19031:10426"
|
||||
className="
|
||||
relative z-[2] flex w-full justify-center
|
||||
relative z-10 flex w-full justify-center
|
||||
p-[var(--spacing-scale-024)]
|
||||
sm:px-0 sm:py-[var(--spacing-scale-032)]
|
||||
"
|
||||
@@ -256,6 +222,7 @@ export default async function BlogPostPage({ params }: PageProps) {
|
||||
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
{/* Related Articles Section */}
|
||||
<RelatedArticles
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Figma: "A Guide to CommunityRule" body ornaments (22078:791901)
|
||||
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=22078-791901
|
||||
*
|
||||
* - 19003:23575 — concentric circles, right (`how-shape-2.svg`)
|
||||
* - 19003:23576 — loop mark, left (`how-shape-1.svg`)
|
||||
*/
|
||||
import {
|
||||
getAssetPath,
|
||||
howItWorksOrnamentLeftPath,
|
||||
howItWorksOrnamentRightPath,
|
||||
} from "../../../../lib/assetUtils";
|
||||
|
||||
export default function HowItWorksDecorativeShapes() {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute z-0 hidden aspect-square md:block left-[84.86%] right-[-9.28%] top-[clamp(200px,20vw,255px)]"
|
||||
data-node-id="19003:23575"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getAssetPath(howItWorksOrnamentRightPath())}
|
||||
alt=""
|
||||
className="pointer-events-none size-full max-w-none object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
aria-hidden
|
||||
className="pointer-events-none absolute z-0 hidden aspect-square md:block left-[-1.66%] right-[88.38%] top-[clamp(520px,55vw,811px)]"
|
||||
data-node-id="19003:23576"
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={getAssetPath(howItWorksOrnamentLeftPath())}
|
||||
alt=""
|
||||
className="pointer-events-none size-full max-w-none object-cover"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
HOW_IT_WORKS_SENTINEL_SLUG,
|
||||
} from "../../../lib/howItWorksSyntheticPost";
|
||||
import ContentBanner from "../../components/sections/ContentBanner";
|
||||
import HowItWorksDecorativeShapes from "./_components/HowItWorksDecorativeShapes";
|
||||
import ContentTemplateDecorativeShapes from "../_components/ContentTemplateDecorativeShapes";
|
||||
import AskOrganizer from "../../components/sections/AskOrganizer";
|
||||
import "../blog/blog.css";
|
||||
|
||||
@@ -103,11 +103,11 @@ export default function HowItWorksPage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative min-h-screen overflow-x-hidden bg-transparent">
|
||||
<div className="relative min-h-screen overflow-x-clip bg-transparent">
|
||||
<ContentBanner post={syntheticPost} variant="guide" />
|
||||
|
||||
<div className="relative w-full">
|
||||
<HowItWorksDecorativeShapes />
|
||||
<div className="relative w-full overflow-x-clip">
|
||||
<ContentTemplateDecorativeShapes variant="guide" />
|
||||
|
||||
<article className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]">
|
||||
<div
|
||||
|
||||
+20
-14
@@ -88,15 +88,15 @@ vi.mock("../../app/components/sections/AskOrganizer", () => {
|
||||
});
|
||||
|
||||
// Mock asset utils
|
||||
vi.mock("../../lib/assetUtils", () => ({
|
||||
vi.mock("../../lib/assetUtils", async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
getAssetPath: vi.fn((asset) =>
|
||||
asset.startsWith("/") ? asset : `/${asset}`,
|
||||
),
|
||||
ASSETS: {
|
||||
CONTENT_SHAPE_1: "assets/shapes/content-shape-1.svg",
|
||||
CONTENT_SHAPE_2: "assets/shapes/content-shape-2.svg",
|
||||
},
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
// Mock blog post data
|
||||
const mockPost = {
|
||||
@@ -238,6 +238,8 @@ describe("BlogPostPage", () => {
|
||||
// Check shape sources
|
||||
expect(shapes[0]).toHaveAttribute("src", "/assets/shapes/content-shape-1.svg");
|
||||
expect(shapes[1]).toHaveAttribute("src", "/assets/shapes/content-shape-2.svg");
|
||||
expect(shapes[0]).toHaveClass("object-contain");
|
||||
expect(shapes[1]).toHaveClass("object-contain");
|
||||
});
|
||||
|
||||
it("applies correct styling to article content", async () => {
|
||||
@@ -349,29 +351,33 @@ describe("BlogPostPage", () => {
|
||||
|
||||
const shapes = screen.getAllByAltText("");
|
||||
|
||||
// First shape (right side)
|
||||
const rightShape = shapes[0].closest("div");
|
||||
expect(rightShape).toHaveClass(
|
||||
"hidden",
|
||||
"md:block",
|
||||
"absolute",
|
||||
"top-1/4",
|
||||
"right-0",
|
||||
"pointer-events-none",
|
||||
"z-10",
|
||||
"z-0",
|
||||
"top-[266px]",
|
||||
"lg:top-[255px]",
|
||||
"left-[87.19%]",
|
||||
"lg:left-[84.86%]",
|
||||
);
|
||||
expect(rightShape).not.toHaveClass("z-10");
|
||||
expect(rightShape).not.toHaveClass("top-1/4");
|
||||
|
||||
// Second shape (left side)
|
||||
const leftShape = shapes[1].closest("div");
|
||||
expect(leftShape).toHaveClass(
|
||||
"hidden",
|
||||
"md:block",
|
||||
"absolute",
|
||||
"top-1/2",
|
||||
"left-0",
|
||||
"pointer-events-none",
|
||||
"z-10",
|
||||
"z-0",
|
||||
"top-[811px]",
|
||||
"left-[-1.66%]",
|
||||
);
|
||||
expect(leftShape).not.toHaveClass("z-10");
|
||||
expect(leftShape).not.toHaveClass("top-1/2");
|
||||
});
|
||||
|
||||
it("handles malformed post data gracefully", async () => {
|
||||
|
||||
@@ -63,4 +63,33 @@ describe("HowItWorksPage", () => {
|
||||
screen.getByText(/mutual aid network, manage an open-source project/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("clips body ornaments at the page frame and keeps them behind copy", () => {
|
||||
const { container } = render(<HowItWorksPage />);
|
||||
|
||||
const page = container.querySelector("div.min-h-screen");
|
||||
expect(page).toHaveClass("overflow-x-clip");
|
||||
|
||||
const right = container.querySelector('[data-node-id="19003:23575"]');
|
||||
const left = container.querySelector('[data-node-id="19003:23576"]');
|
||||
expect(right).toHaveClass(
|
||||
"z-0",
|
||||
"left-[87.19%]",
|
||||
"right-0",
|
||||
"top-[266px]",
|
||||
"lg:left-[84.86%]",
|
||||
"lg:top-[255px]",
|
||||
);
|
||||
expect(left).toHaveClass("z-0", "left-0", "right-[88.38%]", "top-[811px]");
|
||||
|
||||
const rightImg = right?.querySelector("img");
|
||||
const leftImg = left?.querySelector("img");
|
||||
expect(rightImg).toHaveAttribute(
|
||||
"src",
|
||||
"/assets/shapes/how-shape-2.svg",
|
||||
);
|
||||
expect(leftImg).toHaveAttribute("src", "/assets/shapes/how-shape-1.svg");
|
||||
expect(rightImg).toHaveClass("object-contain");
|
||||
expect(leftImg).toHaveClass("object-contain");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user