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 { logger } from "../../../../lib/logger";
|
||||||
import ContentBanner from "../../../components/sections/ContentBanner";
|
import ContentBanner from "../../../components/sections/ContentBanner";
|
||||||
import AskOrganizer from "../../../components/sections/AskOrganizer";
|
import AskOrganizer from "../../../components/sections/AskOrganizer";
|
||||||
import { getAssetPath, ASSETS } from "../../../../lib/assetUtils";
|
import ContentTemplateDecorativeShapes from "../../_components/ContentTemplateDecorativeShapes";
|
||||||
import "../blog.css";
|
import "../blog.css";
|
||||||
|
|
||||||
// Code split RelatedArticles - blog-specific, below the fold
|
// Code split RelatedArticles - blog-specific, below the fold
|
||||||
@@ -205,58 +205,25 @@ export default async function BlogPostPage({ params }: PageProps) {
|
|||||||
className="relative min-h-screen overflow-x-clip"
|
className="relative min-h-screen overflow-x-clip"
|
||||||
style={{ backgroundColor }}
|
style={{ backgroundColor }}
|
||||||
>
|
>
|
||||||
{/* Content Banner */}
|
|
||||||
<ContentBanner post={post} />
|
<ContentBanner post={post} />
|
||||||
|
|
||||||
{/* Decorative Shapes */}
|
<div className="relative w-full overflow-x-clip">
|
||||||
{/* Right Side Shape (3/4 up the page) */}
|
<ContentTemplateDecorativeShapes variant="article" />
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Left Side Shape (3/4 down the page) */}
|
<article
|
||||||
<div
|
data-node-id="19031:10426"
|
||||||
className="hidden md:block absolute top-1/2 left-0 pointer-events-none z-10"
|
className="
|
||||||
style={{ transform: "translateX(-10%)" }}
|
relative z-10 flex w-full justify-center
|
||||||
>
|
p-[var(--spacing-scale-024)]
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
sm:px-0 sm:py-[var(--spacing-scale-032)]
|
||||||
<img
|
"
|
||||||
src={getAssetPath(ASSETS.CONTENT_SHAPE_2)}
|
>
|
||||||
alt=""
|
<div className="post-body w-full text-[var(--color-content-inverse-primary)] text-medium-paragraph sm:text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-[904px]">
|
||||||
className="w-auto h-auto max-w-none"
|
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
|
||||||
style={{
|
</div>
|
||||||
width: "clamp(100px, 12vw, 180px)",
|
</article>
|
||||||
height: "auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</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
|
|
||||||
p-[var(--spacing-scale-024)]
|
|
||||||
sm:px-0 sm:py-[var(--spacing-scale-032)]
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<div className="post-body w-full text-[var(--color-content-inverse-primary)] text-medium-paragraph sm:text-large-paragraph lg:text-x-large-paragraph xl:text-xx-large-paragraph sm:max-w-[390px] md:max-w-[472px] lg:max-w-[700px] xl:max-w-[904px]">
|
|
||||||
<div dangerouslySetInnerHTML={{ __html: post.htmlContent }} />
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
{/* Related Articles Section */}
|
{/* Related Articles Section */}
|
||||||
<RelatedArticles
|
<RelatedArticles
|
||||||
relatedPosts={relatedArticles}
|
relatedPosts={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,
|
HOW_IT_WORKS_SENTINEL_SLUG,
|
||||||
} from "../../../lib/howItWorksSyntheticPost";
|
} from "../../../lib/howItWorksSyntheticPost";
|
||||||
import ContentBanner from "../../components/sections/ContentBanner";
|
import ContentBanner from "../../components/sections/ContentBanner";
|
||||||
import HowItWorksDecorativeShapes from "./_components/HowItWorksDecorativeShapes";
|
import ContentTemplateDecorativeShapes from "../_components/ContentTemplateDecorativeShapes";
|
||||||
import AskOrganizer from "../../components/sections/AskOrganizer";
|
import AskOrganizer from "../../components/sections/AskOrganizer";
|
||||||
import "../blog/blog.css";
|
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" />
|
<ContentBanner post={syntheticPost} variant="guide" />
|
||||||
|
|
||||||
<div className="relative w-full">
|
<div className="relative w-full overflow-x-clip">
|
||||||
<HowItWorksDecorativeShapes />
|
<ContentTemplateDecorativeShapes variant="guide" />
|
||||||
|
|
||||||
<article className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]">
|
<article className="relative z-10 p-[var(--spacing-scale-024)] sm:py-[var(--spacing-scale-032)]">
|
||||||
<div
|
<div
|
||||||
|
|||||||
+23
-17
@@ -88,15 +88,15 @@ vi.mock("../../app/components/sections/AskOrganizer", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mock asset utils
|
// Mock asset utils
|
||||||
vi.mock("../../lib/assetUtils", () => ({
|
vi.mock("../../lib/assetUtils", async (importOriginal) => {
|
||||||
getAssetPath: vi.fn((asset) =>
|
const actual = await importOriginal();
|
||||||
asset.startsWith("/") ? asset : `/${asset}`,
|
return {
|
||||||
),
|
...actual,
|
||||||
ASSETS: {
|
getAssetPath: vi.fn((asset) =>
|
||||||
CONTENT_SHAPE_1: "assets/shapes/content-shape-1.svg",
|
asset.startsWith("/") ? asset : `/${asset}`,
|
||||||
CONTENT_SHAPE_2: "assets/shapes/content-shape-2.svg",
|
),
|
||||||
},
|
};
|
||||||
}));
|
});
|
||||||
|
|
||||||
// Mock blog post data
|
// Mock blog post data
|
||||||
const mockPost = {
|
const mockPost = {
|
||||||
@@ -238,6 +238,8 @@ describe("BlogPostPage", () => {
|
|||||||
// Check shape sources
|
// Check shape sources
|
||||||
expect(shapes[0]).toHaveAttribute("src", "/assets/shapes/content-shape-1.svg");
|
expect(shapes[0]).toHaveAttribute("src", "/assets/shapes/content-shape-1.svg");
|
||||||
expect(shapes[1]).toHaveAttribute("src", "/assets/shapes/content-shape-2.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 () => {
|
it("applies correct styling to article content", async () => {
|
||||||
@@ -349,29 +351,33 @@ describe("BlogPostPage", () => {
|
|||||||
|
|
||||||
const shapes = screen.getAllByAltText("");
|
const shapes = screen.getAllByAltText("");
|
||||||
|
|
||||||
// First shape (right side)
|
|
||||||
const rightShape = shapes[0].closest("div");
|
const rightShape = shapes[0].closest("div");
|
||||||
expect(rightShape).toHaveClass(
|
expect(rightShape).toHaveClass(
|
||||||
"hidden",
|
"hidden",
|
||||||
"md:block",
|
"md:block",
|
||||||
"absolute",
|
"absolute",
|
||||||
"top-1/4",
|
|
||||||
"right-0",
|
|
||||||
"pointer-events-none",
|
"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");
|
const leftShape = shapes[1].closest("div");
|
||||||
expect(leftShape).toHaveClass(
|
expect(leftShape).toHaveClass(
|
||||||
"hidden",
|
"hidden",
|
||||||
"md:block",
|
"md:block",
|
||||||
"absolute",
|
"absolute",
|
||||||
"top-1/2",
|
|
||||||
"left-0",
|
|
||||||
"pointer-events-none",
|
"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 () => {
|
it("handles malformed post data gracefully", async () => {
|
||||||
|
|||||||
@@ -63,4 +63,33 @@ describe("HowItWorksPage", () => {
|
|||||||
screen.getByText(/mutual aid network, manage an open-source project/),
|
screen.getByText(/mutual aid network, manage an open-source project/),
|
||||||
).toBeInTheDocument();
|
).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