Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

### Turn your repo into a launch worth upvoting.

Point Launch Kit at a project folder. It reads your README and drafts **launch‑ready posts — written natively for [Product Hunt, Hacker News, Reddit, and AppSumo](#-one-repo-every-audience)**. Runs 100% on your machine. Bring your own [OpenRouter](https://openrouter.ai) key.
Point Launch Kit at a project folder. It reads your README and drafts **launch‑ready posts — written natively for [Product Hunt, Hacker News, Reddit, AppSumo, and X / social](#-one-repo-every-audience)** — plus a full cross‑platform launch plan. Runs 100% on your machine. Bring your own [OpenRouter](https://openrouter.ai) key.

[![CI](https://img.shields.io/github/actions/workflow/status/AhmedFr/launch-kit/ci.yml?branch=main&style=for-the-badge&logo=githubactions&logoColor=white&label=CI&color=E2563C&labelColor=1c1c1c)](https://github.com/AhmedFr/launch-kit/actions/workflows/ci.yml)
[![GitHub stars](https://img.shields.io/github/stars/AhmedFr/launch-kit?style=for-the-badge&logo=github&color=E2563C&labelColor=1c1c1c)](https://github.com/AhmedFr/launch-kit/stargazers)
Expand All @@ -20,9 +20,10 @@ Point Launch Kit at a project folder. It reads your README and drafts **launch

## ✨ What you get

- **One core, four native posts.** Launch Kit distills your project once, then rewrites it for each platform's audience — not one blurb copy‑pasted everywhere.
- **A guided launch, not just copy.** Gallery shot lists, a demo‑video storyboard, posting times, and a launch‑day checklist.
- **Live previews.** See each post rendered the way it'll actually look on Product Hunt, HN, Reddit, and AppSumo.
- **One core, five native posts.** Launch Kit distills your project once, then rewrites it for each platform's audience — not one blurb copy‑pasted everywhere.
- **A guided launch, not just copy.** A cross‑platform launch plan (L‑6 weeks → L+4 weeks with 30/7/48h countdowns, SEO/GEO, and post‑launch momentum), plus gallery shot lists, a demo‑video storyboard, posting times, a Product Hunt hour‑by‑hour battle plan, and KOL/UGC outreach templates.
- **Battle‑tested playbook baked in.** Every prompt is shaped by proven launch principles — user value first, content over channel, and concentrated, genuine momentum over vanity reach.
- **Live previews.** See each post rendered the way it'll actually look on Product Hunt, HN, Reddit, AppSumo, and X.
- **Your runs, saved.** Every folder you launch is kept locally so you can pick up where you left off.
- **Private by design.** Your code and README never leave your machine. No backend, no database, no account.
- **Bring your own key.** Use the built‑in mock generator key‑free, or plug in your OpenRouter key for the real thing.
Expand Down Expand Up @@ -63,10 +64,13 @@ A three‑step wizard, one run per folder:

| Platform | What Launch Kit writes |
| --- | --- |
| **Product Hunt** | Tagline + alternatives, description, topics, gallery shot list, video storyboard, maker's first comment, launch‑day ops & outreach |
| **Hacker News** | A `Show HN:` title, an honest technical post, your first comment, and posting tips |
| **Reddit** | Subreddit recommendations, a value‑first title + body, and comment etiquette |
| **Product Hunt** | Tagline + alternatives, description, topics, gallery shot list, video storyboard, maker's first comment, and launch‑day ops: hour‑by‑hour battle plan, upvote‑momentum tactics, comment moderation & outreach |
| **Hacker News** | A `Show HN:` title, an honest technical post, your first comment, posting tips, and engagement etiquette |
| **Reddit** | Subreddit recommendations with per‑sub rules, recommended posting timing, a value‑first title + body, and comment etiquette |
| **AppSumo** | Deal headline, lifetime‑deal pitch, what's included, who it's for, and an FAQ |
| **Social / KOL** | An X (Twitter) launch thread, cold‑outreach DM templates for X / LinkedIn / Telegram influencers, and a UGC ask to turn early users into creators |

Plus a **cross‑platform launch plan** spanning every channel: a 10‑week timeline (L‑6 weeks → L+4 weeks), 30/7/48‑hour pre‑launch countdowns, a keyword‑driven SEO/GEO checklist, and a post‑launch momentum cadence.

![Per-platform launch kit](docs/screenshots/kit.png)

Expand Down
10 changes: 9 additions & 1 deletion app/api/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@ export async function POST(req: Request) {
return NextResponse.json({ platform: body.platform, content })
}

// Full generation: distill the core once, then fan out to every platform in parallel.
// Full generation: distill the core once, then fan out to every platform in
// parallel, plus the cross-platform launch plan. The plan is best-effort — a
// failure there must not sink the whole generation.
const core = await provider.generateCore(input)
const planPromise = provider.generatePlan(core, input).catch((err) => {
console.error('[api/generate] plan failed:', err?.message ?? err)
return undefined
})
Comment on lines +38 to +45

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Consider logging the full error stack for plan failures.

The current error logging only captures err?.message ?? err, which loses stack trace information that could be valuable for debugging plan generation failures in production.

📋 Proposed enhancement
 const planPromise = provider.generatePlan(core, input).catch((err) => {
-  console.error('[api/generate] plan failed:', err?.message ?? err)
+  console.error('[api/generate] plan failed:', err)
   return undefined
 })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/api/generate/route.ts` around lines 38 - 45, The error logging in the
catch block of the planPromise handler is only capturing the error message with
`err?.message ?? err`, which loses valuable stack trace information needed for
debugging. Modify the console.error call in the catch handler (where the plan
generation fails) to log the complete error object instead of just the message,
so the full stack trace is preserved in production logs. You can pass the error
object directly as part of the logged output to maintain debugging information.

const results = await Promise.allSettled(
PLATFORM_IDS.map((id) => provider.generatePlatform(id, core, input)),
)
Expand All @@ -60,6 +66,8 @@ export async function POST(req: Request) {
}

const generation: Generation = { core, platforms }
const plan = await planPromise
if (plan) generation.plan = plan
return NextResponse.json({ generation, failed })
} catch (err) {
const message = err instanceof Error ? err.message : 'Generation failed.'
Expand Down
27 changes: 27 additions & 0 deletions app/runs/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ContextReview } from '@/components/context-review/ContextReview'
import { PostKit } from '@/components/post-kit/PostKit'
import { GeneratingKit } from '@/components/generating/GeneratingKit'
import { platformToMarkdown } from '@/lib/export/to-markdown'
import { planToMarkdown } from '@/lib/export/plan-to-markdown'
import { PLATFORMS, type PlatformId } from '@/lib/platforms'
import { useRun } from '@/lib/runs/useRun'
import { findByPath } from '@/lib/runs/store'
Expand Down Expand Up @@ -109,6 +110,30 @@ function RunWizard({ id }: { id: string }) {
toast.success(`${name} kit copied`)
}

function planMarkdown(): string | null {
if (!state.generation?.plan) return null
return planToMarkdown(state.generation.core, state.generation.plan)
}

function exportPlan() {
const md = planMarkdown()
if (!md) return
const blob = new Blob([md], { type: 'text/markdown' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'launch-plan.md'
a.click()
URL.revokeObjectURL(url)
}

async function copyPlan() {
const md = planMarkdown()
if (!md) return
await navigator.clipboard.writeText(md)
toast.success('Launch plan copied')
}
Comment on lines +130 to +135

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle clipboard failures in copyPlan to avoid unhandled promise rejections.

navigator.clipboard.writeText can fail; this path currently has no guard or user-visible error fallback.

Suggested fix
   async function copyPlan() {
     const md = planMarkdown()
     if (!md) return
-    await navigator.clipboard.writeText(md)
-    toast.success('Launch plan copied')
+    try {
+      await navigator.clipboard.writeText(md)
+      toast.success('Launch plan copied')
+    } catch {
+      toast.error('Failed to copy launch plan')
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function copyPlan() {
const md = planMarkdown()
if (!md) return
await navigator.clipboard.writeText(md)
toast.success('Launch plan copied')
}
async function copyPlan() {
const md = planMarkdown()
if (!md) return
try {
await navigator.clipboard.writeText(md)
toast.success('Launch plan copied')
} catch {
toast.error('Failed to copy launch plan')
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/runs/`[id]/page.tsx around lines 130 - 135, The copyPlan function does
not handle potential errors from navigator.clipboard.writeText, which can fail
due to permission issues or clipboard unavailability. Wrap the await
navigator.clipboard.writeText(md) call inside the copyPlan function with a
try-catch block to catch any errors. In the catch block, display an error toast
message to the user indicating that the copy operation failed. Keep the existing
success toast message for when the operation succeeds.


if (status === 'loading') {
return <p className="px-6 py-16 text-center text-sm text-muted-foreground">Loading run…</p>
}
Expand Down Expand Up @@ -166,6 +191,8 @@ function RunWizard({ id }: { id: string }) {
regeneratingPlatform={regeneratingPlatform}
onExportMarkdown={exportMarkdown}
onCopyAll={copyAll}
onCopyPlan={copyPlan}
onExportPlan={exportPlan}
onStartOver={() => router.push('/runs')}
/>
)}
Expand Down
34 changes: 34 additions & 0 deletions components/launch-plan/LaunchPlan/LaunchPlan.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { LaunchPlan } from './LaunchPlan'
import type { LaunchPlan as LaunchPlanData } from '@/lib/types'

const plan: LaunchPlanData = {
phases: [{ window: 'L-6 weeks', goal: 'Lock strategy', tasks: ['Define the ICP'] }],
countdown30: ['Build the landing page'],
countdown7: ['Confirm the hunter'],
countdown48h: ['Final QA'],
seoGeo: ['Configure IndexNow'],
momentum: ['Post a recap'],
}

describe('LaunchPlan', () => {
it('renders the timeline and every checklist section', () => {
render(<LaunchPlan plan={plan} onCopy={() => {}} onExport={() => {}} />)
expect(screen.getByText('L-6 weeks')).toBeInTheDocument()
expect(screen.getByText('Define the ICP')).toBeInTheDocument()
expect(screen.getByText('Configure IndexNow')).toBeInTheDocument()
expect(screen.getByText('Post a recap')).toBeInTheDocument()
})

it('wires the copy and export actions', async () => {
const onCopy = vi.fn()
const onExport = vi.fn()
render(<LaunchPlan plan={plan} onCopy={onCopy} onExport={onExport} />)
await userEvent.click(screen.getByRole('button', { name: 'Copy' }))
await userEvent.click(screen.getByRole('button', { name: 'Export' }))
expect(onCopy).toHaveBeenCalledOnce()
expect(onExport).toHaveBeenCalledOnce()
})
})
50 changes: 50 additions & 0 deletions components/launch-plan/LaunchPlan/LaunchPlan.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { SectionCard } from '@/components/common/SectionCard'
import { Button } from '@/components/ui/button'
import type { LaunchPlanProps } from './LaunchPlan.types'

function Checklist({ title, items }: { title: string; items: string[] }) {
if (items.length === 0) return null
return (
<div>
<div className="text-xs uppercase text-muted-foreground">{title}</div>
<ul className="mt-1 list-disc pl-5">{items.map((i) => <li key={i}>{i}</li>)}</ul>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use collision-safe keys for generated list items.

These lists are AI-generated text; duplicate strings are plausible, so value-only keys can collide and cause unstable reconciliation.

Proposed fix
-      <ul className="mt-1 list-disc pl-5">{items.map((i) => <li key={i}>{i}</li>)}</ul>
+      <ul className="mt-1 list-disc pl-5">{items.map((i, idx) => <li key={`${i}-${idx}`}>{i}</li>)}</ul>
...
-          {plan.phases.map((phase) => (
-            <li key={phase.window} className="border-l-2 border-primary/40 pl-3">
+          {plan.phases.map((phase, idx) => (
+            <li key={`${phase.window}-${idx}`} className="border-l-2 border-primary/40 pl-3">
...
-              <ul className="mt-1 list-disc pl-5">{phase.tasks.map((t) => <li key={t}>{t}</li>)}</ul>
+              <ul className="mt-1 list-disc pl-5">{phase.tasks.map((t, tIdx) => <li key={`${t}-${tIdx}`}>{t}</li>)}</ul>

Also applies to: 29-29, 34-34

🧰 Tools
🪛 React Doctor (0.5.6)

[warning] 10-10: This JSX crashes because React isn't in scope.

If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import React at the top of the file.

(react-in-jsx-scope)


[warning] 10-10: This JSX crashes because React isn't in scope.

If you're on React 17+ with the new JSX transform, disable this rule. Otherwise import React at the top of the file.

(react-in-jsx-scope)


[warning] 10-10: Your users can see & submit the wrong data when this list reorders or filters, so use a stable id like key={item.id}, not the array index "i".

Use a stable id from the item, like key={item.id} or key={item.slug}. Index keys break when the list reorders or filters.

(no-array-index-as-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/launch-plan/LaunchPlan/LaunchPlan.tsx` at line 10, The list items
in the map functions are using only the string value (i) as the React key, which
can cause collisions and unstable reconciliation when duplicate strings appear
in AI-generated lists. Update all three list rendering locations (the one at
line 10 using items.map, and the two others referenced in the comment) to use
collision-safe keys by including the array index in the key. For each map
callback like items.map((i) => ...), add the index parameter to the callback and
create a composite key using both the index and the value, such as using a
template string combining the index position with the item value, ensuring each
key is unique even when duplicate items exist.

</div>
)
}

export function LaunchPlan({ plan, onCopy, onExport }: LaunchPlanProps) {
const actions = (
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={onCopy}>Copy</Button>
<Button size="sm" onClick={onExport}>Export</Button>
</div>
)

return (
<SectionCard title="Launch plan" action={actions}>
<div>
<div className="text-xs uppercase text-muted-foreground">Timeline · L-6 weeks → L+4 weeks</div>
<ol className="mt-2 space-y-3">
{plan.phases.map((phase) => (
<li key={phase.window} className="border-l-2 border-primary/40 pl-3">
<div className="flex flex-wrap items-baseline gap-x-2">
<span className="font-medium text-primary">{phase.window}</span>
<span className="text-muted-foreground">— {phase.goal}</span>
</div>
<ul className="mt-1 list-disc pl-5">{phase.tasks.map((t) => <li key={t}>{t}</li>)}</ul>
</li>
))}
</ol>
</div>

<div className="grid gap-4 sm:grid-cols-3">
<Checklist title="30-day countdown" items={plan.countdown30} />
<Checklist title="7-day countdown" items={plan.countdown7} />
<Checklist title="Final 48 hours" items={plan.countdown48h} />
</div>

<Checklist title="SEO / GEO" items={plan.seoGeo} />
<Checklist title="Post-launch momentum" items={plan.momentum} />
</SectionCard>
)
}
7 changes: 7 additions & 0 deletions components/launch-plan/LaunchPlan/LaunchPlan.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { LaunchPlan as LaunchPlanData } from '@/lib/types'

export type LaunchPlanProps = {
plan: LaunchPlanData
onCopy: () => void
onExport: () => void
}
1 change: 1 addition & 0 deletions components/launch-plan/LaunchPlan/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { LaunchPlan } from './LaunchPlan'
68 changes: 68 additions & 0 deletions components/post-kit/PostKit/PostKit.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, it, expect, beforeAll, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { PostKit } from './PostKit'
import { RuntimeConfigProvider } from '@/lib/config/RuntimeConfigProvider'
import type { RuntimeConfig } from '@/lib/config/runtime-config'
import { MockProvider } from '@/lib/generation/mock-provider'
import type { GenerateInput, Generation } from '@/lib/types'

const input: GenerateInput = {
context: {
name: 'Acme', oneLiner: 'Ship faster', summary: 'Acme helps teams ship faster.',
features: ['Fast builds'], techStack: ['Next.js'], differentiators: ['Zero config'],
links: [], readmeExcerpt: '', warnings: [],
},
refinements: { tone: 'bold', audience: 'developers' },
}

const config: RuntimeConfig = { provider: 'mock', hasKey: false, model: 'mock', localFsAvailable: true }

let generation: Generation

beforeAll(async () => {
const p = new MockProvider()
const core = await p.generateCore(input)
const [ph, hn] = await Promise.all([
p.generatePlatform('product-hunt', core, input),
p.generatePlatform('hacker-news', core, input),
])
generation = { core, platforms: { 'product-hunt': ph, 'hacker-news': hn } as Generation['platforms'] }
})

function renderKit() {
return render(
<RuntimeConfigProvider value={config}>
<PostKit
generation={generation}
productName="Acme"
onRegeneratePlatform={vi.fn()}
regeneratingPlatform={null}
onExportMarkdown={vi.fn()}
onCopyAll={vi.fn()}
onCopyPlan={vi.fn()}
onExportPlan={vi.fn()}
onStartOver={vi.fn()}
/>
</RuntimeConfigProvider>,
)
}

describe('PostKit launch-ops tab', () => {
it('shows a Launch ops tab for Product Hunt and keeps ops out of the Edit view', async () => {
renderKit()
// Product Hunt is the default platform; the third tab is present.
expect(screen.getByRole('button', { name: 'Launch ops' })).toBeInTheDocument()
// Edit view (default) no longer renders the launch-day ops.
expect(screen.queryByText('Hour-by-hour (PT)')).not.toBeInTheDocument()
// Switching to the tab reveals the ops content.
await userEvent.click(screen.getByRole('button', { name: 'Launch ops' }))
expect(screen.getByText('Hour-by-hour (PT)')).toBeInTheDocument()
})

it('hides the Launch ops tab for platforms without launch-day ops', async () => {
renderKit()
await userEvent.click(screen.getByRole('button', { name: 'Hacker News' }))
expect(screen.queryByRole('button', { name: 'Launch ops' })).not.toBeInTheDocument()
})
})
41 changes: 32 additions & 9 deletions components/post-kit/PostKit/PostKit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,35 @@
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { RegenerateButton } from '@/components/common/RegenerateButton'
import { LaunchPlan } from '@/components/launch-plan/LaunchPlan'
import { LaunchOpsSection } from '@/components/post-kit/sections/LaunchOpsSection'
import { PLATFORMS, type PlatformId } from '@/lib/platforms'
import { PREVIEWS } from '@/lib/preview/registry'
import { PLATFORM_SECTIONS } from '@/components/post-kit/platforms/registry'
import { useRuntimeConfig } from '@/lib/config/RuntimeConfigProvider'
import { canGenerate } from '@/lib/config/runtime-config'
import type { ProductHuntContent } from '@/lib/types'
import type { PostKitProps } from './PostKit.types'

// Product Hunt is the only platform with a launch-day ops view; it gets a third tab.
type View = 'edit' | 'launch-ops' | 'preview'
const VIEW_LABELS: Record<View, string> = { edit: 'Edit', 'launch-ops': 'Launch ops', preview: 'Preview' }

export function PostKit({
generation,
productName,
onRegeneratePlatform,
regeneratingPlatform,
onExportMarkdown,
onCopyAll,
onCopyPlan,
onExportPlan,
onStartOver,
}: PostKitProps) {
const { core, platforms } = generation
const { core, platforms, plan } = generation
const firstReady = PLATFORMS.find((p) => platforms[p.id])?.id ?? 'product-hunt'
const [platform, setPlatform] = useState<PlatformId>(firstReady)
const [view, setView] = useState<'edit' | 'preview'>('edit')
const [view, setView] = useState<View>('edit')

const content = platforms[platform]
const Sections = PLATFORM_SECTIONS[platform]
Expand All @@ -30,6 +39,11 @@ export function PostKit({
const config = useRuntimeConfig()
const canRegenerate = canGenerate(config)

// Only Product Hunt has launch-day ops; fall back to Edit when its tab can't apply.
const hasLaunchOps = platform === 'product-hunt' && Boolean(content)
const views: View[] = hasLaunchOps ? ['edit', 'launch-ops', 'preview'] : ['edit', 'preview']
const activeView: View = view === 'launch-ops' && !hasLaunchOps ? 'edit' : view

return (
<div className="mx-auto max-w-5xl px-6">
<div className="reveal reveal-1 mb-6 text-center">
Expand All @@ -39,6 +53,13 @@ export function PostKit({
</h2>
</div>

{/* Cross-platform launch plan — spans every channel, shown above the per-platform kits */}
{plan && (
<div className="reveal reveal-1 mb-6">
<LaunchPlan plan={plan} onCopy={onCopyPlan} onExport={onExportPlan} />
</div>
)}

{/* Platform selector — drives both edit and preview */}
<div className="reveal reveal-2 mb-4 flex flex-wrap justify-center gap-2">
{PLATFORMS.map((p) => {
Expand All @@ -64,18 +85,18 @@ export function PostKit({
})}
</div>

{/* Toolbar: Edit | Preview + per-platform actions */}
{/* Toolbar: Edit | Launch ops (PH only) | Preview + per-platform actions */}
<div className="reveal reveal-2 mb-5 flex flex-wrap items-center justify-between gap-3">
<div className="inline-flex rounded-lg border border-border bg-card p-0.5 shadow-sm">
{(['edit', 'preview'] as const).map((v) => (
{views.map((v) => (
<button
key={v}
onClick={() => setView(v)}
className={`rounded-md px-3 py-1.5 text-sm capitalize transition-colors ${
view === v ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
className={`whitespace-nowrap rounded-md px-3 py-1.5 text-sm transition-colors ${
activeView === v ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'
}`}
>
{v}
{VIEW_LABELS[v]}
</button>
))}
</div>
Expand All @@ -92,13 +113,15 @@ export function PostKit({
</div>
</div>

<div key={`${platform}-${view}`} className="reveal reveal-2">
<div key={`${platform}-${activeView}`} className="reveal reveal-2">
{!content ? (
<p className="text-center text-sm text-muted-foreground">
This platform didn&apos;t generate. Switch to a ready platform, or start over to try again.
</p>
) : view === 'edit' ? (
) : activeView === 'edit' ? (
Sections ? <Sections core={core} content={content} loading={regenerating} /> : null
) : activeView === 'launch-ops' ? (
<LaunchOpsSection kit={content as ProductHuntContent} loading={regenerating} />
) : Preview ? (
<Preview core={core} content={content} productName={productName} />
) : (
Expand Down
2 changes: 2 additions & 0 deletions components/post-kit/PostKit/PostKit.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ export type PostKitProps = {
regeneratingPlatform: PlatformId | null
onExportMarkdown: (platform: PlatformId) => void
onCopyAll: (platform: PlatformId) => void
onCopyPlan: () => void
onExportPlan: () => void
onStartOver: () => void
}
Loading
Loading