# Component Wrapping Guide

> How to take ANY existing React component and make it editable in the Puck visual editor — without changing the original component at all.

---

## The Pattern

For each existing homepage section, you create:

1. **A Puck config entry** — defines editable fields for the component's props
2. **An editor version** (only for server/async components) — client-side preview
3. **A server override** (only for async components) — swaps editor version with real version on the live site

---

## Example 1: Simple Static Component

### Original component:

```typescript
// components/HeroBanner.tsx (EXISTING — do NOT modify)
interface HeroBannerProps {
  title: string;
  subtitle?: string;
  bgImage?: string;
  ctaText?: string;
  ctaUrl?: string;
}

export function HeroBanner({ title, subtitle, bgImage, ctaText, ctaUrl }: HeroBannerProps) {
  return (
    <section className="relative h-[500px] bg-cover bg-center" style={{ backgroundImage: `url(${bgImage})` }}>
      <div className="absolute inset-0 bg-black/40" />
      <div className="relative z-10 container mx-auto px-4 h-full flex flex-col justify-center">
        <h1 className="text-4xl font-bold text-white">{title}</h1>
        {subtitle && <p className="text-xl text-white/80 mt-4">{subtitle}</p>}
        {ctaText && ctaUrl && (
          <a href={ctaUrl} className="mt-6 px-6 py-3 bg-blue-600 text-white rounded-lg inline-block">
            {ctaText}
          </a>
        )}
      </div>
    </section>
  );
}
```

### Puck config entry:

```typescript
import { MediaUploadField } from "@/components/editor/MediaUploadField";
import { HeroBanner } from "@/components/HeroBanner";

// In puckConfig.components:
SiteHeroBanner: {
  label: "Hero Banner",
  fields: {
    title: { type: "text", label: "Title" },
    subtitle: { type: "textarea", label: "Subtitle" },
    bgImage: {
      type: "custom",
      label: "Background Image",
      render: ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
        <MediaUploadField value={value || ''} onChange={onChange} accept="image" />
      ),
    },
    ctaText: { type: "text", label: "Button Text" },
    ctaUrl: { type: "text", label: "Button URL" },
  },
  defaultProps: {
    title: "COPY EXACT TEXT FROM CURRENT PAGE",
    subtitle: "COPY EXACT TEXT",
    bgImage: "COPY EXACT IMAGE URL",
    ctaText: "COPY EXACT BUTTON TEXT",
    ctaUrl: "COPY EXACT URL",
  },
  render: (props: any) => {
    const { puck: _, id: __, ...rest } = props;
    return <HeroBanner {...rest} />;
  },
},
```

**That's it for static components.** No editor version needed. The component renders directly in the editor.

---

## Example 2: Dynamic (DB-Fetching) Component

### Original component (async server component):

```typescript
// components/LatestPosts.tsx (EXISTING — do NOT modify)
import { getLatestArticles } from '@/lib/utils/db';

interface LatestPostsProps {
  title?: string;
  count?: number;
  columns?: number;
}

export async function LatestPosts({ title = '最新記事', count = 4, columns = 2 }: LatestPostsProps) {
  const articles = await getLatestArticles(count);
  
  return (
    <section className="py-12">
      <h2 className="text-2xl font-bold mb-6">{title}</h2>
      <div className={`grid grid-cols-${columns} gap-6`}>
        {articles.map((article) => (
          <a key={article.slug} href={`/post/${article.slug}`} className="bg-white rounded-xl shadow-sm p-4">
            <img src={article.cardImage} alt={article.title} className="rounded-lg mb-4" />
            <h3 className="font-semibold">{article.title}</h3>
            <p className="text-sm text-gray-500 mt-2">{article.excerpt}</p>
          </a>
        ))}
      </div>
    </section>
  );
}
```

### Step 1: Create editor version

```typescript
// components/sections/SiteLatestPostsEditor.tsx (NEW)
'use client';

interface SiteLatestPostsEditorProps {
  title?: string;
  count?: number;
  columns?: number;
}

export function SiteLatestPostsEditor({
  title = '最新記事',
  count = 4,
  columns = 2,
}: SiteLatestPostsEditorProps) {
  return (
    <section className="py-12">
      <h2 className="text-2xl font-bold mb-6">{title}</h2>
      <div className={`grid gap-6`} style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}>
        {Array.from({ length: count }).map((_, i) => (
          <div key={i} className="bg-white rounded-xl shadow-sm p-4">
            <div className="bg-gray-200 rounded-lg aspect-video mb-4 animate-pulse" />
            <div className="bg-gray-200 h-5 rounded w-3/4 mb-2 animate-pulse" />
            <div className="bg-gray-100 h-4 rounded w-full animate-pulse" />
          </div>
        ))}
      </div>
      <p className="text-center text-xs text-gray-400 mt-4">
        ↑ Live preview: {count} latest articles will appear here
      </p>
    </section>
  );
}
```

### Step 2: Puck config entry

```typescript
import { SiteLatestPostsEditor } from "@/components/sections/SiteLatestPostsEditor";

SiteLatestPosts: {
  label: "Latest Posts Grid",
  fields: {
    title: { type: "text", label: "Section Title" },
    count: { type: "number", label: "Number of Posts", min: 1, max: 12 },
    columns: {
      type: "select",
      label: "Columns",
      options: [
        { label: "2 Columns", value: 2 },
        { label: "3 Columns", value: 3 },
        { label: "4 Columns", value: 4 },
      ],
    },
  },
  defaultProps: {
    title: "EXACT HEADING FROM CURRENT PAGE",
    count: 4,    // ← same as current page
    columns: 2,  // ← same as current page
  },
  render: (props: any) => {
    const { puck: _, id: __, ...rest } = props;
    return <SiteLatestPostsEditor {...rest} />;
  },
},
```

### Step 3: Server override in page.tsx

```typescript
import { LatestPosts } from "@/components/LatestPosts";

const serverConfig = {
  ...puckConfig,
  components: {
    ...puckConfig.components,
    SiteLatestPosts: {
      ...puckConfig.components.SiteLatestPosts,
      render: (props: any) => {
        const { puck: _, id: __, ...rest } = props;
        return <LatestPosts {...rest} />;
      },
    },
  },
};
```

---

## Example 3: Component with Array Props (Items/Cards)

### Original component:

```typescript
// components/FeatureCards.tsx
interface Feature {
  icon: string;
  title: string;
  description: string;
}

interface FeatureCardsProps {
  heading: string;
  features: Feature[];
}

export function FeatureCards({ heading, features }: FeatureCardsProps) {
  // renders a grid of feature cards
}
```

### Puck config entry:

```typescript
SiteFeatureCards: {
  label: "Feature Cards",
  fields: {
    heading: { type: "text", label: "Section Heading" },
    features: {
      type: "array",
      label: "Features",
      arrayFields: {
        icon: {
          type: "custom",
          label: "Icon",
          render: ({ value, onChange }: { value: string; onChange: (v: string) => void }) => (
            <IconPickerField value={value || ''} onChange={onChange} />
          ),
        },
        title: { type: "text", label: "Title" },
        description: { type: "textarea", label: "Description" },
      },
      defaultItemProps: {
        icon: "star",
        title: "Feature Title",
        description: "Feature description",
      },
    },
  },
  defaultProps: {
    heading: "EXACT HEADING FROM CURRENT PAGE",
    features: [
      // COPY EACH ITEM EXACTLY FROM CURRENT PAGE
      { icon: "shield", title: "EXACT TITLE 1", description: "EXACT DESC 1" },
      { icon: "zap", title: "EXACT TITLE 2", description: "EXACT DESC 2" },
      { icon: "heart", title: "EXACT TITLE 3", description: "EXACT DESC 3" },
    ],
  },
  render: (props: any) => {
    const { puck: _, id: __, ...rest } = props;
    return <FeatureCards {...rest} />;
  },
},
```

---

## Example 4: Adding dataMode (Auto/Manual) to Dynamic Components

For components that fetch from DB, give users the choice:

```typescript
SiteLatestPosts: {
  label: "Latest Posts Grid",
  fields: {
    dataMode: {
      type: "radio",
      label: "Data Source",
      options: [
        { label: "Auto from database", value: "auto" },
        { label: "Manual (enter items below)", value: "manual" },
      ],
    },
    title: { type: "text", label: "Section Title" },
    count: { type: "number", label: "Number of Posts (Auto mode)" },
    columns: { type: "select", label: "Columns", options: [...] },
    manualItems: {
      type: "array",
      label: "Manual Items (only when Manual mode is selected)",
      arrayFields: {
        title: { type: "text", label: "Title" },
        excerpt: { type: "textarea", label: "Excerpt" },
        image: {
          type: "custom", label: "Image",
          render: ({ value, onChange }: any) => (
            <MediaUploadField value={value || ''} onChange={onChange} accept="image" />
          ),
        },
        linkUrl: { type: "text", label: "Link URL" },
      },
      defaultItemProps: { title: "Article Title", excerpt: "", image: "", linkUrl: "" },
    },
  },
  defaultProps: {
    dataMode: "auto",
    title: "EXACT HEADING",
    count: 4,
    columns: 2,
    manualItems: [],
  },
  // ... render
},
```

The server component then checks `dataMode`:

```typescript
export async function LatestPosts({ dataMode, title, count, columns, manualItems }) {
  const articles = dataMode === 'manual'
    ? manualItems
    : await getLatestArticles(count);
  // render articles
}
```

---

## Naming Convention

Prefix all site-specific wrapped components with `Site`:

| Original Component | Puck Config Key | Editor File |
|---|---|---|
| `HeroBanner` | `SiteHeroBanner` | (not needed — static) |
| `LatestPosts` | `SiteLatestPosts` | `SiteLatestPostsEditor.tsx` |
| `CTASection` | `SiteCTA` | (not needed — static) |
| `AuthorCard` | `SiteAuthorCard` | `SiteAuthorCardEditor.tsx` |

This clearly separates site components from library components in the editor.

---

## Prop Type → Puck Field Quick Reference

| TypeScript Type | Puck Field | Notes |
|---|---|---|
| `string` | `{ type: "text" }` | Short text (titles, labels) |
| `string` (multiline) | `{ type: "textarea" }` | Long text (descriptions, bios) |
| `string` (HTML) | `{ type: "textarea" }` | Can use RichText if needed |
| `string` (URL) | `{ type: "text" }` | Links, hrefs |
| `string` (image URL) | `{ type: "custom", render: MediaUploadField }` | Use `accept="image"` |
| `string` (video URL) | `{ type: "custom", render: MediaUploadField }` | Use `accept="video"` |
| `string` (icon name) | `{ type: "custom", render: IconPickerField }` | Uses lucide-react icons |
| `string` (enum/choice) | `{ type: "select", options: [...] }` | Dropdown |
| `number` | `{ type: "number" }` | Optional: `min`, `max` |
| `boolean` | `{ type: "radio", options: [{label:"Yes",value:true},{label:"No",value:false}] }` | Yes/No toggle |
| `T[]` (array of items) | `{ type: "array", arrayFields: {...} }` | Expandable list |
| `object` | **DO NOT USE `type: "object"`** | Use `type: "array"` with a single item instead. Puck does NOT render object fields in the sidebar. |

---

## Common Mistakes

### ❌ Modifying the original component file
**Never** change the existing component. Wrap it with a config entry instead.

### ❌ Using `type: "object"` for complex fields
Puck does NOT render `type: "object"` fields in the sidebar. Use `type: "array"` instead, even for single items.

### ❌ Forgetting to strip `puck` and `id` from props
Always strip Puck's injected props before passing to the component:
```typescript
render: (props: any) => {
  const { puck: _, id: __, ...rest } = props;
  return <MyComponent {...rest} />;
},
```

### ❌ Using an async server component directly in config render
Server components (async) cannot be used in the editor (client-side). Create an editor version.

### ❌ Not matching defaultProps to the current page content
The defaultProps MUST contain the EXACT current content from the existing page. If the hero says "最新ニュース", the defaultProps must say "最新ニュース" — not generic placeholder text.
