App reorganization

This commit is contained in:
adilallo
2026-04-18 14:12:49 -06:00
parent f866d11ff8
commit e9dab04b34
288 changed files with 2698 additions and 5029 deletions
-381
View File
@@ -1,381 +0,0 @@
# Custom Hooks Documentation
This document provides comprehensive documentation for all custom hooks available in the project.
## Overview
Custom hooks encapsulate reusable logic and patterns across components, improving code organization, maintainability, and consistency.
## Available Hooks
### `useClickOutside`
Detects clicks outside of specified elements. Useful for closing dropdowns, modals, or menus.
**Location:** `app/hooks/useClickOutside.ts`
**Usage:**
```tsx
import { useClickOutside } from "../hooks";
const menuRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const [isOpen, setIsOpen] = useState(false);
useClickOutside([menuRef, buttonRef], () => setIsOpen(false), isOpen);
```
**Parameters:**
- `refs`: Array of refs to elements that should not trigger the callback
- `handler`: Callback function to execute when clicking outside
- `enabled`: Whether the hook is enabled (default: true)
**Example:** Used in `Select.tsx` for closing dropdown menus
---
### `useAnalytics`
Centralized analytics tracking for component interactions. Supports both Google Analytics (gtag) and custom callbacks.
**Location:** `app/hooks/useAnalytics.ts`
**Usage:**
```tsx
import { useAnalytics } from "../hooks";
const { trackEvent, trackCustomEvent } = useAnalytics();
// Standard event tracking
trackEvent({
event: "button_click",
category: "engagement",
label: "contact_button",
component: "AskOrganizer",
});
// Custom event with callback
trackCustomEvent(
"contact_button_click",
{
component: "AskOrganizer",
variant: "centered",
},
onContactClick, // Optional callback
);
```
**Returns:**
- `trackEvent`: Function to track standard analytics events
- `trackCustomEvent`: Function to track custom events with optional callback
**Example:** Used in `AskOrganizer.tsx` for tracking button clicks
---
### `useComponentId`
Generates unique component IDs for accessibility. Provides consistent ID generation pattern.
**Location:** `app/hooks/useComponentId.ts`
**Usage:**
```tsx
import { useComponentId } from "../hooks";
const { id, labelId } = useComponentId("input", props.id);
// id: "input-123" or props.id if provided
// labelId: "input-123-label"
```
**Parameters:**
- `prefix`: Prefix for the generated ID (e.g., "input", "select")
- `providedId`: Optional ID provided via props (takes precedence)
**Returns:**
- `id`: Component ID
- `labelId`: Associated label ID for accessibility
**Example:** Used in `Input.tsx`, `TextArea.tsx`, `Checkbox.tsx`
---
### `useFormField`
Manages form field event handlers with disabled state handling. Ensures handlers respect disabled state.
**Location:** `app/hooks/useFormField.ts`
**Usage:**
```tsx
import { useFormField } from "../hooks";
const { handleChange, handleFocus, handleBlur } = useFormField(disabled, {
onChange: (e) => setValue(e.target.value),
onFocus: (e) => setFocused(true),
onBlur: (e) => setFocused(false),
});
// Use in component
<input onChange={handleChange} onFocus={handleFocus} onBlur={handleBlur} />;
```
**Parameters:**
- `disabled`: Whether the field is disabled
- `handlers`: Object containing onChange, onFocus, onBlur handlers
**Returns:**
- `handleChange`: Wrapped onChange handler
- `handleFocus`: Wrapped onFocus handler
- `handleBlur`: Wrapped onBlur handler
**Example:** Used in `Input.tsx`, `TextArea.tsx`
---
### `useComponentStyles`
Manages component size and state styles. Provides a consistent pattern for styling components.
**Location:** `app/hooks/useComponentStyles.ts`
**Usage:**
```tsx
import { useComponentStyles } from "../hooks";
const sizeStyles = {
small: { input: "h-8 text-xs", label: "text-xs" },
medium: { input: "h-10 text-sm", label: "text-sm" },
};
const stateStyles = {
default: { input: "border-gray-300", label: "text-gray-700" },
focus: { input: "border-blue-500", label: "text-gray-700" },
};
const { sizeClasses, stateClasses } = useComponentStyles({
size: "medium",
state: "default",
disabled: false,
error: false,
sizeStyles,
stateStyles,
});
```
**Note:** This hook is available but styling logic is often component-specific. Consider using it when you have multiple components with similar styling patterns.
---
### `useSchemaData`
Generates Schema.org structured data (JSON-LD) for SEO and search engines.
**Location:** `app/hooks/useSchemaData.ts`
**Usage:**
```tsx
import { useSchemaData } from "../hooks";
// HowTo schema
const schemaData = useSchemaData({
type: "HowTo",
name: "How to build a community",
description: "Step-by-step guide",
steps: [
{ name: "Step 1", text: "Start here" },
{ name: "Step 2", text: "Continue here" },
],
});
// Organization schema
const orgSchema = useSchemaData({
type: "Organization",
name: "Media Economies Design Lab",
url: "https://communityrule.com",
email: "medlab@colorado.edu",
sameAs: ["https://twitter.com/medlab"],
});
// Render in component
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }}
/>;
```
**Supported Types:**
- `Organization` - Organization information
- `WebSite` - Website navigation and search
- `HowTo` - Step-by-step instructions
- `Article` - Blog posts and articles
- `BreadcrumbList` - Navigation breadcrumbs
**Example:** Used in `NumberedCards.tsx`, `Header.tsx`, `Footer.tsx`
---
### `useMediaQuery`
Responsive breakpoint detection using window.matchMedia.
**Location:** `app/hooks/useMediaQuery.ts`
**Usage:**
```tsx
import { useMediaQuery, useIsMobile, useIsDesktop } from "../hooks";
// Using breakpoint key
const isMobile = useMediaQuery("lg", "max");
// Returns true if screen width < 1024px
// Using custom query
const isDesktop = useMediaQuery("(min-width: 1024px)");
// Convenience hooks
const isMobile = useIsMobile(); // Below lg breakpoint
const isDesktop = useIsDesktop(); // lg breakpoint and above
```
**Available Breakpoints:**
- `sm`: 640px
- `md`: 768px
- `lg`: 1024px
- `xl`: 1280px
- `2xl`: 1536px
**Example:** Used in `RelatedArticles.tsx` for responsive behavior
---
### `useFormValidation`
Form validation with field-level error handling.
**Location:** `app/hooks/useFormValidation.ts`
**Usage:**
```tsx
import { useFormValidation, validationRules } from "../hooks";
const {
values,
errors,
touched,
handleChange,
handleBlur,
validate,
isValid,
reset,
} = useFormValidation({
initialValues: { email: "", password: "" },
validationRules: {
email: [validationRules.required, validationRules.email],
password: [validationRules.required, validationRules.minLength(8)],
},
validateOnChange: true,
validateOnBlur: true,
});
// In component
<input
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>;
{
errors.email && touched.email && <span>{errors.email}</span>;
}
```
**Available Validation Rules:**
- `validationRules.required` - Field is required
- `validationRules.email` - Valid email format
- `validationRules.minLength(n)` - Minimum length
- `validationRules.maxLength(n)` - Maximum length
- `validationRules.pattern(regex, message)` - Custom regex pattern
**Returns:**
- `values` - Current form values
- `errors` - Field error messages
- `touched` - Fields that have been interacted with
- `handleChange` - Change handler
- `handleBlur` - Blur handler
- `validate` - Manual validation function
- `isValid` - Boolean indicating if form is valid
- `reset` - Reset form to initial values
- `setValue` - Programmatically set field value
---
## Best Practices
1. **Import from index:** Always import hooks from `app/hooks` index file:
```tsx
import { useAnalytics, useComponentId } from "../hooks";
```
2. **TypeScript:** All hooks are fully typed. Use TypeScript for better IDE support.
3. **Testing:** Hooks should be tested independently. See `tests/unit/hooks/` for examples.
4. **Documentation:** When creating new hooks, add JSDoc comments and update this documentation.
5. **Performance:** Hooks use `useMemo` and `useCallback` where appropriate to prevent unnecessary re-renders.
## Refactored Components
The following components have been refactored to use custom hooks:
- **Select** - Uses `useClickOutside` (now uses Container/Presentation pattern)
- **AskOrganizer** - Uses `useAnalytics` (now uses Container/Presentation pattern)
- **Input.tsx** - Uses `useComponentId` and `useFormField`
- **TextArea.tsx** - Uses `useComponentId` and `useFormField`
- **Checkbox.tsx** - Uses `useComponentId`
- **NumberedCards** - Uses `useSchemaData` (now uses Container/Presentation pattern)
- **RelatedArticles.tsx** - Uses `useIsMobile`
> **Note**: Components marked with "Container/Presentation pattern" have been refactored to separate logic (container) from presentation (view). Hooks are used in the container components. See [Container/Presentation Pattern Guide](./guides/container-presentation-pattern.md) for details.
## Adding New Hooks
When creating a new hook:
1. Create the hook file in `app/hooks/`
2. Export it from `app/hooks/index.ts`
3. Add JSDoc comments with examples
4. Write unit tests in `tests/unit/hooks/`
5. Update this documentation
6. Refactor at least one component to use it as a proof of concept
## Testing
All hooks have unit tests in `tests/unit/hooks/`. Run tests with:
```bash
npm test -- tests/unit/hooks
```
## See Also
- [React Hooks Documentation](https://react.dev/reference/react) - Official React hooks documentation
+29 -64
View File
@@ -1,76 +1,41 @@
# Documentation
This directory contains project documentation organized by topic.
User-facing docs. Implementation conventions live in `.cursor/rules/`.
## Documentation Structure
## Canonical references
### Core Documentation
- [create-flow.md](./create-flow.md) — Custom create-rule wizard: stages,
URLs, persistence. Source of truth for product/eng alignment.
- [testing-guide.md](./testing-guide.md) — Testing philosophy and what to
cover at each layer.
- **[TESTING_GUIDE.md](./TESTING_GUIDE.md)** - Complete testing guide covering component tests, E2E tests, accessibility, and testing philosophy
- **[CUSTOM_HOOKS.md](./CUSTOM_HOOKS.md)** - Documentation for all custom React hooks used in the project
## Author guides (`guides/`)
### Guides (`guides/`)
- [content-creation.md](./guides/content-creation.md) — Writing and
publishing blog posts.
- [i18n-translation-workflow.md](./guides/i18n-translation-workflow.md) —
Editing UI copy and translation bundles.
- **[container-presentation-pattern.md](./guides/container-presentation-pattern.md)** - Container/Presentation pattern guide for component architecture
- **[content-creation.md](./guides/content-creation.md)** - Guide for creating blog content
- **[i18n-translation-workflow.md](./guides/i18n-translation-workflow.md)** - Guide for working with translations and UI content management
## Temporary backend planning
## Quick Navigation
These will be deleted once the backend services are stood up:
### For Testing
- [guides/backend-roadmap.md](./guides/backend-roadmap.md)
- [guides/backend-linear-tickets.md](./guides/backend-linear-tickets.md)
- [guides/template-recommendation-matrix.md](./guides/template-recommendation-matrix.md)
Start with **[TESTING_GUIDE.md](./TESTING_GUIDE.md)** for:
## Cursor rules
- Testing philosophy and approach
- Component testing with `componentTestSuite`
- E2E testing with Playwright
- Accessibility testing
- How to add tests for new components
Implementation contracts are enforced by `.cursor/rules/*.mdc`:
### For Custom Hooks
See **[CUSTOM_HOOKS.md](./CUSTOM_HOOKS.md)** for:
- Available custom hooks
- Usage examples
- API documentation
### For Component Architecture
See **[container-presentation-pattern.md](./guides/container-presentation-pattern.md)** for:
- Container/Presentation pattern overview
- When and how to use the pattern
- Migration guide for existing components
- Best practices and examples
### For Content Creation
See **[content-creation.md](./guides/content-creation.md)** for:
- How to create blog articles
- Content guidelines
- Contribution workflow
### For Translations and UI Content
See **[i18n-translation-workflow.md](./guides/i18n-translation-workflow.md)** for:
- How to update UI text content
- Translation key structure and naming conventions
- How to extract strings from components
- How to add new translation keys
- Best practices for content creators and developers
## Additional Resources
- **Vitest Documentation**: https://vitest.dev/
- **Playwright Documentation**: https://playwright.dev/
- **React Testing Library**: https://testing-library.com/
- **Lighthouse CI**: https://github.com/GoogleChrome/lighthouse-ci
- **Next.js Documentation**: https://nextjs.org/docs
---
**Last Updated**: January 2025
**Maintained by**: CommunityRule Development Team
| Rule | Scope |
| --- | --- |
| `component-structure.mdc` | 4-file split (container/view/types/index). |
| `component-props.mdc` | Lowercase enum prop convention + Figma traceability. |
| `tailwind-styling.mdc` | Token usage and class composition. |
| `localization.mdc` | `messages/` bundles and `useMessages()`. |
| `testing.mdc` | Test layout, helpers, required imports. |
| `storybook.mdc` | Story location and `argTypes` patterns. |
| `hooks.mdc` | Custom hook authoring + TSDoc as the API reference. |
| `create-flow.mdc` | Wizard step / screen registry conventions. |
| `api-routes.mdc` | API route handler conventions. |
-266
View File
@@ -1,266 +0,0 @@
## Testing Guide
### Philosophy
- **Test behaviour, not implementation**: Focus on what the user can see and do, not internal details.
- **Single source of truth per component**: Each component should have **one consolidated test file**.
- **Accessibility is mandatory**: Basic a11y checks run as part of every component suite.
- **E2E is sparse**: Only cover critical user journeys that span pages or systems.
### Test Structure
The test directory structure is organized as follows:
```text
tests/
components/ # All component-focused tests (Vitest + RTL)
Button.test.tsx
Input.test.tsx
Checkbox.test.tsx
Select.test.tsx
Switch.test.tsx
pages/ # Page-level tests (home, blog, etc.)
home.test.jsx
blog.test.jsx
e2e/ # True endtoend flows + visual regression (Playwright)
critical-journeys.spec.ts # Main user journeys (homepage, navigation, interactions)
visual-regression.spec.ts # Critical page screenshots only (5 tests)
edge-cases.spec.ts # Critical error scenarios (4 tests)
performance.spec.ts # Essential performance checks (2 tests)
utils/ # Shared test utilities
componentTestSuite.tsx
msw/ # MSW server setup for mocking
server.ts
accessibility/
e2e/ # E2E accessibility checks (WCAG compliance)
wcag-compliance.spec.ts
```
**Component tests** (`tests/components/`) use the standard `componentTestSuite` utility to ensure consistent baseline coverage for all UI components. **Page tests** (`tests/pages/`) cover page-level rendering and flows. **E2E tests** (`tests/e2e/`) focus on critical user journeys, visual regression, and performance. **Accessibility E2E** (`tests/accessibility/e2e/`) provides high-level WCAG compliance checks.
### E2E Testing Philosophy
E2E tests follow a **sparse, critical-path approach** optimized for open source projects:
- **Focus on user value**: Test critical user journeys that span multiple pages or systems, not individual component interactions
- **Maintainability over coverage**: Keep tests maintainable and contributor-friendly rather than comprehensive
- **Visual regression is minimal**: Only capture screenshots of major pages (homepage, blog listing/post, 404), not every component or viewport
- **Performance monitoring is essential**: Track homepage load and Core Web Vitals, but detailed performance analysis is handled by Lighthouse CI
- **Edge cases are critical only**: Test scenarios that would break user experience (slow network, offline mode, JS errors, missing images)
This approach reduces test maintenance burden while ensuring critical functionality remains stable.
### Standard Component Test Suite
Use the shared suite in `tests/utils/componentTestSuite.tsx` to get a consistent baseline:
```ts
import Component from "../../app/components/Component";
import {
componentTestSuite,
type ComponentTestSuiteConfig,
} from "../utils/componentTestSuite";
type Props = React.ComponentProps<typeof Component>;
const config: ComponentTestSuiteConfig<Props> = {
component: Component,
name: "Component",
props: {
/* default props */
} as Props,
requiredProps: ["label"],
optionalProps: {
disabled: true,
},
queries: {
getPrimaryElement: (s) => s.getByRole("button"),
},
variants: {
disabled: {
props: { disabled: true },
assert: (el) => {
expect(el).toBeDisabled();
},
},
error: {
props: { error: true } as Partial<Props>,
assert: (el) => {
expect(el).toHaveClass(
"border-[var(--color-border-default-utility-negative)]",
);
},
},
},
testCases: {
renders: true,
accessibility: true,
keyboardNavigation: true,
disabledState: true,
errorState: true,
},
};
componentTestSuite<Props>(config);
```
#### What the Standard Suite Covers
- **Rendering**
- Component renders without throwing using the provided `props`.
- Required props are present and do not break rendering.
- Optional props can be applied without breaking.
- **Accessibility**
- Runs `axe` against the rendered output.
- Fails on common WCAG 2.1 issues (roles, labels, contrast, etc.).
- **Keyboard Navigation**
- Ensures the primary element can receive focus.
- Smoketests basic keyboard activation (`Enter`, `Space`) without runtime errors.
- **Disabled State**
- Uses `variants.disabled` to verify disabled behaviour (e.g., `aria-disabled`, `disabled` attribute, tab index).
- **Error State**
- Uses `variants.error` to verify error styling/attributes when applicable.
### When to Add Custom Tests
Use the standard suite for **baseline coverage**, then add custom `describe` blocks in the same file when:
- The component has **important variants** (different sizes, modes, label variants).
- There is **nontrivial interaction** (menus, dropdowns, complex keyboard behaviour).
- You need to exercise **stateful flows** (forms, validation, error messages).
Example (inside the same `*.test.tsx` file):
```ts
describe("Input behaviour specifics", () => {
it("calls onChange when user types", async () => {
// ...
});
});
```
### Test Commands
- **All component tests** (Vitest + RTL):
```bash
npm test
```
- **Component-only tests** (faster inner loop, focused on `tests/components/`):
```bash
npm run test:component
# filter by name:
npm run test:component -- --run tests/components/Button.test.tsx
```
- **E2E tests only** (Playwright):
```bash
npm run test:e2e
# or, equivalently:
npm run e2e
```
### What to Test vs. What Not to Test
- **Do test**
- Public behaviour: visible text, roles, labels, ARIA, keyboard paths.
- State transitions that users rely on (error -> success, disabled -> enabled).
- Critical component interactions (clicks, form submissions, dropdown selection).
- Accessibility invariants (no axe violations, basic keyboard support).
- **Avoid testing**
- Pure styling details that are likely to change frequently (exact shadow radius, minor spacing).
- Internal implementation details (private helpers, hook internals, memoisation specifics).
- Responsive visibility in JSDOM (use Playwright visual / responsive tests instead).
### Adding Tests for a New Component (≈5 minutes)
1. **Create the component file** in `app/components/`.
2. **Create a test file** in `tests/components/ComponentName.test.tsx`.
3. **Wire the standard suite** using `componentTestSuite`.
4. **Add 13 custom tests** for any unique behaviours.
5. Run:
```bash
npm run test:component -- --run tests/components/ComponentName.test.tsx
```
### E2E and Visual Regression
E2E tests are organized into focused files:
- **`critical-journeys.spec.ts`**: Main user journeys (homepage loads, navigation, key interactions)
- **`visual-regression.spec.ts`**: Critical page screenshots only (homepage full/viewport, blog listing/post, 404)
- **`edge-cases.spec.ts`**: Critical error scenarios (slow network, offline mode, JS errors, missing images)
- **`performance.spec.ts`**: Essential performance checks (homepage load, Core Web Vitals)
**Commands:**
```bash
# Run all E2E tests
npm run test:e2e
# Run visual regression tests only
npm run visual:test
# Update visual regression snapshots (after UI changes)
npm run visual:update
# Run specific test file
npx playwright test tests/e2e/critical-journeys.spec.ts
```
**When to add E2E tests:**
- **Add E2E tests** when:
- A new critical user journey is introduced (e.g., new multi-step flow)
- A major page is added that needs visual regression coverage
- A critical error scenario needs to be tested (e.g., payment failure, form submission errors)
- **Don't add E2E tests** for:
- Component-level interactions (use component tests instead)
- Single-page functionality (use page tests instead)
- Minor UI changes (visual regression will catch major regressions)
- Edge cases that don't impact core user experience
**Visual regression snapshots:**
Visual regression tests capture screenshots of critical pages. When UI changes are intentional, update snapshots:
```bash
npm run visual:update
```
This updates snapshots for all 5 critical page tests. Review the changes carefully before committing.
### Storybook
**Storybook is used for component documentation and visual review only.** It is not used for automated testing.
- Component tests (`tests/components/*.test.tsx`) provide all test coverage previously handled by Storybook test-runner
- Storybook stories (`stories/*.stories.js`) serve as living documentation and visual examples
- Interaction functions are inlined in story files for demonstration purposes
- Run Storybook locally with `npm run storybook` for component development and review
### Accessibility Testing
Accessibility is tested at two levels:
1. **Component-level accessibility** (`tests/components/*.test.tsx`):
- Automatically covered by `componentTestSuite` using `jest-axe`
- Tests roles, labels, ARIA attributes, keyboard navigation
- Runs as part of every component test suite
2. **Full-page accessibility** (`tests/accessibility/e2e/wcag-compliance.spec.ts`):
- E2E tests using Playwright and `@axe-core/playwright`
- Validates WCAG 2.1 AA compliance across entire pages
- Tests complete user journeys for accessibility barriers
This two-tier approach ensures both individual components and full page experiences meet accessibility standards.
+16 -14
View File
@@ -1,20 +1,20 @@
# Create rule flow (custom wizard) — canonical reference
Product/engineering reference for the **custom** “Create rule” experience: URL order, persistence, and entry points. **Implementation work** to align code with this doc (progress bar, resume redirects, etc.) is tracked in Linear **[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)** and [docs/backend-linear-tickets.md](backend-linear-tickets.md) **Ticket 17**.
Product/engineering reference for the **custom** “Create rule” experience: URL order, persistence, and entry points. **Implementation work** to align code with this doc (progress bar, resume redirects, etc.) is tracked in Linear **[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)** and [docs/guides/backend-linear-tickets.md](guides/backend-linear-tickets.md) **Ticket 17**.
---
## Product stages (Figma)
The Figma **Create Community** sequence is the **source of truth** for the first segment of the wizard (eight frames). After **`review`**, the flow continues with **Create Custom CommunityRule** and **Review and complete** stages. The shipped URL sequence in [`FLOW_STEP_ORDER`](../app/create/utils/flowSteps.ts) **follows that trajectory**; stages are a **product** slice of that linear order, not separate routers today.
The Figma **Create Community** sequence is the **source of truth** for the first segment of the wizard (eight frames). After **`review`**, the flow continues with **Create Custom CommunityRule** and **Review and complete** stages. The shipped URL sequence in [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSteps.ts) **follows that trajectory**; stages are a **product** slice of that linear order, not separate routers today.
| Stage (Figma) | Purpose (summary) | `CreateFlowStep` values (in order) |
| --- | --- | --- |
| **Create Community** | Intro, naming, structure, context, size, upload, save progress (email), then community review. | `informational``community-name``community-structure``community-context``community-size``community-upload``community-save``review` |
| **Create Custom CommunityRule** | Author the CommunityRule content and structure. | `core-values``communication-methods``right-rail` (further card-stack steps get their own `screenId` and `screens/card/*Screen.tsx`; `right-rail` uses `layoutKind: "right-rail"`) |
| **Create Custom CommunityRule** | Author the CommunityRule content and structure (core values + four card-stack categories). | `core-values``communication-methods``membership-methods``decision-approaches``conflict-management` |
| **Review and complete** | Stakeholders, final card, publish, success. | `confirm-stakeholders``final-review``completed` |
Treat these stages as the **canonical product sections** when adding chrome (e.g. stage headers, progress copy), breaking work across teams, or reusing flows in other surfaces. **Layout kind** is **not** encoded in the URL; it lives in [`CREATE_FLOW_SCREEN_REGISTRY`](../app/create/utils/createFlowScreenRegistry.ts) (Figma node id + `layoutKind` per step). Figma defines eight layout kinds: **informational**, **text**, **select**, **upload**, **review**, **card**, **right-rail**, **completed**`CreateFlowLayoutKind` and [`app/create/screens/`](../app/create/screens/) mirror that list (one folder per kind; multiple steps may share a kind, e.g. several **select** screens).
Treat these stages as the **canonical product sections** when adding chrome (e.g. stage headers, progress copy), breaking work across teams, or reusing flows in other surfaces. **Layout kind** is **not** encoded in the URL; it lives in [`CREATE_FLOW_SCREEN_REGISTRY`](../app/(app)/create/utils/createFlowScreenRegistry.ts) (Figma node id + `layoutKind` per step). Figma defines eight layout kinds: **informational**, **text**, **select**, **upload**, **review**, **card**, **right-rail**, **completed**`CreateFlowLayoutKind` and [`app/(app)/create/screens/`](../app/(app)/create/screens/) mirror that list (one folder per kind; multiple steps may share a kind, e.g. several **select** screens).
**Create from template (future):** A full **template-driven** create path is **not** finalized; it will likely live on **additional route(s)** (and may reuse these stages where it overlaps the custom trajectory). Today, **`/create/review-template/[slug]`** is only an auxiliary **preview** in the create shell; it is **not** a Figma stage and not the final template-create entry. See **Out of scope** in [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo).
@@ -22,7 +22,7 @@ Treat these stages as the **canonical product sections** when adding chrome (e.g
## Step order and URLs
Order is defined in code by [`FLOW_STEP_ORDER`](../app/create/utils/flowSteps.ts) and the [`CreateFlowStep`](../app/create/types.ts) type. Wizard steps use a **single dynamic route**: [`app/create/[screenId]/page.tsx`](../app/create/[screenId]/page.tsx), which validates `screenId` and renders [`CreateFlowScreenView`](../app/create/screens/CreateFlowScreenView.tsx). Implementation files are grouped under [`app/create/screens/`](../app/create/screens/) by Figma **layout kind** (subfolders: informational, text, select, upload, review, card, right-rail, completed). **`/create`** redirects to the first step.
Order is defined in code by [`FLOW_STEP_ORDER`](../app/(app)/create/utils/flowSteps.ts) and the [`CreateFlowStep`](../app/(app)/create/types.ts) type. Wizard steps use a **single dynamic route**: [`app/(app)/create/[screenId]/page.tsx`](../app/(app)/create/[screenId]/page.tsx), which validates `screenId` and renders [`CreateFlowScreenView`](../app/(app)/create/screens/CreateFlowScreenView.tsx). Implementation files are grouped under [`app/(app)/create/screens/`](../app/(app)/create/screens/) by Figma **layout kind** (subfolders: informational, text, select, upload, review, card, right-rail, completed). **`/create`** redirects to the first step.
| Order | Figma stage | Step ID (`screenId`) | Path |
| ----: | ----------- | -------------------- | ---- |
@@ -36,14 +36,16 @@ Order is defined in code by [`FLOW_STEP_ORDER`](../app/create/utils/flowSteps.ts
| 8 | Create Community (review frame) | `review` | `/create/review` |
| 9 | Create Custom CommunityRule | `core-values` | `/create/core-values` |
| 10 | Create Custom CommunityRule | `communication-methods` | `/create/communication-methods` |
| 11 | Create Custom CommunityRule | `right-rail` | `/create/right-rail` |
| 12 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` |
| 13 | Review and complete | `final-review` | `/create/final-review` |
| 14 | Review and complete | `completed` | `/create/completed` |
| 11 | Create Custom CommunityRule | `membership-methods` | `/create/membership-methods` |
| 12 | Create Custom CommunityRule | `decision-approaches` | `/create/decision-approaches` |
| 13 | Create Custom CommunityRule | `conflict-management` | `/create/conflict-management` |
| 14 | Review and complete | `confirm-stakeholders` | `/create/confirm-stakeholders` |
| 15 | Review and complete | `final-review` | `/create/final-review` |
| 16 | Review and complete | `completed` | `/create/completed` |
**Primary entry:** marketing header “Create rule” navigates to **`/create`**, which redirects to **`/create/informational`** (see [`TopNav.container.tsx`](../app/components/navigation/TopNav/TopNav.container.tsx)).
Active step for chrome and navigation is resolved from the pathname via [`parseCreateFlowScreenFromPathname`](../app/create/utils/flowSteps.ts) inside [`useCreateFlowNavigation`](../app/create/hooks/useCreateFlowNavigation.ts).
Active step for chrome and navigation is resolved from the pathname via [`parseCreateFlowScreenFromPathname`](../app/(app)/create/utils/flowSteps.ts) inside [`useCreateFlowNavigation`](../app/(app)/create/hooks/useCreateFlowNavigation.ts).
---
@@ -61,10 +63,10 @@ From that page, **Customize** currently navigates to `/create/informational?temp
| Mode | Where progress lives | Save & Exit / server draft |
| --- | --- | --- |
| **Anonymous** | `localStorage` key **`create-flow-anonymous`** | **Exit** opens save-progress magic link; after verify, optional **PUT** `/api/drafts/me` when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` (see Tickets 45 in [backend-linear-tickets.md](backend-linear-tickets.md)). |
| **Anonymous** | `localStorage` key **`create-flow-anonymous`** | **Exit** opens save-progress magic link; after verify, optional **PUT** `/api/drafts/me` when `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` (see Tickets 45 in [guides/backend-linear-tickets.md](guides/backend-linear-tickets.md)). |
| **Signed-in** | In-memory React state in **`CreateFlowContext`** | **Save & Exit** from the **`community-structure`** step onward (step index ≥ `community-structure`) may **PUT** `/api/drafts/me` when sync is on. **Sign out** is on profile, not in the create top nav. |
Details and edge cases (conflict confirm, banners, `?syncDraft=1`) match **Ticket 4**, **Ticket 5**, and [`docs/backend-roadmap.md`](backend-roadmap.md) §12.
Details and edge cases (conflict confirm, banners, `?syncDraft=1`) match **Ticket 4**, **Ticket 5**, and [`docs/guides/backend-roadmap.md`](guides/backend-roadmap.md) §12.
---
@@ -77,5 +79,5 @@ Details and edge cases (conflict confirm, banners, `?syncDraft=1`) match **Ticke
## Related docs
- [docs/backend-roadmap.md](backend-roadmap.md) §12 — Frontend hook-up
- [docs/backend-linear-tickets.md](backend-linear-tickets.md) — Tickets 4, 5, 6, 17
- [docs/guides/backend-roadmap.md](guides/backend-roadmap.md) §12 — Frontend hook-up
- [docs/guides/backend-linear-tickets.md](guides/backend-linear-tickets.md) — Tickets 4, 5, 6, 17
@@ -86,7 +86,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Depends on:** Ticket 1 (optional but keeps docs honest).
**Goal:** Replace the open `[key: string]: unknown` shape in [app/create/types.ts](app/create/types.ts) with real fields (or nested objects) agreed with design/product, and validate JSON on the server for drafts and publish.
**Goal:** Replace the open `[key: string]: unknown` shape in [app/(app)/create/types.ts](app/(app)/create/types.ts) with real fields (or nested objects) agreed with design/product, and validate JSON on the server for drafts and publish.
**Context:** `PUT /api/drafts/me` and `POST /api/rules` accept loose objects today; oversized or malformed payloads are a stability and security concern.
@@ -107,7 +107,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Status:** [CR-73](https://linear.app/community-rule/issue/CR-73/backend-formalize-createflowstate-validate-draftpublish-api-payloads) **Done**.
**Files:** [app/create/types.ts](app/create/types.ts), [app/api/drafts/me/route.ts](app/api/drafts/me/route.ts), [app/api/rules/route.ts](app/api/rules/route.ts), [lib/server/validation/](lib/server/validation/) (Zod + plain-JSON checks), [package.json](package.json) (`zod`).
**Files:** [app/(app)/create/types.ts](app/(app)/create/types.ts), [app/api/drafts/me/route.ts](app/api/drafts/me/route.ts), [app/api/rules/route.ts](app/api/rules/route.ts), [lib/server/validation/](lib/server/validation/) (Zod + plain-JSON checks), [package.json](package.json) (`zod`).
**Note:** Repo-wide **API error JSON shape** and **request-id logging** are **Ticket 13 / CR-84**—coordinate 400 response bodies with that issue so validation errors match the agreed `{ error: { code, message } }` pattern.
@@ -125,12 +125,12 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Implementation (shipped):**
1. **`/login`** route **and** **header modal** — primary **Log in** entry is [`AuthModalProvider`](app/contexts/AuthModalContext.tsx) + [app/components/modals/Login/](app/components/modals/Login/); [app/login/page.tsx](app/login/page.tsx) (solid shell, `usePortal={false}`) remains for verify **error** redirects and bookmarks.
1. **`/login`** route **and** **header modal** — primary **Log in** entry is [`AuthModalProvider`](app/contexts/AuthModalContext.tsx) + [app/components/modals/Login/](app/components/modals/Login/); [app/(app)/login/page.tsx](app/(app)/login/page.tsx) (solid shell, `usePortal={false}`) remains for verify **error** redirects and bookmarks.
2. Flow: email → “Send link” → user opens link (email, Mailhog, or dev log) → `GET /api/auth/magic-link/verify?token=...` sets session and redirects; optional `next` for post-login path.
3. Surface API errors: invalid email, 429 `retryAfterMs`, expired/invalid token, network failure (accessible copy).
4. Ensure `fetch` calls use `credentials: "include"` where needed (see [lib/create/api.ts](lib/create/api.ts)).
5. **Dev:** without `SMTP_URL`, verify URL is logged; with Mailhog, use [docker-compose.yml](docker-compose.yml) and `SMTP_URL=smtp://localhost:1025`.
6. **Marketing header:** When signed in (`fetchAuthSession`), **Log in** becomes **Profile** linking to [`/profile`](app/profile/page.tsx) (placeholder until Ticket 15 / CR-86). Implemented in [TopNavWithPathname.tsx](app/components/navigation/TopNav/TopNavWithPathname.tsx) + [TopNav.container.tsx](app/components/navigation/TopNav/TopNav.container.tsx).
6. **Marketing header:** When signed in (`fetchAuthSession`), **Log in** becomes **Profile** linking to [`/profile`](app/(app)/profile/page.tsx) (placeholder until Ticket 15 / CR-86). Implemented in [TopNavWithPathname.tsx](app/components/navigation/TopNav/TopNavWithPathname.tsx) + [TopNav.container.tsx](app/components/navigation/TopNav/TopNav.container.tsx).
**Acceptance criteria:**
@@ -141,7 +141,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Status:** [CR-74](https://linear.app/community-rule/issue/CR-74/backend-magic-link-sign-in-ui-apis-ticket-3-cr-75-done) **Done** for shipped UI/APIs. **Residual checklist** below: repo doc items are **done**; use Linear (CR-74 or child issue) to track **per-environment** staging URL checks.
**Files:** [app/login/](app/login/), [app/profile/](app/profile/) (placeholder), [app/components/modals/Login/](app/components/modals/Login/), [messages/en/pages/login.json](messages/en/pages/login.json), [messages/en/pages/profile.json](messages/en/pages/profile.json), [messages/en/components/header.json](messages/en/components/header.json), [app/components/navigation/TopNav/TopNav.container.tsx](app/components/navigation/TopNav/TopNav.container.tsx), [app/components/navigation/TopNav/TopNavWithPathname.tsx](app/components/navigation/TopNav/TopNavWithPathname.tsx), [lib/create/api.ts](lib/create/api.ts), [app/api/auth/magic-link/request/route.ts](app/api/auth/magic-link/request/route.ts), [app/api/auth/magic-link/verify/route.ts](app/api/auth/magic-link/verify/route.ts), [prisma/schema.prisma](prisma/schema.prisma) (`MagicLinkToken`), [lib/server/mail.ts](lib/server/mail.ts). Onboarding: [CONTRIBUTING.md](CONTRIBUTING.md), [`.env.example`](.env.example).
**Files:** [app/(app)/login/](app/(app)/login/), [app/(app)/profile/](app/(app)/profile/) (placeholder), [app/components/modals/Login/](app/components/modals/Login/), [messages/en/pages/login.json](messages/en/pages/login.json), [messages/en/pages/profile.json](messages/en/pages/profile.json), [messages/en/components/header.json](messages/en/components/header.json), [app/components/navigation/TopNav/TopNav.container.tsx](app/components/navigation/TopNav/TopNav.container.tsx), [app/components/navigation/TopNav/TopNavWithPathname.tsx](app/components/navigation/TopNav/TopNavWithPathname.tsx), [lib/create/api.ts](lib/create/api.ts), [app/api/auth/magic-link/request/route.ts](app/api/auth/magic-link/request/route.ts), [app/api/auth/magic-link/verify/route.ts](app/api/auth/magic-link/verify/route.ts), [prisma/schema.prisma](prisma/schema.prisma) (`MagicLinkToken`), [lib/server/mail.ts](lib/server/mail.ts). Onboarding: [CONTRIBUTING.md](CONTRIBUTING.md), [`.env.example`](.env.example).
### Residual / before CR-75 (create-flow session UI)
@@ -158,24 +158,24 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Depends on:** Ticket 3.
**Goal:** In `/create/*`, **Exit** / **Save & Exit** (from `select` onward for signed-in users) is the only top-nav chrome—no email or Sign out in the create shell. **Anonymous:** progress in **`create-flow-anonymous`** localStorage; **Exit** opens the global **Save your progress?** auth modal (magic link + `?syncDraft=1` return); after verify, [`PostLoginDraftTransfer`](app/create/PostLoginDraftTransfer.tsx) **PUT**s to `/api/drafts/me` when sync is on. **Signed-in:** **Save & Exit** **PUT**s via [`useCreateFlowExit`](app/create/hooks/useCreateFlowExit.ts) when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC`**. **Sign out** for QA lives on **[ProfilePageClient](app/profile/ProfilePageClient.tsx)**. Site **Log in** opens the same modal overlay ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)), not only `/login`.
**Goal:** In `/create/*`, **Exit** / **Save & Exit** (from `select` onward for signed-in users) is the only top-nav chrome—no email or Sign out in the create shell. **Anonymous:** progress in **`create-flow-anonymous`** localStorage; **Exit** opens the global **Save your progress?** auth modal (magic link + `?syncDraft=1` return); after verify, [`PostLoginDraftTransfer`](app/(app)/create/PostLoginDraftTransfer.tsx) **PUT**s to `/api/drafts/me` when sync is on. **Signed-in:** **Save & Exit** **PUT**s via [`useCreateFlowExit`](app/(app)/create/hooks/useCreateFlowExit.ts) when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC`**. **Sign out** for QA lives on **[ProfilePageClient](app/(app)/profile/ProfilePageClient.tsx)**. Site **Log in** opens the same modal overlay ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)), not only `/login`.
**Context:** **`saveDraftOnExit`** is gated on **session + step ≥ select**. Layout **`fetchAuthSession`** drives anonymous vs authenticated persistence and exit behavior. **Save & Exit** styling: Figma [20907:212637](https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=20907-212637). Save-progress exit modal: Figma `22398:23743`.
**Implementation (repo):**
1. [app/create/layout.tsx](app/create/layout.tsx): session + `enableAnonymousPersistence`; anonymous exit → `openLogin({ variant: 'saveProgress', nextPath })`; signed-in exit → `useCreateFlowExit`.
1. [app/(app)/create/layout.tsx](app/(app)/create/layout.tsx): session + `enableAnonymousPersistence`; anonymous exit → `openLogin({ variant: 'saveProgress', nextPath })`; signed-in exit → `useCreateFlowExit`.
2. [CreateFlowTopNav](app/components/utility/CreateFlowTopNav/): i18n [`messages/en/create/topNav.json`](messages/en/create/topNav.json); logo + Share/Export/Edit (completed) + Exit/Save & Exit only.
3. [useCreateFlowExit](app/create/hooks/useCreateFlowExit.ts): `saveDraftToServer` when sync + signed in; `clearState` + home.
4. [CreateFlowContext](app/create/context/CreateFlowContext.tsx): optional anonymous localStorage mirror via `enableAnonymousPersistence`.
5. **QA:** [ProfilePageClient](app/profile/ProfilePageClient.tsx) Sign out when session present.
3. [useCreateFlowExit](app/(app)/create/hooks/useCreateFlowExit.ts): `saveDraftToServer` when sync + signed in; `clearState` + home.
4. [CreateFlowContext](app/(app)/create/context/CreateFlowContext.tsx): optional anonymous localStorage mirror via `enableAnonymousPersistence`.
5. **QA:** [ProfilePageClient](app/(app)/profile/ProfilePageClient.tsx) Sign out when session present.
**Acceptance criteria:**
- [x] Completed step still works; **Save & Exit** gating uses session + step (not conflated with `completed` only).
- [x] Signed in + sync: Save & Exit persists server-side; anonymous: localStorage + exit modal + transfer after magic link. Sign out on profile clears session. _(Re-verify on staging/prod as needed.)_
**Files:** [app/create/layout.tsx](app/create/layout.tsx), [app/create/hooks/useCreateFlowExit.ts](app/create/hooks/useCreateFlowExit.ts), [app/components/utility/CreateFlowTopNav/](app/components/utility/CreateFlowTopNav/), [app/create/context/CreateFlowContext.tsx](app/create/context/CreateFlowContext.tsx), [messages/en/create/topNav.json](messages/en/create/topNav.json), [app/profile/ProfilePageClient.tsx](app/profile/ProfilePageClient.tsx).
**Files:** [app/(app)/create/layout.tsx](app/(app)/create/layout.tsx), [app/(app)/create/hooks/useCreateFlowExit.ts](app/(app)/create/hooks/useCreateFlowExit.ts), [app/components/utility/CreateFlowTopNav/](app/components/utility/CreateFlowTopNav/), [app/(app)/create/context/CreateFlowContext.tsx](app/(app)/create/context/CreateFlowContext.tsx), [messages/en/create/topNav.json](messages/en/create/topNav.json), [app/(app)/profile/ProfilePageClient.tsx](app/(app)/profile/ProfilePageClient.tsx).
---
@@ -189,9 +189,9 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Implementation:**
1. **Hydration:** **Done:** [SignedInDraftHydration](app/create/SignedInDraftHydration.tsx) + [messages/en/create/draftHydration.json](messages/en/create/draftHydration.json); skips `?syncDraft=1` / transfer-pending (PostLogin owns that). Wired in [layout](app/create/layout.tsx).
2. **Conflict:** **Done:** If `create-flow-anonymous` and server draft are both non-empty, `window.confirm` (OK = account draft, Cancel = browser copy); documented on [anonymousDraftStorage](app/create/utils/anonymousDraftStorage.ts). Newer-`updatedAt` client compare remains optional.
3. **Save failures (API surface):** **Done (CR-76):** [saveDraftToServer](lib/create/api.ts) returns `SaveDraftResult` with parsed API `message`; wired in [useCreateFlowExit](app/create/hooks/useCreateFlowExit.ts) and [PostLoginDraftTransfer](app/create/PostLoginDraftTransfer.tsx).
1. **Hydration:** **Done:** [SignedInDraftHydration](app/(app)/create/SignedInDraftHydration.tsx) + [messages/en/create/draftHydration.json](messages/en/create/draftHydration.json); skips `?syncDraft=1` / transfer-pending (PostLogin owns that). Wired in [layout](app/(app)/create/layout.tsx).
2. **Conflict:** **Done:** If `create-flow-anonymous` and server draft are both non-empty, `window.confirm` (OK = account draft, Cancel = browser copy); documented on [anonymousDraftStorage](app/(app)/create/utils/anonymousDraftStorage.ts). Newer-`updatedAt` client compare remains optional.
3. **Save failures (API surface):** **Done (CR-76):** [saveDraftToServer](lib/create/api.ts) returns `SaveDraftResult` with parsed API `message`; wired in [useCreateFlowExit](app/(app)/create/hooks/useCreateFlowExit.ts) and [PostLoginDraftTransfer](app/(app)/create/PostLoginDraftTransfer.tsx).
4. **Save failures (UX):** **Done (CR-76):** Dismissible banner with server `message` (no second confirm to leave); post-login transfer shows reason; unit tests in `tests/unit/saveDraftToServer.test.ts`. Retry/backoff remains optional.
5. **Tests:** `saveDraftToServer` unit tests; [draftHydrationUtils](lib/create/draftHydrationUtils.ts) unit tests. Playwright against Next standalone + route mocks for `/api/auth/session` was flaky here; cover hydration with **manual QA** (signed in + sync on + server draft) or add a future E2E with a dedicated auth fixture.
@@ -200,7 +200,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
- [x] No silent data loss when server save fails (user sees reason in banner; stays in flow to retry Save & Exit or leave via e.g. logo).
- [x] User understands when server draft replaced local state (if applicable) — conflict `window.confirm` when both browser anonymous draft and account draft exist; otherwise silent apply of single source.
**Files:** [lib/create/api.ts](lib/create/api.ts), [app/create/hooks/useCreateFlowExit.ts](app/create/hooks/useCreateFlowExit.ts), [app/create/PostLoginDraftTransfer.tsx](app/create/PostLoginDraftTransfer.tsx), [app/create/SignedInDraftHydration.tsx](app/create/SignedInDraftHydration.tsx), [app/create/layout.tsx](app/create/layout.tsx), [CreateFlowContext](app/create/context/CreateFlowContext.tsx), tests under `tests/`.
**Files:** [lib/create/api.ts](lib/create/api.ts), [app/(app)/create/hooks/useCreateFlowExit.ts](app/(app)/create/hooks/useCreateFlowExit.ts), [app/(app)/create/PostLoginDraftTransfer.tsx](app/(app)/create/PostLoginDraftTransfer.tsx), [app/(app)/create/SignedInDraftHydration.tsx](app/(app)/create/SignedInDraftHydration.tsx), [app/(app)/create/layout.tsx](app/(app)/create/layout.tsx), [CreateFlowContext](app/(app)/create/context/CreateFlowContext.tsx), tests under `tests/`.
---
@@ -210,7 +210,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Goal:** Completing the flow persists a **PublishedRule** via existing [publishRule](lib/create/api.ts).
**Context:** [lib/create/api.ts](lib/create/api.ts) already wraps `POST /api/rules`. UI on the `final-review` / `completed` steps (see [app/create/screens/CreateFlowScreenView.tsx](app/create/screens/CreateFlowScreenView.tsx) and `app/create/screens/`) must call it with `{ title, summary?, document }` derived from `CreateFlowState`.
**Context:** [lib/create/api.ts](lib/create/api.ts) already wraps `POST /api/rules`. UI on the `final-review` / `completed` steps (see [app/(app)/create/screens/CreateFlowScreenView.tsx](app/(app)/create/screens/CreateFlowScreenView.tsx) and `app/(app)/create/screens/`) must call it with `{ title, summary?, document }` derived from `CreateFlowState`.
**Implementation:**
@@ -224,7 +224,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
- [ ] Published row appears in Postgres (`PublishedRule`) and `GET /api/rules` lists it.
- [ ] User sees clear success/failure.
**Files:** relevant `app/create/*/page.tsx`, [lib/create/api.ts](lib/create/api.ts) if request shape changes, types from Ticket 2.
**Files:** relevant `app/(app)/create/*/page.tsx`, [lib/create/api.ts](lib/create/api.ts) if request shape changes, types from Ticket 2.
---
@@ -258,7 +258,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Goal:** Home or create entry surfaces use live template data instead of only static i18n JSON.
**Context:** [RuleStack.view.tsx](app/components/sections/RuleStack/RuleStack.view.tsx) and create entry surfaces reference future template work. Wizard URLs are static segments under `app/create/`; see [`docs/create-flow.md`](create-flow.md) and **Ticket 17** for the canonical custom flow.
**Context:** [RuleStack.view.tsx](app/components/sections/RuleStack/RuleStack.view.tsx) and create entry surfaces reference future template work. Wizard URLs are static segments under `app/(app)/create/`; see [`docs/create-flow.md`](create-flow.md) and **Ticket 17** for the canonical custom flow.
**Implementation:**
@@ -271,7 +271,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
- [ ] Changing a template row in Prisma Studio reflects after refresh (or revalidate).
- [ ] No layout shift regression on LCP-critical pages (use skeletons).
**Files:** [app/components/sections/RuleStack/](app/components/sections/RuleStack/), create-flow entry routes under [app/create/](app/create/), possibly new `lib/templates/fetchTemplates.ts`.
**Files:** [app/components/sections/RuleStack/](app/components/sections/RuleStack/), create-flow entry routes under [app/(app)/create/](app/(app)/create/), possibly new `lib/templates/fetchTemplates.ts`.
**Follow-up:** **Ticket 16** — dynamic recommendations from authoring spreadsheets and create-flow answers.
@@ -311,14 +311,14 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
**Goal:** Establish the **official custom** create-rule flow (ordered steps, URLs, persistence, entry points, **Figma three-stage framing**) in repo docs and close gaps between that spec and the implementation (routing clutter, progress UI, step source of truth, resume vs URL).
**Context:** Step order lives in [`app/create/utils/flowSteps.ts`](app/create/utils/flowSteps.ts). Wizard screens render from [`app/create/[screenId]/page.tsx`](app/create/[screenId]/page.tsx) plus [`CREATE_FLOW_SCREEN_REGISTRY`](app/create/utils/createFlowScreenRegistry.ts) (Figma node + layout family per slug). [`docs/create-flow.md`](create-flow.md) is the **canonical** URL/persistence summary: **Create Community** (eight semantic steps ending at `review`) → **Create Custom CommunityRule****Review and complete**. **Full create-from-template** will likely use **additional route(s)** when product defines it; **`/create/review-template/[slug]`** remains auxiliary preview only. **Template → `final-review` or mid-wizard prefill** is **out of scope** here (future ticket); `/create/informational?template=` is a **no-op** until then.
**Context:** Step order lives in [`app/(app)/create/utils/flowSteps.ts`](app/(app)/create/utils/flowSteps.ts). Wizard screens render from [`app/(app)/create/[screenId]/page.tsx`](app/(app)/create/[screenId]/page.tsx) plus [`CREATE_FLOW_SCREEN_REGISTRY`](app/(app)/create/utils/createFlowScreenRegistry.ts) (Figma node + layout family per slug). [`docs/create-flow.md`](create-flow.md) is the **canonical** URL/persistence summary: **Create Community** (eight semantic steps ending at `review`) → **Create Custom CommunityRule****Review and complete**. **Full create-from-template** will likely use **additional route(s)** when product defines it; **`/create/review-template/[slug]`** remains auxiliary preview only. **Template → `final-review` or mid-wizard prefill** is **out of scope** here (future ticket); `/create/informational?template=` is a **no-op** until then.
**Implementation:**
1. Keep [`docs/create-flow.md`](create-flow.md) in sync with product/Figma (stage ↔ step mapping, future template routes).
2. ~~Remove legacy [`app/create/[step]/page.tsx`](app/create/[step]/page.tsx)~~ — replaced by [`app/create/[screenId]/page.tsx`](app/create/[screenId]/page.tsx) with real screens; unknown slugs `notFound()`.
3. Unify **step source of truth**: URL via [`useCreateFlowNavigation`](app/create/hooks/useCreateFlowNavigation.ts) vs unused [`CreateFlowContext`](app/create/context/CreateFlowContext.tsx) `currentStep` — pick one model; align [`useCreateFlowExit`](app/create/hooks/useCreateFlowExit.ts) / draft payload if needed.
4. **Resume:** After [`SignedInDraftHydration`](app/create/SignedInDraftHydration.tsx), decide redirect to `/create/${state.currentStep}` vs stay on current URL; test or document.
2. ~~Remove legacy [`app/(app)/create/[step]/page.tsx`](app/(app)/create/[step]/page.tsx)~~ — replaced by [`app/(app)/create/[screenId]/page.tsx`](app/(app)/create/[screenId]/page.tsx) with real screens; unknown slugs `notFound()`.
3. Unify **step source of truth**: URL via [`useCreateFlowNavigation`](app/(app)/create/hooks/useCreateFlowNavigation.ts) vs unused [`CreateFlowContext`](app/(app)/create/context/CreateFlowContext.tsx) `currentStep` — pick one model; align [`useCreateFlowExit`](app/(app)/create/hooks/useCreateFlowExit.ts) / draft payload if needed.
4. **Resume:** After [`SignedInDraftHydration`](app/(app)/create/SignedInDraftHydration.tsx), decide redirect to `/create/${state.currentStep}` vs stay on current URL; test or document.
5. Wire [`CreateFlowFooter`](app/components/utility/CreateFlowFooter/) `ProportionBar` to step progress from `FLOW_STEP_ORDER` (and `review-template` / `completed` exceptions per design); optional **two-level progress** (stage + step within stage) when design specifies.
6. When Figma hands off, surface **stage labels** in create shell (top nav, footer, or step chrome) using the mapping in `create-flow.md`.
@@ -330,7 +330,7 @@ Optional: **Docker image deploy** using the repo [Dockerfile](Dockerfile)—admi
- [ ] Hydration + `currentStep` behavior is verified (redirect vs stay).
- [ ] `?template=` documented as deferred; no implied “template customize → full wizard” parity.
**Files:** [`docs/create-flow.md`](create-flow.md), [`app/create/`](app/create/), [`app/components/utility/CreateFlowFooter/`](app/components/utility/CreateFlowFooter/), optionally [`docs/backend-roadmap.md`](backend-roadmap.md) §12 cross-links.
**Files:** [`docs/create-flow.md`](create-flow.md), [`app/(app)/create/`](app/(app)/create/), [`app/components/utility/CreateFlowFooter/`](app/components/utility/CreateFlowFooter/), optionally [`docs/backend-roadmap.md`](backend-roadmap.md) §12 cross-links.
**Linear:** [CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo) (**Backlog**). **Parallel** to templates (78) and publish (6); not part of **CR-72 → CR-83**.
@@ -9,7 +9,7 @@ Temporary working notes for building the backend. Safe to delete once the stack
- **Next.js 16** single repo ([`package.json`](package.json)).
- **PostgreSQL + Prisma**: schema and migrations under `prisma/`; product APIs under `app/api/*` (health, auth/magic-link, session, drafts, rules, templates, web-vitals).
- **Server modules** in `lib/server/` (db, session, mail, rate limiting, etc.).
- **Create flow:** **Anonymous** users mirror in-progress state to **`create-flow-anonymous`** in `localStorage`; **Exit** opens the save-progress magic-link modal; after verify, [`PostLoginDraftTransfer`](app/create/PostLoginDraftTransfer.tsx) can **PUT** `/api/drafts/me` when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**. **Signed-in** users get a **fresh** in-memory session per “Create rule” entry, but with sync on the layout may **hydrate** from **`GET /api/drafts/me`** via [`SignedInDraftHydration`](app/create/SignedInDraftHydration.tsx); **Save & Exit** (from `community-structure` onward) **PUT**s when sync is on. **Log in** from the marketing header uses the global modal ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)); **`/login`** remains for verify errors and deep links. **Step order and URLs:** [`docs/create-flow.md`](docs/create-flow.md) and [`app/create/utils/flowSteps.ts`](app/create/utils/flowSteps.ts).
- **Create flow:** **Anonymous** users mirror in-progress state to **`create-flow-anonymous`** in `localStorage`; **Exit** opens the save-progress magic-link modal; after verify, [`PostLoginDraftTransfer`](app/(app)/create/PostLoginDraftTransfer.tsx) can **PUT** `/api/drafts/me` when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**. **Signed-in** users get a **fresh** in-memory session per “Create rule” entry, but with sync on the layout may **hydrate** from **`GET /api/drafts/me`** via [`SignedInDraftHydration`](app/(app)/create/SignedInDraftHydration.tsx); **Save & Exit** (from `community-structure` onward) **PUT**s when sync is on. **Log in** from the marketing header uses the global modal ([`AuthModalProvider`](app/contexts/AuthModalContext.tsx)); **`/login`** remains for verify errors and deep links. **Step order and URLs:** [`docs/create-flow.md`](docs/create-flow.md) and [`app/(app)/create/utils/flowSteps.ts`](app/(app)/create/utils/flowSteps.ts).
- **Web vitals** [`app/api/web-vitals/route.ts`](app/api/web-vitals/route.ts) still use **file-based** storage under `.next` (not suitable for multi-instance production).
- **CI:** [`.gitea/workflows/ci.yaml`](.gitea/workflows/ci.yaml) (build, test, lint, `prisma validate`); no in-repo production deploy definition.
@@ -88,7 +88,7 @@ Plain-English entities (names can evolve):
**RuleDraft future (not v1):** versioning, multiple drafts per user, easier corruption recovery—only if product needs them.
Align JSON shapes with `app/create/types.ts` as it matures.
Align JSON shapes with `app/(app)/create/types.ts` as it matures.
---
@@ -218,7 +218,7 @@ npm run dev
## 12. Frontend hook-up
**Step 1.** **Anonymous** create flow: in-progress state is stored in **`create-flow-anonymous`** (`localStorage`). **Signed-in** users: when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**, the create layout may **hydrate** in-memory flow state from **`GET /api/drafts/me`** once per session ([`SignedInDraftHydration`](../app/create/SignedInDraftHydration.tsx)), including conflict handling if anonymous storage also has data. Without sync, signed-in progress stays **in memory** until **Save & Exit** (no automatic server read on entry). **Canonical wizard step order, URLs, and Figma product stages** (**Create Community** → **Create Custom CommunityRule****Review and complete**) are documented in [`docs/create-flow.md`](create-flow.md). The route **`/create/review-template/[slug]`** is an **auxiliary** template preview (not a numbered wizard step); a **full create-from-template** path will likely be **separate route(s)** when defined. **Prefilling the wizard or landing on `final-review` from a template** is **not** shipped yet — see **[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)** / Ticket 17 in [docs/backend-linear-tickets.md](backend-linear-tickets.md).
**Step 1.** **Anonymous** create flow: in-progress state is stored in **`create-flow-anonymous`** (`localStorage`). **Signed-in** users: when **`NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true`**, the create layout may **hydrate** in-memory flow state from **`GET /api/drafts/me`** once per session ([`SignedInDraftHydration`](../app/(app)/create/SignedInDraftHydration.tsx)), including conflict handling if anonymous storage also has data. Without sync, signed-in progress stays **in memory** until **Save & Exit** (no automatic server read on entry). **Canonical wizard step order, URLs, and Figma product stages** (**Create Community** → **Create Custom CommunityRule****Review and complete**) are documented in [`docs/create-flow.md`](create-flow.md). The route **`/create/review-template/[slug]`** is an **auxiliary** template preview (not a numbered wizard step); a **full create-from-template** path will likely be **separate route(s)** when defined. **Prefilling the wizard or landing on `final-review` from a template** is **not** shipped yet — see **[CR-89](https://linear.app/community-rule/issue/CR-89/product-canon-custom-create-rule-wizard-routes-resume-progress-repo)** / Ticket 17 in [docs/backend-linear-tickets.md](backend-linear-tickets.md).
**Step 2.** Set `NEXT_PUBLIC_ENABLE_BACKEND_SYNC=true` to enable **PUT** on **Save & Exit** and after **magic-link transfer** from the save-progress exit modal.
@@ -1,401 +0,0 @@
# Container/Presentation Pattern
## Overview
The Container/Presentation pattern separates component logic from presentation, improving testability, reusability, and maintainability. This pattern is now the standard for complex components in this codebase.
## Motivation
### Benefits
- **Testability**: Pure presentation components can be tested independently with simple prop assertions
- **Reusability**: Presentation components can be reused with different data sources or logic
- **Maintainability**: Clear separation makes it easier to locate and modify specific concerns
- **Performance**: Easier to optimize rendering with React.memo on pure components
### When to Use
Use this pattern for components that have:
- Business logic or state management
- Data fetching or API calls
- Analytics tracking
- Complex event handlers
- Custom hooks usage
- Dynamic imports or side effects
Simple presentational components (e.g., `Button`, `Avatar`) can remain as single files.
## Folder Structure
Each component following this pattern should have this structure:
```
app/components/[ComponentName]/
├── index.tsx # Exports container as default
├── [ComponentName].container.tsx # Logic, hooks, state management
├── [ComponentName].view.tsx # Pure presentation component
└── [ComponentName].types.ts # Shared TypeScript types
```
### File Responsibilities
#### `index.tsx`
- Exports the container component as the default export
- Optionally exports types for external use
- Maintains backward compatibility with existing import paths
```typescript
export { default } from "./AskOrganizer.container";
export type { AskOrganizerProps } from "./AskOrganizer.types";
```
#### `[ComponentName].container.tsx`
**Contains all logic:**
- React hooks (`useState`, `useEffect`, custom hooks)
- Event handlers and business logic
- Data fetching and API calls
- Analytics tracking
- State management
- Computed values and derived state
- Side effects
**Should NOT contain:**
- JSX layout details (beyond composing the view)
- Inline styles or complex className logic (pass as props)
- Direct DOM manipulation
```typescript
"use client";
import { memo } from "react";
import { useAnalytics } from "../../hooks";
import { AskOrganizerView } from "./AskOrganizer.view";
import type { AskOrganizerProps } from "./AskOrganizer.types";
function AskOrganizerContainer(props: AskOrganizerProps) {
const { trackEvent } = useAnalytics();
const handleContactClick = () => {
trackEvent({
event: "contact_button_click",
category: "engagement",
component: "AskOrganizer",
});
// ... additional logic
};
// Compute derived props
const variantStyles = computeVariantStyles(props.variant);
return (
<AskOrganizerView
{...props}
onContactClick={handleContactClick}
variantStyles={variantStyles}
/>
);
}
export default memo(AskOrganizerContainer);
```
#### `[ComponentName].view.tsx`
**Pure presentation:**
- Receives all data via props
- Renders JSX based on props
- No hooks, no state, no side effects
- Only imports other presentational components
**Should NOT contain:**
- `useState`, `useEffect`, or any hooks
- Event handler implementations (receive as callbacks)
- Data fetching or API calls
- Analytics tracking
- Business logic
```typescript
import ContentLockup from "../ContentLockup";
import Button from "../Button";
import type { AskOrganizerViewProps } from "./AskOrganizer.types";
export function AskOrganizerView({
title,
subtitle,
description,
buttonText,
buttonHref,
variant,
onContactClick,
variantStyles,
...props
}: AskOrganizerViewProps) {
return (
<section className={variantStyles.container}>
<ContentLockup
title={title}
subtitle={subtitle}
description={description}
/>
<Button
href={buttonHref}
onClick={onContactClick}
>
{buttonText}
</Button>
</section>
);
}
```
#### `[ComponentName].types.ts`
- Shared TypeScript interfaces and types
- Public props interface (used by consumers)
- Internal view props (used between container and view)
- Any utility types specific to the component
```typescript
export interface AskOrganizerProps {
title?: string;
subtitle?: string;
buttonText?: string;
buttonHref?: string;
variant?: "centered" | "left-aligned" | "compact" | "inverse";
onContactClick?: (data: ContactClickData) => void;
}
export interface AskOrganizerViewProps extends AskOrganizerProps {
onContactClick: () => void;
variantStyles: {
container: string;
buttonContainer: string;
};
}
```
## Rules of Thumb
### Container Components
**DO:**
- Use React hooks (`useState`, `useEffect`, custom hooks)
- Handle all event handlers and business logic
- Fetch data and manage loading states
- Track analytics events
- Compute derived values from props/state
- Compose the view component with computed props
**DON'T:**
- Include complex JSX layout (delegate to view)
- Mix presentation logic with business logic
- Access DOM directly (use refs when necessary)
### View Components
**DO:**
- Receive all data via props
- Render JSX based on props
- Import only presentational components
- Use simple conditional rendering
- Accept callback props for user interactions
**DON'T:**
- Use any React hooks
- Manage state or side effects
- Fetch data or make API calls
- Track analytics directly
- Implement business logic
- Access browser APIs directly
## Example: AskOrganizer
### Before (Monolithic)
```typescript
"use client";
import { memo } from "react";
import { useAnalytics } from "../hooks";
import ContentLockup from "./ContentLockup";
import Button from "./Button";
const AskOrganizer = memo(({ title, variant, ...props }) => {
const { trackEvent } = useAnalytics();
const handleContactClick = () => {
trackEvent({ event: "contact_click", component: "AskOrganizer" });
};
return (
<section>
<ContentLockup title={title} />
<Button onClick={handleContactClick}>Ask an organizer</Button>
</section>
);
});
```
### After (Container/Presentation)
**AskOrganizer.container.tsx:**
```typescript
"use client";
import { memo } from "react";
import { useAnalytics } from "../../hooks";
import { AskOrganizerView } from "./AskOrganizer.view";
import type { AskOrganizerProps } from "./AskOrganizer.types";
function AskOrganizerContainer(props: AskOrganizerProps) {
const { trackEvent } = useAnalytics();
const handleContactClick = () => {
trackEvent({ event: "contact_click", component: "AskOrganizer" });
};
return <AskOrganizerView {...props} onContactClick={handleContactClick} />;
}
export default memo(AskOrganizerContainer);
```
**AskOrganizer.view.tsx:**
```typescript
import ContentLockup from "../ContentLockup";
import Button from "../Button";
import type { AskOrganizerViewProps } from "./AskOrganizer.types";
export function AskOrganizerView({
title,
onContactClick,
...props
}: AskOrganizerViewProps) {
return (
<section>
<ContentLockup title={title} />
<Button onClick={onContactClick}>Ask an organizer</Button>
</section>
);
}
```
## Migration Checklist
When converting an existing component to this pattern:
- [ ] **Identify separation points**
- [ ] List all hooks and state management
- [ ] List all event handlers and business logic
- [ ] List all data fetching and side effects
- [ ] Identify pure presentation JSX
- [ ] **Create folder structure**
- [ ] Create `[ComponentName]/` folder
- [ ] Create `[ComponentName].types.ts` with shared types
- [ ] Create `[ComponentName].view.tsx` with pure presentation
- [ ] Create `[ComponentName].container.tsx` with all logic
- [ ] Create `index.tsx` exporting container
- [ ] **Extract types**
- [ ] Move component props interface to `types.ts`
- [ ] Create view props interface extending container props
- [ ] Export types from `index.tsx` for external use
- [ ] **Move presentation to view**
- [ ] Copy JSX to view component
- [ ] Remove all hooks and state
- [ ] Replace event handlers with callback props
- [ ] Replace computed values with props
- [ ] Ensure view is a pure function
- [ ] **Move logic to container**
- [ ] Move all hooks to container
- [ ] Move event handlers to container
- [ ] Move data fetching to container
- [ ] Compute derived props in container
- [ ] Render view component with computed props
- [ ] **Update exports**
- [ ] Export container as default from `index.tsx`
- [ ] Export types from `index.tsx`
- [ ] Delete original component file
- [ ] Verify import paths still work
- [ ] **Update tests**
- [ ] Verify tests still pass (imports should resolve automatically)
- [ ] Update any tests that relied on implementation details
- [ ] Add tests for view component with mocked props if needed
- [ ] **Update Storybook**
- [ ] Verify stories still work (imports should resolve automatically)
- [ ] Optionally add view-only stories with mocked props
## Refactored Components
The following components have been refactored to use this pattern:
-**AskOrganizer** - Analytics tracking and event handlers
-**NumberedCards** - Schema generation with `useSchemaData` hook
-**FeatureGrid** - Memoized feature data structures
-**WebVitalsDashboard** - Dynamic imports, data fetching, complex state
-**Select** - Complex form state management with refs and keyboard navigation
These components serve as reference implementations for the pattern.
## Remaining Components
The following components are candidates for future conversion:
### High Priority (Complex Logic)
- `Header` / `HomeHeader` - Navigation state, conditional rendering logic
- `MenuBar` - Menu state management, keyboard navigation
- `ContextMenu` - Positioning logic, click outside handling
- `RadioGroup` - Group state management
- `ToggleGroup` - Group state management
### Medium Priority (Some Logic)
- `ContentContainer` - Data fetching or transformation
- `RelatedArticles` - Data fetching, filtering logic
- `RuleStack` - Complex rendering logic
- `LogoWall` - Animation or interaction logic
### Low Priority (Mostly Presentational)
- `Button`, `Avatar`, `Checkbox`, `Input`, `TextArea` - Simple presentational components
- `Separator`, `SectionHeader`, `SectionNumber` - Pure presentation
- `QuoteBlock`, `QuoteDecor`, `HeroDecor` - Decorative components
## Best Practices
1. **Start with complex components** - Components with the most logic benefit most from separation
2. **Keep it simple** - Don't over-engineer simple presentational components
3. **Maintain backward compatibility** - Import paths should remain unchanged
4. **Test both layers** - Test container for logic, view for presentation
5. **Document the pattern** - Add comments explaining non-obvious prop flows
6. **Use TypeScript strictly** - Leverage types to enforce the separation
## Additional Resources
- [React Container/Presenter Pattern](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0)
- [Separation of Concerns in React](https://react.dev/learn/thinking-in-react)
---
**Last Updated**: April 2025
**Maintained by**: CommunityRule Development Team
+55 -356
View File
@@ -1,382 +1,81 @@
# i18n Translation Workflow Guide
# Translations & UI copy workflow
This guide explains how to work with translations in the CommunityRule application. The app uses a hybrid approach combining globalized, shared UI elements with context-aware, localized content pages, making it easy for content creators and contributors to update text without modifying component code.
This guide is for **content editors** updating user-visible text. The
implementation contract (file layout, `useMessages` access pattern, key
casing rules) lives in `.cursor/rules/localization.mdc`.
## Overview
## Where copy lives
All UI text is stored in JSON files under `messages/en/`. The structure follows best practices:
All UI text is JSON under `messages/en/`:
- **Page-specific content** lives in `pages/` (varies by page context)
- **Component defaults** live in `components/` (shared across pages)
- **Common strings** live in `common.json` (shared UI elements)
Components reference these translations using keys, allowing content to be edited independently of the codebase.
## Directory Structure
```
messages/
en/
pages/
home.json # Home page specific content
learn.json # Learn page specific content
components/
heroBanner.json # Component defaults (aria-labels, alt texts)
numberedCards.json # Component defaults
askOrganizer.json # Component defaults
featureGrid.json # Component defaults
footer.json # Shared across pages
header.json # Shared across pages
common.json # Shared UI strings (buttons, links, labels)
navigation.json # Navigation items
metadata.json # Page metadata (title, description)
index.ts # Exports all messages
```text
messages/en/
common.json # shared strings (buttons, links, generic labels)
navigation.json # site nav items
metadata.json # page <title> / description
pages/<slug>.json # one file per page (home, learn, …)
components/<name>.json # one file per shared component default
create/<step>.json # one file per create-flow step
index.ts # wires all bundles together
```
## When to Use `pages/` vs `components/`
The split is intentional:
### Use `pages/` for:
- **`pages/`** — copy that varies by page context (titles, hero subheads).
- **`components/`** — defaults that ride along with a component on every
page (aria-labels, alt text patterns).
- **`common.json`** — strings reused across many components (e.g. "Cancel",
"Learn more").
- **`create/`** — wizard step copy (mirrors the `screenId`).
- **Page-specific content**: Titles, subtitles, descriptions that vary by page
- **Context-aware text**: Content that changes based on where the component is used
- **User-facing content**: All text that users see on a specific page
## Editing existing copy
**Example:** The home page hero banner title "Collaborate" goes in `pages/home.json`, not `components/heroBanner.json`
1. Find the bundle: search `messages/en/` for the existing English string.
2. Edit the value. Leave the key alone.
3. Save and run `npm run dev` — text updates on reload.
### Use `components/` for:
If you can't find the string, it may still be hard-coded. Open an issue or
ping a developer; do not change the component file directly.
- **Component defaults**: Aria-labels, alt text patterns, shared behavior text
- **Shared across pages**: Text that doesn't vary by page context
- **Accessibility text**: Aria-labels and alt texts that are component-level
## Adding a new key
**Example:** The hero banner image alt text "Hero illustration" stays in `components/heroBanner.json` because it's the same across all pages
### Use `common.json` for:
- **Shared UI strings**: Buttons, links, labels used across multiple components
- **Global strings**: Text that appears in many places
## Page-Specific Translations
For page-specific content, use the `pages.*` namespace pattern:
**Server Components:**
```typescript
import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
export default function LearnPage() {
const t = (key: string) => getTranslation(messages, key);
const contentLockupData = {
title: t("pages.learn.contentLockup.title"),
subtitle: t("pages.learn.contentLockup.subtitle"),
};
return <ContentLockup {...contentLockupData} />;
}
```
**Client Components:**
```typescript
"use client";
import { useTranslation } from "../../contexts/MessagesContext";
export default function MyComponent() {
const t = useTranslation("pages.home.heroBanner");
return <h1>{t("title")}</h1>;
}
```
## Adding New Translation Keys
### 1. Identify the Component
Determine which component needs the translation. If it's a shared string (like a button label), add it to `common.json`. Otherwise, add it to the component-specific file.
### 2. Add the Key to the JSON File
Open the appropriate JSON file and add your translation key. Use descriptive, semantic keys:
**Good:**
1. Decide which bundle owns the copy (page vs component vs common).
2. Add a descriptive camelCase key. Group related copy in nested objects.
3. If you created a new JSON file, register it in `messages/en/index.ts`
(a developer will help if you're unsure).
4. Reference the key from the component using `useMessages()` (see the
localization rule for the snippet).
```json
{
"heroBanner": {
"title": "Collaborate",
"subtitle": "with clarity"
}
}
```
**Bad:**
```json
{
"text1": "Collaborate",
"text2": "with clarity"
}
```
### 3. Use Nested Objects for Organization
Group related translations together:
```json
{
"numberedCards": {
"title": "How CommunityRule works",
"buttons": {
"createCommunityRule": "Create CommunityRule",
"seeHowItWorks": "See how it works"
}
}
}
```
### 4. Update the Component or Page
**For Page Components (Server Components):**
```typescript
import messages from "../../messages/en/index";
import { getTranslation } from "../../lib/i18n/getTranslation";
export default function MyPage() {
const t = (key: string) => getTranslation(messages, key);
// Use page-specific keys
const data = {
title: t("pages.home.heroBanner.title"),
subtitle: t("pages.home.heroBanner.subtitle"),
};
return <HeroBanner {...data} />;
}
```
**For Client Components:**
```typescript
"use client";
import { useTranslation } from "../../contexts/MessagesContext";
export default function MyComponent() {
// For page-specific content
const t = useTranslation("pages.home.heroBanner");
return <h1>{t("title")}</h1>;
// For component defaults
const tDefault = useTranslation("heroBanner");
return <img alt={tDefault("imageAlt")} />;
}
```
## Translation Key Naming Conventions
1. **Use camelCase** for keys: `buttonText`, `ariaLabel`
2. **Use descriptive names**: `createCommunityRule` not `btn1`
3. **Group by component**: Each component has its own namespace
4. **Use nested objects** for related strings: `buttons.createCommunityRule`
5. **Include context in comments**: Use `_comment` fields for clarity
Example:
```json
{
"_comment": "HeroBanner component translations",
"title": "Collaborate",
"subtitle": "with clarity",
"description": "Help your community make important decisions..."
}
```
## Extracting Strings from Components
When migrating a component to use translations:
1. **Identify hardcoded strings** in the component
2. **Create translation keys** in the appropriate JSON file
3. **Replace hardcoded strings** with `t("key.path")` calls
4. **Test the component** to ensure translations load correctly
### Example Migration
**Before:**
```typescript
export default function HeroBanner() {
return (
<div>
<h1>Collaborate</h1>
<p>with clarity</p>
</div>
);
}
```
**After:**
```typescript
"use client";
import { useTranslations } from "next-intl";
export default function HeroBanner() {
const t = useTranslations("heroBanner");
return (
<div>
<h1>{t("title")}</h1>
<p>{t("subtitle")}</p>
</div>
);
}
```
## Adding a New Page
When creating a new page that needs translations:
1. **Create a page translation file**: `messages/en/pages/about.json` (for example)
2. **Add page-specific content**: All user-facing text for that page
3. **Import in index.ts**: Add the import and export in `messages/en/index.ts`
4. **Use in page component**: Use `t("pages.about.*")` pattern in your page
**Example:**
```typescript
// messages/en/pages/about.json
{
"_comment": "About page hero copy",
"hero": {
"title": "About Us",
"subtitle": "Learn more about our mission"
}
}
// app/about/page.tsx
const t = (key: string) => getTranslation(messages, key);
const title = t("pages.about.hero.title");
```
## Adding New Languages (Future)
When adding support for a new language:
1. **Create a new locale directory**: `messages/es/` (for Spanish, for example)
2. **Copy the English files** as a starting point (including `pages/` structure)
3. **Translate all strings** in the JSON files
4. **Test thoroughly** to ensure all translations are present
## Testing Translations
1. **Check for missing keys**: Ensure all translation keys used in components exist in the JSON files
2. **Verify type safety**: TypeScript will catch typos in translation keys at compile time
3. **Test in browser**: Run the dev server and verify text displays correctly
4. **Check for fallbacks**: Missing translations will show the key path (e.g., `heroBanner.title`)
## Best Practices
### For Content Creators
- **Edit JSON files directly**: No need to understand React or TypeScript
- **Use descriptive comments**: Add `_comment` fields to explain context
- **Maintain consistency**: Use the same terminology across components
- **Test changes**: Run the dev server to see your changes immediately
### For Developers
- **Use TypeScript**: Translation keys are type-safe
- **Namespace when possible**: Use `useTranslations("namespace")` for better organization
- **Server components first**: Prefer server-side translations for better performance
- **Extract incrementally**: Migrate components one at a time
## Common Patterns
### Buttons and CTAs
```json
{
"buttons": {
"createCommunityRule": "Create CommunityRule",
"seeHowItWorks": "See how it works"
"title": "About us",
"subtitle": "Why CommunityRule exists"
}
}
```
### Aria Labels
## Style notes
```json
{
"ariaLabels": {
"followBluesky": "Follow us on Bluesky",
"followGitlab": "Follow us on GitLab"
}
}
```
- **camelCase** for structural keys (`compactTitle`, `imageAlt`).
- **kebab-case** for content ids that match a URL slug, card id, or step
id (`"in-person-meetings"`, `"peer-mediation"`).
- Use `_comment` to leave context for the next editor.
- Keep terminology consistent — search the messages folder before coining a
new label.
### Dynamic Content
## Adding a new language (future)
For content that varies (like card text), use arrays or numbered keys:
```json
{
"cards": {
"card1": { "text": "First step" },
"card2": { "text": "Second step" },
"card3": { "text": "Third step" }
}
}
```
1. Create `messages/<locale>/` mirroring `messages/en/`.
2. Translate strings; keep keys identical.
3. Update `messages/en/index.ts` (or split it per locale) — a developer
will wire the locale switcher.
## Troubleshooting
### Translation Key Not Found
If you see a key path like `heroBanner.title` instead of the text:
1. Check the JSON file exists and has the key
2. Verify the key path matches exactly (case-sensitive)
3. Restart the dev server if you just added the key
### TypeScript Errors
If TypeScript complains about translation keys:
1. Ensure the key exists in the JSON file
2. Check for typos in the key path
3. Verify the namespace is correct if using `useTranslations("namespace")`
### Missing Translations
If text doesn't appear:
1. Check the browser console for errors
2. Verify the component is wrapped in `MessagesProvider` (for client components)
3. Ensure `getTranslation()` is called correctly in server components
4. Check if you're using the correct namespace (`pages.*` vs component defaults)
## Architecture: Hybrid Approach
This implementation follows the recognized best practice of combining:
- **Globalized, shared UI elements**: Component defaults in `components/` (aria-labels, alt texts)
- **Context-aware, localized content pages**: Page-specific content in `pages/` (titles, descriptions)
This allows:
- Components to remain flexible and reusable
- Page content to be easily edited without code changes
- Clear separation between shared defaults and page-specific content
- Scalable structure for adding new pages
## Resources
- Component defaults in `messages/en/components/`
- Page-specific content in `messages/en/pages/`
- Shared UI strings in `messages/en/common.json`
---
**Last Updated**: January 2025
**Maintained by**: CommunityRule Development Team
| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Key path renders instead of text (e.g. `hero.title`) | Missing key or typo | Check spelling and bundle path |
| Copy doesn't update | Dev server cache | Restart `npm run dev` |
| TypeScript red squiggle | Bundle not registered | Add the import in `messages/en/index.ts` |
@@ -78,7 +78,7 @@ Order is preserved here because the columns are positional in the sheets:
see §2.4 data-quality issues).
- Wizard chip ids are **positional 1..N** within each `messages/en/create/*`
array (see `chipRowsFromLabels` in
`app/create/screens/select/CommunityStructureSelectScreen.tsx` lines 4957).
`app/(app)/create/screens/select/CommunityStructureSelectScreen.tsx` lines 4957).
The importer should emit a stable lookup table mapping
`(facetGroup, label) → wizardChipId` so the recommendation engine can match
a user's `selectedXxxIds` against the matrix without depending on label
@@ -90,7 +90,7 @@ Order is preserved here because the columns are positional in the sheets:
Maps 1:1 to `messages/en/create/communication.json` and the
`communication-methods` step
(`app/create/screens/card/CommunicationMethodsScreen.tsx`).
(`app/(app)/create/screens/card/CommunicationMethodsScreen.tsx`).
**Content columns (positions 15):**
@@ -112,7 +112,7 @@ Matrix / Element · GitHub / GitLab · Discord · Email Distribution List · Sla
### 2.3 Membership / Group-Membership (`Group_Membership_Methods.xlsx`, sheet `Current`)
Maps to the `membership-methods` step
(`app/create/screens/card/MembershipMethodsScreen.tsx`) and
(`app/(app)/create/screens/card/MembershipMethodsScreen.tsx`) and
`messages/en/create/membership.json`.
**Content columns (positions 15):**
@@ -142,7 +142,7 @@ Evaluation · Lottery / Sortition.
### 2.4 Decision-making (`Decision-making.xlsx`, sheet `Current`)
Maps to the `decision-approaches` step
(`app/create/screens/right-rail/DecisionApproachesScreen.tsx`) and
(`app/(app)/create/screens/right-rail/DecisionApproachesScreen.tsx`) and
`messages/en/create/rightRail.json`.
**Content columns (positions 17):**
@@ -182,7 +182,7 @@ Hierarchical Decision-Making · Negotiated Decisions.
### 2.5 Conflict Management (`Conflict Management Methods.xlsx`, sheet `Current`)
Maps to the `conflict-management` step
(`app/create/screens/card/ConflictManagementScreen.tsx`) and
(`app/(app)/create/screens/card/ConflictManagementScreen.tsx`) and
`messages/en/create/conflictManagement.json`.
**Content columns (positions 16):**
@@ -251,7 +251,7 @@ function governancePatternBody(coreValues: string): Prisma.InputJsonValue {
### 3.2 Wizard facets captured today (`CreateFlowState`)
```83:95:app/create/types.ts
```83:95:app/(app)/create/types.ts
selectedCommunitySizeIds?: string[];
selectedOrganizationTypeIds?: string[];
selectedScaleIds?: string[];
@@ -275,7 +275,7 @@ the facet shape.
### 3.3 Wizard step order
Source of truth is `app/create/utils/flowSteps.ts` (`FLOW_STEP_ORDER`). The
Source of truth is `app/(app)/create/utils/flowSteps.ts` (`FLOW_STEP_ORDER`). The
relevant slice is:
```
@@ -289,10 +289,10 @@ decision-approaches → conflict-management → confirm-stakeholders → final-r
| Surface | File |
|---|---|
| Marketing home "Popular templates" | `app/(marketing)/MarketingRuleStackSection.tsx` |
| Marketing home "Popular templates" | `app/(marketing)/_components/MarketingRuleStackSection.tsx` |
| Templates index | `app/(marketing)/templates/page.tsx` |
| Template preview (by slug) | `app/create/review-template/[slug]/page.tsx` |
| "Use without changes" → publish | `app/create/CreateFlowLayoutClient.tsx` `handleUseTemplateWithoutChanges` |
| Template preview (by slug) | `app/(app)/create/review-template/[slug]/page.tsx` |
| "Use without changes" → publish | `app/(app)/create/CreateFlowLayoutClient.tsx` `handleUseTemplateWithoutChanges` |
| API list | `app/api/templates/route.ts` (GET only, no params) |
There is currently **no** recommendation logic, no facet filtering, and the
@@ -697,12 +697,12 @@ Once the API exists:
- `lib/server/validation/plainJson.ts``assertPlainJsonValue` /
`DEFAULT_PLAIN_JSON_LIMITS`.
- `lib/logger.ts` — server-side `logger`.
- `app/create/types.ts``CreateFlowState` and facet fields.
- `app/create/utils/flowSteps.ts` — canonical step order.
- `app/create/utils/createFlowScreenRegistry.ts` — screen layout per step.
- `app/create/screens/select/CommunityStructureSelectScreen.tsx` — chip-id
- `app/(app)/create/types.ts``CreateFlowState` and facet fields.
- `app/(app)/create/utils/flowSteps.ts` — canonical step order.
- `app/(app)/create/utils/createFlowScreenRegistry.ts` — screen layout per step.
- `app/(app)/create/screens/select/CommunityStructureSelectScreen.tsx` — chip-id
derivation pattern (positional `String(i+1)`).
- `app/create/screens/card/CommunicationMethodsScreen.tsx` — section-field
- `app/(app)/create/screens/card/CommunicationMethodsScreen.tsx` — section-field
contract (`SECTION_FIELDS`).
- `messages/en/create/{communitySize,communityStructure,communication,membership,rightRail,conflictManagement}.json`
current static card / chip copy that the matrix supersedes.
+68
View File
@@ -0,0 +1,68 @@
# Testing guide
This is the **why** of testing in CommunityRule. For file layout, helper
APIs, and required imports see `.cursor/rules/testing.mdc`.
## Philosophy
- **Test behaviour, not implementation.** Assert on what a user can see and
do (visible text, roles, labels, keyboard paths). Avoid leaning on
internal class names, hook internals, or memoization details.
- **One consolidated file per component.** A component's standard suite,
variant assertions, and behaviour-specific cases all live in
`tests/components/<Name>.test.tsx`.
- **Accessibility is non-negotiable.** Every component suite runs
`jest-axe`; full-page WCAG runs in Playwright. A failing axe check is a
failing build.
- **E2E is sparse and high-signal.** Playwright covers critical journeys,
visual regression of major pages, and a handful of edge cases — not
per-component clicks. Component coverage is the job of Vitest.
## Layered coverage
| Layer | Tool | Scope |
| --- | --- | --- |
| Unit | Vitest | Pure logic, reducers, utilities (`tests/unit/`). |
| Component | Vitest + RTL | DS components in isolation (`tests/components/`). |
| Page / context | Vitest + RTL | Screens and provider wiring (`tests/pages/`, `tests/contexts/`). |
| Accessibility (page) | Playwright + axe | WCAG 2.1 AA on key pages (`tests/accessibility/e2e/`). |
| E2E | Playwright | Critical journeys, visual regression, edge cases (`tests/e2e/`). |
## What to test vs. skip
**Test:**
- Public behaviour — visible text, roles, ARIA, keyboard activation.
- State transitions users observe (error → success, disabled → enabled).
- Interaction contracts (click handlers, form submission, dropdown
selection).
- Accessibility invariants.
**Skip:**
- Pure styling that changes with the design system (exact shadow radius,
minor spacing).
- Hook internals or memoization specifics.
- Responsive visibility in JSDOM — use Playwright instead.
## Running tests
```bash
npm test # vitest run with coverage
npm run test:component # vitest, components only (faster inner loop)
npm run e2e # playwright (alias: test:e2e)
npm run visual:update # refresh playwright screenshots after UI changes
```
## Adding tests for a new component
1. Create `app/components/<Name>/`.
2. Create `tests/components/<Name>.test.tsx` and call `componentTestSuite`
(see `.cursor/rules/testing.mdc`).
3. Add behaviour-specific `describe` blocks for any unique interactions.
4. Run `npm run test:component -- --run tests/components/<Name>.test.tsx`.
## Storybook is documentation, not tests
Stories are visual examples for design review and Code Connect — they do
not replace component tests. Run `npm run storybook` locally to browse.