diff --git a/app/runs/[id]/page.tsx b/app/runs/[id]/page.tsx index c14cf35..51fe54b 100644 --- a/app/runs/[id]/page.tsx +++ b/app/runs/[id]/page.tsx @@ -27,7 +27,6 @@ function RunWizard({ id }: { id: string }) { const router = useRouter() const { state, dispatch, status } = useRun(id) const [generating, setGenerating] = useState(false) - const [regeneratingPlatform, setRegeneratingPlatform] = useState(null) function handleAnalyzed(context: ProjectContext) { // One run per folder: if this path already belongs to another run, go there. @@ -54,7 +53,7 @@ function RunWizard({ id }: { id: string }) { dispatch({ type: 'GENERATED', generation: data.generation }) if (Array.isArray(data.failed) && data.failed.length) { const names = data.failed.map((f: PlatformId) => PLATFORMS.find((p) => p.id === f)?.name ?? f) - toast.warning(`Couldn't generate: ${names.join(', ')}. You can regenerate them.`) + toast.warning(`Couldn't generate: ${names.join(', ')}.`) } } catch (err) { toast.error(err instanceof Error ? err.message : 'Generation failed') @@ -63,26 +62,6 @@ function RunWizard({ id }: { id: string }) { } } - async function regeneratePlatform(platform: PlatformId) { - if (!state.context || !state.generation) return - const body = { context: state.context, refinements: state.refinements, platform, core: state.generation.core } - setRegeneratingPlatform(platform) - try { - const res = await fetch('/api/generate', { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(body), - }) - const data = await res.json() - if (!res.ok) throw new Error(data.error ?? 'Generation failed') - dispatch({ type: 'REGEN_PLATFORM', platform, content: data.content }) - } catch (err) { - toast.error(err instanceof Error ? err.message : 'Generation failed') - } finally { - setRegeneratingPlatform(null) - } - } - function markdownFor(platform: PlatformId): string | null { if (!state.generation) return null const content = state.generation.platforms[platform] @@ -102,14 +81,6 @@ function RunWizard({ id }: { id: string }) { URL.revokeObjectURL(url) } - async function copyAll(platform: PlatformId) { - const md = markdownFor(platform) - if (!md) return - await navigator.clipboard.writeText(md) - const name = PLATFORMS.find((p) => p.id === platform)?.name ?? platform - toast.success(`${name} kit copied`) - } - function planMarkdown(): string | null { if (!state.generation?.plan) return null return planToMarkdown(state.generation.core, state.generation.plan) @@ -187,10 +158,7 @@ function RunWizard({ id }: { id: string }) { router.push('/runs')} diff --git a/components/brand/PlatformLogo/PlatformLogo.tsx b/components/brand/PlatformLogo/PlatformLogo.tsx new file mode 100644 index 0000000..f9ce0e5 --- /dev/null +++ b/components/brand/PlatformLogo/PlatformLogo.tsx @@ -0,0 +1,47 @@ +import type { PlatformId } from '@/lib/platforms' +import type { PlatformLogoProps } from './PlatformLogo.types' + +// Brand marks per platform. The four with official Simple Icons render as the real +// logo in the brand color; AppSumo has no open logo, so it falls back to a monogram. +const MARKS: Record< + PlatformId, + { color: string; path: string } | { color: string; bg: string; glyph: string } +> = { + 'product-hunt': { + color: '#DA552F', + path: 'M13.604 8.4h-3.405V12h3.405c.995 0 1.801-.806 1.801-1.801 0-.993-.805-1.799-1.801-1.799zM12 0C5.372 0 0 5.372 0 12s5.372 12 12 12 12-5.372 12-12S18.628 0 12 0zm1.604 14.4h-3.405V18H7.801V6h5.804c2.319 0 4.2 1.88 4.2 4.199 0 2.321-1.881 4.201-4.201 4.201z', + }, + 'hacker-news': { + color: '#FF6600', + path: 'M0 24V0h24v24H0zM6.951 5.896l4.112 7.708v5.064h1.583v-4.972l4.148-7.799h-1.749l-2.457 4.875c-.372.745-.688 1.434-.688 1.434s-.297-.708-.651-1.434L8.831 5.896h-1.88z', + }, + reddit: { + color: '#FF4500', + path: 'M12 0C5.373 0 0 5.373 0 12c0 3.314 1.343 6.314 3.515 8.485l-2.286 2.286C.775 23.225 1.097 24 1.738 24H12c6.627 0 12-5.373 12-12S18.627 0 12 0Zm4.388 3.199c1.104 0 1.999.895 1.999 1.999 0 1.105-.895 2-1.999 2-.946 0-1.739-.657-1.947-1.539v.002c-1.147.162-2.032 1.15-2.032 2.341v.007c1.776.067 3.4.567 4.686 1.363.473-.363 1.064-.58 1.707-.58 1.547 0 2.802 1.254 2.802 2.802 0 1.117-.655 2.081-1.601 2.531-.088 3.256-3.637 5.876-7.997 5.876-4.361 0-7.905-2.617-7.998-5.87-.954-.447-1.614-1.415-1.614-2.538 0-1.548 1.255-2.802 2.803-2.802.645 0 1.239.218 1.712.585 1.275-.79 2.881-1.291 4.64-1.365v-.01c0-1.663 1.263-3.034 2.88-3.207.188-.911.993-1.595 1.959-1.595Zm-8.085 8.376c-.784 0-1.459.78-1.506 1.797-.047 1.016.64 1.429 1.426 1.429.786 0 1.371-.369 1.418-1.385.047-1.017-.553-1.841-1.338-1.841Zm7.406 0c-.786 0-1.385.824-1.338 1.841.047 1.017.634 1.385 1.418 1.385.785 0 1.473-.413 1.426-1.429-.046-1.017-.721-1.797-1.506-1.797Zm-3.703 4.013c-.974 0-1.907.048-2.77.135-.147.015-.241.168-.183.305.483 1.154 1.622 1.964 2.953 1.964 1.33 0 2.47-.81 2.953-1.964.057-.137-.037-.29-.184-.305-.863-.087-1.795-.135-2.769-.135Z', + }, + social: { + color: '#0f0f0f', + path: 'M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z', + }, + appsumo: { color: '#1c1c1c', bg: '#FCCD13', glyph: 'AS' }, +} + +export function PlatformLogo({ id, className = 'size-5' }: PlatformLogoProps) { + const mark = MARKS[id] + if ('path' in mark) { + return ( + + + + ) + } + return ( + + {mark.glyph} + + ) +} diff --git a/components/brand/PlatformLogo/PlatformLogo.types.ts b/components/brand/PlatformLogo/PlatformLogo.types.ts new file mode 100644 index 0000000..2de2553 --- /dev/null +++ b/components/brand/PlatformLogo/PlatformLogo.types.ts @@ -0,0 +1,7 @@ +import type { PlatformId } from '@/lib/platforms' + +export type PlatformLogoProps = { + id: PlatformId + /** Tailwind size utility (width + height). Defaults to `size-5`. */ + className?: string +} diff --git a/components/brand/PlatformLogo/index.ts b/components/brand/PlatformLogo/index.ts new file mode 100644 index 0000000..d3fac50 --- /dev/null +++ b/components/brand/PlatformLogo/index.ts @@ -0,0 +1 @@ +export { PlatformLogo } from './PlatformLogo' diff --git a/components/post-kit/PostKit/PostKit.test.tsx b/components/post-kit/PostKit/PostKit.test.tsx index e23ecef..a7101e4 100644 --- a/components/post-kit/PostKit/PostKit.test.tsx +++ b/components/post-kit/PostKit/PostKit.test.tsx @@ -36,10 +36,7 @@ function renderKit() { = { edit: 'Edit', 'launch-ops': 'Launch ops', preview: 'Preview' } -export function PostKit({ - generation, - productName, - onRegeneratePlatform, - regeneratingPlatform, - onExportMarkdown, - onCopyAll, - onCopyPlan, - onExportPlan, - onStartOver, -}: PostKitProps) { +export function PostKit({ generation, productName, onExportMarkdown, onCopyPlan, onExportPlan, onStartOver }: PostKitProps) { const { core, platforms, plan } = generation const firstReady = PLATFORMS.find((p) => platforms[p.id])?.id ?? 'product-hunt' const [platform, setPlatform] = useState(firstReady) const [view, setView] = useState('edit') + const [planMode, setPlanMode] = useState(false) const content = platforms[platform] const Sections = PLATFORM_SECTIONS[platform] const Preview = PREVIEWS[platform] - const regenerating = regeneratingPlatform === platform - 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 (
-
- Your launch kit is ready -

- {core.essence} -

-
- - {/* Cross-platform launch plan — spans every channel, shown above the per-platform kits */} - {plan && ( -
- + {/* Header: product name + global actions */} +
+

{productName}

+
+ +
- )} - - {/* Platform selector — drives both edit and preview */} -
- {PLATFORMS.map((p) => { - const ready = Boolean(platforms[p.id]) - const isActive = platform === p.id - return ( - - ) - })}
- {/* Toolbar: Edit | Launch ops (PH only) | Preview + per-platform actions */} + {/* Toolbar: Plan + platform-logo selector (left), view tabs (right) */}
-
- {views.map((v) => ( - - ))} -
-
- onRegeneratePlatform(platform)} - regenerating={regenerating} - disabled={!canRegenerate} - title={canRegenerate ? undefined : 'Add OPENROUTER_API_KEY to your .env to regenerate'} - /> - - - +
+ {plan && ( + <> + + + + )} + {PLATFORMS.map((p) => { + const ready = Boolean(platforms[p.id]) + const active = !planMode && platform === p.id + return ( + + ) + })}
+ + {!planMode && content && ( +
+ {views.map((v) => ( + + ))} +
+ )}
-
- {!content ? ( +
+ {planMode && plan ? ( + + ) : !content ? (

- This platform didn't generate. Switch to a ready platform, or start over to try again. + This platform didn't generate. Pick another, or start over to try again.

) : activeView === 'edit' ? ( - Sections ? : null + Sections ? : null ) : activeView === 'launch-ops' ? ( - + ) : Preview ? ( ) : ( diff --git a/components/post-kit/PostKit/PostKit.types.ts b/components/post-kit/PostKit/PostKit.types.ts index 8f4b2fd..99892dd 100644 --- a/components/post-kit/PostKit/PostKit.types.ts +++ b/components/post-kit/PostKit/PostKit.types.ts @@ -4,10 +4,7 @@ import type { PlatformId } from '@/lib/platforms' export type PostKitProps = { generation: Generation productName: string - onRegeneratePlatform: (platform: PlatformId) => void - regeneratingPlatform: PlatformId | null onExportMarkdown: (platform: PlatformId) => void - onCopyAll: (platform: PlatformId) => void onCopyPlan: () => void onExportPlan: () => void onStartOver: () => void