Claude
Skills
Sign in
Back

copy-preview

Included with Lifetime
$97 forever

Platform-specific copy preview cards — tweet, Instagram caption, LinkedIn post, and email subject line mockups for previewing AI-generated copy in context. Use this skill when the user says "add copy preview", "platform preview", "social preview cards", "tweet preview", or "caption preview".

Productivity

What this skill does


# Copy Preview

A set of realistic platform mockup components for previewing AI-generated copy in context. Supports Twitter/X, Instagram, LinkedIn, and email. Exposed through a single unified `<CopyPreview>` component that selects the correct card from a `platform` prop.

## Prerequisites

- Next.js app with App Router (no `src/` directory)
- shadcn/ui installed (from the `add-shadcn` skill)
- `@phosphor-icons/react` installed

## Installation

```bash
bun add @phosphor-icons/react
bunx shadcn@latest add badge
```

## What Gets Created

```
components/
└── copy-preview/
    ├── tweet-card.tsx
    ├── instagram-caption.tsx
    ├── linkedin-post.tsx
    ├── email-subject.tsx
    └── copy-preview.tsx
```

## Setup Steps

### Step 1: Create `components/copy-preview/tweet-card.tsx`

```typescript
"use client"

import {
  ArrowsClockwise,
  ChatCircle,
  Export,
  Heart,
  ChartBar,
} from "@phosphor-icons/react"
import { useId } from "react"

type TweetCardProps = {
  copy: string
  authorName?: string
  authorHandle?: string
  authorAvatar?: string
  className?: string
}

const CHAR_LIMIT = 280

function renderCopyText(text: string): React.ReactNode[] {
  const id = Math.random().toString(36).slice(2)
  return text.split(/(\s+)/).map((word, i) => {
    const key = `${id}-word-${i}`
    if (/^#\w+/.test(word)) {
      return <span key={key} className="text-sky-500">{word}</span>
    }
    if (/^@\w+/.test(word)) {
      return <span key={key} className="text-sky-500">{word}</span>
    }
    if (/^https?:\/\/\S+/.test(word)) {
      return <span key={key} className="text-sky-500">{word}</span>
    }
    return <span key={key}>{word}</span>
  })
}

function InitialsAvatar({ name, size }: { name: string; size: number }) {
  const initials = name
    .split(" ")
    .map((p) => p[0])
    .join("")
    .slice(0, 2)
    .toUpperCase()

  return (
    <div
      style={{ width: size, height: size }}
      className="flex items-center justify-center rounded-full bg-gradient-to-br from-sky-400 to-blue-600 text-white font-bold shrink-0"
      style2={{ fontSize: size * 0.38 }}
    >
      <span style={{ fontSize: size * 0.38 }}>{initials}</span>
    </div>
  )
}

export function TweetCard({
  copy,
  authorName = "Your Name",
  authorHandle = "@yourhandle",
  authorAvatar,
  className,
}: TweetCardProps) {
  const charCount = copy.length
  const isOver = charCount > CHAR_LIMIT
  const engagementId = useId()

  const engagementItems = [
    { icon: <ChatCircle size={18} />, label: "Reply" },
    { icon: <ArrowsClockwise size={18} />, label: "Repost" },
    { icon: <Heart size={18} />, label: "Like" },
    { icon: <ChartBar size={18} />, label: "Views" },
    { icon: <Export size={18} />, label: "Share" },
  ]

  return (
    <div
      className={[
        "w-[598px] rounded-2xl border border-zinc-200 bg-white p-4 shadow-sm",
        className,
      ]
        .filter(Boolean)
        .join(" ")}
    >
      {/* Header */}
      <div className="flex items-start justify-between">
        <div className="flex items-center gap-3">
          {authorAvatar ? (
            <img
              src={authorAvatar}
              alt={authorName}
              className="h-10 w-10 rounded-full object-cover"
            />
          ) : (
            <InitialsAvatar name={authorName} size={40} />
          )}
          <div>
            <p className="text-sm font-bold leading-tight text-zinc-900">{authorName}</p>
            <p className="text-sm leading-tight text-zinc-500">{authorHandle}</p>
          </div>
        </div>
        {/* X logo */}
        <svg viewBox="0 0 24 24" className="h-5 w-5 fill-zinc-900" aria-hidden="true">
          <path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.766l7.73-8.835L1.254 2.25H8.08l4.259 5.63 5.905-5.63zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
        </svg>
      </div>

      {/* Body */}
      <div className="mt-3 whitespace-pre-wrap text-[15px] leading-relaxed text-zinc-900">
        {renderCopyText(copy)}
      </div>

      {/* Char counter */}
      <div className="mt-3 flex items-center justify-end">
        <span
          className={[
            "text-sm font-medium tabular-nums",
            isOver ? "text-red-500" : "text-green-600",
          ].join(" ")}
        >
          {charCount}/{CHAR_LIMIT}
        </span>
      </div>

      {/* Engagement row */}
      <div className="mt-3 flex items-center justify-between border-t border-zinc-100 pt-3">
        {engagementItems.map((item, i) => {
          const key = `${engagementId}-eng-${i}`
          return (
            <button
              key={key}
              aria-label={item.label}
              className="flex items-center gap-1 text-zinc-400 hover:text-sky-500 transition-colors"
            >
              {item.icon}
            </button>
          )
        })}
      </div>
    </div>
  )
}
```

### Step 2: Create `components/copy-preview/instagram-caption.tsx`

```typescript
"use client"

import { DotsThree, Heart } from "@phosphor-icons/react"
import { useId, useState } from "react"

type InstagramCaptionProps = {
  copy: string
  authorName?: string
  authorHandle?: string
  authorAvatar?: string
  className?: string
}

const TRUNCATE_AT = 125

function renderHashtags(text: string, prefix: string): React.ReactNode[] {
  return text.split(/(\s+)/).map((word, i) => {
    const key = `${prefix}-word-${i}`
    if (/^#\w+/.test(word)) {
      return <span key={key} className="text-sky-500">{word}</span>
    }
    return <span key={key}>{word}</span>
  })
}

function InitialsAvatar({ name, size }: { name: string; size: number }) {
  const initials = name
    .split(" ")
    .map((p) => p[0])
    .join("")
    .slice(0, 2)
    .toUpperCase()
  return (
    <div
      style={{ width: size, height: size }}
      className="flex shrink-0 items-center justify-center rounded-full bg-gradient-to-br from-pink-400 via-red-400 to-yellow-400 font-bold text-white"
    >
      <span style={{ fontSize: size * 0.38 }}>{initials}</span>
    </div>
  )
}

export function InstagramCaption({
  copy,
  authorName = "Your Name",
  authorHandle = "@yourhandle",
  authorAvatar,
  className,
}: InstagramCaptionProps) {
  const [expanded, setExpanded] = useState(false)
  const wordId = useId()
  const isTruncated = copy.length > TRUNCATE_AT
  const displayText = isTruncated && !expanded ? copy.slice(0, TRUNCATE_AT) : copy

  return (
    <div
      className={[
        "w-[400px] overflow-hidden rounded-2xl border border-zinc-200 bg-white shadow-sm",
        className,
      ]
        .filter(Boolean)
        .join(" ")}
    >
      {/* Header */}
      <div className="flex items-center justify-between px-3 py-2.5">
        <div className="flex items-center gap-2.5">
          {authorAvatar ? (
            <img
              src={authorAvatar}
              alt={authorName}
              className="h-8 w-8 rounded-full object-cover ring-2 ring-pink-400"
            />
          ) : (
            <div className="rounded-full p-0.5 bg-gradient-to-br from-pink-400 via-red-400 to-yellow-400">
              <InitialsAvatar name={authorName} size={30} />
            </div>
          )}
          <span className="text-sm font-semibold text-zinc-900">{authorHandle}</span>
        </div>
        <DotsThree size={20} className="text-zinc-500" />
      </div>

      {/* Simulated image placeholder */}
      <div className="aspect-square w-full bg-gradient-to-br from-zinc-200 via-zinc-100 to-zinc-200 flex items-center justify-center">
        <svg viewBox="0 0 24 24" className="h-12 w-12 fill-zinc-300" aria-hidden="true">
          <path d="M21 15.5a3.5 3.5 0 0 1-3.5 3.5h-11A3.5 3.5 0 0 1 3 15.5v-7A3.5 3.5 0 0 1 6.5 5h11A3.5 3.5 0 0 1 21 8.5v7zM8.75 10a2.25 2.25 0 1 0 0-4.5 2.25 2.25 0 0 0 0 4.5zm11.5 5-3.97-3.97a.75.75 0 0 0-1.06 0L11.5 14.75 9.28 12.53a.75.75 0 0 0-1.06 0L3.5 17.25" />
        </svg>
      </div>

      {/* Like row */}
      <div className="fle
Files: 1
Size: 21.7 KB
Complexity: 32/100
Category: Productivity

Related in Productivity