Refine use cases rule examples

This commit is contained in:
adilallo
2026-05-19 22:16:08 -06:00
parent 7c46cbd87b
commit 2f2b5d0dc2
65 changed files with 3129 additions and 252 deletions
+14
View File
@@ -0,0 +1,14 @@
import type { ReactNode } from "react";
/** Full-viewport case-study surfaces (completed rule demos) — no marketing footer. */
export default function MarketingCaseStudyLayout({
children,
}: {
children: ReactNode;
}) {
return (
<main className="flex h-dvh min-h-0 flex-col overflow-hidden">
{children}
</main>
);
}
@@ -0,0 +1,136 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import CommunityRule from "../../../../../components/type/CommunityRule";
import type { CommunityRuleSection } from "../../../../../components/type/CommunityRule/CommunityRule.types";
import CreateFlowTopNav from "../../../../../components/navigation/CreateFlowTopNav";
import Share from "../../../../../components/modals/Share";
import Alert from "../../../../../components/modals/Alert";
import { CreateFlowHeaderLockup } from "../../../../../(app)/create/components/CreateFlowHeaderLockup";
import {
CREATE_FLOW_MD_UP_GRID_CELL_CLASS,
CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS,
} from "../../../../../(app)/create/components/createFlowLayoutTokens";
import { useCreateFlowMdUp } from "../../../../../(app)/create/hooks/useCreateFlowMdUp";
import { useTranslation } from "../../../../../contexts/MessagesContext";
import type { UseCaseDetailSlug } from "../../../../../../lib/useCaseSyntheticPost";
import type { UseCaseCompletedRuleFixture } from "../../../../../../lib/useCaseCompletedRule";
import {
useUseCaseCompletedRuleActions,
type UseCaseCompletedRuleActionBanner,
} from "./useUseCaseCompletedRuleActions";
export type UseCaseCompletedRuleViewProps = {
slug: UseCaseDetailSlug;
fixture: UseCaseCompletedRuleFixture;
sections: CommunityRuleSection[];
};
/** Figma: Completed CR — use case demos (21995:39476, 21995:40092, 22015:42413). */
export function UseCaseCompletedRuleView({
slug,
fixture,
sections,
}: UseCaseCompletedRuleViewProps) {
const router = useRouter();
const mdUp = useCreateFlowMdUp();
const tTopNav = useTranslation("pages.useCasesCompletedRule.topNav");
const [shareModalOpen, setShareModalOpen] = useState(false);
const [actionBanner, setActionBanner] =
useState<UseCaseCompletedRuleActionBanner | null>(null);
const { copyPageLink, mailtoPageLink, handleDuplicate } =
useUseCaseCompletedRuleActions({
slug,
fixture,
setActionBanner,
});
const pageBg = fixture.pageBackground;
return (
<>
{/*
Mobile: grid scrolls (title sticky at top of scrollport).
Desktop: viewport-tall columns; rule scrolls in the right column only.
*/}
<div
className="flex min-h-0 w-full flex-1 flex-col overflow-hidden md:h-full"
style={{ background: pageBg }}
>
{actionBanner ? (
<div className="pointer-events-none fixed inset-x-0 top-0 z-20 flex justify-center px-5 pt-3">
<div className="pointer-events-auto w-full max-w-[639px]">
<Alert
type="banner"
status={actionBanner.status}
title={actionBanner.title}
description={actionBanner.description}
hasLeadingIcon
hasBodyText={Boolean(actionBanner.description)}
onClose={() => setActionBanner(null)}
className="w-full"
/>
</div>
</div>
) : null}
<Share
isOpen={shareModalOpen}
onClose={() => setShareModalOpen(false)}
onCopyLink={() => void copyPageLink()}
onEmailShare={mailtoPageLink}
onSignalShare={() => void copyPageLink()}
onSlackShare={() => void copyPageLink()}
onDiscordShare={() => void copyPageLink()}
/>
<CreateFlowTopNav
hasShare
hasDuplicate
duplicateLabel={tTopNav("duplicate")}
duplicateAriaLabel={tTopNav("duplicateAriaLabel")}
exitLabel={tTopNav("return")}
buttonPalette="inverse"
className="shrink-0 !bg-transparent"
onShare={() => setShareModalOpen(true)}
onDuplicate={() => void handleDuplicate()}
onExit={() => router.push(`/use-cases/${slug}`)}
/>
<div
className={`mx-auto grid w-full min-h-0 flex-1 grid-cols-1 gap-4 px-5 max-md:max-w-[639px] max-md:gap-6 max-md:overflow-y-auto max-md:overscroll-y-contain max-md:pt-[var(--space-800)] max-md:pb-8 md:h-full md:flex-1 md:grid-cols-2 md:grid-rows-1 md:items-start md:justify-items-center md:gap-[var(--measures-spacing-1200,48px)] md:overflow-hidden md:px-12 md:py-0 ${CREATE_FLOW_TWO_COLUMN_MAX_WIDTH_CLASS}`}
>
<div
className={`relative z-[1] flex flex-col justify-start max-md:sticky max-md:top-0 max-md:z-10 max-md:shrink-0 max-md:pb-4 md:sticky md:top-0 md:z-[1] md:flex md:h-[calc(100dvh-4rem)] md:max-h-[calc(100dvh-4rem)] md:flex-col md:justify-center md:self-start md:overflow-hidden md:pb-8 ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
style={{ background: pageBg }}
>
<CreateFlowHeaderLockup
title={fixture.title}
description={fixture.summary}
justification="left"
palette="inverse"
/>
</div>
<div
className={`scrollbar-hide relative z-0 flex min-h-min flex-col overflow-x-hidden max-md:shrink-0 md:h-[calc(100dvh-4rem)] md:max-h-[calc(100dvh-4rem)] md:min-h-0 md:self-start md:overflow-y-auto ${CREATE_FLOW_MD_UP_GRID_CELL_CLASS}`}
>
<div
className="pointer-events-none sticky top-0 z-10 hidden h-5 shrink-0 md:block"
style={{
backgroundImage: `linear-gradient(to bottom, color-mix(in srgb, ${pageBg} 55%, transparent), color-mix(in srgb, ${pageBg} 20%, transparent) 50%, transparent)`,
}}
aria-hidden
/>
<div className="w-full min-w-0 py-0 md:pb-8">
<CommunityRule
sections={sections}
useCardStyle={!mdUp}
cardAccentColor={pageBg}
className={mdUp ? "min-w-0" : "w-full min-w-0 p-4"}
/>
</div>
</div>
</div>
</div>
</>
);
}
@@ -0,0 +1,122 @@
"use client";
import { useCallback, useState } from "react";
import { usePathname, useRouter } from "next/navigation";
import { useAuthModal } from "../../../../../contexts/AuthModalContext";
import { useTranslation } from "../../../../../contexts/MessagesContext";
import {
duplicateUseCaseTemplate,
fetchAuthSession,
} from "../../../../../../lib/create/api";
import type { UseCaseDetailSlug } from "../../../../../../lib/useCaseSyntheticPost";
import type { UseCaseCompletedRuleFixture } from "../../../../../../lib/useCaseCompletedRule";
export type UseCaseCompletedRuleActionBanner = {
key: string;
status: "positive" | "danger";
title: string;
description?: string;
};
export function useUseCaseCompletedRuleActions({
slug,
fixture,
setActionBanner,
}: {
slug: UseCaseDetailSlug;
fixture: UseCaseCompletedRuleFixture;
setActionBanner: (_: UseCaseCompletedRuleActionBanner | null) => void;
}) {
const router = useRouter();
const pathname = usePathname();
const { openLogin } = useAuthModal();
const t = useTranslation("pages.useCasesCompletedRule.topNav");
const [duplicateBusy, setDuplicateBusy] = useState(false);
const copyPageLink = useCallback(async () => {
if (typeof window === "undefined") return;
try {
await navigator.clipboard.writeText(window.location.href);
setActionBanner({
key: "shareCopied",
status: "positive",
title: t("shareLinkCopiedTitle"),
description: t("shareLinkCopiedDescription"),
});
} catch {
setActionBanner({
key: "shareCopyFailed",
status: "danger",
title: t("shareCopyFailedTitle"),
description: t("shareCopyFailedDescription"),
});
}
}, [setActionBanner, t]);
const mailtoPageLink = useCallback(() => {
if (typeof window === "undefined") return;
const url = window.location.href;
const subject = encodeURIComponent(fixture.title);
const body = encodeURIComponent(`${fixture.summary}\n\n${url}`);
window.location.href = `mailto:?subject=${subject}&body=${body}`;
}, [fixture.summary, fixture.title]);
const handleDuplicate = useCallback(async () => {
if (duplicateBusy) return;
setActionBanner(null);
const { user } = await fetchAuthSession();
if (!user) {
openLogin({
nextPath:
pathname && pathname.length > 0
? pathname
: `/use-cases/${slug}/rule`,
backdropVariant: "blurredYellow",
});
return;
}
setDuplicateBusy(true);
const res = await duplicateUseCaseTemplate(slug);
setDuplicateBusy(false);
if (res.ok === false) {
if (res.status === 401) {
openLogin({
nextPath:
pathname && pathname.length > 0
? pathname
: `/use-cases/${slug}/rule`,
backdropVariant: "blurredYellow",
});
return;
}
setActionBanner({
key: "duplicateFailed",
status: "danger",
title: t("duplicateFailedTitle"),
description:
res.status === 404 ? t("duplicateNotFoundDescription") : res.error,
});
return;
}
router.push("/profile");
}, [
duplicateBusy,
openLogin,
pathname,
router,
setActionBanner,
slug,
t,
]);
return {
copyPageLink,
mailtoPageLink,
handleDuplicate,
};
}
@@ -0,0 +1,66 @@
/**
* Figma: Completed CR — use case community rule demos
* (21995:39476, 21995:40092, 22015:42413)
*/
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import messages from "../../../../../messages/en/index";
import { resolveUseCaseCompletedRule } from "../../../../../lib/useCaseCompletedRule";
import {
USE_CASE_DETAIL_SLUGS,
useCaseContentKeyForSlug,
} from "../../../../../lib/useCaseSyntheticPost";
import { UseCaseCompletedRuleView } from "./_components/UseCaseCompletedRule.view";
type PageProps = {
params: Promise<{ slug: string }>;
};
export function generateStaticParams() {
return USE_CASE_DETAIL_SLUGS.map((slug) => ({ slug }));
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const resolved = resolveUseCaseCompletedRule(
slug,
messages.pages.useCasesCompletedRules,
);
if (!resolved) {
return {};
}
const contentKey = useCaseContentKeyForSlug(resolved.slug);
const meta = messages.metadata.useCasesCompletedRule[contentKey];
return {
title: meta.title,
description: meta.description,
keywords: meta.keywords,
openGraph: {
title: meta.title,
description: meta.description,
type: "website",
siteName: "CommunityRule",
},
};
}
export default async function UseCaseCompletedRulePage({ params }: PageProps) {
const { slug } = await params;
const resolved = resolveUseCaseCompletedRule(
slug,
messages.pages.useCasesCompletedRules,
);
if (!resolved) {
notFound();
}
return (
<UseCaseCompletedRuleView
slug={resolved.slug}
fixture={resolved.fixture}
sections={resolved.sections}
/>
);
}