|
| 1 | +import Link from "next/link"; |
| 2 | +import { notFound } from "next/navigation"; |
| 3 | +import { createClient } from "@/lib/supabase/server"; |
| 4 | +import { cadenceLabel } from "@/lib/promote/generatePitch"; |
| 5 | +import { PromoteListActions } from "@/components/promote/list-actions"; |
| 6 | +import { PromoteEditForm } from "@/components/promote/promote-edit-form"; |
| 7 | +import { AddLinksForm } from "@/components/promote/add-links-form"; |
| 8 | +import { LinkList } from "@/components/promote/link-list"; |
| 9 | + |
| 10 | +export const metadata = { title: "Promote list" }; |
| 11 | + |
| 12 | +type Props = { params: Promise<{ id: string }> }; |
| 13 | + |
| 14 | +export default async function PromoteDetailPage({ params }: Props) { |
| 15 | + const { id } = await params; |
| 16 | + const supabase = await createClient(); |
| 17 | + const { |
| 18 | + data: { user }, |
| 19 | + } = await supabase.auth.getUser(); |
| 20 | + if (!user) notFound(); |
| 21 | + |
| 22 | + const { data: list } = await supabase |
| 23 | + .from("promo_list") |
| 24 | + .select("*") |
| 25 | + .eq("id", id) |
| 26 | + .eq("user_id", user.id) |
| 27 | + .maybeSingle(); |
| 28 | + if (!list) notFound(); |
| 29 | + |
| 30 | + const [{ data: links }, { data: posts }, { data: accounts }] = await Promise.all([ |
| 31 | + supabase |
| 32 | + .from("promo_link") |
| 33 | + .select("id, url, title, angle, enabled, times_promoted, last_promoted_at, created_at") |
| 34 | + .eq("list_id", id) |
| 35 | + .order("created_at", { ascending: true }), |
| 36 | + supabase |
| 37 | + .from("promo_post") |
| 38 | + .select("id, link_id, platform, body, status, error, credits_spent, posted_at, provider, external_post_id") |
| 39 | + .eq("list_id", id) |
| 40 | + .order("created_at", { ascending: false }) |
| 41 | + .limit(50), |
| 42 | + supabase |
| 43 | + .from("sp_account") |
| 44 | + .select("id, platform, handle") |
| 45 | + .eq("user_id", user.id) |
| 46 | + .eq("status", "active") |
| 47 | + .order("platform"), |
| 48 | + ]); |
| 49 | + |
| 50 | + const linkRows = (links ?? []) as Array<{ |
| 51 | + id: string; |
| 52 | + url: string; |
| 53 | + title: string | null; |
| 54 | + angle: string | null; |
| 55 | + enabled: boolean; |
| 56 | + times_promoted: number; |
| 57 | + last_promoted_at: string | null; |
| 58 | + created_at: string; |
| 59 | + }>; |
| 60 | + |
| 61 | + const postRows = (posts ?? []) as Array<{ |
| 62 | + id: string; |
| 63 | + link_id: string; |
| 64 | + platform: string; |
| 65 | + body: string; |
| 66 | + status: string; |
| 67 | + error: string | null; |
| 68 | + credits_spent: number; |
| 69 | + posted_at: string | null; |
| 70 | + provider: string | null; |
| 71 | + external_post_id: string | null; |
| 72 | + }>; |
| 73 | + |
| 74 | + const accountRows = (accounts ?? []) as Array<{ |
| 75 | + id: string; |
| 76 | + platform: string; |
| 77 | + handle: string; |
| 78 | + }>; |
| 79 | + |
| 80 | + const totalPosts = postRows.filter((p) => p.status === "posted").length; |
| 81 | + const totalCredits = postRows.reduce((sum, p) => sum + (p.credits_spent ?? 0), 0); |
| 82 | + |
| 83 | + return ( |
| 84 | + <div className="mx-auto max-w-4xl"> |
| 85 | + <div className="flex items-center justify-between gap-4"> |
| 86 | + <div> |
| 87 | + <Link href="/promote" className="text-sm text-[var(--color-muted)] hover:text-[var(--color-fg)]"> |
| 88 | + ← All lists |
| 89 | + </Link> |
| 90 | + <h1 className="text-3xl font-bold">{list.name}</h1> |
| 91 | + </div> |
| 92 | + <PromoteListActions id={list.id} status={list.status} showEdit={false} /> |
| 93 | + </div> |
| 94 | + |
| 95 | + {/* Status bar */} |
| 96 | + <div className="mt-4 flex flex-wrap gap-3"> |
| 97 | + <span |
| 98 | + className={`badge ${ |
| 99 | + list.status === "running" |
| 100 | + ? "badge-pass" |
| 101 | + : list.status === "paused" |
| 102 | + ? "badge-warn" |
| 103 | + : "" |
| 104 | + }`} |
| 105 | + > |
| 106 | + {list.status} |
| 107 | + </span> |
| 108 | + {list.pause_reason === "insufficient_credits" && ( |
| 109 | + <Link href="/settings/billing" className="badge badge-fail"> |
| 110 | + Low credits — buy more |
| 111 | + </Link> |
| 112 | + )} |
| 113 | + <span className="text-sm text-[var(--color-muted)]"> |
| 114 | + {cadenceLabel(list.cadence_seconds)} · {list.post_mode} · {linkRows.length} link{linkRows.length !== 1 ? "s" : ""} · {totalPosts} posts · {totalCredits} credits spent |
| 115 | + </span> |
| 116 | + </div> |
| 117 | + |
| 118 | + {/* Edit form */} |
| 119 | + <section className="card mt-6 p-4"> |
| 120 | + <h2 className="text-lg font-semibold">Settings</h2> |
| 121 | + <PromoteEditForm |
| 122 | + list={{ |
| 123 | + id: list.id, |
| 124 | + name: list.name, |
| 125 | + cadence_seconds: list.cadence_seconds, |
| 126 | + post_mode: list.post_mode, |
| 127 | + brand_voice: list.brand_voice ?? "", |
| 128 | + target_account_ids: list.target_account_ids, |
| 129 | + }} |
| 130 | + accounts={accountRows} |
| 131 | + /> |
| 132 | + </section> |
| 133 | + |
| 134 | + {/* Links */} |
| 135 | + <section className="mt-6"> |
| 136 | + <div className="flex items-center justify-between"> |
| 137 | + <h2 className="text-lg font-semibold">Links</h2> |
| 138 | + </div> |
| 139 | + <LinkList links={linkRows} /> |
| 140 | + <div className="mt-4"> |
| 141 | + <AddLinksForm listId={list.id} /> |
| 142 | + </div> |
| 143 | + </section> |
| 144 | + |
| 145 | + {/* Recent posts */} |
| 146 | + <section className="mt-6"> |
| 147 | + <h2 className="text-lg font-semibold">Recent posts</h2> |
| 148 | + {postRows.length === 0 ? ( |
| 149 | + <p className="mt-2 text-sm text-[var(--color-muted)]">No posts yet. The first drip will fire shortly if the list is running.</p> |
| 150 | + ) : ( |
| 151 | + <ul className="mt-2 space-y-2"> |
| 152 | + {postRows.map((p) => ( |
| 153 | + <li key={p.id} className="card p-3 text-sm"> |
| 154 | + <div className="flex items-center justify-between gap-2"> |
| 155 | + <div className="flex items-center gap-2"> |
| 156 | + <span className="font-mono text-xs text-[var(--color-muted)]">{p.platform}</span> |
| 157 | + <span |
| 158 | + className={`badge text-xs ${ |
| 159 | + p.status === "posted" ? "badge-pass" : p.status === "failed" ? "badge-fail" : "" |
| 160 | + }`} |
| 161 | + > |
| 162 | + {p.status} |
| 163 | + </span> |
| 164 | + {p.provider && ( |
| 165 | + <span className="text-xs text-[var(--color-muted)]">via {p.provider}</span> |
| 166 | + )} |
| 167 | + </div> |
| 168 | + <span className="text-xs text-[var(--color-muted)]"> |
| 169 | + {p.posted_at ? new Date(p.posted_at).toLocaleString() : "—"} |
| 170 | + </span> |
| 171 | + </div> |
| 172 | + <p className="mt-1 whitespace-pre-wrap break-words text-[var(--color-fg)]"> |
| 173 | + {p.body.length > 300 ? p.body.slice(0, 300) + "..." : p.body} |
| 174 | + </p> |
| 175 | + {p.error && ( |
| 176 | + <p className="mt-1 text-xs text-[var(--color-fail)]">{p.error}</p> |
| 177 | + )} |
| 178 | + </li> |
| 179 | + ))} |
| 180 | + </ul> |
| 181 | + )} |
| 182 | + </section> |
| 183 | + </div> |
| 184 | + ); |
| 185 | +} |
0 commit comments