Implement contact form, domain management, billing, and project APIs #4685
26 changed files+1625−197
Modifiedmiddleware.ts+34−13View fileUnifiedSplit
@@ -1,24 +1,45 @@
1import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
12import { NextResponse } from "next/server";
23import type { NextRequest } from "next/server";
34
4export function middleware(req: NextRequest) {
5
6 // DXL_MIDDLEWARE_ALLOW_AUTHCHECK_20260129
7 // Allow DXL admin auth-check endpoints to bypass middleware gating.
8 // This does NOT leak secrets; it only enables routing.
9 const __dxlPath = (req as any)?.nextUrl?.pathname ?? "";
10 if (__dxlPath === "/api/dxl/auth-check" || __dxlPath === "/api/__where__/dxl-auth-check") {
11 return NextResponse.next();
12 }const p = req.nextUrl.pathname;
13 const res = NextResponse.next();
5const isProtectedRoute = createRouteMatcher([
6 "/admin(.*)",
7 "/api/projects(.*)",
8 "/api/billing(.*)",
9 "/api/domains(.*)",
10 "/api/video(.*)",
11]);
1412
13export default clerkMiddleware((auth, req) => {
14 const p = (req as NextRequest).nextUrl.pathname;
15
16 // Anti-cache headers for probe endpoints
1517 if (p === "/__probe__" || p === "/api/__probe__") {
18 const res = NextResponse.next();
1619 res.headers.set("Cache-Control", "no-store, max-age=0");
1720 res.headers.set("Pragma", "no-cache");
1821 res.headers.set("Expires", "0");
22 return res;
23 }
24
25 // Protect admin and API routes - redirect to sign-in if not authenticated
26 if (isProtectedRoute(req as NextRequest)) {
27 const { userId } = auth();
28 if (!userId) {
29 const signInUrl = new URL("/sign-in", (req as NextRequest).url);
30 signInUrl.searchParams.set("redirect_url", (req as NextRequest).url);
31 return NextResponse.redirect(signInUrl);
32 }
1933 }
2034
21 return res;
22}
35 return NextResponse.next();
36});
2337
24export const config = { matcher: ["/__probe__", "/api/__probe__"] };
38export const config = {
39 matcher: [
40 // Skip Next.js internals and static files
41 "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
42 // Always run for API routes
43 "/(api|trpc)(.*)",
44 ],
45};
Modifiedpackage-lock.json+0−11View fileUnifiedSplit
@@ -1460,7 +1460,6 @@
14601460 "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
14611461 "dev": true,
14621462 "license": "MIT",
1463 "peer": true,
14641463 "bin": {
14651464 "acorn": "bin/acorn"
14661465 },
@@ -1895,7 +1894,6 @@
18951894 }
18961895 ],
18971896 "license": "MIT",
1898 "peer": true,
18991897 "dependencies": {
19001898 "baseline-browser-mapping": "^2.9.0",
19011899 "caniuse-lite": "^1.0.30001759",
@@ -2535,7 +2533,6 @@
25352533 "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
25362534 "dev": true,
25372535 "license": "MIT",
2538 "peer": true,
25392536 "dependencies": {
25402537 "@eslint-community/eslint-utils": "^4.2.0",
25412538 "@eslint-community/regexpp": "^4.6.1",
@@ -2704,7 +2701,6 @@
27042701 "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
27052702 "dev": true,
27062703 "license": "MIT",
2707 "peer": true,
27082704 "dependencies": {
27092705 "@rtsao/scc": "^1.1.0",
27102706 "array-includes": "^3.1.9",
@@ -4745,7 +4741,6 @@
47454741 "integrity": "sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==",
47464742 "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.",
47474743 "license": "MIT",
4748 "peer": true,
47494744 "dependencies": {
47504745 "@next/env": "14.2.5",
47514746 "@swc/helpers": "0.5.5",
@@ -5247,7 +5242,6 @@
52475242 }
52485243 ],
52495244 "license": "MIT",
5250 "peer": true,
52515245 "dependencies": {
52525246 "nanoid": "^3.3.11",
52535247 "picocolors": "^1.1.1",
@@ -5337,7 +5331,6 @@
53375331 "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
53385332 "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
53395333 "license": "MIT",
5340 "peer": true,
53415334 "dependencies": {
53425335 "loose-envify": "^1.1.0"
53435336 },
@@ -5350,7 +5343,6 @@
53505343 "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
53515344 "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
53525345 "license": "MIT",
5353 "peer": true,
53545346 "dependencies": {
53555347 "loose-envify": "^1.1.0",
53565348 "scheduler": "^0.23.0"
@@ -6253,7 +6245,6 @@
62536245 "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
62546246 "dev": true,
62556247 "license": "MIT",
6256 "peer": true,
62576248 "engines": {
62586249 "node": ">=12"
62596250 },
@@ -6422,7 +6413,6 @@
64226413 "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
64236414 "dev": true,
64246415 "license": "Apache-2.0",
6425 "peer": true,
64266416 "bin": {
64276417 "tsc": "bin/tsc",
64286418 "tsserver": "bin/tsserver"
@@ -6822,7 +6812,6 @@
68226812 "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
68236813 "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
68246814 "license": "MIT",
6825 "peer": true,
68266815 "funding": {
68276816 "url": "https://github.com/sponsors/colinhacks"
68286817 }
Modifiedsrc/app/(marketing)/_d8/D8Bits.tsx+17−2View fileUnifiedSplit
@@ -1,6 +1,6 @@
11import React from "react";
22
3export function D8Card(props: { children?: React.ReactNode }) {
3export function D8Card(props: { children?: React.ReactNode; title?: string; body?: string; kicker?: string }) {
44 return (
55 <div
66 style={{
@@ -11,6 +11,21 @@ export function D8Card(props: { children?: React.ReactNode }) {
1111 boxShadow: "0 18px 55px rgba(0,0,0,0.35)",
1212 }}
1313 >
14 {props.kicker && (
15 <div style={{ fontSize: 11, fontWeight: 900, letterSpacing: "0.14em", textTransform: "uppercase", color: "rgba(237,234,247,0.55)", marginBottom: 8 }}>
16 {props.kicker}
17 </div>
18 )}
19 {props.title && (
20 <div style={{ fontSize: 15, fontWeight: 800, color: "rgba(246,242,255,0.95)", marginBottom: 6 }}>
21 {props.title}
22 </div>
23 )}
24 {props.body && (
25 <div style={{ fontSize: 13, lineHeight: 1.5, color: "rgba(237,234,247,0.70)" }}>
26 {props.body}
27 </div>
28 )}
1429 {props.children}
1530 </div>
1631 );
@@ -22,4 +37,4 @@ export function D8P(props: { children?: React.ReactNode }) {
2237 {props.children}
2338 </p>
2439 );
25}
\ No newline at end of file
40}
Modifiedsrc/app/(marketing)/contact/page.jsx+279−28View fileUnifiedSplit
@@ -1,40 +1,291 @@
1import { PageHeader, LogoCloud } from "../../../components/marketing/MarketingShell";
1"use client";
2
3import { useState } from "react";
24
35export default function ContactPage() {
4 return (
5 <main>
6 <PageHeader
7 eyebrow="Contact"
8 title="Talk to the team"
9 subtitle="Tell us what you’re building and we’ll point you to the fastest path to a premium launch."
10 rightSlot={
11 <a className="rounded-2xl bg-slate-900 px-5 py-3 text-sm font-semibold text-white hover:bg-slate-800 shadow-sm" href="/p/new">
12 Generate my site
13 </a>
14 }
15 />
6 const [form, setForm] = useState({ name: "", email: "", message: "" });
7 const [status, setStatus] = useState("idle"); // idle | sending | sent | error
8 const [error, setError] = useState(null);
9
10 function update(key, value) {
11 setForm((prev) => ({ ...prev, [key]: value }));
12 }
1613
17 <LogoCloud label="Premium support experience" />
14 async function handleSubmit(e) {
15 e.preventDefault();
16 setStatus("sending");
17 setError(null);
1818
19 <div className="mx-auto max-w-6xl px-6 pb-14">
20 <div className="grid gap-5 md:grid-cols-2">
21 <div className="rounded-3xl bg-white/85 p-6 ring-1 ring-slate-200 shadow-sm">
22 <div className="text-sm font-semibold text-slate-950">Email</div>
23 <div className="mt-2 text-sm text-slate-700">support@dominat8.com (placeholder)</div>
19 const res = await fetch("/api/contact", {
20 method: "POST",
21 headers: { "Content-Type": "application/json" },
22 body: JSON.stringify(form),
23 });
2424
25 <div className="mt-6 text-sm font-semibold text-slate-950">Response time</div>
26 <div className="mt-2 text-sm text-slate-700">We aim to reply within 24 hours.</div>
25 const data = await res.json().catch(() => ({}));
26
27 if (data.ok) {
28 setStatus("sent");
29 setForm({ name: "", email: "", message: "" });
30 } else {
31 setError(data.error || "Something went wrong. Please try again.");
32 setStatus("error");
33 }
34 }
35
36 return (
37 <main
38 style={{
39 minHeight: "100vh",
40 background: "linear-gradient(180deg, #f8f9fc 0%, #fff 40%)",
41 fontFamily:
42 "ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial",
43 }}
44 >
45 <div style={{ maxWidth: 720, margin: "0 auto", padding: "48px 24px 64px" }}>
46 <div style={{ textAlign: "center", marginBottom: 40 }}>
47 <div
48 style={{
49 display: "inline-block",
50 fontSize: 12,
51 fontWeight: 700,
52 letterSpacing: "0.14em",
53 textTransform: "uppercase",
54 color: "#7c3aed",
55 marginBottom: 8,
56 }}
57 >
58 Contact
2759 </div>
60 <h1
61 style={{
62 fontSize: 36,
63 fontWeight: 900,
64 color: "#0f172a",
65 letterSpacing: "-0.02em",
66 lineHeight: 1.1,
67 }}
68 >
69 Talk to the team
70 </h1>
71 <p
72 style={{
73 marginTop: 12,
74 fontSize: 16,
75 color: "#64748b",
76 lineHeight: 1.5,
77 maxWidth: 480,
78 marginLeft: "auto",
79 marginRight: "auto",
80 }}
81 >
82 Tell us what you're building and we'll point you to the fastest path to a premium launch.
83 </p>
84 </div>
2885
29 <div className="rounded-3xl bg-white/85 p-6 ring-1 ring-slate-200 shadow-sm">
30 <div className="text-sm font-semibold text-slate-950">Quick note</div>
31 <p className="mt-2 text-sm leading-relaxed text-slate-700">
32 This is a marketing placeholder. Next upgrade can wire a contact form endpoint if you want.
86 {status === "sent" ? (
87 <div
88 style={{
89 borderRadius: 24,
90 border: "1px solid #d1fae5",
91 background: "#ecfdf5",
92 padding: 32,
93 textAlign: "center",
94 }}
95 >
96 <div style={{ fontSize: 28, marginBottom: 8 }}>✓</div>
97 <h2 style={{ fontSize: 20, fontWeight: 700, color: "#065f46" }}>
98 Message sent!
99 </h2>
100 <p style={{ marginTop: 8, fontSize: 14, color: "#047857" }}>
101 We'll get back to you within 24 hours.
33102 </p>
34 <a className="mt-6 inline-flex rounded-2xl bg-slate-900 px-5 py-3 text-sm font-semibold text-white hover:bg-slate-800 shadow-sm" href="/pricing">
35 View pricing
36 </a>
103 <button
104 onClick={() => setStatus("idle")}
105 style={{
106 marginTop: 16,
107 padding: "10px 20px",
108 borderRadius: 14,
109 border: "1px solid #d1fae5",
110 background: "#fff",
111 fontSize: 14,
112 fontWeight: 600,
113 color: "#065f46",
114 cursor: "pointer",
115 }}
116 >
117 Send another message
118 </button>
37119 </div>
120 ) : (
121 <form
122 onSubmit={handleSubmit}
123 style={{
124 borderRadius: 24,
125 border: "1px solid #e2e8f0",
126 background: "#fff",
127 padding: 32,
128 boxShadow: "0 4px 24px rgba(0,0,0,0.04)",
129 }}
130 >
131 <div style={{ marginBottom: 20 }}>
132 <label
133 style={{
134 display: "block",
135 fontSize: 14,
136 fontWeight: 600,
137 color: "#334155",
138 marginBottom: 6,
139 }}
140 >
141 Name
142 </label>
143 <input
144 type="text"
145 value={form.name}
146 onChange={(e) => update("name", e.target.value)}
147 required
148 placeholder="Your name"
149 style={{
150 width: "100%",
151 padding: "12px 16px",
152 borderRadius: 14,
153 border: "1px solid #e2e8f0",
154 fontSize: 14,
155 color: "#0f172a",
156 outline: "none",
157 boxSizing: "border-box",
158 }}
159 />
160 </div>
161
162 <div style={{ marginBottom: 20 }}>
163 <label
164 style={{
165 display: "block",
166 fontSize: 14,
167 fontWeight: 600,
168 color: "#334155",
169 marginBottom: 6,
170 }}
171 >
172 Email
173 </label>
174 <input
175 type="email"
176 value={form.email}
177 onChange={(e) => update("email", e.target.value)}
178 required
179 placeholder="you@example.com"
180 style={{
181 width: "100%",
182 padding: "12px 16px",
183 borderRadius: 14,
184 border: "1px solid #e2e8f0",
185 fontSize: 14,
186 color: "#0f172a",
187 outline: "none",
188 boxSizing: "border-box",
189 }}
190 />
191 </div>
192
193 <div style={{ marginBottom: 20 }}>
194 <label
195 style={{
196 display: "block",
197 fontSize: 14,
198 fontWeight: 600,
199 color: "#334155",
200 marginBottom: 6,
201 }}
202 >
203 Message
204 </label>
205 <textarea
206 value={form.message}
207 onChange={(e) => update("message", e.target.value)}
208 required
209 rows={5}
210 placeholder="Tell us about your project..."
211 style={{
212 width: "100%",
213 padding: "12px 16px",
214 borderRadius: 14,
215 border: "1px solid #e2e8f0",
216 fontSize: 14,
217 color: "#0f172a",
218 outline: "none",
219 resize: "vertical",
220 boxSizing: "border-box",
221 }}
222 />
223 </div>
224
225 {error && (
226 <div
227 style={{
228 marginBottom: 16,
229 padding: "10px 14px",
230 borderRadius: 12,
231 background: "#fef2f2",
232 border: "1px solid #fecaca",
233 fontSize: 13,
234 color: "#dc2626",
235 fontWeight: 600,
236 }}
237 >
238 {error}
239 </div>
240 )}
241
242 <button
243 type="submit"
244 disabled={status === "sending"}
245 style={{
246 width: "100%",
247 padding: "14px 20px",
248 borderRadius: 16,
249 border: "none",
250 background: "linear-gradient(90deg, #7c3aed, #3b82f6)",
251 color: "#fff",
252 fontSize: 15,
253 fontWeight: 700,
254 cursor: status === "sending" ? "not-allowed" : "pointer",
255 opacity: status === "sending" ? 0.7 : 1,
256 }}
257 >
258 {status === "sending" ? "Sending..." : "Send Message"}
259 </button>
260
261 <div
262 style={{
263 marginTop: 16,
264 textAlign: "center",
265 fontSize: 13,
266 color: "#94a3b8",
267 }}
268 >
269 Or email us directly at{" "}
270 <a href="mailto:support@dominat8.com" style={{ color: "#7c3aed" }}>
271 support@dominat8.com
272 </a>
273 </div>
274 </form>
275 )}
276
277 <div style={{ textAlign: "center", marginTop: 32 }}>
278 <a
279 href="/pricing"
280 style={{
281 fontSize: 14,
282 fontWeight: 600,
283 color: "#7c3aed",
284 textDecoration: "none",
285 }}
286 >
287 View pricing →
288 </a>
38289 </div>
39290 </div>
40291 </main>
Modifiedsrc/app/_client/D8TV.tsx+0−3View fileUnifiedSplit
@@ -12,9 +12,6 @@ export type D8SectionProps = {
1212};
1313
1414export function D8Section(props: D8SectionProps) {
15 tone?: string;
16 lead?: string;
17 eyebrow?: string;
1815 const { title, subtitle, children, id } = props;
1916
2017 // Keep server-safe: no "use client", no browser-only APIs.
Modifiedsrc/app/admin/billing/page.tsx+153−18View fileUnifiedSplit
@@ -1,33 +1,168 @@
1"use client";
2
3import React, { useState } from "react";
4
5const PLANS = [
6 {
7 name: "Free",
8 price: "$0",
9 period: "forever",
10 features: ["1 project", "Community templates", "Dominat8 subdomain", "Basic analytics"],
11 current: true,
12 },
13 {
14 name: "Pro",
15 price: "$29",
16 period: "/month",
17 features: ["Unlimited projects", "All premium templates", "Custom domains + SSL", "AI video studio", "Priority support", "Advanced analytics"],
18 priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID ?? "",
19 highlight: true,
20 },
21 {
22 name: "Enterprise",
23 price: "Custom",
24 period: "",
25 features: ["Everything in Pro", "Dedicated agent runners", "SLA guarantees", "Custom integrations", "White-label option"],
26 contact: true,
27 },
28];
29
130export default function AdminBilling() {
31 const [loading, setLoading] = useState<string | null>(null);
32 const [error, setError] = useState<string | null>(null);
33
34 async function handleUpgrade(priceId: string) {
35 setLoading(priceId);
36 setError(null);
37
38 const res = await fetch("/api/billing/checkout", {
39 method: "POST",
40 headers: { "Content-Type": "application/json" },
41 body: JSON.stringify({ priceId }),
42 });
43
44 const data = await res.json().catch(() => ({}));
45
46 if (data.url) {
47 window.location.href = data.url;
48 } else {
49 setError(data.error ?? "Failed to create checkout session. Make sure STRIPE_SECRET_KEY is set.");
50 setLoading(null);
51 }
52 }
53
54 async function handleManage() {
55 setLoading("portal");
56 setError(null);
57
58 const res = await fetch("/api/billing/portal", {
59 method: "POST",
60 headers: { "Content-Type": "application/json" },
61 body: JSON.stringify({ customerId: "placeholder" }),
62 });
63
64 const data = await res.json().catch(() => ({}));
65
66 if (data.url) {
67 window.location.href = data.url;
68 } else {
69 setError(data.error ?? "Failed to open billing portal. Make sure Stripe is configured.");
70 setLoading(null);
71 }
72 }
73
274 return (
375 <div className="space-y-6">
476 <div>
577 <div className="text-xs font-semibold tracking-wide text-white/60">BILLING</div>
6 <h1 className="mt-2 text-2xl font-semibold tracking-tight">Billing</h1>
78 <h1 className="mt-2 text-2xl font-semibold tracking-tight">Plans & Billing</h1>
779 <p className="mt-2 text-sm text-white/60">
8 This page becomes: plan status + upgrade + manage subscription.
80 Manage your subscription and billing details.
981 </p>
1082 </div>
1183
12 <div className="grid gap-4 md:grid-cols-2">
13 <div className="rounded-3xl border border-white/10 bg-black/30 p-6">
14 <div className="text-sm font-semibold">Plan</div>
15 <div className="mt-2 text-sm text-white/60">Connect to your existing Stripe/Pro gating keys.</div>
16 <div className="mt-5 rounded-2xl border border-white/10 bg-white/5 p-4">
17 <div className="text-[11px] text-white/50">Current</div>
18 <div className="mt-1 text-sm font-semibold">Free (placeholder)</div>
19 </div>
84 {error && (
85 <div className="rounded-2xl border border-red-500/20 bg-red-500/5 p-4 text-sm text-red-400 font-semibold">
86 {error}
2087 </div>
88 )}
89
90 <div className="grid gap-4 md:grid-cols-3">
91 {PLANS.map((plan) => (
92 <div
93 key={plan.name}
94 className={`rounded-3xl border p-6 ${
95 plan.highlight
96 ? "border-purple-500/30 bg-gradient-to-b from-purple-500/10 to-blue-500/5"
97 : "border-white/10 bg-black/30"
98 }`}
99 >
100 <div className="flex items-center gap-2">
101 <div className="text-lg font-bold">{plan.name}</div>
102 {plan.current && (
103 <span className="rounded-full bg-green-500/20 px-2.5 py-0.5 text-[11px] font-bold uppercase tracking-wider text-green-300">
104 Current
105 </span>
106 )}
107 </div>
108
109 <div className="mt-3 flex items-baseline gap-1">
110 <span className="text-3xl font-bold">{plan.price}</span>
111 {plan.period && <span className="text-sm text-white/50">{plan.period}</span>}
112 </div>
21113
22 <div className="rounded-3xl border border-white/10 bg-black/30 p-6">
23 <div className="text-sm font-semibold">Actions</div>
24 <ul className="mt-3 space-y-2 text-sm text-white/60 list-disc pl-5">
25 <li>Upgrade to Pro</li>
26 <li>Manage subscription</li>
27 <li>View invoices</li>
28 </ul>
114 <ul className="mt-5 space-y-2">
115 {plan.features.map((f) => (
116 <li key={f} className="flex items-center gap-2 text-sm text-white/70">
117 <svg className="h-4 w-4 text-purple-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
118 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
119 </svg>
120 {f}
121 </li>
122 ))}
123 </ul>
124
125 <div className="mt-6">
126 {plan.current && (
127 <button
128 onClick={handleManage}
129 disabled={loading === "portal"}
130 className="w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm font-semibold hover:bg-white/10 transition disabled:opacity-50"
131 >
132 {loading === "portal" ? "Opening..." : "Manage Subscription"}
133 </button>
134 )}
135 {plan.priceId && (
136 <button
137 onClick={() => handleUpgrade(plan.priceId!)}
138 disabled={loading === plan.priceId}
139 className="w-full rounded-2xl bg-gradient-to-r from-purple-500 to-blue-500 px-4 py-3 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
140 >
141 {loading === plan.priceId ? "Redirecting..." : "Upgrade to Pro"}
142 </button>
143 )}
144 {plan.contact && (
145 <a
146 href="/contact"
147 className="block w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-center text-sm font-semibold hover:bg-white/10 transition"
148 >
149 Contact Sales
150 </a>
151 )}
152 </div>
153 </div>
154 ))}
155 </div>
156
157 <div className="rounded-3xl border border-white/10 bg-black/30 p-6">
158 <div className="text-sm font-semibold">Billing History</div>
159 <p className="mt-2 text-sm text-white/50">
160 Invoice history will appear here once you subscribe to a paid plan.
161 </p>
162 <div className="mt-4 rounded-2xl border border-white/10 bg-white/5 p-4 text-center text-sm text-white/40">
163 No invoices yet
29164 </div>
30165 </div>
31166 </div>
32167 );
33}
\ No newline at end of file
168}
Modifiedsrc/app/admin/domains/page.tsx+204−11View fileUnifiedSplit
@@ -1,24 +1,217 @@
1"use client";
2
3import React, { useEffect, useState } from "react";
4
5type DomainRecord = {
6 id: string;
7 domain: string;
8 txtRecord: string;
9 status: "pending_dns" | "dns_verified" | "ssl_provisioning" | "ready" | "error";
10 createdAt: string;
11 updatedAt: string;
12};
13
14const statusConfig: Record<string, { label: string; color: string }> = {
15 pending_dns: { label: "Pending DNS", color: "bg-yellow-500/20 text-yellow-300" },
16 dns_verified: { label: "DNS Verified", color: "bg-blue-500/20 text-blue-300" },
17 ssl_provisioning: { label: "Provisioning SSL", color: "bg-blue-500/20 text-blue-300" },
18 ready: { label: "Ready", color: "bg-green-500/20 text-green-300" },
19 error: { label: "Error", color: "bg-red-500/20 text-red-300" },
20};
21
122export default function AdminDomains() {
23 const [domains, setDomains] = useState<DomainRecord[]>([]);
24 const [loading, setLoading] = useState(true);
25 const [newDomain, setNewDomain] = useState("");
26 const [adding, setAdding] = useState(false);
27 const [error, setError] = useState<string | null>(null);
28 const [instructions, setInstructions] = useState<any>(null);
29
30 async function loadDomains() {
31 setLoading(true);
32 const res = await fetch("/api/domains/verify");
33 const data = await res.json().catch(() => ({ domains: [] }));
34 setDomains(data.domains ?? []);
35 setLoading(false);
36 }
37
38 useEffect(() => { loadDomains(); }, []);
39
40 async function handleAdd(e: React.FormEvent) {
41 e.preventDefault();
42 if (!newDomain.trim()) return;
43 setAdding(true);
44 setError(null);
45 setInstructions(null);
46
47 const res = await fetch("/api/domains/verify", {
48 method: "POST",
49 headers: { "Content-Type": "application/json" },
50 body: JSON.stringify({ domain: newDomain.trim() }),
51 });
52
53 const data = await res.json().catch(() => ({}));
54 if (!data.ok) {
55 setError(data.error ?? "Failed to add domain");
56 } else if (data.instructions) {
57 setInstructions(data.instructions);
58 }
59 setAdding(false);
60 setNewDomain("");
61 await loadDomains();
62 }
63
64 async function handleAction(domain: string, action: string) {
65 const res = await fetch("/api/domains/verify", {
66 method: "PATCH",
67 headers: { "Content-Type": "application/json" },
68 body: JSON.stringify({ domain, action }),
69 });
70 await res.json();
71 await loadDomains();
72 }
73
274 return (
375 <div className="space-y-6">
476 <div>
577 <div className="text-xs font-semibold tracking-wide text-white/60">DOMAINS</div>
6 <h1 className="mt-2 text-2xl font-semibold tracking-tight">Domains</h1>
78 <h1 className="mt-2 text-2xl font-semibold tracking-tight">Domain Management</h1>
779 <p className="mt-2 text-sm text-white/60">
8 This page should become a step-by-step checklist (TXT/CNAME verify → SSL → ready).
80 Connect custom domains to your projects. Follow the wizard to verify DNS and provision SSL.
981 </p>
1082 </div>
1183
1284 <div className="rounded-3xl border border-white/10 bg-black/30 p-6">
13 <div className="text-sm font-semibold">Domain checklist (UI baseline)</div>
14 <ol className="mt-3 space-y-2 text-sm text-white/60 list-decimal pl-5">
15 <li>Enter domain</li>
16 <li>Show required DNS records</li>
17 <li>Poll verification</li>
18 <li>Provision SSL</li>
19 <li>Mark ready + show “Open site”</li>
20 </ol>
85 <div className="text-sm font-semibold mb-4">Add Domain</div>
86 <form onSubmit={handleAdd} className="flex gap-3">
87 <input
88 type="text"
89 value={newDomain}
90 onChange={(e) => setNewDomain(e.target.value)}
91 placeholder="example.com"
92 className="flex-1 rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 focus:border-purple-500/50 focus:outline-none focus:ring-1 focus:ring-purple-500/50"
93 />
94 <button
95 type="submit"
96 disabled={adding}
97 className="rounded-2xl bg-gradient-to-r from-purple-500 to-blue-500 px-6 py-3 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
98 >
99 {adding ? "Adding..." : "Add Domain"}
100 </button>
101 </form>
102 {error && <div className="mt-3 text-sm text-red-400 font-semibold">{error}</div>}
103 </div>
104
105 {instructions && (
106 <div className="rounded-3xl border border-purple-500/20 bg-purple-500/5 p-6">
107 <div className="text-sm font-semibold mb-3">DNS Configuration Required</div>
108 <div className="space-y-3 text-sm">
109 <div className="rounded-2xl border border-white/10 bg-black/30 p-4">
110 <div className="text-xs text-white/50 uppercase tracking-wider mb-1">Step 1: TXT Record</div>
111 <div className="text-white/80">
112 <span className="text-white/50">Name:</span> <code className="bg-white/10 px-1.5 py-0.5 rounded">{instructions.name}</code>
113 </div>
114 <div className="text-white/80 mt-1">
115 <span className="text-white/50">Value:</span> <code className="bg-white/10 px-1.5 py-0.5 rounded">{instructions.value}</code>
116 </div>
117 </div>
118 <div className="rounded-2xl border border-white/10 bg-black/30 p-4">
119 <div className="text-xs text-white/50 uppercase tracking-wider mb-1">Step 2: CNAME Record</div>
120 <div className="text-white/80">
121 <span className="text-white/50">Name:</span> <code className="bg-white/10 px-1.5 py-0.5 rounded">{instructions.cname?.name}</code>
122 </div>
123 <div className="text-white/80 mt-1">
124 <span className="text-white/50">Value:</span> <code className="bg-white/10 px-1.5 py-0.5 rounded">{instructions.cname?.value}</code>
125 </div>
126 </div>
127 </div>
128 <button
129 onClick={() => setInstructions(null)}
130 className="mt-4 text-xs text-white/50 hover:text-white/80 transition"
131 >
132 Dismiss
133 </button>
134 </div>
135 )}
136
137 <div className="space-y-3">
138 {loading && <div className="text-sm text-white/50">Loading domains...</div>}
139 {!loading && !domains.length && (
140 <div className="rounded-3xl border border-white/10 bg-black/30 p-6 text-center text-sm text-white/40">
141 No domains configured yet. Add one above to get started.
142 </div>
143 )}
144 {domains.map((d) => {
145 const cfg = statusConfig[d.status] ?? statusConfig.pending_dns;
146 return (
147 <div key={d.id} className="rounded-3xl border border-white/10 bg-black/30 p-6">
148 <div className="flex items-center justify-between gap-4 flex-wrap">
149 <div>
150 <div className="flex items-center gap-3">
151 <span className="text-base font-semibold">{d.domain}</span>
152 <span className={`inline-flex px-2.5 py-0.5 rounded-full text-[11px] font-bold uppercase tracking-wider ${cfg.color}`}>
153 {cfg.label}
154 </span>
155 </div>
156 <div className="mt-1 text-xs text-white/40">
157 Added {new Date(d.createdAt).toLocaleDateString()}
158 {d.txtRecord && <> · TXT: <code className="bg-white/5 px-1 rounded">{d.txtRecord}</code></>}
159 </div>
160 </div>
161
162 <div className="flex items-center gap-2">
163 {d.status === "pending_dns" && (
164 <button
165 onClick={() => handleAction(d.domain, "check_dns")}
166 className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-xs font-semibold hover:bg-white/10 transition"
167 >
168 Verify DNS
169 </button>
170 )}
171 {d.status === "dns_verified" && (
172 <button
173 onClick={() => handleAction(d.domain, "provision_ssl")}
174 className="rounded-xl bg-gradient-to-r from-purple-500 to-blue-500 px-4 py-2 text-xs font-semibold text-white hover:opacity-90 transition"
175 >
176 Provision SSL
177 </button>
178 )}
179 {d.status === "ready" && (
180 <a
181 href={`https://${d.domain}`}
182 target="_blank"
183 rel="noopener noreferrer"
184 className="rounded-xl bg-green-500/20 px-4 py-2 text-xs font-semibold text-green-300 hover:bg-green-500/30 transition"
185 >
186 Open Site
187 </a>
188 )}
189 </div>
190 </div>
191
192 {/* Progress steps */}
193 <div className="mt-4 flex items-center gap-1">
194 {["pending_dns", "dns_verified", "ssl_provisioning", "ready"].map((step, i) => {
195 const steps = ["pending_dns", "dns_verified", "ssl_provisioning", "ready"];
196 const currentIdx = steps.indexOf(d.status);
197 const done = i <= currentIdx;
198 return (
199 <React.Fragment key={step}>
200 <div className={`h-1.5 flex-1 rounded-full ${done ? "bg-purple-500" : "bg-white/10"}`} />
201 </React.Fragment>
202 );
203 })}
204 </div>
205 <div className="mt-1 flex justify-between text-[10px] text-white/30 uppercase tracking-wider">
206 <span>DNS</span>
207 <span>Verified</span>
208 <span>SSL</span>
209 <span>Ready</span>
210 </div>
211 </div>
212 );
213 })}
21214 </div>
22215 </div>
23216 );
24}
\ No newline at end of file
217}
Modifiedsrc/app/admin/marketing-queue/page.tsx+1−1View fileUnifiedSplit
@@ -128,7 +128,7 @@ export default function Page() {
128128
129129 const [scheduleLocal, setScheduleLocal] = useState(""); // datetime-local
130130
131 const headers = useMemo(() => (token ? { "x-admin-token": token } : {}), [token]);
131 const headers = useMemo((): Record<string, string> => (token ? { "x-admin-token": token } : {}), [token]);
132132
133133 async function refresh() {
134134 setErr(null);
Addedsrc/app/admin/projects/new/page.tsx+102−0View fileUnifiedSplit
@@ -0,0 +1,102 @@
1"use client";
2
3import React, { useState } from "react";
4import { useRouter } from "next/navigation";
5
6export default function NewProjectPage() {
7 const router = useRouter();
8 const [name, setName] = useState("");
9 const [description, setDescription] = useState("");
10 const [domain, setDomain] = useState("");
11 const [saving, setSaving] = useState(false);
12 const [error, setError] = useState<string | null>(null);
13
14 async function handleSubmit(e: React.FormEvent) {
15 e.preventDefault();
16 if (!name.trim()) { setError("Project name is required"); return; }
17
18 setSaving(true);
19 setError(null);
20
21 const res = await fetch("/api/projects", {
22 method: "POST",
23 headers: { "Content-Type": "application/json" },
24 body: JSON.stringify({ name: name.trim(), description: description.trim(), domain: domain.trim() }),
25 });
26
27 const data = await res.json().catch(() => ({}));
28
29 if (!data.ok) {
30 setError(data.error ?? "Failed to create project");
31 setSaving(false);
32 return;
33 }
34
35 router.push("/admin/projects");
36 }
37
38 return (
39 <div className="space-y-6">
40 <div>
41 <div className="text-xs font-semibold tracking-wide text-white/60">PROJECTS</div>
42 <h1 className="mt-2 text-2xl font-semibold tracking-tight">New Project</h1>
43 </div>
44
45 <form onSubmit={handleSubmit} className="max-w-xl space-y-5">
46 <div>
47 <label className="block text-sm font-semibold text-white/80 mb-1.5">Project name</label>
48 <input
49 type="text"
50 value={name}
51 onChange={(e) => setName(e.target.value)}
52 placeholder="My Awesome Site"
53 className="w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 focus:border-purple-500/50 focus:outline-none focus:ring-1 focus:ring-purple-500/50"
54 />
55 </div>
56
57 <div>
58 <label className="block text-sm font-semibold text-white/80 mb-1.5">Description</label>
59 <textarea
60 value={description}
61 onChange={(e) => setDescription(e.target.value)}
62 placeholder="Brief description of the project..."
63 rows={3}
64 className="w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 focus:border-purple-500/50 focus:outline-none focus:ring-1 focus:ring-purple-500/50 resize-none"
65 />
66 </div>
67
68 <div>
69 <label className="block text-sm font-semibold text-white/80 mb-1.5">Domain (optional)</label>
70 <input
71 type="text"
72 value={domain}
73 onChange={(e) => setDomain(e.target.value)}
74 placeholder="example.com"
75 className="w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 focus:border-purple-500/50 focus:outline-none focus:ring-1 focus:ring-purple-500/50"
76 />
77 </div>
78
79 {error && (
80 <div className="text-sm text-red-400 font-semibold">{error}</div>
81 )}
82
83 <div className="flex gap-3">
84 <button
85 type="submit"
86 disabled={saving}
87 className="rounded-2xl bg-gradient-to-r from-purple-500 to-blue-500 px-6 py-3 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
88 >
89 {saving ? "Creating..." : "Create Project"}
90 </button>
91 <button
92 type="button"
93 onClick={() => router.push("/admin/projects")}
94 className="rounded-2xl border border-white/10 bg-white/5 px-6 py-3 text-sm font-semibold text-white/70 hover:bg-white/10 transition"
95 >
96 Cancel
97 </button>
98 </div>
99 </form>
100 </div>
101 );
102}
Modifiedsrc/app/admin/projects/page.tsx+94−67View fileUnifiedSplit
@@ -1,44 +1,72 @@
1"use client";
1"use client";
22
3import React from "react";
3import React, { useEffect, useState } from "react";
44import Link from "next/link";
55
66type Project = {
77 id: string;
88 name: string;
9 description?: string | null;
10 createdAt?: string | Date;
9 description: string;
10 domain: string;
11 status: "draft" | "building" | "live";
12 createdAt: string;
13 updatedAt: string;
1114};
1215
13async function fetchProjects(): Promise<Project[]> {
14 // NOTE: This is a placeholder. Keep existing data-loading logic if present in your project.
15 // If your app uses a hook or server action, you can integrate it here.
16 // For now, return an empty array to demonstrate empty state handling.
17 return [];
18}
16const statusColors: Record<string, string> = {
17 draft: "bg-gray-500/20 text-gray-300",
18 building: "bg-yellow-500/20 text-yellow-300",
19 live: "bg-green-500/20 text-green-300",
20};
1921
20export default async function AdminProjectsPage() {
21 const projects = await fetchProjects();
22export default function AdminProjectsPage() {
23 const [projects, setProjects] = useState<Project[]>([]);
24 const [loading, setLoading] = useState(true);
25 const [deleting, setDeleting] = useState<string | null>(null);
26
27 async function load() {
28 setLoading(true);
29 const res = await fetch("/api/projects");
30 const data = await res.json().catch(() => ({ projects: [] }));
31 setProjects(data.projects ?? []);
32 setLoading(false);
33 }
34
35 useEffect(() => { load(); }, []);
36
37 async function handleDelete(id: string) {
38 if (!confirm("Delete this project?")) return;
39 setDeleting(id);
40 await fetch(`/api/projects/${id}`, { method: "DELETE" });
41 await load();
42 setDeleting(null);
43 }
2244
23 const hasProjects = Array.isArray(projects) && projects.length > 0;
45 if (loading) {
46 return (
47 <div className="p-6">
48 <div className="text-white/60 text-sm">Loading projects...</div>
49 </div>
50 );
51 }
2452
25 if (!hasProjects) {
53 if (!projects.length) {
2654 return (
2755 <div className="p-6">
28 <div className="mx-auto max-w-3xl rounded-lg border border-gray-200 bg-white p-8 text-center shadow-sm">
29 <div className="mx-auto mb-4 h-12 w-12 rounded-full bg-gray-100 flex items-center justify-center">
30 <svg className="h-6 w-6 text-gray-500" viewBox="0 0 24 24" fill="none" aria-hidden="true">
31 <path d="M7 7h10M7 12h10M7 17h6" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
56 <div className="mx-auto max-w-3xl rounded-3xl border border-white/10 bg-black/30 p-8 text-center">
57 <div className="mx-auto mb-4 h-12 w-12 rounded-full bg-white/5 flex items-center justify-center">
58 <svg className="h-6 w-6 text-white/40" viewBox="0 0 24 24" fill="none" aria-hidden="true">
59 <path d="M12 4v16m-8-8h16" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
3260 </svg>
3361 </div>
34 <h1 className="text-lg font-semibold text-gray-900">No projects yet</h1>
35 <p className="mt-2 text-sm text-gray-600">
36 Get started by creating your first project. You can add details and manage everything from here.
62 <h1 className="text-lg font-semibold text-white">No projects yet</h1>
63 <p className="mt-2 text-sm text-white/60">
64 Create your first project to start building conversion-first sites with AI.
3765 </p>
3866 <div className="mt-6">
3967 <Link
4068 href="/admin/projects/new"
41 className="inline-flex items-center rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"
69 className="inline-flex items-center rounded-2xl bg-gradient-to-r from-purple-500 to-blue-500 px-5 py-3 text-sm font-semibold text-white hover:opacity-90 transition"
4270 >
4371 Create project
4472 </Link>
@@ -48,59 +76,58 @@ export default async function AdminProjectsPage() {
4876 );
4977 }
5078
51 // Existing list/table UI remains unchanged below.
5279 return (
53 <div className="p-6">
54 <div className="mb-6 flex items-center justify-between">
55 <h1 className="text-xl font-semibold text-gray-900">Projects</h1>
80 <div className="space-y-6">
81 <div className="flex items-center justify-between">
82 <div>
83 <div className="text-xs font-semibold tracking-wide text-white/60">PROJECTS</div>
84 <h1 className="mt-2 text-2xl font-semibold tracking-tight">Projects</h1>
85 </div>
5686 <Link
5787 href="/admin/projects/new"
58 className="inline-flex items-center rounded-md bg-black px-4 py-2 text-sm font-medium text-white hover:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2"
88 className="inline-flex items-center rounded-2xl bg-gradient-to-r from-purple-500 to-blue-500 px-5 py-3 text-sm font-semibold text-white hover:opacity-90 transition"
5989 >
6090 New project
6191 </Link>
6292 </div>
6393
64 <div className="overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm">
65 <table className="min-w-full divide-y divide-gray-200">
66 <thead className="bg-gray-50">
67 <tr>
68 <th scope="col" className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
69 Name
70 </th>
71 <th scope="col" className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
72 Description
73 </th>
74 <th scope="col" className="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500">
75 Created
76 </th>
77 <th scope="col" className="px-6 py-3" />
78 </tr>
79 </thead>
80 <tbody className="divide-y divide-gray-200 bg-white">
81 {projects.map((p) => (
82 <tr key={p.id}>
83 <td className="whitespace-nowrap px-6 py-4 text-sm font-medium text-gray-900">{p.name}</td>
84 <td className="px-6 py-4 text-sm text-gray-600">
85 {p.description ?? <span className="text-gray-400">—</span>}
86 </td>
87 <td className="px-6 py-4 text-sm text-gray-600">
88 {p.createdAt
89 ? new Date(p.createdAt).toLocaleDateString()
90 : <span className="text-gray-400">—</span>}
91 </td>
92 <td className="px-6 py-4 text-right">
93 <Link
94 href={`/admin/projects/${p.id}`}
95 className="text-sm font-medium text-black hover:underline"
96 >
97 Manage
98 </Link>
99 </td>
100 </tr>
101 ))}
102 </tbody>
103 </table>
94 <div className="grid gap-4">
95 {projects.map((p) => (
96 <div key={p.id} className="rounded-3xl border border-white/10 bg-black/30 p-6">
97 <div className="flex items-start justify-between gap-4">
98 <div className="flex-1 min-w-0">
99 <div className="flex items-center gap-3">
100 <h2 className="text-base font-semibold truncate">{p.name}</h2>
101 <span className={`inline-flex px-2.5 py-0.5 rounded-full text-[11px] font-bold uppercase tracking-wider ${statusColors[p.status] ?? statusColors.draft}`}>
102 {p.status}
103 </span>
104 </div>
105 {p.description && (
106 <p className="mt-1 text-sm text-white/60 line-clamp-2">{p.description}</p>
107 )}
108 <div className="mt-3 flex items-center gap-4 text-xs text-white/40">
109 {p.domain && <span>{p.domain}</span>}
110 <span>Created {new Date(p.createdAt).toLocaleDateString()}</span>
111 </div>
112 </div>
113 <div className="flex items-center gap-2">
114 <Link
115 href={`/admin/cockpit?projectId=${p.id}`}
116 className="rounded-xl border border-white/10 bg-white/5 px-4 py-2 text-xs font-semibold hover:bg-white/10 transition"
117 >
118 Open in Cockpit
119 </Link>
120 <button
121 onClick={() => handleDelete(p.id)}
122 disabled={deleting === p.id}
123 className="rounded-xl border border-red-500/20 bg-red-500/5 px-4 py-2 text-xs font-semibold text-red-400 hover:bg-red-500/10 transition disabled:opacity-50"
124 >
125 {deleting === p.id ? "..." : "Delete"}
126 </button>
127 </div>
128 </div>
129 </div>
130 ))}
104131 </div>
105132 </div>
106133 );
Modifiedsrc/app/admin/settings/page.tsx+148−6View fileUnifiedSplit
@@ -1,20 +1,162 @@
1"use client";
2
3import React, { useEffect, useState } from "react";
4
5type Settings = {
6 defaultProjectId: string;
7 debugMode: boolean;
8 autoPublish: boolean;
9 agentConcurrency: number;
10 notificationsEnabled: boolean;
11 theme: "dark" | "system";
12};
13
14const DEFAULT_SETTINGS: Settings = {
15 defaultProjectId: "",
16 debugMode: false,
17 autoPublish: false,
18 agentConcurrency: 3,
19 notificationsEnabled: true,
20 theme: "dark",
21};
22
123export default function AdminSettings() {
24 const [settings, setSettings] = useState<Settings>(DEFAULT_SETTINGS);
25 const [saving, setSaving] = useState(false);
26 const [saved, setSaved] = useState(false);
27
28 useEffect(() => {
29 const stored = localStorage.getItem("d8:settings");
30 if (stored) {
31 try {
32 setSettings({ ...DEFAULT_SETTINGS, ...JSON.parse(stored) });
33 } catch {}
34 }
35 }, []);
36
37 function update<K extends keyof Settings>(key: K, value: Settings[K]) {
38 setSettings((prev) => ({ ...prev, [key]: value }));
39 setSaved(false);
40 }
41
42 function handleSave() {
43 setSaving(true);
44 localStorage.setItem("d8:settings", JSON.stringify(settings));
45 setTimeout(() => {
46 setSaving(false);
47 setSaved(true);
48 setTimeout(() => setSaved(false), 2000);
49 }, 300);
50 }
51
52 function handleReset() {
53 if (!confirm("Reset all settings to defaults?")) return;
54 setSettings(DEFAULT_SETTINGS);
55 localStorage.removeItem("d8:settings");
56 setSaved(false);
57 }
58
259 return (
360 <div className="space-y-6">
461 <div>
562 <div className="text-xs font-semibold tracking-wide text-white/60">SETTINGS</div>
663 <h1 className="mt-2 text-2xl font-semibold tracking-tight">Settings</h1>
764 <p className="mt-2 text-sm text-white/60">
8 Keep this simple. Only the controls you actually use.
65 Configure your workspace preferences and feature toggles.
966 </p>
1067 </div>
1168
12 <div className="rounded-3xl border border-white/10 bg-black/30 p-6">
13 <div className="text-sm font-semibold">Workspace</div>
14 <div className="mt-2 text-sm text-white/60">
15 Add items like: default projectId, feature toggles, debug mode, and safe admin tools.
69 <div className="max-w-2xl space-y-4">
70 {/* Workspace */}
71 <div className="rounded-3xl border border-white/10 bg-black/30 p-6 space-y-5">
72 <div className="text-sm font-semibold">Workspace</div>
73
74 <div>
75 <label className="block text-sm text-white/70 mb-1.5">Default Project ID</label>
76 <input
77 type="text"
78 value={settings.defaultProjectId}
79 onChange={(e) => update("defaultProjectId", e.target.value)}
80 placeholder="proj_..."
81 className="w-full rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white placeholder-white/30 focus:border-purple-500/50 focus:outline-none focus:ring-1 focus:ring-purple-500/50"
82 />
83 <p className="mt-1 text-xs text-white/40">Used as the default when creating new runs or videos.</p>
84 </div>
85
86 <div>
87 <label className="block text-sm text-white/70 mb-1.5">Agent Concurrency</label>
88 <input
89 type="number"
90 value={settings.agentConcurrency}
91 onChange={(e) => update("agentConcurrency", Math.max(1, Math.min(10, Number(e.target.value))))}
92 min={1}
93 max={10}
94 className="w-32 rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white focus:border-purple-500/50 focus:outline-none focus:ring-1 focus:ring-purple-500/50"
95 />
96 <p className="mt-1 text-xs text-white/40">Max concurrent agent runs (1-10).</p>
97 </div>
98
99 <div>
100 <label className="block text-sm text-white/70 mb-1.5">Theme</label>
101 <select
102 value={settings.theme}
103 onChange={(e) => update("theme", e.target.value as "dark" | "system")}
104 className="rounded-2xl border border-white/10 bg-white/5 px-4 py-3 text-sm text-white focus:border-purple-500/50 focus:outline-none focus:ring-1 focus:ring-purple-500/50"
105 >
106 <option value="dark">Dark</option>
107 <option value="system">System</option>
108 </select>
109 </div>
110 </div>
111
112 {/* Feature Toggles */}
113 <div className="rounded-3xl border border-white/10 bg-black/30 p-6 space-y-4">
114 <div className="text-sm font-semibold">Feature Toggles</div>
115
116 {([
117 { key: "debugMode" as const, label: "Debug Mode", desc: "Show extra logging and diagnostic info in the admin console." },
118 { key: "autoPublish" as const, label: "Auto-Publish", desc: "Automatically publish marketing content after agent approval." },
119 { key: "notificationsEnabled" as const, label: "Notifications", desc: "Show browser notifications for agent run completions." },
120 ]).map(({ key, label, desc }) => (
121 <label key={key} className="flex items-start gap-3 cursor-pointer group">
122 <div className="relative mt-0.5">
123 <input
124 type="checkbox"
125 checked={settings[key] as boolean}
126 onChange={(e) => update(key, e.target.checked)}
127 className="sr-only peer"
128 />
129 <div className="h-5 w-9 rounded-full bg-white/10 peer-checked:bg-purple-500 transition" />
130 <div className="absolute left-0.5 top-0.5 h-4 w-4 rounded-full bg-white shadow transition peer-checked:translate-x-4" />
131 </div>
132 <div>
133 <div className="text-sm font-semibold text-white/80 group-hover:text-white transition">{label}</div>
134 <div className="text-xs text-white/40">{desc}</div>
135 </div>
136 </label>
137 ))}
138 </div>
139
140 {/* Actions */}
141 <div className="flex gap-3 items-center">
142 <button
143 onClick={handleSave}
144 disabled={saving}
145 className="rounded-2xl bg-gradient-to-r from-purple-500 to-blue-500 px-6 py-3 text-sm font-semibold text-white hover:opacity-90 transition disabled:opacity-50"
146 >
147 {saving ? "Saving..." : "Save Settings"}
148 </button>
149 <button
150 onClick={handleReset}
151 className="rounded-2xl border border-white/10 bg-white/5 px-6 py-3 text-sm font-semibold text-white/70 hover:bg-white/10 transition"
152 >
153 Reset to Defaults
154 </button>
155 {saved && (
156 <span className="text-sm text-green-400 font-semibold">Saved!</span>
157 )}
16158 </div>
17159 </div>
18160 </div>
19161 );
20}
\ No newline at end of file
162}
Modifiedsrc/app/api/__probe__/route.ts+1−1View fileUnifiedSplit
@@ -4,7 +4,7 @@ export const runtime = "nodejs";
44export const dynamic = "force-dynamic";
55
66function safeEnv(name: string): string | null {
7 try: any {
7 try {
88 // @ts-ignore
99 const v = process?.env?.[name];
1010 return typeof v === "string" && v.length ? v : null;
Addedsrc/app/api/billing/checkout/route.ts+36−0View fileUnifiedSplit
@@ -0,0 +1,36 @@
1import { NextRequest, NextResponse } from "next/server";
2import Stripe from "stripe";
3
4function getStripe() {
5 const key = process.env.STRIPE_SECRET_KEY;
6 if (!key) throw new Error("STRIPE_SECRET_KEY is not set");
7 return new Stripe(key, { apiVersion: "2023-10-16" as any });
8}
9
10export async function POST(req: NextRequest) {
11 const body = await req.json().catch(() => null);
12 const priceId = body?.priceId;
13 const returnUrl = body?.returnUrl ?? `${req.nextUrl.origin}/thanks`;
14
15 if (!priceId) {
16 return NextResponse.json({ ok: false, error: "priceId is required" }, { status: 400 });
17 }
18
19 try {
20 const stripe = getStripe();
21
22 const session = await stripe.checkout.sessions.create({
23 mode: "subscription",
24 line_items: [{ price: priceId, quantity: 1 }],
25 success_url: `${returnUrl}?session_id={CHECKOUT_SESSION_ID}`,
26 cancel_url: `${req.nextUrl.origin}/pricing`,
27 });
28
29 return NextResponse.json({ ok: true, url: session.url });
30 } catch (err: any) {
31 return NextResponse.json(
32 { ok: false, error: err?.message ?? "Stripe checkout failed" },
33 { status: 500 }
34 );
35 }
36}
Addedsrc/app/api/billing/portal/route.ts+36−0View fileUnifiedSplit
@@ -0,0 +1,36 @@
1import { NextRequest, NextResponse } from "next/server";
2import Stripe from "stripe";
3
4function getStripe() {
5 const key = process.env.STRIPE_SECRET_KEY;
6 if (!key) throw new Error("STRIPE_SECRET_KEY is not set");
7 return new Stripe(key, { apiVersion: "2023-10-16" as any });
8}
9
10export async function POST(req: NextRequest) {
11 const body = await req.json().catch(() => null);
12 const customerId = body?.customerId;
13
14 if (!customerId) {
15 return NextResponse.json(
16 { ok: false, error: "customerId is required" },
17 { status: 400 }
18 );
19 }
20
21 try {
22 const stripe = getStripe();
23
24 const session = await stripe.billingPortal.sessions.create({
25 customer: customerId,
26 return_url: `${req.nextUrl.origin}/admin/billing`,
27 });
28
29 return NextResponse.json({ ok: true, url: session.url });
30 } catch (err: any) {
31 return NextResponse.json(
32 { ok: false, error: err?.message ?? "Portal session failed" },
33 { status: 500 }
34 );
35 }
36}
Addedsrc/app/api/contact/route.ts+47−0View fileUnifiedSplit
@@ -0,0 +1,47 @@
1import { NextRequest, NextResponse } from "next/server";
2import { kvGetJson, kvSetJson } from "@/lib/d8kv";
3
4const CONTACTS_KEY = "d8:contacts";
5
6type ContactSubmission = {
7 id: string;
8 name: string;
9 email: string;
10 message: string;
11 createdAt: string;
12};
13
14export async function POST(req: NextRequest) {
15 const body = await req.json().catch(() => null);
16
17 if (!body?.name || !body?.email || !body?.message) {
18 return NextResponse.json(
19 { ok: false, error: "Name, email, and message are required." },
20 { status: 400 }
21 );
22 }
23
24 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
25 if (!emailRegex.test(body.email)) {
26 return NextResponse.json(
27 { ok: false, error: "Invalid email address." },
28 { status: 400 }
29 );
30 }
31
32 const submission: ContactSubmission = {
33 id: `contact_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
34 name: body.name.slice(0, 200),
35 email: body.email.slice(0, 200),
36 message: body.message.slice(0, 5000),
37 createdAt: new Date().toISOString(),
38 };
39
40 const existing = (await kvGetJson<ContactSubmission[]>(CONTACTS_KEY)) ?? [];
41 existing.push(submission);
42 // Keep only the last 500 submissions
43 const trimmed = existing.slice(-500);
44 await kvSetJson(CONTACTS_KEY, trimmed);
45
46 return NextResponse.json({ ok: true, id: submission.id });
47}
Addedsrc/app/api/domains/verify/route.ts+117−0View fileUnifiedSplit
@@ -0,0 +1,117 @@
1import { NextRequest, NextResponse } from "next/server";
2import { kvGetJson, kvSetJson } from "@/lib/d8kv";
3
4const DOMAINS_KEY = "d8:domains";
5
6export type DomainRecord = {
7 id: string;
8 domain: string;
9 txtRecord: string;
10 status: "pending_dns" | "dns_verified" | "ssl_provisioning" | "ready" | "error";
11 createdAt: string;
12 updatedAt: string;
13 errorMsg?: string;
14};
15
16async function getDomains(): Promise<DomainRecord[]> {
17 return (await kvGetJson<DomainRecord[]>(DOMAINS_KEY)) ?? [];
18}
19
20export async function GET(req: NextRequest) {
21 const domainParam = req.nextUrl.searchParams.get("domain");
22 const domains = await getDomains();
23
24 if (domainParam) {
25 const record = domains.find((d) => d.domain === domainParam);
26 if (!record) return NextResponse.json({ ok: false, error: "Domain not found" }, { status: 404 });
27 return NextResponse.json({ ok: true, domain: record });
28 }
29
30 return NextResponse.json({ ok: true, domains });
31}
32
33export async function POST(req: NextRequest) {
34 const body = await req.json().catch(() => null);
35 const domain = body?.domain?.trim()?.toLowerCase();
36
37 if (!domain) {
38 return NextResponse.json({ ok: false, error: "domain is required" }, { status: 400 });
39 }
40
41 const domainRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*\.[a-z]{2,}$/;
42 if (!domainRegex.test(domain)) {
43 return NextResponse.json({ ok: false, error: "Invalid domain format" }, { status: 400 });
44 }
45
46 const domains = await getDomains();
47 const existing = domains.find((d) => d.domain === domain);
48 if (existing) {
49 return NextResponse.json({ ok: true, domain: existing, existing: true });
50 }
51
52 const now = new Date().toISOString();
53 const txtValue = `d8-verify=${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
54
55 const record: DomainRecord = {
56 id: `dom_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
57 domain,
58 txtRecord: txtValue,
59 status: "pending_dns",
60 createdAt: now,
61 updatedAt: now,
62 };
63
64 domains.push(record);
65 await kvSetJson(DOMAINS_KEY, domains);
66
67 return NextResponse.json({
68 ok: true,
69 domain: record,
70 instructions: {
71 type: "TXT",
72 name: `_dominat8.${domain}`,
73 value: txtValue,
74 cname: { name: domain, value: "cname.vercel-dns.com" },
75 },
76 }, { status: 201 });
77}
78
79export async function PATCH(req: NextRequest) {
80 const body = await req.json().catch(() => null);
81 const domain = body?.domain?.trim()?.toLowerCase();
82 const action = body?.action; // "check_dns" | "provision_ssl" | "mark_ready"
83
84 if (!domain || !action) {
85 return NextResponse.json({ ok: false, error: "domain and action required" }, { status: 400 });
86 }
87
88 const domains = await getDomains();
89 const idx = domains.findIndex((d) => d.domain === domain);
90 if (idx === -1) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
91
92 const record = domains[idx];
93 const now = new Date().toISOString();
94
95 if (action === "check_dns") {
96 // In production, this would do a real DNS lookup for the TXT record
97 // For now, simulate DNS verification after domain is added
98 record.status = "dns_verified";
99 record.updatedAt = now;
100 } else if (action === "provision_ssl") {
101 if (record.status !== "dns_verified") {
102 return NextResponse.json({ ok: false, error: "DNS must be verified first" }, { status: 400 });
103 }
104 record.status = "ssl_provisioning";
105 record.updatedAt = now;
106 // Simulate SSL provisioning completing quickly
107 record.status = "ready";
108 } else if (action === "mark_ready") {
109 record.status = "ready";
110 record.updatedAt = now;
111 }
112
113 domains[idx] = record;
114 await kvSetJson(DOMAINS_KEY, domains);
115
116 return NextResponse.json({ ok: true, domain: record });
117}
Addedsrc/app/api/projects/[id]/route.ts+55−0View fileUnifiedSplit
@@ -0,0 +1,55 @@
1import { NextRequest, NextResponse } from "next/server";
2import { kvGetJson, kvSetJson } from "@/lib/d8kv";
3
4const PROJECTS_KEY = "d8:projects";
5
6type ProjectRecord = {
7 id: string;
8 name: string;
9 description: string;
10 domain: string;
11 status: "draft" | "building" | "live";
12 createdAt: string;
13 updatedAt: string;
14};
15
16async function getProjects(): Promise<ProjectRecord[]> {
17 return (await kvGetJson<ProjectRecord[]>(PROJECTS_KEY)) ?? [];
18}
19
20export async function GET(_req: NextRequest, { params }: { params: { id: string } }) {
21 const projects = await getProjects();
22 const project = projects.find((p) => p.id === params.id);
23 if (!project) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
24 return NextResponse.json({ ok: true, project });
25}
26
27export async function PATCH(req: NextRequest, { params }: { params: { id: string } }) {
28 const body = await req.json().catch(() => null);
29 if (!body) return NextResponse.json({ ok: false, error: "Invalid body" }, { status: 400 });
30
31 const projects = await getProjects();
32 const idx = projects.findIndex((p) => p.id === params.id);
33 if (idx === -1) return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
34
35 const allowed = ["name", "description", "domain", "status"] as const;
36 for (const key of allowed) {
37 if (body[key] !== undefined) {
38 (projects[idx] as any)[key] = body[key];
39 }
40 }
41 projects[idx].updatedAt = new Date().toISOString();
42
43 await kvSetJson(PROJECTS_KEY, projects);
44 return NextResponse.json({ ok: true, project: projects[idx] });
45}
46
47export async function DELETE(_req: NextRequest, { params }: { params: { id: string } }) {
48 const projects = await getProjects();
49 const filtered = projects.filter((p) => p.id !== params.id);
50 if (filtered.length === projects.length) {
51 return NextResponse.json({ ok: false, error: "Not found" }, { status: 404 });
52 }
53 await kvSetJson(PROJECTS_KEY, filtered);
54 return NextResponse.json({ ok: true });
55}
Addedsrc/app/api/projects/route.ts+47−0View fileUnifiedSplit
@@ -0,0 +1,47 @@
1import { NextRequest, NextResponse } from "next/server";
2import { kvGetJson, kvSetJson } from "@/lib/d8kv";
3
4const PROJECTS_KEY = "d8:projects";
5
6export type ProjectRecord = {
7 id: string;
8 name: string;
9 description: string;
10 domain: string;
11 status: "draft" | "building" | "live";
12 createdAt: string;
13 updatedAt: string;
14};
15
16async function getProjects(): Promise<ProjectRecord[]> {
17 return (await kvGetJson<ProjectRecord[]>(PROJECTS_KEY)) ?? [];
18}
19
20export async function GET() {
21 const projects = await getProjects();
22 return NextResponse.json({ ok: true, projects });
23}
24
25export async function POST(req: NextRequest) {
26 const body = await req.json().catch(() => null);
27 if (!body?.name) {
28 return NextResponse.json({ ok: false, error: "Name is required" }, { status: 400 });
29 }
30
31 const projects = await getProjects();
32 const now = new Date().toISOString();
33 const project: ProjectRecord = {
34 id: `proj_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
35 name: body.name,
36 description: body.description ?? "",
37 domain: body.domain ?? "",
38 status: "draft",
39 createdAt: now,
40 updatedAt: now,
41 };
42
43 projects.push(project);
44 await kvSetJson(PROJECTS_KEY, projects);
45
46 return NextResponse.json({ ok: true, project }, { status: 201 });
47}
Addedsrc/app/api/video/publish/route.ts+55−0View fileUnifiedSplit
@@ -0,0 +1,55 @@
1import { NextRequest, NextResponse } from "next/server";
2import { kvSetJson } from "@/lib/d8kv";
3
4export async function POST(req: NextRequest) {
5 const formData = await req.formData().catch(() => null);
6
7 if (!formData) {
8 return NextResponse.json({ ok: false, error: "Invalid form data" }, { status: 400 });
9 }
10
11 const projectId = formData.get("projectId") as string;
12 const file = formData.get("file") as File | null;
13
14 if (!projectId) {
15 return NextResponse.json({ ok: false, error: "projectId is required" }, { status: 400 });
16 }
17 if (!file) {
18 return NextResponse.json({ ok: false, error: "file is required" }, { status: 400 });
19 }
20
21 try {
22 // Try Vercel Blob if available
23 const blobToken = process.env.BLOB_READ_WRITE_TOKEN;
24
25 let videoUrl: string;
26
27 if (blobToken) {
28 const { put } = await import("@vercel/blob");
29 const blob = await put(`videos/${projectId}/${file.name}`, file, {
30 access: "public",
31 token: blobToken,
32 });
33 videoUrl = blob.url;
34 } else {
35 // Fallback: store as a data URL reference in KV
36 // In production, you'd want Blob storage
37 videoUrl = `/projects/${encodeURIComponent(projectId)}/video?stored=kv&ts=${Date.now()}`;
38 }
39
40 // Store the video URL in KV
41 await kvSetJson(`d8:project:${projectId}:video:latest`, {
42 url: videoUrl,
43 publishedAt: new Date().toISOString(),
44 fileName: file.name,
45 size: file.size,
46 });
47
48 return NextResponse.json({ ok: true, url: videoUrl });
49 } catch (err: any) {
50 return NextResponse.json(
51 { ok: false, error: err?.message ?? "Publish failed" },
52 { status: 500 }
53 );
54 }
55}
Addedsrc/app/api/video/storyboard/route.ts+82−0View fileUnifiedSplit
@@ -0,0 +1,82 @@
1import { NextRequest, NextResponse } from "next/server";
2import type { D8Storyboard, D8VideoShot } from "@/lib/video/d8VideoTypes";
3
4function fallbackStoryboard(topic: string, seconds: number): D8Storyboard {
5 const shotCount = Math.max(3, Math.min(8, Math.round(seconds / 7)));
6 const perShot = Math.round(seconds / shotCount);
7
8 const templates: { title: string; lines: string[] }[] = [
9 { title: "The Problem", lines: ["Most sites look the same", "Low conversions, high bounce"] },
10 { title: "Meet Dominat8", lines: ["AI-powered site builder", "Conversion-first design"] },
11 { title: "How It Works", lines: ["Pick a template", "AI generates your content", "Publish in minutes"] },
12 { title: "Key Features", lines: ["Locked visual layer", "Inline-safe styling", "Works without Tailwind"] },
13 { title: "AI Agents", lines: ["Creative Director", "Copy Chief", "SEO Optimizer"] },
14 { title: "Real Results", lines: ["2x conversion rates", "90+ Lighthouse scores", "Zero downtime deploys"] },
15 { title: "Custom Domains", lines: ["One-click SSL", "DNS verification", "Global CDN"] },
16 { title: "Get Started", lines: [`Build with ${topic}`, "Start free today", "dominat8.com"] },
17 ];
18
19 const shots: D8VideoShot[] = templates.slice(0, shotCount).map((t, i) => ({
20 id: `shot_${i}`,
21 title: t.title,
22 durationSec: perShot,
23 lines: t.lines,
24 }));
25
26 return {
27 brand: "Dominat8",
28 title: topic,
29 shots,
30 voiceover: shots.map((s) => `${s.title}: ${s.lines.join(". ")}.`),
31 createdAtIso: new Date().toISOString(),
32 mode: "fallback",
33 };
34}
35
36export async function POST(req: NextRequest) {
37 const body = await req.json().catch(() => null);
38 const topic = body?.topic ?? "How Dominat8 Works";
39 const seconds = Math.max(15, Math.min(90, Number(body?.seconds) || 35));
40
41 // Try OpenAI if available, otherwise use fallback
42 const openaiKey = process.env.OPENAI_API_KEY;
43
44 if (openaiKey) {
45 try {
46 const { default: OpenAI } = await import("openai");
47 const client = new OpenAI({ apiKey: openaiKey });
48
49 const completion = await client.chat.completions.create({
50 model: "gpt-4o-mini",
51 messages: [
52 {
53 role: "system",
54 content: `You generate video storyboards for the Dominat8 AI website builder. Return valid JSON matching this schema: { brand: string, title: string, shots: [{ id: string, title: string, durationSec: number, lines: string[] }], voiceover: string[] }. The total duration of all shots should be approximately ${seconds} seconds. Each shot should have 2-4 lines. Create ${Math.round(seconds / 7)} shots.`,
55 },
56 { role: "user", content: `Create a storyboard about: ${topic}` },
57 ],
58 response_format: { type: "json_object" },
59 max_tokens: 1500,
60 });
61
62 const parsed = JSON.parse(completion.choices[0]?.message?.content ?? "{}");
63
64 if (parsed.shots?.length) {
65 const storyboard: D8Storyboard = {
66 brand: parsed.brand ?? "Dominat8",
67 title: parsed.title ?? topic,
68 shots: parsed.shots,
69 voiceover: parsed.voiceover ?? [],
70 createdAtIso: new Date().toISOString(),
71 mode: "ai",
72 };
73 return NextResponse.json({ ok: true, storyboard });
74 }
75 } catch {
76 // Fall through to fallback
77 }
78 }
79
80 const storyboard = fallbackStoryboard(topic, seconds);
81 return NextResponse.json({ ok: true, storyboard });
82}
Modifiedsrc/app/page.tsx+1−6View fileUnifiedSplit
@@ -1,8 +1,3 @@
1// === D8_AUTOREPAIR_DEMO_START ===
2// Intentionally missing import to demonstrate Auto Repair fixing a build.
3// This produces a deterministic "Module not found" error with zero UI impact.
4import AutoRepairDemoWidget from "./_client/AutoRepairDemoWidget";
5// === D8_AUTOREPAIR_DEMO_END ===
61export const dynamic = "force-dynamic";
72export const revalidate = 0;
83
@@ -182,7 +177,7 @@ export default function HomePage() {
182177
183178 <div style={{ display: "flex", gap: 12, flexWrap: "wrap", marginTop: 18, alignItems: "center" }}>
184179 <a
185 href="/builder"
180 href="/templates"
186181 style={{
187182 display: "inline-flex",
188183 alignItems: "center",
Modifiedsrc/app/thanks/page.tsx+105−26View fileUnifiedSplit
@@ -1,39 +1,118 @@
1"use client";
2
13import Link from "next/link";
4import { useSearchParams } from "next/navigation";
5import { Suspense, useEffect, useState } from "react";
6
7function ThanksContent() {
8 const searchParams = useSearchParams();
9 const sessionId = searchParams?.get("session_id") ?? null;
10 const [checkoutStatus, setCheckoutStatus] = useState<"loading" | "success" | "no_session">(
11 sessionId ? "loading" : "no_session"
12 );
213
3const MARKER = "THANKS_20260126_BUNDLE2";
14 useEffect(() => {
15 if (!sessionId) return;
16 // In production, you'd verify the session with Stripe here
17 // For now, having a session_id means checkout completed
18 setCheckoutStatus("success");
19 }, [sessionId]);
420
5export default function ThanksPage() {
621 return (
722 <div className="min-h-screen bg-black text-white">
823 <div className="mx-auto max-w-3xl px-4 py-14">
9 <div className="text-xs text-white/40">{MARKER}</div>
10
11 <h1 className="mt-6 text-4xl font-semibold tracking-[-0.04em]">
12 You’re on the path.
13 </h1>
14 <p className="mt-4 text-white/70 leading-relaxed">
15 This page completes the conversion flow visually. Next bundle wires real checkout + plan gating.
16 </p>
17
18 <div className="mt-8 rounded-2xl border border-white/10 bg-white/5 p-6">
19 <div className="text-xs uppercase tracking-[0.22em] text-white/60">Next upgrades</div>
20 <div className="mt-3 grid gap-2 text-white/75">
21 <div>1) Stripe checkout</div>
22 <div>2) Pro gating (publish/custom domain)</div>
23 <div>3) Post-checkout success handling</div>
24 <div>4) Conversion analytics</div>
25 </div>
26 </div>
24 {checkoutStatus === "success" ? (
25 <>
26 <div className="flex items-center gap-3">
27 <div className="flex h-12 w-12 items-center justify-center rounded-full bg-green-500/20">
28 <svg className="h-6 w-6 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
29 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
30 </svg>
31 </div>
32 <div>
33 <div className="text-xs uppercase tracking-[0.22em] text-green-400/80">Payment Confirmed</div>
34 </div>
35 </div>
36
37 <h1 className="mt-6 text-4xl font-semibold tracking-[-0.04em]">
38 Welcome to Pro!
39 </h1>
40 <p className="mt-4 text-white/70 leading-relaxed">
41 Your subscription is now active. You have access to all Pro features including unlimited projects,
42 custom domains, AI video studio, and priority support.
43 </p>
44
45 <div className="mt-8 rounded-2xl border border-white/10 bg-white/5 p-6">
46 <div className="text-xs uppercase tracking-[0.22em] text-white/60">What's unlocked</div>
47 <div className="mt-4 grid gap-3 sm:grid-cols-2">
48 {[
49 "Unlimited projects",
50 "Custom domains + SSL",
51 "AI Video Studio",
52 "Premium templates",
53 "Advanced analytics",
54 "Priority support",
55 ].map((feature) => (
56 <div key={feature} className="flex items-center gap-2 text-sm text-white/80">
57 <svg className="h-4 w-4 text-purple-400 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
58 <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
59 </svg>
60 {feature}
61 </div>
62 ))}
63 </div>
64 </div>
65 </>
66 ) : (
67 <>
68 <h1 className="mt-6 text-4xl font-semibold tracking-[-0.04em]">
69 Thanks for your interest!
70 </h1>
71 <p className="mt-4 text-white/70 leading-relaxed">
72 Ready to unlock the full power of Dominat8? Choose a plan that fits your needs.
73 </p>
74 </>
75 )}
2776
28 <div className="mt-8 flex gap-3">
29 <Link href="/" className="rounded-xl border border-white/15 bg-white/5 px-5 py-3 text-sm font-semibold hover:bg-white/10 transition">
30 Home
77 <div className="mt-8 grid gap-3 sm:grid-cols-3">
78 <Link
79 href="/admin"
80 className="flex items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-purple-500 to-blue-500 px-5 py-3 text-sm font-semibold hover:opacity-95 transition"
81 >
82 Go to Dashboard
3183 </Link>
32 <Link href="/pricing" className="rounded-xl bg-white text-black px-5 py-3 text-sm font-semibold hover:opacity-95 transition">
33 Back to pricing
84 <Link
85 href="/admin/projects"
86 className="flex items-center justify-center gap-2 rounded-xl border border-white/15 bg-white/5 px-5 py-3 text-sm font-semibold hover:bg-white/10 transition"
87 >
88 My Projects
89 </Link>
90 <Link
91 href="/pricing"
92 className="flex items-center justify-center gap-2 rounded-xl border border-white/15 bg-white/5 px-5 py-3 text-sm font-semibold hover:bg-white/10 transition"
93 >
94 {checkoutStatus === "success" ? "View Plans" : "Choose a Plan"}
3495 </Link>
3596 </div>
97
98 {checkoutStatus === "success" && (
99 <div className="mt-6 text-center text-xs text-white/40">
100 Session: {sessionId?.slice(0, 20)}...
101 <span className="mx-2">·</span>
102 <Link href="/admin/billing" className="text-purple-400/80 hover:text-purple-400 transition">
103 Manage billing
104 </Link>
105 </div>
106 )}
36107 </div>
37108 </div>
38109 );
39}
\ No newline at end of file
110}
111
112export default function ThanksPage() {
113 return (
114 <Suspense fallback={<div className="min-h-screen bg-black" />}>
115 <ThanksContent />
116 </Suspense>
117 );
118}
Modifiedsrc/app/video/_client/VideoStudioClient.tsx+3−2View fileUnifiedSplit
@@ -151,8 +151,9 @@ export default function VideoStudioClient() {
151151 setErr(null);
152152 if (!canvasRef.current) return;
153153 const canvas = canvasRef.current;
154 const ctx = canvas.getContext("2d");
155 if (!ctx) return;
154 const ctxOrNull = canvas.getContext("2d");
155 if (!ctxOrNull) return;
156 const ctx: CanvasRenderingContext2D = ctxOrNull;
156157
157158 stopAnimation();
158159
Modifiedsrc/lib/d8kv.ts+3−0View fileUnifiedSplit
@@ -50,4 +50,7 @@ export function d8Key_bundleRun(projectId: string, runId: string) {
5050}
5151export function d8Key_patchLatest(projectId: string) {
5252 return `agentPatch:project:${projectId}:latest`;
53}
54export function projectVideoKey(projectId: string) {
55 return `d8:project:${projectId}:video:latest`;
5356}
\ No newline at end of file
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts