QA pass: layout, create-flow, and About books #68

Merged
an.di merged 38 commits from adilallo/fix/CR-129-create-rule-button into main 2026-08-26 15:46:56 +00:00
7 changed files with 56 additions and 29 deletions
Showing only changes of commit aa4fe47554 - Show all commits
+6 -2
View File
@@ -3,7 +3,8 @@ import dynamic from "next/dynamic";
import Link from "next/link"; import Link from "next/link";
import { Suspense } from "react"; import { Suspense } from "react";
import messages from "../../../messages/en/index"; import messages from "../../../messages/en/index";
import { getAllBlogPosts } from "../../../lib/content"; import { CONTENT_CATALOG_SLUG_ORDER } from "../../../lib/assetUtils";
import { getAllBlogPosts, getRelatedBlogPosts } from "../../../lib/content";
import PageHeader from "../../components/type/PageHeader"; import PageHeader from "../../components/type/PageHeader";
import CaseStudy from "../../components/cards/CaseStudy"; import CaseStudy from "../../components/cards/CaseStudy";
import UseCasesOrgs from "../../components/sections/UseCasesOrgs"; import UseCasesOrgs from "../../components/sections/UseCasesOrgs";
@@ -102,7 +103,10 @@ export default function UseCasesPage() {
}; };
const allPosts = getAllBlogPosts(); const allPosts = getAllBlogPosts();
const relatedPosts = allPosts.slice(0, 8); const relatedPosts = getRelatedBlogPosts(
USE_CASES_RELATED_SENTINEL_SLUG,
CONTENT_CATALOG_SLUG_ORDER.slice(0, 3),
);
const slugOrder = allPosts.map((p) => p.slug); const slugOrder = allPosts.map((p) => p.slug);
const tripleStepSteps = asArray<{ title: string; body: string }>( const tripleStepSteps = asArray<{ title: string; body: string }>(
@@ -74,8 +74,8 @@ const ContentContainerContainer = memo<ContentContainerProps>(
const titleClasses = const titleClasses =
size === "xs" size === "xs"
? `font-bricolage font-medium text-[18px] leading-[22px] transition-colors ${titleColor}` ? `font-bricolage-grotesque font-medium text-[18px] leading-[22px] transition-colors ${titleColor}`
: `font-bricolage font-medium text-xx-small-display sm:text-x-small-display md:text-[32px] md:leading-[110%] lg:text-medium-display xl:text-x-large-display transition-colors ${titleColor}`; : `font-bricolage-grotesque font-medium text-xx-small-display sm:text-x-small-display md:text-[32px] md:leading-[110%] lg:text-medium-display xl:text-x-large-display transition-colors ${titleColor}`;
const descriptionClasses = const descriptionClasses =
size === "xs" size === "xs"
@@ -20,6 +20,7 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
heading, heading,
}) => { }) => {
const messages = useMessages(); const messages = useMessages();
const isUseCases = variant === "useCases";
// Memoize filtered posts to prevent unnecessary re-computations // Memoize filtered posts to prevent unnecessary re-computations
const filteredPosts = useMemo( const filteredPosts = useMemo(
() => relatedPosts.filter((post) => post.slug !== currentPostSlug), () => relatedPosts.filter((post) => post.slug !== currentPostSlug),
@@ -29,6 +30,8 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
const [currentIndex, setCurrentIndex] = useState(0); const [currentIndex, setCurrentIndex] = useState(0);
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const autoScroll = !isUseCases && isMobile;
const manualScroll = isUseCases || !isMobile;
// Memoize the mouse down handler to prevent unnecessary re-renders // Memoize the mouse down handler to prevent unnecessary re-renders
const handleMouseDown = useCallback( const handleMouseDown = useCallback(
@@ -57,14 +60,15 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
// Memoize transform style to prevent unnecessary recalculations // Memoize transform style to prevent unnecessary recalculations
const transformStyle = useMemo( const transformStyle = useMemo(
() => ({ () => ({
transform: isMobile transform:
? `translateX(calc(50% - 130px - ${currentIndex * 260}px))` !isUseCases && isMobile
: "none", ? `translateX(calc(50% - 130px - ${currentIndex * 260}px))`
scrollBehavior: (!isMobile : "none",
scrollBehavior: (manualScroll
? "smooth" ? "smooth"
: "auto") as React.CSSProperties["scrollBehavior"], : "auto") as React.CSSProperties["scrollBehavior"],
}), }),
[isMobile, currentIndex], [isMobile, currentIndex, isUseCases, manualScroll],
); );
// Memoize progress bar style calculation // Memoize progress bar style calculation
@@ -80,9 +84,9 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
[currentIndex, progress], [currentIndex, progress],
); );
// Auto-advance every 3 seconds (only on mobile) // Auto-advance every 3 seconds (blog carousel on small viewports only)
useEffect(() => { useEffect(() => {
if (filteredPosts.length <= 1 || !isMobile) return; if (!autoScroll || filteredPosts.length <= 1) return;
const interval = setInterval(() => { const interval = setInterval(() => {
setProgress(0); setProgress(0);
@@ -90,11 +94,11 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
}, 3000); }, 3000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [filteredPosts.length, isMobile]); }, [autoScroll, filteredPosts.length]);
// Progress animation (only on mobile) // Progress animation (only on mobile)
useEffect(() => { useEffect(() => {
if (filteredPosts.length <= 1 || !isMobile) return; if (!autoScroll || filteredPosts.length <= 1) return;
const progressInterval = setInterval(() => { const progressInterval = setInterval(() => {
setProgress((prev) => { setProgress((prev) => {
@@ -106,18 +110,18 @@ const RelatedArticlesContainer = memo<RelatedArticlesProps>(
}, 30); // 30ms intervals for smooth animation }, 30); // 30ms intervals for smooth animation
return () => clearInterval(progressInterval); return () => clearInterval(progressInterval);
}, [currentIndex, filteredPosts.length, isMobile]); }, [autoScroll, currentIndex, filteredPosts.length]);
const useCasesHeadingLines = const useCasesHeadingLines = isUseCases
variant === "useCases" ? messages.pages.useCases.relatedArticles.title
? messages.pages.useCases.relatedArticles.title : undefined;
: undefined;
return ( return (
<RelatedArticlesView <RelatedArticlesView
filteredPosts={filteredPosts} filteredPosts={filteredPosts}
slugOrder={slugOrder} slugOrder={slugOrder}
isMobile={isMobile} isMobile={isMobile}
manualScroll={manualScroll}
transformStyle={transformStyle} transformStyle={transformStyle}
getProgressStyle={getProgressStyle} getProgressStyle={getProgressStyle}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
@@ -25,6 +25,7 @@ export interface RelatedArticlesViewProps {
filteredPosts: BlogPost[]; filteredPosts: BlogPost[];
slugOrder: string[]; slugOrder: string[];
isMobile: boolean; isMobile: boolean;
manualScroll: boolean;
transformStyle: React.CSSProperties; transformStyle: React.CSSProperties;
getProgressStyle: (_index: number) => React.CSSProperties; getProgressStyle: (_index: number) => React.CSSProperties;
onMouseDown?: (_e: React.MouseEvent<HTMLDivElement>) => void; onMouseDown?: (_e: React.MouseEvent<HTMLDivElement>) => void;
@@ -5,6 +5,7 @@ export function RelatedArticlesView({
filteredPosts, filteredPosts,
slugOrder, slugOrder,
isMobile, isMobile,
manualScroll,
transformStyle, transformStyle,
getProgressStyle, getProgressStyle,
onMouseDown, onMouseDown,
@@ -76,12 +77,13 @@ export function RelatedArticlesView({
? "lg:gap-[var(--spacing-scale-012)] lg:pl-[var(--spacing-scale-024)]" ? "lg:gap-[var(--spacing-scale-012)] lg:pl-[var(--spacing-scale-024)]"
: "" : ""
} ${ } ${
!isMobile manualScroll
? "overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing" ? "overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing"
: "" : ""
}`} }`}
data-testid="related-articles-track"
style={transformStyle} style={transformStyle}
onMouseDown={!isMobile ? onMouseDown : undefined} onMouseDown={manualScroll ? onMouseDown : undefined}
> >
{filteredPosts.map((relatedPost) => ( {filteredPosts.map((relatedPost) => (
<div <div
@@ -100,8 +102,7 @@ export function RelatedArticlesView({
</div> </div>
</div> </div>
{/* Progress bars - only show on mobile */} {isMobile && !isUseCases && (
{isMobile && (
<div className="flex justify-center gap-[var(--measures-spacing-008)] px-[var(--measures-spacing-064)]"> <div className="flex justify-center gap-[var(--measures-spacing-008)] px-[var(--measures-spacing-064)]">
{filteredPosts.map((relatedPost, index) => ( {filteredPosts.map((relatedPost, index) => (
<div <div
+23 -6
View File
@@ -98,11 +98,28 @@ describe("RelatedArticles", () => {
variant="useCases" variant="useCases"
/>, />,
); );
expect( const heading = screen.getByRole("heading", {
screen.getByRole("heading", { level: 2,
level: 2, name: /Tools to set your group up for success/,
name: /Tools to set your group up for success/, });
}), expect(heading).toBeInTheDocument();
).toBeInTheDocument(); expect(heading).toHaveClass(
"font-bricolage-grotesque",
"lg:text-xx-large-heading",
);
});
it("useCases variant uses a mouse-draggable track", () => {
render(
<RelatedArticles
relatedPosts={mockPosts}
currentPostSlug="current"
variant="useCases"
/>,
);
expect(screen.getByTestId("related-articles-track")).toHaveClass(
"overflow-x-auto",
"cursor-grab",
);
}); });
}); });
+1 -1
View File
@@ -72,7 +72,7 @@ describe("ContentContainer", () => {
const title = screen.getByText("Test Article Title"); const title = screen.getByText("Test Article Title");
expect(title).toBeInTheDocument(); expect(title).toBeInTheDocument();
expect(title).toHaveClass( expect(title).toHaveClass(
"font-bricolage", "font-bricolage-grotesque",
"font-medium", "font-medium",
"text-xx-small-display", "text-xx-small-display",
"text-[var(--color-content-inverse-brand-royal)]", "text-[var(--color-content-inverse-brand-royal)]",