# Dealer Editors

> **Note:** This document covers the editing interfaces only. For dashboard features like leads, settings, subscription management, stats, and preview, see [DEALER_DASHBOARD.md](./DEALER_DASHBOARD.md).

## Self-Serve Quick Reference (for support)

When a dealer asks us to change something on their site, most of the time they could do it themselves but don't know where to look. Direct them to:

| Dealer asks for…                     | Send them to                                | Notes                                                                       |
| ------------------------------------ | ------------------------------------------- | --------------------------------------------------------------------------- |
| Logo change                          | `/dashboard/editor`                         | Logo upload field is on the editor page (`LogoUpload` component)            |
| Business name / address / phone      | `/dashboard/editor`                         | Editable fields on the same page as the logo                                |
| Header text / display name           | `/dashboard/editor`                         | Driven by the `businessName` field; some dealers call this "header title"   |
| Tracking scripts (GA, AdWords, etc.) | `/dashboard/editor`                         | Head/body script fields available on most tiers; see in-editor syntax notes |
| Hide phone or address                | Tier-gated - see `PLAN_FEATURES_MATRIX.md`  | Not available on starter; dealers may need to upgrade                       |
| Media library (general uploads)      | `/dashboard/media`                          | Top-level route, not under `/cms/`                                          |
| Slider images / banner               | `/dashboard/slider-editor`                  | Enhanced+ tier only; top-level route, not under `/cms/`                     |
| Page content (CMS)                   | `/dashboard/cms/pages`                      | Professional tier only                                                      |
| Custom domain setup                  | `/dashboard/settings/domains`               | The settings page is currently domains-only                                 |
| Email forwarding                     | **Not supported** - we provide website only | Dealer should set up email through their domain registrar or email provider |
| Subdomain change                     | **Admin-only** - escalate                   | Subdomains are read-only after creation; admins can override via API        |
| Dealer number change                 | **Admin-only** - escalate                   | Changing this can break referral tracking                                   |

If a dealer's tier doesn't include the feature they want, point them at `PLAN_FEATURES_MATRIX.md` (or have the support team summarize it for them).

## Overview

The dealer editors provide AMSOIL dealers with tools to customize their business information and site content. This document covers the editing interfaces available within the dealer dashboard:

### Editor Access by Tier

```mermaid
flowchart LR
    subgraph Starter["Starter Tier"]
        S_BASIC["Basic Editor"]
    end

    subgraph Growth["Growth Tier"]
        G_BASIC["Basic Editor"]
        G_MEDIA["Media Library"]
        G_LINKS["Link Editor"]
    end

    subgraph Enhanced["Enhanced Tier"]
        E_BASIC["Basic Editor"]
        E_MEDIA["Media Library"]
        E_LINKS["Link Editor"]
        E_SLIDER["Slider Editor"]
        E_NAV["Navigation Editor"]
        E_CMS["CMS Page Editor<br/>(edit only)"]
    end

    subgraph Professional["Professional Tier"]
        P_BASIC["Basic Editor"]
        P_MEDIA["Media Library"]
        P_LINKS["Link Editor"]
        P_SLIDER["Slider Editor"]
        P_NAV["Navigation Editor"]
        P_CMS["CMS Page Editor<br/>(create + edit)"]
    end

    classDef available fill:#d4edda,stroke:#28a745,color:#1a1a1a
    class S_BASIC,G_BASIC,G_MEDIA,G_LINKS,E_BASIC,E_MEDIA,E_LINKS,E_SLIDER,E_NAV,E_CMS,P_BASIC,P_MEDIA,P_LINKS,P_SLIDER,P_NAV,P_CMS available
```

1. **Basic Editor** (`/dashboard/editor`) - Business information and contact details
2. **CMS Page Editor** (`/dashboard/cms/pages/[id]/edit`) - Visual page editing with Puck
3. **Navigation Editor** (`/dashboard/cms/navigation`) - Site navigation management
4. **Slider Editor** (`/dashboard/slider-editor`) - Hero slider customization
5. **Media Library** (`/dashboard/media`) - Image upload and management
6. **Link Editor** (`/dashboard/cms/links`) - AMSOIL quick link configuration

## Architecture

### Component Structure

```
app/dashboard/
  editor/
    page.tsx           # Server component - fetches dealer data
    EditorForm.tsx     # Client component - form logic and submission
    README.md          # Internal component documentation
  cms/
    pages/
      page.tsx         # Page listing
      new/page.tsx     # Create new page
      [id]/edit/page.tsx  # Puck visual editor
    navigation/page.tsx   # Navigation tree editor
    links/page.tsx        # Quick links editor
  slider-editor/page.tsx  # Hero slider editor
  media/page.tsx          # Media library
```

### Key Library Files

```
lib/
  dealer-validation.ts    # Input validation and sanitization
  cms/
    puck-config.tsx       # Puck editor component configuration
    types.ts              # CMS type definitions and tier access
    validation.ts         # Page validation rules
    sanitize.ts           # HTML sanitization for security
    LexicalField.tsx      # Rich text editor integration
    ImagePickerField.tsx  # Media picker for Puck
```

---

## Basic Editor (`/dashboard/editor`)

### Features

- Pre-filled form with existing dealer data from database
- Real-time subdomain availability checking (for first-time setup)
- Client-side and server-side validation with field-level error feedback
- Domain prefix selection (myamsoil or shopamsoil)
- Custom domain configuration (Growth+ tiers)
- Address visibility toggle (hide address from public page)
- Automatic publish after save

### Editable Fields

All fields EXCEPT subdomain (after first publish):

| Field          | Description          | Validation                                                                                                        |
| -------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `dealerNumber` | ZO Account Number    | Required (non-empty, digits-only at intake); max 20 chars. See [ZO Number Compliance](./ZO_NUMBER_COMPLIANCE.md). |
| `businessName` | Business Name        | Required, max 200 chars                                                                                           |
| `contactName`  | Contact Name         | Max 100 chars                                                                                                     |
| `address`      | Street Address       | Required, max 255 chars                                                                                           |
| `city`         | City                 | Required, max 100 chars                                                                                           |
| `state`        | State/Province       | Required, 2-letter code                                                                                           |
| `zip`          | ZIP/Postal Code      | Required, max 20 chars                                                                                            |
| `country`      | Country              | Required, max 100 chars                                                                                           |
| `phone`        | Phone Number         | Required, min 10 digits                                                                                           |
| `contactEmail` | Public Contact Email | Valid email format, max 254 chars                                                                                 |
| `description`  | Business Description | Max 50000 chars, WYSIWYG editor with formatting (bold, italic, lists, links)                                      |
| `customDomain` | Custom Domain        | Valid domain format, must be unique                                                                               |
| `hideAddress`  | Hide Address Toggle  | Boolean                                                                                                           |

### Subdomain Policy

**Subdomain is READ-ONLY after initial setup.**

- The subdomain can only be set once during first publish
- If the subdomain is "not-set", the editor allows selection
- Once published, the subdomain becomes locked permanently
- This prevents broken links, SEO issues, and customer confusion
- Admins can override via `/api/admin/dealers/[id]/subdomain`

### Email Separation

The system maintains two distinct email addresses:

- **Login Email** (`User.email`) - OAuth email, used for authentication only
- **Display Email** (`Dealer.contactEmail`) - Public contact email, fully editable

### Data Flow

1. **Server Component** (`page.tsx`):
   - Verifies authentication via NextAuth session
   - Fetches dealer data with Prisma
   - Redirects to `/` if no dealer found
   - Passes initial data to EditorForm

2. **Client Component** (`EditorForm.tsx`):
   - Maintains form state with React useState
   - Debounced subdomain availability checks (500ms)
   - Submits to `POST /api/dealer/update`
   - On success, calls `POST /api/dealers/[id]/publish`
   - Displays success/error feedback

3. **API Endpoints**:
   - `POST /api/dealer/update` - Validates and saves dealer data
   - `POST /api/dealers/[id]/publish` - Creates DNS record and activates site

---

## CMS Page Editor (`/dashboard/cms/pages/[id]/edit`)

### Overview

The CMS page editor uses Puck, a visual page builder that allows dealers to create custom pages using drag-and-drop components. The editor stores page content as JSON in the `puckData` field.

### Access Control

CMS access is tier-gated:

| Tier         | View Pages | Edit Pages    | Create Pages | Full Puck Editor |
| ------------ | ---------- | ------------- | ------------ | ---------------- |
| Starter      | Yes        | Migrated only | No           | No               |
| Growth       | Yes        | Migrated only | No           | No               |
| Enhanced     | Yes        | Yes           | No           | Yes              |
| Professional | Yes        | Yes           | Yes          | Yes              |

### Available Components

The Puck configuration (`lib/cms/puck-config.tsx`) provides these components:

**Content Components:**

- **Heading** - H2/H3/H4 with alignment options
- **RichText** - Lexical-powered rich text editor
- **Image** - Image with caption, width, and alignment

**Interactive Components:**

- **Accordion** - Collapsible FAQ-style sections
- **Tabs** - Tabbed content panels

**Layout Components:**

- **TwoColumn** - 50/50, 33/67, or 67/33 column layouts
- **Spacer** - Vertical spacing (16px, 32px, 64px)
- **Divider** - Horizontal line separators

### Rich Text Editor (Lexical)

The RichText component uses Lexical (`LexicalField.tsx`) for WYSIWYG editing:

- Bold, italic, underline formatting
- Link insertion with URL validation
- Paragraph and heading styles
- Per-paragraph text alignment (left/center/right); disabled inside lists, where Lexical exports but does not re-import `text-align`
- Clean HTML output sanitized with DOMPurify

### Image Handling

Images are managed through the Media Library (`ImagePickerField.tsx`):

1. Click "Choose Image" in the Puck sidebar
2. Select from uploaded images or AMSOIL brand library
3. Images are stored on Cloudflare R2
4. URLs are validated and stored in component props

### Page Settings

Each page has metadata accessible via the Settings panel:

- **Title** - Page title (used in browser tab)
- **Slug** - URL path segment (lowercase, hyphens only)
- **Meta Title** - SEO title override
- **Meta Description** - Search engine description
- **Author** - (Blog posts only) Author name
- **Excerpt** - (Blog posts only) Short summary

**Meta Title** (60 chars) and **Meta Description** (160 chars) show a live character counter. If a value exceeds its limit - including longer copy carried over from a migrated page - the counter turns red and prompts you to shorten it. A save blocked by an over-limit SEO field is reported inline and automatically opens the Settings panel at the offending field, so the editor and any unsaved content are never discarded.

### Save vs Publish Workflow

> **Publish prerequisite:** Publishing requires a non-blank ZO number on the dealer. If missing, the API returns `422 ZO_REQUIRED` and the editor surfaces a specific message. Saving a draft is unaffected. See [ZO Number Compliance](./ZO_NUMBER_COMPLIANCE.md).

```mermaid
flowchart TD
    EDIT["Edit Content in Puck Editor"]
    SAVE["Save Draft"]
    PUBLISH["Publish"]

    EDIT --> SAVE
    EDIT --> PUBLISH

    subgraph SaveFlow["Save Flow"]
        S1["Update puckData in DB"]
        S2["Keep page status unchanged"]
        S3["Changes NOT visible on public site"]
    end

    subgraph PublishFlow["Publish Flow"]
        P1["Save all changes to DB"]
        P2["Set page status to 'published'"]
        P3["Call /api/dealers/id/publish"]
        P4["Trigger ISR revalidation"]
        P5["Changes visible on public site"]
    end

    SAVE --> S1 --> S2 --> S3
    PUBLISH --> P1 --> P2 --> P3 --> P4 --> P5

    classDef draft fill:#fff3cd,stroke:#ffc107,color:#1a1a1a
    classDef live fill:#d4edda,stroke:#28a745,color:#1a1a1a
    class S1,S2,S3 draft
    class P1,P2,P3,P4,P5 live
```

1. **Save** - Stores changes without making them live
   - Updates `puckData` in database
   - Does NOT trigger ISR revalidation

2. **Publish** - Makes changes live on the public site
   - Saves all changes
   - Sets page status to 'published'
   - Calls `/api/dealers/[id]/publish` endpoint
   - Triggers ISR revalidation for the page

### Preview

The editor includes a live preview with:

- Dealer header with business name and navigation
- Full-width content area with actual components
- Dealer footer with contact information
- Viewport controls (desktop/tablet/mobile)

---

## Navigation Editor (`/dashboard/cms/navigation`)

### Access Control

Navigation editor is available for Enhanced and Professional tiers.

### Features

- Drag-and-drop reordering of menu items
- Two-level navigation hierarchy (parent + children)
- Link types: Internal page, External URL, or None (parent-only)
- Open in new tab toggle
- Default items with reset capability

### Data Structure

Navigation items are stored in the `NavigationItem` table:

```typescript
interface NavigationItem {
  id: string;
  dealerId: string;
  label: string; // Display text
  pageId: string | null; // Link to CMS page
  externalUrl: string | null; // External link
  parentId: string | null; // Parent item (for children)
  sortOrder: number; // Display order
  openInNewTab: boolean; // Target behavior
  isDefault: boolean; // System-provided item
  defaultKey: string | null; // Identifies default item
}
```

### Default Navigation

On first publish, dealers receive default navigation items seeded by `seedDefaultNavigation()`. These include:

- Home (links to dealer homepage)
- Shop (links to AMSOIL catalog with dealer number)
- About (links to About page if created)
- Contact (links to Contact page if created)

### URL Auto-Prefixing

When entering external URLs, the navigation editor automatically handles common URL formats:

| Input                      | Saved As                   | Notes                              |
| -------------------------- | -------------------------- | ---------------------------------- |
| `example.com`              | `https://example.com`      | Bare domains get `https://` prefix |
| `http://example.com`       | `http://example.com`       | Existing protocols preserved       |
| `https://example.com`      | `https://example.com`      | Already valid                      |
| `/about`                   | `/about`                   | Relative paths unchanged           |
| `./page`                   | `./page`                   | Relative paths unchanged           |
| `#section`                 | `#section`                 | Anchor links unchanged             |
| `mailto:dealer@amsoil.com` | `mailto:dealer@amsoil.com` | Contact protocols preserved        |
| `tel:+1-555-123-4567`      | `tel:+1-555-123-4567`      | Contact protocols preserved        |

**Edge case:** URLs with ports (e.g., `example.com:8080`) are ambiguous and may not be auto-prefixed correctly. Always include `https://` when specifying ports.

**Implementation layers:**

- **Client-side:** URL normalized on blur (visual feedback) and on submit
- **Server-side:** URL normalized again as defense-in-depth

---

## Slider Editor (`/dashboard/slider-editor`)

### Access Control

Slider editor is available for Enhanced and Professional tiers.

### Features

- Add/remove/reorder hero slides
- Upload or select images for each slide
- Caption and call-to-action button configuration
- Live preview while editing
- Reset to default AMSOIL slider

### Slide Properties

Each slide can have:

- **Image** - Background image (required)
- **Caption** - Text overlay
- **Button Text** - CTA button label
- **Button URL** - CTA button destination

### Save Workflow

1. Edit slides in the editor
2. Click "Publish" to save and publish
3. Slider content is saved via server action
4. Site is republished for changes to appear

---

## Media Library (`/dashboard/media`)

### Access Control

Media library is available for all paying tiers (Starter+).

### Storage Limits

| Tier         | Image Limit |
| ------------ | ----------- |
| Starter      | 100 images  |
| Growth       | 100 images  |
| Enhanced     | 500 images  |
| Professional | 1000 images |

### Supported Formats

- JPEG/JPG
- PNG
- WebP
- Maximum file size: 10MB

### Features

- Drag-and-drop upload
- Grid view with thumbnails
- Delete functionality
- Usage tracking (current/limit)
- AMSOIL brand image library tab

### Image Storage

Uploaded images are stored on Cloudflare R2 and served via Cloudflare CDN. Images are automatically optimized for web delivery.

---

## API Endpoints

### Dealer Update

**Route:** `POST /api/dealer/update`

**Authentication:** Required (enforced by middleware)

**Authorization:** User can only update their own dealer record

**Request Body:**

```json
{
  "businessName": "Required",
  "address": "Required",
  "city": "Required",
  "state": "Required (2-letter code)",
  "zip": "Required",
  "country": "Required",
  "phone": "Required (min 10 digits)",
  "contactEmail": "Optional (valid email)",
  "customDomain": "Optional (unique domain)",
  "description": "Optional",
  "hideAddress": false
}
```

**Success Response:** `200 OK`

```json
{
  "success": true,
  "dealer": { ... }
}
```

**Error Responses:**

- `400` - Validation failed
- `401` - Not authenticated
- `403` - Subdomain change attempted after setup
- `404` - Dealer not found
- `409` - Custom domain conflict

### Publish Site

**Route:** `POST /api/dealers/[id]/publish`

**Purpose:** Activates dealer site and creates DNS record

> **ZO precondition:** This route - along with `POST /api/cms/publish` and the publish path of `PUT /api/cms/pages/[id]` - is gated by `lib/zo-guard.ts`. A dealer with a missing or blank `dealerNumber` receives `422 { code: 'ZO_REQUIRED' }` before any work is done; live sites are never unpublished by the guard. See [ZO Number Compliance](./ZO_NUMBER_COMPLIANCE.md).

**First Publish:**

1. Creates Cloudflare DNS record for subdomain
2. Seeds default navigation items
3. Sets dealer status to 'active'
4. Triggers ISR revalidation

**Republish:**

1. Triggers ISR revalidation
2. Updates `lastPublishedAt` timestamp

---

## Validation

### Field Validation (`lib/dealer-validation.ts`)

Validation includes:

- **Required field checks** - businessName, address, city, state, zip, country, phone
- **Length limits** - All fields have maximum character limits
- **Format validation** - Email regex, 2-letter state code, 10-digit phone
- **Domain validation** - Custom domain format check

### Sanitization

All input is sanitized before storage:

- **Plain text fields** - HTML tags and control characters are stripped
- **Description field** - Uses DOMPurify to allow safe HTML formatting (bold, italic, lists, links) while blocking XSS vectors (scripts, event handlers, dangerous protocols)

HTML entities are NOT encoded at storage because React automatically escapes JSX content for XSS protection.

---

## Styling

The editor uses the same card styling as registration/onboarding:

- `.editor-page-root` class applies card styling
- NOT included in `body:has()` selectors (unlike signin/welcome/onboarding)
- This allows dashboard header to remain full-width

See `app/styles/registration-theme.css` for implementation.

---

## Security

### Authentication

- All editor pages require NextAuth session
- Middleware enforces authentication for `/dashboard/*` routes
- Sessions use secure HTTP-only cookies

### Authorization

- Users can only edit their own dealer records
- Tier-based access control for CMS features
- Impersonation actions are logged for audit

### XSS Prevention

- Input sanitization strips HTML tags
- Puck content is sanitized with DOMPurify
- React's JSX escaping prevents XSS in templates

---

## Testing

Run tests for editor components:

```bash
# Editor form integration tests
npm test -- app/dashboard/editor

# Dealer validation unit tests
npm test -- lib/__tests__/dealer-validation.test.ts

# API endpoint tests (if available)
npm test -- app/api/dealer/update
```

---

## Extending Puck Components

The Puck visual editor is the primary extensibility point for the CMS. This section covers how to add new components, custom fields, and modify existing behavior.

### Adding a New Component

**File:** `lib/cms/puck-config.tsx`

**Step 1: Define the props interface**

```typescript
export interface CalloutProps {
  type: 'info' | 'warning' | 'success';
  title: string;
  content: string;
}
```

**Step 2: Create the render component**

See existing components like `AccordionComponent` or `RichTextComponent` for the pattern. Key points:

- Use inline styles (Tailwind classes don't work in Puck preview)
- For HTML content, use `sanitizeHtmlWithSecureLinks()` from `./sanitize-client`
- Follow the existing color scheme (`#093b66` for headings, `#c6c6c6` for borders)

**Step 3: Register in puckConfig.components**

```typescript
Callout: {
  label: 'Callout Box',
  fields: {
    type: {
      type: 'select',
      label: 'Type',
      options: [
        { label: 'Info', value: 'info' },
        { label: 'Warning', value: 'warning' },
        { label: 'Success', value: 'success' },
      ],
    },
    title: { type: 'text', label: 'Title' },
    content: {
      type: 'custom',
      label: 'Content',
      render: ({ value, onChange }) => (
        <LexicalField value={value || ''} onChange={onChange} />
      ),
    },
  },
  defaultProps: {
    type: 'info',
    title: 'Note',
    content: '<p>Enter callout content...</p>',
  },
  render: CalloutComponent as any,
},
```

**Step 4: Add to a category**

```typescript
categories: {
  content: {
    title: 'Content',
    components: ['Heading', 'RichText', 'Image', 'Callout'],  // Added
  },
  // ...
},
```

### Custom Field Types

Puck supports custom fields via `type: 'custom'`. The project uses two:

| Custom Field       | File                           | Purpose              |
| ------------------ | ------------------------------ | -------------------- |
| `LexicalField`     | `lib/cms/LexicalField.tsx`     | Rich text (WYSIWYG)  |
| `ImagePickerField` | `lib/cms/ImagePickerField.tsx` | Media library picker |

**Creating a custom field:**

```typescript
// In puck-config.tsx
myField: {
  type: 'custom',
  label: 'My Field',
  render: ({ value, onChange }) => (
    <MyCustomComponent value={value} onChange={onChange} />
  ),
},
```

### Component Lifecycle Hooks

**`resolveData`** - Transform props before rendering (async supported):

```typescript
Accordion: {
  // ... fields and defaultProps
  resolveData: async ({ props }) => {
    // Ensure every item has a unique ID
    const items = props.items?.map((item) => ({
      ...item,
      id: item.id || crypto.randomUUID(),
    }));
    return { props: { ...props, items } };
  },
  render: AccordionComponent,
},
```

### Extending the Rich Text Editor

**File:** `lib/cms/LexicalField.tsx`

The Lexical editor can be extended with additional nodes and plugins:

```typescript
// Add new node types to initialConfig.nodes
nodes: [
  HeadingNode, QuoteNode, ListNode, ListItemNode,
  LinkNode, AutoLinkNode,
  // Add your custom nodes here
],
```

**Available Lexical formatting:**

- Bold, italic, strikethrough
- Headings (H2, H3, H4)
- Bullet and numbered lists
- Blockquotes
- Links (with URL validation)
- Undo/Redo

### Security Requirements

**All HTML content must be sanitized.** Import and use `sanitizeHtmlWithSecureLinks()` from `./sanitize-client` before rendering any user-provided HTML content.

This sanitization function prevents XSS attacks by:

- Stripping dangerous tags (`<script>`, `<iframe>`, etc.)
- Validating URL protocols (blocks `javascript:`)
- Adding `rel="noopener noreferrer"` to external links

See `RichTextComponent` and `AccordionComponent` in `puck-config.tsx` for examples of the correct pattern.

---

## Known Limitations

1. **Subdomain Lock** - Cannot be changed after first publish
2. **No Version History** - Page edits overwrite previous versions
3. **Single Timezone** - All dates are stored in UTC
4. **Image Size** - Large images may slow page load
5. **Mobile Editor** - Puck editor not optimized for mobile editing

---

## Future Enhancements

Planned improvements from `app/dashboard/editor/README.md`:

- Image upload for business logo
- Change history / audit log
- Email verification for contactEmail changes
