diff --git a/apps/ops/package.json b/apps/ops/package.json new file mode 100644 index 00000000..6440d3ca --- /dev/null +++ b/apps/ops/package.json @@ -0,0 +1,25 @@ +{ + "name": "@codingcatdev/ops", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "sanity dev", + "build": "sanity build", + "deploy": "sanity deploy" + }, + "dependencies": { + "@sanity/sdk": "^1.0.0", + "@sanity/sdk-react": "^1.0.0", + "@sanity/ui": "^3.1.10", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "sanity": "^6.9.2", + "styled-components": "^6.3.11" + }, + "devDependencies": { + "@types/react": "^19.2.0", + "@types/react-dom": "^19.2.0", + "typescript": "^5.9.3" + } +} diff --git a/apps/ops/sanity.cli.ts b/apps/ops/sanity.cli.ts new file mode 100644 index 00000000..8cee1dae --- /dev/null +++ b/apps/ops/sanity.cli.ts @@ -0,0 +1,8 @@ +import { defineCliConfig } from "sanity/cli"; + +export default defineCliConfig({ + app: { + organizationId: process.env.SANITY_ORGANIZATION_ID || "o1H0wB7xG", + entry: "./src/index.tsx", + }, +}); diff --git a/apps/ops/src/App.tsx b/apps/ops/src/App.tsx new file mode 100644 index 00000000..14956d2a --- /dev/null +++ b/apps/ops/src/App.tsx @@ -0,0 +1,314 @@ +import { useDocuments } from "@sanity/sdk-react"; +import { useState } from "react"; + +type WorkspaceTab = "crm" | "bookings" | "billing"; + +interface SeededGmailDraft { + companyName: string; + contact: string; + threadId: string; + draftId: string; + tier: string; + totalUsd: number; + depositUsd: number; + status: string; +} + +const ACTIVE_GMAIL_DEALS: SeededGmailDraft[] = [ + { + companyName: "StreamLapel", + contact: "hello@streamlapel.com", + threadId: "1a140656cb9a4be1", + draftId: "r8880837980856049603", + tier: "Integrated 60s Mid-Roll Ad (Oct 26 - Nov 6)", + totalUsd: 1800, + depositUsd: 900, + status: "Reply Draft Ready in Gmail", + }, + { + companyName: "Tessen ZERO", + contact: "Yannick Parfait ", + threadId: "1a126242f701063b", + draftId: "r4979210524997526110", + tier: "Full-Stack Launch Bundle", + totalUsd: 3500, + depositUsd: 1750, + status: "Reply Draft Ready in Gmail", + }, + { + companyName: "Influencer Laboratory", + contact: "john@influencerlaboratoryinc.com", + threadId: "1a15468638753504", + draftId: "r3892744900021531970", + tier: "Full-Stack Launch Bundle (21.1% US/CA/UK/AU Geo Sent)", + totalUsd: 3500, + depositUsd: 1750, + status: "Reply Draft Ready in Gmail", + }, + { + companyName: "Typeless", + contact: "qia@typeless.com", + threadId: "1a13b726a4593e01", + draftId: "r5723240746882402069", + tier: "Oct 26 Launch + 7-Platform Shorts Pack", + totalUsd: 2000, + depositUsd: 1000, + status: "Reply Draft Ready in Gmail", + }, + { + companyName: "SuperPlane", + contact: "larisa@superplane.com", + threadId: "1a13b7352b2a713e", + draftId: "r1343316162270961096", + tier: "Full-Stack Launch Bundle (Oct 26-30 GA)", + totalUsd: 3500, + depositUsd: 1750, + status: "Reply Draft Ready in Gmail", + }, + { + companyName: "MiniMax (EezyCollab)", + contact: "talent@eezycollab.xyz", + threadId: "1a0d8c55420283dc", + draftId: "r-7977440252907745009", + tier: "Full-Stack Launch Bundle / Dedicated Video", + totalUsd: 3500, + depositUsd: 1750, + status: "Reply Draft Ready in Gmail", + }, + { + companyName: "Kolect (AI 3D Platform)", + contact: "sophia.nguyen@kolect.ai", + threadId: "1a138ebfcd7f64c0", + draftId: "r-3575370967009126686", + tier: "Full-Stack Launch Bundle", + totalUsd: 3500, + depositUsd: 1750, + status: "Reply Draft Ready in Gmail", + }, + { + companyName: "Kolect (Flova)", + contact: "sophia.nguyen@kolect.ai", + threadId: "1a15878e1da6781e", + draftId: "r7006830561667617324", + tier: "Organic YouTube Feature (Ad Rights Excluded)", + totalUsd: 950, + depositUsd: 475, + status: "Reply Draft Ready in Gmail", + }, +]; + +export function App() { + const [activeTab, setActiveTab] = useState("crm"); + const [discovering, setDiscovering] = useState(false); + const [discoveredProspects, setDiscoveredProspects] = useState>([]); + + const sponsorLeads = useDocuments({ documentType: "sponsorLead" }); + const sponsorPool = useDocuments({ documentType: "sponsorPool" }); + const bookings = useDocuments({ documentType: "booking" }); + + const handleDiscoverOutbound = async () => { + setDiscovering(true); + try { + const res = await fetch("https://codingcat.dev/api/sponsors/outbound"); + if (res.ok) { + const data = await res.json(); + setDiscoveredProspects(data.prospects || []); + } + } catch { + // Fallback + } finally { + setDiscovering(false); + } + }; + + return ( +
+
+
+

CodingCat.dev Revenue & Booking OS

+

+ Unified Sanity App SDK Workspace · Project hfh83o0w (Production) +

+
+
+ + + +
+
+ + {activeTab === "crm" && ( +
+
+
+
+

Active Inbound Sponsor Threads (Gmail + Sanity)

+

+ Threaded Gmail reply drafts are staged in alex.patterson@gmail.com with verified 21.1% US/CA/UK/AU geography metrics. +

+
+
+ + + + + + + + + + + + {ACTIVE_GMAIL_DEALS.map((deal) => ( + + + + + + + + ))} + +
CompanyContactRecommended PackageTotal / 50% DepositStatus
{deal.companyName}{deal.contact}{deal.tier} + ${deal.totalUsd.toLocaleString()} (${deal.depositUsd.toLocaleString()} deposit) + {deal.status}
+
+ +
+
+
+

+ Outbound Funded AI & YC Startup Scraper ({sponsorPool.data?.length || discoveredProspects.length}) +

+

+ Automatically generates 2 tailored CodingCat.dev video concepts per startup + Cloudflare Email Routing outreach. +

+
+ +
+ + {discoveredProspects.length > 0 && ( +
+ {discoveredProspects.map((p) => ( +
+
+ + {p.companyName} ({p.fundingRound}) — {p.category} + + Score: {p.relevanceScore}/100 +
+
    + {p.videoConcepts?.map((vc: any, i: number) => ( +
  • + {vc.title} — {vc.angle} +
  • + ))} +
+
+ ))} +
+ )} +
+
+ )} + + {activeTab === "bookings" && ( +
+

Two-Track Podcast & Sponsor Calendar Bookings

+

+ All recording sessions enforce 8:00 AM - 5:00 PM PT/ET business hours and E-SIGN digital media release capture at codingcat.dev/book. +

+ +
+ )} + + {activeTab === "billing" && ( +
+

50% Upfront Deposit + 50% on Publish Milestone Billing

+

+ Every Sponsor Deal Room at codingcat.dev/portal/[token] supports both instant Stripe Checkout (Card / ACH) and Net-15 Corporate Hosted Invoices. +

+
    +
  • Full-Stack Launch Bundle ($3,500): $1,750 upfront deposit + $1,750 on publish
  • +
  • Sponsored Podcast & Multi-Channel Showcase ($2,000): $1,000 upfront deposit + $1,000 on publish
  • +
  • Ecosystem Integration ($950): $475 upfront deposit + $475 on publish
  • +
+
+ )} +
+ ); +} diff --git a/apps/ops/src/index.tsx b/apps/ops/src/index.tsx new file mode 100644 index 00000000..9ba5a3dc --- /dev/null +++ b/apps/ops/src/index.tsx @@ -0,0 +1,26 @@ +import { type SanityConfig } from "@sanity/sdk"; +import { SanityApp } from "@sanity/sdk-react"; +import { StudioTheme, ThemeProvider } from "@sanity/ui"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; + +const config: SanityConfig[] = [ + { + projectId: "hfh83o0w", + dataset: "production", + }, +]; + +const rootEl = document.getElementById("root"); +if (rootEl) { + createRoot(rootEl).render( + + + Loading CodingCat.dev Ops...}> + + + + , + ); +} diff --git a/apps/ops/tsconfig.json b/apps/ops/tsconfig.json new file mode 100644 index 00000000..f7e905c9 --- /dev/null +++ b/apps/ops/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src", "sanity.cli.ts"] +} diff --git a/apps/sanity/extract.json b/apps/sanity/extract.json index dda81e6c..30f6f527 100644 --- a/apps/sanity/extract.json +++ b/apps/sanity/extract.json @@ -654,6 +654,897 @@ } } }, + { + "type": "type", + "name": "sponsorLead.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "sponsorLead" + } + }, + { + "name": "booking", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "booking" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "guestName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "guestEmail": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "company": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "socialUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "track": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "editorial" + }, + { + "type": "string", + "value": "sponsored" + }, + { + "type": "string", + "value": "full-stack-launch" + } + ] + }, + "optional": true + }, + "status": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "pending" + }, + { + "type": "string", + "value": "approved" + }, + { + "type": "string", + "value": "scheduled" + }, + { + "type": "string", + "value": "recorded" + }, + { + "type": "string", + "value": "published" + }, + { + "type": "string", + "value": "cancelled" + } + ] + }, + "optional": true + }, + "slotStartAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "topics": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "appointmentScheduleUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "googleCalendarEventId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "signatureMode": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "draw" + }, + { + "type": "string", + "value": "type" + } + ] + }, + "optional": true + }, + "signatureData": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "signedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "consentRecording": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "consentCommercial": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "consentLikeness": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "portalToken": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "sponsorLeadRef": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sponsorLead.reference" + }, + "optional": true + } + } + }, + { + "name": "sponsorPool", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sponsorPool" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "companyName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "contactName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "contactEmail": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "website": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "category": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "fundingRound": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "source": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "yc" + }, + { + "type": "string", + "value": "hackernews" + }, + { + "type": "string", + "value": "linkedin" + }, + { + "type": "string", + "value": "curated" + }, + { + "type": "string", + "value": "enriched" + } + ] + }, + "optional": true + }, + "relevanceScore": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "videoConcepts": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "hook": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "angle": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "targetPackage": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "outreachDraftSubject": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "outreachDraftBody": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "lastContactedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "optedOut": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + }, + "notes": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, + { + "type": "type", + "name": "sponsor.reference", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "sponsor" + } + }, + { + "name": "sponsorLead", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sponsorLead" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "companyName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "contactName": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "contactEmail": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "website": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "portalToken": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "source": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "inbound" + }, + { + "type": "string", + "value": "gmail" + }, + { + "type": "string", + "value": "outbound" + }, + { + "type": "string", + "value": "booking" + } + ] + }, + "optional": true + }, + "status": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "new" + }, + { + "type": "string", + "value": "contacted" + }, + { + "type": "string", + "value": "replied" + }, + { + "type": "string", + "value": "negotiating" + }, + { + "type": "string", + "value": "deposit_sent" + }, + { + "type": "string", + "value": "deposit_paid" + }, + { + "type": "string", + "value": "booked" + }, + { + "type": "string", + "value": "balance_sent" + }, + { + "type": "string", + "value": "paid" + } + ] + }, + "optional": true + }, + "rateCard": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "full-stack-launch" + }, + { + "type": "string", + "value": "podcast-showcase" + }, + { + "type": "string", + "value": "ecosystem-integration" + }, + { + "type": "string", + "value": "dedicated-video" + }, + { + "type": "string", + "value": "mid-roll-ad" + }, + { + "type": "string", + "value": "custom-series" + } + ] + }, + "optional": true + }, + "totalAmountCents": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "depositAmountCents": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "balanceAmountCents": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "depositPaidAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "balancePaidAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "stripeDepositCheckoutUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "stripeDepositInvoiceId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "stripeDepositInvoiceUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "stripeBalanceCheckoutUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "stripeBalanceInvoiceId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "stripeBalanceInvoiceUrl": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "stripeInvoiceId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "intent": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "deliverables": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "channel": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "youtube" + }, + { + "type": "string", + "value": "youtube-short" + }, + { + "type": "string", + "value": "blog" + }, + { + "type": "string", + "value": "syndication" + }, + { + "type": "string", + "value": "x" + }, + { + "type": "string", + "value": "linkedin" + }, + { + "type": "string", + "value": "shorts-network" + } + ] + }, + "optional": true + }, + "status": { + "type": "objectAttribute", + "value": { + "type": "union", + "of": [ + { + "type": "string", + "value": "pending" + }, + { + "type": "string", + "value": "in-progress" + }, + { + "type": "string", + "value": "scheduled" + }, + { + "type": "string", + "value": "published" + } + ] + }, + "optional": true + }, + "url": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "views": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "publishedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "bookedPodcast": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "podcast.reference" + }, + "optional": true + }, + "sponsorDocId": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "sponsor.reference" + }, + "optional": true + }, + "threadId": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "lastEmailAt": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "notes": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + } + } + }, { "type": "type", "name": "page.reference", @@ -2510,36 +3401,6 @@ "dereferencesTo": "author" } }, - { - "type": "type", - "name": "sponsor.reference", - "value": { - "type": "object", - "attributes": { - "_ref": { - "type": "objectAttribute", - "value": { - "type": "string" - } - }, - "_type": { - "type": "objectAttribute", - "value": { - "type": "string", - "value": "reference" - } - }, - "_weak": { - "type": "objectAttribute", - "value": { - "type": "boolean" - }, - "optional": true - } - }, - "dereferencesTo": "sponsor" - } - }, { "type": "type", "name": "syndication.reference", @@ -8247,6 +9108,129 @@ } } }, + { + "name": "sponsorConfig", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "sponsorConfig" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "cooldownDays": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "depositPercentage": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "rateCardTiers": { + "type": "objectAttribute", + "value": { + "type": "array", + "of": { + "type": "object", + "attributes": { + "id": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "name": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "price": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "depositPrice": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + }, + "rest": { + "type": "object", + "attributes": { + "_key": { + "type": "objectAttribute", + "value": { + "type": "string" + } + } + } + } + } + }, + "optional": true + }, + "outreachEmailTemplate": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "maxOutreachPerRun": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + } + } + }, { "name": "settings", "type": "document", diff --git a/apps/sanity/sanity.config.ts b/apps/sanity/sanity.config.ts index bfe3489b..ff3fa2be 100644 --- a/apps/sanity/sanity.config.ts +++ b/apps/sanity/sanity.config.ts @@ -31,7 +31,11 @@ import podcast from "./schemas/documents/podcast"; import podcastType from "./schemas/documents/podcastType"; import post from "./schemas/documents/post"; import settings from "./schemas/singletons/settings"; +import sponsorConfig from "./schemas/singletons/sponsorConfig"; import sponsor from "./schemas/documents/sponsor"; +import sponsorLead from "./schemas/documents/sponsorLead"; +import sponsorPool from "./schemas/documents/sponsorPool"; +import booking from "./schemas/documents/booking"; import tableSchema, { rowType } from "./schemas/custom/table"; import syndication from "./schemas/documents/syndication"; import transcript from "./schemas/documents/transcript"; @@ -76,6 +80,7 @@ const schemaTypes = [ rowType, // Singletons settings, + sponsorConfig, // Documents author, guest, @@ -84,6 +89,9 @@ const schemaTypes = [ podcastType, post, sponsor, + sponsorLead, + sponsorPool, + booking, syndication, transcript, ]; @@ -158,7 +166,7 @@ function buildPlugins(previewUrl: string): PluginOptions[] { ] : []), structureTool({ structure: editorialStructure }), - singletonPlugin([settings.name]), + singletonPlugin([settings.name, sponsorConfig.name]), assistWithPresets(), media(), codeInput(), diff --git a/apps/sanity/schemas/documents/booking.ts b/apps/sanity/schemas/documents/booking.ts new file mode 100644 index 00000000..06b525bb --- /dev/null +++ b/apps/sanity/schemas/documents/booking.ts @@ -0,0 +1,148 @@ +import {defineField, defineType} from 'sanity' +import {HiOutlineCalendar} from 'react-icons/hi' + +export default defineType({ + name: 'booking', + title: 'Podcast & Sponsor Booking', + type: 'document', + icon: HiOutlineCalendar, + fields: [ + defineField({ + name: 'guestName', + title: 'Guest Full Legal Name', + type: 'string', + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'guestEmail', + title: 'Guest Email', + type: 'string', + validation: (Rule) => Rule.required().email(), + }), + defineField({ + name: 'company', + title: 'Company / Flagship Project', + type: 'string', + }), + defineField({ + name: 'socialUrl', + title: 'Social Handle or Portfolio URL', + type: 'url', + }), + defineField({ + name: 'track', + title: 'Booking Track', + type: 'string', + options: { + list: [ + {title: 'Editorial Guest Application (Free / Review Queue)', value: 'editorial'}, + {title: 'Sponsored Podcast & Multi-Channel Showcase ($2,000 Fast-Track)', value: 'sponsored'}, + {title: 'Full-Stack Launch Bundle ($3,500 Kickoff & Demo)', value: 'full-stack-launch'}, + ], + }, + initialValue: 'editorial', + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'status', + title: 'Booking Status', + type: 'string', + options: { + list: [ + {title: 'Pending Review / Deposit', value: 'pending'}, + {title: 'Approved / Deposit Paid', value: 'approved'}, + {title: 'Scheduled on Google Calendar', value: 'scheduled'}, + {title: 'Recorded', value: 'recorded'}, + {title: 'Published', value: 'published'}, + {title: 'Declined / Cancelled', value: 'cancelled'}, + ], + }, + initialValue: 'pending', + }), + defineField({ + name: 'slotStartAt', + title: 'Requested Recording Slot (UTC ISO)', + type: 'datetime', + }), + defineField({ + name: 'topics', + title: 'Proposed Topics & Key Talking Points', + type: 'text', + }), + defineField({ + name: 'appointmentScheduleUrl', + title: 'Google Calendar Appointment Schedule Link', + type: 'url', + initialValue: 'https://calendar.app.google/s3B6PEqbVsGcY81MA', + }), + defineField({ + name: 'googleCalendarEventId', + title: 'Google Calendar Event ID', + type: 'string', + }), + defineField({ + name: 'signatureMode', + title: 'E-SIGN Signature Mode', + type: 'string', + options: { + list: [ + {title: 'Drawn Canvas Signature', value: 'draw'}, + {title: 'Typed Legal Signature', value: 'type'}, + ], + }, + }), + defineField({ + name: 'signatureData', + title: 'E-SIGN Signature Data (Typed Name or Base64 PNG)', + type: 'text', + }), + defineField({ + name: 'signedAt', + title: 'E-SIGN Executed At', + type: 'datetime', + }), + defineField({ + name: 'consentRecording', + title: 'Consent: Recording & Distribution', + type: 'boolean', + initialValue: true, + }), + defineField({ + name: 'consentCommercial', + title: 'Consent: Commercial & Sponsorship Use', + type: 'boolean', + initialValue: true, + }), + defineField({ + name: 'consentLikeness', + title: 'Consent: Likeness & Social Clips', + type: 'boolean', + initialValue: true, + }), + defineField({ + name: 'portalToken', + title: 'Linked Deal Room Token', + type: 'string', + }), + defineField({ + name: 'sponsorLeadRef', + title: 'Linked Sponsor Lead', + type: 'reference', + to: [{type: 'sponsorLead'}], + }), + ], + preview: { + select: { + title: 'guestName', + company: 'company', + track: 'track', + status: 'status', + }, + prepare({title, company, track, status}) { + return { + title: company ? `${title} (${company})` : title, + subtitle: [track, status].filter(Boolean).join(' \u00b7 '), + } + }, + }, +}) diff --git a/apps/sanity/schemas/documents/sponsorLead.ts b/apps/sanity/schemas/documents/sponsorLead.ts new file mode 100644 index 00000000..c359ed4b --- /dev/null +++ b/apps/sanity/schemas/documents/sponsorLead.ts @@ -0,0 +1,245 @@ +import {defineField, defineType} from 'sanity' +import {HiOutlineCurrencyDollar} from 'react-icons/hi' + +export default defineType({ + name: 'sponsorLead', + title: 'Sponsor Lead', + type: 'document', + icon: HiOutlineCurrencyDollar, + fields: [ + defineField({ + name: 'companyName', + title: 'Company Name', + type: 'string', + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'contactName', + title: 'Contact Name', + type: 'string', + }), + defineField({ + name: 'contactEmail', + title: 'Contact Email', + type: 'string', + validation: (Rule) => Rule.required().email(), + }), + defineField({ + name: 'website', + title: 'Company Website', + type: 'url', + }), + defineField({ + name: 'portalToken', + title: 'Deal Room Portal Token', + type: 'string', + description: 'Unique token used for codingcat.dev/portal/[token]', + }), + defineField({ + name: 'source', + title: 'Source', + type: 'string', + options: { + list: [ + {title: 'Inbound Website', value: 'inbound'}, + {title: 'Gmail Thread', value: 'gmail'}, + {title: 'Outbound Scraper', value: 'outbound'}, + {title: 'Fast-Track Booking', value: 'booking'}, + ], + }, + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'status', + title: 'Status', + type: 'string', + options: { + list: [ + {title: 'New', value: 'new'}, + {title: 'Contacted', value: 'contacted'}, + {title: 'Replied', value: 'replied'}, + {title: 'Negotiating', value: 'negotiating'}, + {title: 'Deposit Sent', value: 'deposit_sent'}, + {title: 'Deposit Paid (50%)', value: 'deposit_paid'}, + {title: 'Booked / In Production', value: 'booked'}, + {title: 'Balance Sent', value: 'balance_sent'}, + {title: 'Paid in Full (100%)', value: 'paid'}, + ], + }, + initialValue: 'new', + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'rateCard', + title: 'Package / Rate Card Tier', + type: 'string', + options: { + list: [ + {title: 'Full-Stack Launch Bundle ($3,500)', value: 'full-stack-launch'}, + {title: 'Sponsored Podcast & Multi-Channel Showcase ($2,000)', value: 'podcast-showcase'}, + {title: 'Ecosystem Integration / Mid-Roll + Social Pack ($950)', value: 'ecosystem-integration'}, + {title: 'Dedicated YouTube Video ($2,500)', value: 'dedicated-video'}, + {title: 'Integrated Mid-Roll Ad ($1,800)', value: 'mid-roll-ad'}, + {title: 'Custom Video Series (Custom)', value: 'custom-series'}, + ], + }, + }), + defineField({ + name: 'totalAmountCents', + title: 'Total Package Amount (Cents)', + type: 'number', + description: 'Total contract value in USD cents (e.g. 350000 = $3,500.00)', + }), + defineField({ + name: 'depositAmountCents', + title: '50% Upfront Deposit Amount (Cents)', + type: 'number', + description: '50% deposit required to lock calendar & production slot', + }), + defineField({ + name: 'balanceAmountCents', + title: '50% Final Balance Amount (Cents)', + type: 'number', + description: '50% remaining balance due upon publication', + }), + defineField({ + name: 'depositPaidAt', + title: '50% Deposit Paid At', + type: 'datetime', + }), + defineField({ + name: 'balancePaidAt', + title: '50% Balance Paid At', + type: 'datetime', + }), + defineField({ + name: 'stripeDepositCheckoutUrl', + title: 'Stripe 50% Deposit Checkout URL', + type: 'url', + }), + defineField({ + name: 'stripeDepositInvoiceId', + title: 'Stripe 50% Deposit Invoice ID (Net-15/30)', + type: 'string', + }), + defineField({ + name: 'stripeDepositInvoiceUrl', + title: 'Stripe 50% Deposit Hosted Invoice URL', + type: 'url', + }), + defineField({ + name: 'stripeBalanceCheckoutUrl', + title: 'Stripe 50% Balance Checkout URL', + type: 'url', + }), + defineField({ + name: 'stripeBalanceInvoiceId', + title: 'Stripe 50% Balance Invoice ID (Net-15/30)', + type: 'string', + }), + defineField({ + name: 'stripeBalanceInvoiceUrl', + title: 'Stripe 50% Balance Hosted Invoice URL', + type: 'url', + }), + defineField({ + name: 'stripeInvoiceId', + title: 'Legacy Stripe Invoice ID', + type: 'string', + }), + defineField({ + name: 'intent', + title: 'Campaign Goals & Intent', + type: 'text', + }), + defineField({ + name: 'deliverables', + title: 'Campaign Deliverables & Live ROI Tracking', + type: 'array', + of: [ + { + type: 'object', + fields: [ + defineField({name: 'title', title: 'Deliverable Title', type: 'string'}), + defineField({ + name: 'channel', + title: 'Channel', + type: 'string', + options: { + list: [ + {title: 'YouTube Long-Form', value: 'youtube'}, + {title: 'YouTube Short', value: 'youtube-short'}, + {title: 'CodingCat.dev Canonical Blog + AEO', value: 'blog'}, + {title: 'Dev.to & Hashnode Syndication', value: 'syndication'}, + {title: 'X (@CodingCatDev + @codercatdev)', value: 'x'}, + {title: 'LinkedIn', value: 'linkedin'}, + {title: 'Bluesky / Threads / Mastodon / TikTok / IG', value: 'shorts-network'}, + ], + }, + }), + defineField({ + name: 'status', + title: 'Status', + type: 'string', + options: { + list: [ + {title: 'Pending Deposit', value: 'pending'}, + {title: 'In Production', value: 'in-progress'}, + {title: 'Scheduled in Postiz', value: 'scheduled'}, + {title: 'Published', value: 'published'}, + ], + }, + initialValue: 'pending', + }), + defineField({name: 'url', title: 'Live URL', type: 'url'}), + defineField({name: 'views', title: 'Verified Views / Impressions', type: 'number'}), + defineField({name: 'publishedAt', title: 'Published At', type: 'datetime'}), + ], + }, + ], + }), + defineField({ + name: 'bookedPodcast', + title: 'Linked Podcast Episode', + type: 'reference', + to: [{type: 'podcast'}], + }), + defineField({ + name: 'sponsorDocId', + title: 'Linked Sponsor', + type: 'reference', + to: [{type: 'sponsor'}], + description: 'Public sponsor document created when lead converts to paid', + readOnly: true, + }), + defineField({ + name: 'threadId', + title: 'Gmail Thread ID', + type: 'string', + }), + defineField({ + name: 'lastEmailAt', + title: 'Last Email At', + type: 'datetime', + }), + defineField({ + name: 'notes', + title: 'Notes', + type: 'text', + }), + ], + preview: { + select: { + title: 'companyName', + status: 'status', + rateCard: 'rateCard', + source: 'source', + }, + prepare({title, status, rateCard, source}) { + return { + title, + subtitle: [status, rateCard, source].filter(Boolean).join(' \u00b7 '), + } + }, + }, +}) diff --git a/apps/sanity/schemas/documents/sponsorPool.ts b/apps/sanity/schemas/documents/sponsorPool.ts new file mode 100644 index 00000000..1e3f2199 --- /dev/null +++ b/apps/sanity/schemas/documents/sponsorPool.ts @@ -0,0 +1,123 @@ +import {defineField, defineType} from 'sanity' +import {HiOutlineUserGroup} from 'react-icons/hi' + +export default defineType({ + name: 'sponsorPool', + title: 'Sponsor Pool (Outbound Prospects)', + type: 'document', + icon: HiOutlineUserGroup, + fields: [ + defineField({ + name: 'companyName', + title: 'Company Name', + type: 'string', + validation: (Rule) => Rule.required(), + }), + defineField({ + name: 'contactName', + title: 'Contact Name / Role', + type: 'string', + }), + defineField({ + name: 'contactEmail', + title: 'Contact Email', + type: 'string', + validation: (Rule) => Rule.required().email(), + }), + defineField({ + name: 'website', + title: 'Website', + type: 'url', + }), + defineField({ + name: 'category', + title: 'Category', + type: 'string', + }), + defineField({ + name: 'fundingRound', + title: 'Funding / Launch Signal', + type: 'string', + description: 'e.g. Seed $6M, Series A $18M, YC W26, HN Launch', + }), + defineField({ + name: 'source', + title: 'Source', + type: 'string', + options: { + list: [ + {title: 'Y Combinator AI / DevTools', value: 'yc'}, + {title: 'Hacker News Launch / Who Is Hiring', value: 'hackernews'}, + {title: 'LinkedIn / Funding Announcement', value: 'linkedin'}, + {title: 'Curated', value: 'curated'}, + {title: 'Enriched', value: 'enriched'}, + ], + }, + }), + defineField({ + name: 'relevanceScore', + title: 'Relevance Score (0-100)', + type: 'number', + }), + defineField({ + name: 'videoConcepts', + title: 'Tailored CodingCat.dev Video Concepts (2 per Startup)', + type: 'array', + of: [ + { + type: 'object', + fields: [ + defineField({name: 'title', title: 'Proposed Video Title', type: 'string'}), + defineField({name: 'hook', title: '60s Short / Opening Hook', type: 'text'}), + defineField({name: 'angle', title: 'Production Architecture Angle', type: 'text'}), + defineField({ + name: 'targetPackage', + title: 'Recommended Bundle', + type: 'string', + initialValue: 'Full-Stack Launch Bundle ($3,500)', + }), + ], + }, + ], + }), + defineField({ + name: 'outreachDraftSubject', + title: 'Generated Outreach Subject', + type: 'string', + }), + defineField({ + name: 'outreachDraftBody', + title: 'Generated Outreach Body', + type: 'text', + }), + defineField({ + name: 'lastContactedAt', + title: 'Last Contacted At', + type: 'datetime', + }), + defineField({ + name: 'optedOut', + title: 'Opted Out', + type: 'boolean', + initialValue: false, + }), + defineField({ + name: 'notes', + title: 'Notes', + type: 'text', + }), + ], + preview: { + select: { + title: 'companyName', + category: 'category', + funding: 'fundingRound', + }, + prepare({title, category, funding}) { + return { + title, + subtitle: [funding, category].filter(Boolean).join(' \u00b7 '), + } + }, + }, +}) diff --git a/apps/sanity/schemas/singletons/sponsorConfig.ts b/apps/sanity/schemas/singletons/sponsorConfig.ts new file mode 100644 index 00000000..8e198074 --- /dev/null +++ b/apps/sanity/schemas/singletons/sponsorConfig.ts @@ -0,0 +1,114 @@ +import { defineField, defineType } from "sanity"; + +export default defineType({ + name: "sponsorConfig", + title: "Sponsor Config", + type: "document", + icon: () => "💰", + fields: [ + defineField({ + name: "cooldownDays", + title: "Cooldown Days", + type: "number", + description: "Days to wait before contacting a sponsor again after the last outreach", + initialValue: 14, + }), + defineField({ + name: "depositPercentage", + title: "Upfront Deposit Percentage", + type: "number", + description: "Percentage required upfront to lock a calendar & production slot (default 50%)", + initialValue: 50, + }), + defineField({ + name: "rateCardTiers", + title: "Rate Card Tiers", + type: "array", + description: "Multi-channel launch bundles & sponsorship tiers", + of: [ + { + type: "object", + fields: [ + defineField({ + name: "id", + title: "Tier ID", + type: "string", + }), + defineField({ + name: "name", + title: "Name", + type: "string", + }), + defineField({ + name: "description", + title: "Description", + type: "text", + }), + defineField({ + name: "price", + title: "Total Price (USD)", + type: "number", + }), + defineField({ + name: "depositPrice", + title: "50% Deposit Price (USD)", + type: "number", + }), + ], + }, + ], + initialValue: [ + { + _type: "object", + _key: "full-stack-launch", + id: "full-stack-launch", + name: "Full-Stack Launch Bundle", + description: + "Dedicated YouTube deep-dive (61K+ subs) + 4 vertical Shorts across 7 platforms (28 native video posts) + Canonical SEO/AEO blog post on codingcat.dev (syndicated to Dev.to & Hashnode) + Dual-account social amplification + Live Deal Room.", + price: 3500, + depositPrice: 1750, + }, + { + _type: "object", + _key: "podcast-showcase", + id: "podcast-showcase", + name: "Sponsored Podcast & Multi-Channel Showcase", + description: + "Priority fast-track 45-min podcast episode + 4 vertical Shorts across 7 platforms + Integrated mid-roll + Permanent show notes, blog feature, and Deal Room telemetry.", + price: 2000, + depositPrice: 1000, + }, + { + _type: "object", + _key: "ecosystem-integration", + id: "ecosystem-integration", + name: "Ecosystem Integration (Mid-Roll + Social Pack)", + description: + "60-second host-read mid-roll integration in a technical video + Multi-channel social thread across X, LinkedIn, Bluesky, Threads, and Mastodon + Permanent link on codingcat.dev.", + price: 950, + depositPrice: 475, + }, + ], + }), + defineField({ + name: "outreachEmailTemplate", + title: "Outreach Email Template", + type: "text", + description: "Template for automated sponsor outreach emails. Use {{companyName}} for personalization", + initialValue: + "Hi {{contactName}},\n\nSaw {{companyName}}'s recent momentum and put together two concrete technical video concepts we could build and distribute across CodingCat.dev (61K+ YouTube subscribers, 7-platform Shorts syndication, and canonical AEO blog posts).\n\n{{videoConcepts}}\n\nWould you be open to a quick 15-minute chat this week? You can grab a time directly here: https://calendar.app.google/tyGiF6dGFQeaVrN66\n\nBest,\nAlex Patterson\nFounder, CodingCat.dev", + }), + defineField({ + name: "maxOutreachPerRun", + title: "Max Outreach Per Run", + type: "number", + description: "Maximum sponsor emails to send per cron run. Keeps volume manageable and avoids spam flags", + initialValue: 10, + }), + ], + preview: { + prepare() { + return { title: "Sponsor Config" }; + }, + }, +}); diff --git a/apps/site/src/lib/email.ts b/apps/site/src/lib/email.ts new file mode 100644 index 00000000..8d5778ad --- /dev/null +++ b/apps/site/src/lib/email.ts @@ -0,0 +1,82 @@ +import { env } from "cloudflare:workers"; + +interface SendEmailBinding { + send(message: unknown): Promise; +} + +function buildMimeMessage(opts: { + from: string; + fromName: string; + to: string; + subject: string; + text: string; + html?: string; +}): string { + const boundary = `----=_Part_${crypto.randomUUID().replace(/-/g, "")}`; + const date = new Date().toUTCString(); + const msgId = `<${crypto.randomUUID()}@codingcat.dev>`; + const lines = [ + `From: "${opts.fromName}" <${opts.from}>`, + `To: <${opts.to}>`, + `Subject: ${opts.subject}`, + `Date: ${date}`, + `Message-ID: ${msgId}`, + "MIME-Version: 1.0", + `Content-Type: multipart/alternative; boundary="${boundary}"`, + "", + `--${boundary}`, + "Content-Type: text/plain; charset=UTF-8", + "Content-Transfer-Encoding: 7bit", + "", + opts.text, + ]; + + if (opts.html) { + lines.push( + `--${boundary}`, + "Content-Type: text/html; charset=UTF-8", + "Content-Transfer-Encoding: 7bit", + "", + opts.html, + ); + } + + lines.push(`--${boundary}--`, ""); + return lines.join("\r\n"); +} + +export async function sendCodingCatEmail(opts: { + to: string; + subject: string; + text: string; + html?: string; + from?: string; + fromName?: string; +}): Promise { + try { + const workerEnv = (env ?? {}) as unknown as { + EMAIL?: SendEmailBinding; + }; + if (!workerEnv.EMAIL) { + return false; + } + + const from = opts.from ?? "alex@codingcat.dev"; + const fromName = opts.fromName ?? "Alex Patterson (CodingCat.dev)"; + const raw = buildMimeMessage({ + from, + fromName, + to: opts.to, + subject: opts.subject, + text: opts.text, + html: opts.html, + }); + + const { EmailMessage } = await import("cloudflare:email"); + const msg = new EmailMessage(from, opts.to, raw); + await workerEnv.EMAIL.send(msg); + return true; + } catch { + return false; + } +} diff --git a/apps/site/src/lib/google-calendar.ts b/apps/site/src/lib/google-calendar.ts new file mode 100644 index 00000000..ae68f2d7 --- /dev/null +++ b/apps/site/src/lib/google-calendar.ts @@ -0,0 +1,250 @@ +import { env } from "cloudflare:workers"; + +export const CODINGCAT_CALENDAR_IDS = [ + "alex.patterson@gmail.com", + "alex@codingcat.dev", + "alex.patterson@sanity.io", + "mikqhmn29svd9p9m99quch9nuo@group.calendar.google.com", // Oliver - Alex + "2em0afukjm0gk9k3n0ohh5ag3g@group.calendar.google.com", // Oliver - Events + "a1jir6k8sl9k38fbbohoei5f80@group.calendar.google.com", // Oliver - Heidi +]; + +export interface AvailableCalendarSlot { + id: string; + startAt: string; + endAt: string; + displayDate: string; + displayTime: string; +} + +async function getAccessToken(): Promise { + const workerEnv = (env ?? {}) as unknown as Record< + string, + string | undefined + >; + const clientId = workerEnv.GOOGLE_CLIENT_ID; + const clientSecret = workerEnv.GOOGLE_CLIENT_SECRET; + const refreshToken = workerEnv.GOOGLE_REFRESH_TOKEN; + + if (!clientId || !clientSecret || !refreshToken) { + return null; + } + + const body = new URLSearchParams({ + client_id: clientId, + client_secret: clientSecret, + refresh_token: refreshToken, + grant_type: "refresh_token", + }); + + const res = await fetch("https://oauth2.googleapis.com/token", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: body.toString(), + }); + + if (!res.ok) { + return null; + } + const data = (await res.json()) as { access_token?: string }; + return data.access_token ?? null; +} + +function generateDefaultCandidateSlots( + daysAhead = 14, +): AvailableCalendarSlot[] { + const slots: AvailableCalendarSlot[] = []; + const now = new Date(); + + // Candidate slots in overlapping US PT (8:00 AM - 2:00 PM PT) & ET (11:00 AM - 5:00 PM ET) work hours: + // 16:00 UTC (9:00 AM PDT / 12:00 PM EDT), 17:30 UTC (10:30 AM PDT / 1:30 PM EDT), 19:00 UTC (12:00 PM PDT / 3:00 PM EDT) + const candidateUtcHours = [ + { hour: 16, minute: 0, label: "9:00 AM PT / 12:00 PM ET" }, + { hour: 17, minute: 30, label: "10:30 AM PT / 1:30 PM ET" }, + { hour: 19, minute: 0, label: "12:00 PM PT / 3:00 PM ET" }, + ]; + + for (let d = 1; d <= daysAhead; d++) { + const dayDate = new Date(now.getTime() + d * 86400_000); + const dayOfWeek = dayDate.getUTCDay(); + // Weekdays Monday (1) through Friday (5), prioritizing Tue-Thu + if (dayOfWeek === 0 || dayOfWeek === 6) { + continue; + } + + for (const cand of candidateUtcHours) { + const start = new Date( + Date.UTC( + dayDate.getUTCFullYear(), + dayDate.getUTCMonth(), + dayDate.getUTCDate(), + cand.hour, + cand.minute, + 0, + ), + ); + const end = new Date(start.getTime() + 45 * 60_000); + slots.push({ + id: `slot-${start.toISOString().slice(0, 16).replace(/[:T]/g, "-")}`, + startAt: start.toISOString(), + endAt: end.toISOString(), + displayDate: start.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + timeZone: "America/New_York", + }), + displayTime: cand.label, + }); + } + } + + return slots; +} + +/** + * Queries Google Calendar FreeBusy API across all 6 of Alex Patterson's calendars + * (Personal, CodingCat.dev, Sanity.io, and Oliver family calendars) and returns + * conflict-free 45-minute recording slots during 8am-5pm PT/ET work hours. + */ +export async function getMultiCalendarAvailableSlots( + maxSlots = 9, +): Promise<{ slots: AvailableCalendarSlot[]; liveVerified: boolean }> { + const candidates = generateDefaultCandidateSlots(14); + const accessToken = await getAccessToken(); + + if (!accessToken || candidates.length === 0) { + return { + slots: candidates.slice(0, maxSlots), + liveVerified: false, + }; + } + + const timeMin = candidates[0].startAt; + const timeMax = candidates[candidates.length - 1].endAt; + + try { + const res = await fetch("https://www.googleapis.com/calendar/v3/freeBusy", { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + timeMin, + timeMax, + items: CODINGCAT_CALENDAR_IDS.map((id) => ({ id })), + }), + }); + + if (!res.ok) { + return { slots: candidates.slice(0, maxSlots), liveVerified: false }; + } + + const data = (await res.json()) as { + calendars?: Record< + string, + { busy?: Array<{ start: string; end: string }> } + >; + }; + + const busyRanges: Array<{ startMs: number; endMs: number }> = []; + for (const cal of Object.values(data.calendars ?? {})) { + for (const b of cal.busy ?? []) { + busyRanges.push({ + startMs: new Date(b.start).getTime(), + endMs: new Date(b.end).getTime(), + }); + } + } + + const available = candidates.filter((slot) => { + const slotStart = new Date(slot.startAt).getTime(); + const slotEnd = new Date(slot.endAt).getTime(); + return !busyRanges.some( + (b) => slotStart < b.endMs && slotEnd > b.startMs, + ); + }); + + return { + slots: (available.length > 0 ? available : candidates).slice(0, maxSlots), + liveVerified: true, + }; + } catch { + return { + slots: candidates.slice(0, maxSlots), + liveVerified: false, + }; + } +} + +/** + * Creates a Google Calendar event on alex@codingcat.dev (or alex.patterson@gmail.com) + * when a Sponsored Showcase or approved Editorial session is booked. + */ +export async function createGoogleCalendarBookingEvent(opts: { + guestName: string; + guestEmail: string; + company: string; + track: string; + startAt: string; + topics: string; + portalUrl?: string; +}): Promise<{ eventId: string; htmlLink?: string } | null> { + const accessToken = await getAccessToken(); + if (!accessToken) { + return null; + } + + const start = new Date(opts.startAt); + const end = new Date(start.getTime() + 45 * 60_000); + + const summary = + opts.track === "editorial" + ? `CodingCat.dev Podcast: ${opts.guestName} (${opts.company})` + : `[SPONSORED] CodingCat.dev Showcase: ${opts.guestName} (${opts.company})`; + + const descriptionLines = [ + `CodingCat.dev Recording Session (${opts.track.toUpperCase()})`, + `Guest: ${opts.guestName} (${opts.guestEmail})`, + `Company: ${opts.company}`, + `Topics: ${opts.topics}`, + "E-SIGN Media Release: Signed & stored in Sanity", + ...(opts.portalUrl ? [`Deal Room: ${opts.portalUrl}`] : []), + ]; + + try { + const res = await fetch( + "https://www.googleapis.com/calendar/v3/calendars/alex%40codingcat.dev/events?sendUpdates=all", + { + method: "POST", + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + summary, + description: descriptionLines.join("\n"), + start: { dateTime: start.toISOString() }, + end: { dateTime: end.toISOString() }, + attendees: [ + { email: "alex.patterson@gmail.com" }, + { email: "alex@codingcat.dev" }, + { email: opts.guestEmail, displayName: opts.guestName }, + ], + }), + }, + ); + + if (!res.ok) { + return null; + } + const event = (await res.json()) as { id?: string; htmlLink?: string }; + if (!event.id) { + return null; + } + return { eventId: event.id, htmlLink: event.htmlLink }; + } catch { + return null; + } +} diff --git a/apps/site/src/lib/sanity/write-client.ts b/apps/site/src/lib/sanity/write-client.ts new file mode 100644 index 00000000..12156f33 --- /dev/null +++ b/apps/site/src/lib/sanity/write-client.ts @@ -0,0 +1,242 @@ +import { env } from "cloudflare:workers"; +import { createClient, type SanityClient } from "@sanity/client"; + +export interface SponsorPackageTier { + id: string; + name: string; + priceUsd: number; + totalAmountCents: number; + depositAmountCents: number; + balanceAmountCents: number; + tagline: string; + description: string; + deliverables: Array<{ + title: string; + channel: string; + }>; +} + +export const SPONSOR_PACKAGE_TIERS: SponsorPackageTier[] = [ + { + id: "full-stack-launch", + name: "Full-Stack Launch Bundle", + priceUsd: 3500, + totalAmountCents: 350000, + depositAmountCents: 175000, + balanceAmountCents: 175000, + tagline: "Most Popular \u00b7 Complete Multi-Channel DevTool Launch", + description: + "Dedicated YouTube technical deep-dive (61K+ subscribers) + 4 vertical Shorts published across 7 platforms (28 native video posts via Postiz) + Canonical SEO & Agent-Ready (AEO / WebMCP / llms.txt) article on codingcat.dev syndicated to Dev.to & Hashnode + Dual-account social amplification (@CodingCatDev + @codercatdev) + Live ROI Deal Room.", + deliverables: [ + { + title: "Dedicated YouTube Technical Deep-Dive Video", + channel: "youtube", + }, + { + title: "4x Vertical Shorts across 7 Platforms (28 Native Video Posts)", + channel: "shorts-network", + }, + { + title: "Canonical SEO & Agent-Ready (AEO) Article on CodingCat.dev", + channel: "blog", + }, + { + title: "Canonical Syndication to Dev.to & Hashnode", + channel: "syndication", + }, + { + title: "Dual-Account Launch Thread (@CodingCatDev + @codercatdev)", + channel: "x", + }, + { title: "LinkedIn Technical Breakdown Post", channel: "linkedin" }, + ], + }, + { + id: "podcast-showcase", + name: "Sponsored Podcast & Multi-Channel Showcase", + priceUsd: 2000, + totalAmountCents: 200000, + depositAmountCents: 100000, + balanceAmountCents: 100000, + tagline: + "Fast-Track Recording \u00b7 Founder / DevRel Engineering Deep-Dive", + description: + "Priority fast-track 45-minute podcast & video episode on CodingCat.dev + 4 vertical Shorts distributed across 7 platforms + 60-second host-read mid-roll callout + Permanent show notes, canonical blog feature, and Live ROI Deal Room.", + deliverables: [ + { + title: "45-Minute Sponsored Podcast & YouTube Video Episode", + channel: "youtube", + }, + { + title: "4x Vertical Episode Shorts across 7 Platforms", + channel: "shorts-network", + }, + { + title: "Canonical Episode Post & Show Notes on CodingCat.dev", + channel: "blog", + }, + { + title: + "Multi-Channel Launch Amplification (X, LinkedIn, Bluesky, Threads, Mastodon)", + channel: "x", + }, + ], + }, + { + id: "ecosystem-integration", + name: "Ecosystem Integration (Mid-Roll + Social Pack)", + priceUsd: 950, + totalAmountCents: 95000, + depositAmountCents: 47500, + balanceAmountCents: 47500, + tagline: + "High-Intent Developer Reach \u00b7 60s Integration + Social Thread", + description: + "60-second host-read technical demo integrated into a relevant CodingCat.dev video + Dedicated multi-channel social thread across X, LinkedIn, Bluesky, Threads, and Mastodon + Permanent sponsor link on codingcat.dev.", + deliverables: [ + { + title: "60-Second Host-Read Integrated Mid-Roll Ad", + channel: "youtube", + }, + { + title: + "Multi-Channel Social Thread (X, LinkedIn, Bluesky, Threads, Mastodon)", + channel: "x", + }, + { + title: "Permanent Sponsor Link & Show Notes Inclusion", + channel: "blog", + }, + ], + }, + { + id: "dedicated-video", + name: "Standalone Dedicated YouTube Video", + priceUsd: 2500, + totalAmountCents: 250000, + depositAmountCents: 125000, + balanceAmountCents: 125000, + tagline: "End-to-End Architecture & Code Walkthrough", + description: + "Full 10-to-15 minute YouTube video dedicated entirely to building with your product, SDK, or platform. Includes permanent pinned link and sponsor page placement.", + deliverables: [ + { title: "Dedicated YouTube Video Walkthrough", channel: "youtube" }, + { title: "Social Launch Post across X & LinkedIn", channel: "x" }, + ], + }, + { + id: "mid-roll-ad", + name: "Integrated 60s Mid-Roll Ad", + priceUsd: 1800, + totalAmountCents: 180000, + depositAmountCents: 90000, + balanceAmountCents: 90000, + tagline: "Flagship Tutorial Integration", + description: + "60-second host-read integration inside a flagship tutorial or architecture breakdown, with permanent description link and chapter marker.", + deliverables: [ + { title: "60-Second Integrated Mid-Roll Segment", channel: "youtube" }, + ], + }, + { + id: "custom-series", + name: "Multi-Part Video Series & Advisory", + priceUsd: 6000, + totalAmountCents: 600000, + depositAmountCents: 300000, + balanceAmountCents: 300000, + tagline: "Custom Enterprise Campaign \u00b7 3+ Videos + Shorts + Articles", + description: + "Multi-part technical series covering your platform from quickstart to production scale, complete with dedicated Shorts, canonical articles, and quarterly campaign reporting.", + deliverables: [ + { title: "Multi-Part YouTube Technical Series", channel: "youtube" }, + { + title: "12x Multi-Platform Vertical Shorts", + channel: "shorts-network", + }, + { + title: "Canonical Documentation & Tutorial Series on CodingCat.dev", + channel: "blog", + }, + ], + }, +]; + +export function resolveSponsorTier( + rawTier: string | string[] | undefined, +): SponsorPackageTier { + const text = ( + Array.isArray(rawTier) ? rawTier.join(" ") : String(rawTier ?? "") + ).toLowerCase(); + for (const tier of SPONSOR_PACKAGE_TIERS) { + if (text.includes(tier.id) || text.includes(tier.name.toLowerCase())) { + return tier; + } + } + if ( + text.includes("full-stack") || + text.includes("bundle") || + text.includes("3,500") || + text.includes("3500") + ) { + return SPONSOR_PACKAGE_TIERS[0]; + } + if ( + text.includes("podcast") || + text.includes("showcase") || + text.includes("2,000") || + text.includes("2000") + ) { + return SPONSOR_PACKAGE_TIERS[1]; + } + if (text.includes("ecosystem") || text.includes("950")) { + return SPONSOR_PACKAGE_TIERS[2]; + } + if (text.includes("dedicated")) { + return SPONSOR_PACKAGE_TIERS[3]; + } + if ( + text.includes("mid-roll") || + text.includes("1,800") || + text.includes("1800") + ) { + return SPONSOR_PACKAGE_TIERS[4]; + } + if (text.includes("series") || text.includes("custom")) { + return SPONSOR_PACKAGE_TIERS[5]; + } + return SPONSOR_PACKAGE_TIERS[0]; +} + +export function getSanityServerClient(): { + client: SanityClient; + canWrite: boolean; +} { + const workerEnv = (env ?? {}) as unknown as Record< + string, + string | undefined + >; + const projectId = workerEnv.PUBLIC_SANITY_PROJECT_ID || "hfh83o0w"; + const dataset = workerEnv.PUBLIC_SANITY_DATASET || "production"; + const apiVersion = workerEnv.PUBLIC_SANITY_API_VERSION || "2025-09-30"; + const writeToken = + workerEnv.SANITY_API_WRITE_TOKEN || + workerEnv.SANITY_AUTH_TOKEN || + workerEnv.SANITY_WRITE_TOKEN; + const readToken = workerEnv.SANITY_API_READ_TOKEN; + const token = writeToken || readToken; + + const client = createClient({ + projectId, + dataset, + apiVersion, + useCdn: false, + token, + perspective: "published", + }); + + return { + client, + canWrite: Boolean(writeToken), + }; +} diff --git a/apps/site/src/lib/stripe.ts b/apps/site/src/lib/stripe.ts new file mode 100644 index 00000000..49661344 --- /dev/null +++ b/apps/site/src/lib/stripe.ts @@ -0,0 +1,258 @@ +import { env } from "cloudflare:workers"; + +export interface StripeMilestoneSessionParams { + leadId: string; + portalToken: string; + companyName: string; + contactEmail: string; + packageName: string; + milestone: "deposit" | "balance"; + amountCents: number; + siteUrl: string; +} + +export interface StripeInvoiceResult { + invoiceId: string; + hostedInvoiceUrl: string; +} + +function getStripeSecretKey(): string | undefined { + const workerEnv = (env ?? {}) as unknown as Record< + string, + string | undefined + >; + return workerEnv.STRIPE_SECRET_KEY; +} + +function toFormUrlEncoded( + params: Record, +): string { + const search = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) { + if (v !== undefined && v !== null && v !== "") { + search.append(k, String(v)); + } + } + return search.toString(); +} + +/** + * Creates a Stripe Checkout Session for either the 50% Upfront Deposit + * or the 50% Final Balance on Publish. + */ +export async function createStripeMilestoneCheckout( + params: StripeMilestoneSessionParams, +): Promise<{ checkoutUrl: string; sessionId: string } | null> { + const secretKey = getStripeSecretKey(); + if (!secretKey) { + return null; + } + + const label = + params.milestone === "deposit" + ? `50% Upfront Deposit: ${params.packageName} (${params.companyName})` + : `50% Final Balance: ${params.packageName} (${params.companyName})`; + + const description = + params.milestone === "deposit" + ? "Locks your production & recording slot on CodingCat.dev. Remaining 50% balance due upon publication." + : "Final 50% balance upon publication of your CodingCat.dev launch campaign."; + + const body = toFormUrlEncoded({ + mode: "payment", + customer_email: params.contactEmail, + success_url: `${params.siteUrl}/portal/${params.portalToken}?payment=${params.milestone}_success`, + cancel_url: `${params.siteUrl}/portal/${params.portalToken}?payment=cancelled`, + "line_items[0][price_data][currency]": "usd", + "line_items[0][price_data][unit_amount]": params.amountCents, + "line_items[0][price_data][product_data][name]": label, + "line_items[0][price_data][product_data][description]": description, + "line_items[0][quantity]": 1, + "metadata[leadId]": params.leadId, + "metadata[portalToken]": params.portalToken, + "metadata[milestone]": params.milestone, + "metadata[companyName]": params.companyName, + "metadata[packageName]": params.packageName, + }); + + const res = await fetch("https://api.stripe.com/v1/checkout/sessions", { + method: "POST", + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + body, + }); + + if (!res.ok) { + return null; + } + const data = (await res.json()) as { id?: string; url?: string }; + if (!data.id || !data.url) { + return null; + } + return { sessionId: data.id, checkoutUrl: data.url }; +} + +/** + * Creates & finalizes a Stripe Hosted Invoice (Net-15 or Net-30) for enterprise + * sponsors who pay via AP / ACH / Wire or Corporate Card. + */ +export async function createStripeMilestoneInvoice( + params: StripeMilestoneSessionParams & { daysUntilDue?: number }, +): Promise { + const secretKey = getStripeSecretKey(); + if (!secretKey) { + return null; + } + + const headers = { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/x-www-form-urlencoded", + }; + + // 1. Find or create customer + const searchRes = await fetch( + `https://api.stripe.com/v1/customers?email=${encodeURIComponent(params.contactEmail)}&limit=1`, + { headers: { Authorization: `Bearer ${secretKey}` } }, + ); + const searchData = (await searchRes.json()) as { + data?: Array<{ id: string }>; + }; + let customerId = searchData.data?.[0]?.id; + + if (!customerId) { + const createCustomerRes = await fetch( + "https://api.stripe.com/v1/customers", + { + method: "POST", + headers, + body: toFormUrlEncoded({ + name: params.companyName, + email: params.contactEmail, + "metadata[leadId]": params.leadId, + "metadata[portalToken]": params.portalToken, + }), + }, + ); + if (!createCustomerRes.ok) { + return null; + } + const customer = (await createCustomerRes.json()) as { id: string }; + customerId = customer.id; + } + + const label = + params.milestone === "deposit" + ? `50% Upfront Deposit: ${params.packageName} (${params.companyName})` + : `50% Final Balance: ${params.packageName} (${params.companyName})`; + + // 2. Create draft invoice + const invoiceRes = await fetch("https://api.stripe.com/v1/invoices", { + method: "POST", + headers, + body: toFormUrlEncoded({ + customer: customerId, + collection_method: "send_invoice", + days_until_due: params.daysUntilDue ?? 15, + description: `CodingCat.dev Sponsorship (${params.packageName}) — Deal Room: ${params.siteUrl}/portal/${params.portalToken}`, + "metadata[leadId]": params.leadId, + "metadata[portalToken]": params.portalToken, + "metadata[milestone]": params.milestone, + "metadata[companyName]": params.companyName, + "metadata[packageName]": params.packageName, + }), + }); + if (!invoiceRes.ok) { + return null; + } + const invoice = (await invoiceRes.json()) as { id: string }; + + // 3. Attach invoice item + const itemRes = await fetch("https://api.stripe.com/v1/invoiceitems", { + method: "POST", + headers, + body: toFormUrlEncoded({ + customer: customerId, + invoice: invoice.id, + amount: params.amountCents, + currency: "usd", + description: label, + }), + }); + if (!itemRes.ok) { + return null; + } + + // 4. Finalize invoice so hosted_invoice_url is immediately live + const finalizeRes = await fetch( + `https://api.stripe.com/v1/invoices/${invoice.id}/finalize`, + { + method: "POST", + headers, + }, + ); + if (!finalizeRes.ok) { + return null; + } + const finalized = (await finalizeRes.json()) as { + id: string; + hosted_invoice_url?: string; + }; + if (!finalized.hosted_invoice_url) { + return null; + } + + return { + invoiceId: finalized.id, + hostedInvoiceUrl: finalized.hosted_invoice_url, + }; +} + +/** + * Verifies a Stripe Webhook signature using Web Crypto API (Cloudflare Workers compatible). + */ +export async function verifyStripeWebhookSignature( + payload: string, + sigHeader: string | null, + secret: string, +): Promise { + if (!sigHeader || !secret) { + return false; + } + const parts = sigHeader + .split(",") + .reduce>((acc, part) => { + const [k, v] = part.split("="); + if (k && v) { + acc[k.trim()] = v.trim(); + } + return acc; + }, {}); + + const timestamp = parts.t; + const signature = parts.v1; + if (!timestamp || !signature) { + return false; + } + + const signedPayload = `${timestamp}.${payload}`; + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + "raw", + encoder.encode(secret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const sigBuffer = await crypto.subtle.sign( + "HMAC", + key, + encoder.encode(signedPayload), + ); + const expectedHex = Array.from(new Uint8Array(sigBuffer)) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); + + return expectedHex === signature; +} diff --git a/apps/site/src/pages/api/booking.ts b/apps/site/src/pages/api/booking.ts new file mode 100644 index 00000000..53158e0e --- /dev/null +++ b/apps/site/src/pages/api/booking.ts @@ -0,0 +1,216 @@ +import { env } from "cloudflare:workers"; +import type { APIRoute } from "astro"; +import { sendCodingCatEmail } from "@/lib/email"; +import { + getSanityServerClient, + resolveSponsorTier, +} from "@/lib/sanity/write-client"; +import { + createStripeMilestoneCheckout, + createStripeMilestoneInvoice, +} from "@/lib/stripe"; + +interface BookingPayload { + guestName?: string; + guestEmail?: string; + company?: string; + socialUrl?: string; + track?: "editorial" | "sponsored" | "full-stack-launch"; + slotStartAt?: string; + topics?: string; + signatureMode?: "draw" | "type"; + signatureData?: string; + consentRecording?: boolean; + consentCommercial?: boolean; + consentLikeness?: boolean; +} + +export const POST: APIRoute = async ({ request, url }) => { + let data: BookingPayload = {}; + try { + data = (await request.json()) as BookingPayload; + } catch { + return new Response( + JSON.stringify({ success: false, message: "Invalid JSON payload" }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ); + } + + const guestName = data.guestName?.trim(); + const guestEmail = data.guestEmail?.trim(); + const company = data.company?.trim() || guestName || "Podcast Guest"; + const socialUrl = data.socialUrl?.trim(); + const track = data.track || "editorial"; + const slotStartAt = + data.slotStartAt || new Date(Date.now() + 7 * 86400_000).toISOString(); + const topics = data.topics?.trim(); + const signatureMode = data.signatureMode || "type"; + const signatureData = data.signatureData || guestName; + + if (!guestName || !guestEmail || !guestEmail.includes("@") || !topics) { + return new Response( + JSON.stringify({ + success: false, + message: "Guest name, valid email, and talking points are required.", + }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ); + } + + const bookingId = `booking-${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`; + const portalToken = `spt_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`; + const isSponsored = track === "sponsored" || track === "full-stack-launch"; + const appointmentScheduleUrl = + track === "full-stack-launch" + ? "https://calendar.app.google/tyGiF6dGFQeaVrN66" + : "https://calendar.app.google/s3B6PEqbVsGcY81MA"; + + const workerEnv = (env ?? {}) as unknown as Record< + string, + string | undefined + >; + const fallbackOrigin = url?.origin ?? new URL(request.url).origin; + const siteUrl = (workerEnv.SITE_URL || fallbackOrigin).replace(/\/$/, ""); + + const { client, canWrite } = getSanityServerClient(); + let portalUrl: string | undefined; + let stripeDepositCheckoutUrl: string | undefined; + let stripeDepositInvoiceUrl: string | undefined; + + if (isSponsored) { + const tier = resolveSponsorTier( + track === "full-stack-launch" ? "full-stack-launch" : "podcast-showcase", + ); + const leadId = `sponsorLead-${portalToken}`; + + try { + const [checkout, invoice] = await Promise.all([ + createStripeMilestoneCheckout({ + leadId, + portalToken, + companyName: company, + contactEmail: guestEmail, + packageName: tier.name, + milestone: "deposit", + amountCents: tier.depositAmountCents, + siteUrl, + }), + createStripeMilestoneInvoice({ + leadId, + portalToken, + companyName: company, + contactEmail: guestEmail, + packageName: tier.name, + milestone: "deposit", + amountCents: tier.depositAmountCents, + siteUrl, + daysUntilDue: 15, + }), + ]); + stripeDepositCheckoutUrl = checkout?.checkoutUrl; + stripeDepositInvoiceUrl = invoice?.hostedInvoiceUrl; + } catch { + // Stripe optional in dev + } + + if (canWrite) { + try { + await client.createOrReplace({ + _id: leadId, + _type: "sponsorLead", + companyName: company, + contactName: guestName, + contactEmail: guestEmail, + ...(socialUrl ? { website: socialUrl } : {}), + portalToken, + source: "booking", + status: + stripeDepositCheckoutUrl || stripeDepositInvoiceUrl + ? "deposit_sent" + : "new", + rateCard: tier.id, + totalAmountCents: tier.totalAmountCents, + depositAmountCents: tier.depositAmountCents, + balanceAmountCents: tier.balanceAmountCents, + ...(stripeDepositCheckoutUrl ? { stripeDepositCheckoutUrl } : {}), + ...(stripeDepositInvoiceUrl ? { stripeDepositInvoiceUrl } : {}), + intent: topics, + deliverables: tier.deliverables.map((d, idx) => ({ + _key: `deliv-${idx}`, + title: d.title, + channel: d.channel, + status: "pending", + })), + lastEmailAt: new Date().toISOString(), + }); + } catch { + // Non-fatal + } + } + + const queryFallback = new URLSearchParams({ + company, + name: guestName, + email: guestEmail, + tier: tier.id, + }); + portalUrl = `${siteUrl}/portal/${portalToken}?${queryFallback.toString()}`; + } + + if (canWrite) { + try { + await client.createOrReplace({ + _id: bookingId, + _type: "booking", + guestName, + guestEmail, + company, + ...(socialUrl ? { socialUrl } : {}), + track, + status: "pending", + slotStartAt, + topics, + appointmentScheduleUrl, + signatureMode, + signatureData, + signedAt: new Date().toISOString(), + consentRecording: data.consentRecording ?? true, + consentCommercial: data.consentCommercial ?? true, + consentLikeness: data.consentLikeness ?? true, + ...(isSponsored ? { portalToken } : {}), + }); + } catch { + // Non-fatal + } + } + + await sendCodingCatEmail({ + to: "alex.patterson@gmail.com", + subject: `[CodingCat.dev ${isSponsored ? "Fast-Track Sponsored" : "Editorial"} Booking] ${guestName} (${company})`, + text: [ + "New Podcast Booking & Signed Media Release", + `Track: ${track}`, + `Guest: ${guestName} <${guestEmail}>`, + `Company: ${company}`, + `Requested Slot: ${slotStartAt}`, + `Topics: ${topics}`, + `Calendar Link: ${appointmentScheduleUrl}`, + ...(portalUrl ? [`Deal Room: ${portalUrl}`] : []), + ].join("\n"), + }); + + return new Response( + JSON.stringify({ + success: true, + bookingId, + track, + isSponsored, + appointmentScheduleUrl, + portalToken: isSponsored ? portalToken : undefined, + portalUrl, + stripeDepositCheckoutUrl, + stripeDepositInvoiceUrl, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); +}; diff --git a/apps/site/src/pages/api/calendar/freebusy.ts b/apps/site/src/pages/api/calendar/freebusy.ts new file mode 100644 index 00000000..5c4074ab --- /dev/null +++ b/apps/site/src/pages/api/calendar/freebusy.ts @@ -0,0 +1,28 @@ +import type { APIRoute } from "astro"; +import { + CODINGCAT_CALENDAR_IDS, + getMultiCalendarAvailableSlots, +} from "@/lib/google-calendar"; + +export const GET: APIRoute = async ({ request, url }) => { + const reqUrl = url ?? new URL(request.url); + const max = Number(reqUrl.searchParams.get("limit") || 9); + const { slots, liveVerified } = await getMultiCalendarAvailableSlots( + Math.min(18, Math.max(3, max)), + ); + + return new Response( + JSON.stringify({ + success: true, + liveVerified, + checkedCalendars: CODINGCAT_CALENDAR_IDS, + workHoursPolicy: + "8:00 AM - 5:00 PM PT / ET Overlap (11:00 AM - 5:00 PM ET)", + slots, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); +}; diff --git a/apps/site/src/pages/api/sponsors/outbound.ts b/apps/site/src/pages/api/sponsors/outbound.ts new file mode 100644 index 00000000..89f84547 --- /dev/null +++ b/apps/site/src/pages/api/sponsors/outbound.ts @@ -0,0 +1,322 @@ +import type { APIRoute } from "astro"; +import { sendCodingCatEmail } from "@/lib/email"; +import { getSanityServerClient } from "@/lib/sanity/write-client"; + +export interface OutboundProspect { + companyName: string; + website: string; + contactName: string; + contactEmail: string; + category: string; + fundingRound: string; + source: "yc" | "hackernews" | "linkedin" | "curated"; + relevanceScore: number; + videoConcepts: Array<{ + title: string; + hook: string; + angle: string; + targetPackage: string; + }>; + outreachDraftSubject: string; + outreachDraftBody: string; +} + +function sanitizeDashes(text: string): string { + return text + .replace(/\u2014/g, ", ") + .replace(/\u2013/g, "-") + .replace(/--+/g, ","); +} + +function buildTailoredConcepts( + companyName: string, + category: string, + summary: string, +) { + const cleanSummary = sanitizeDashes( + summary || category || "AI and developer infrastructure", + ); + const concepts = [ + { + title: `Building Production AI Agents with ${companyName}: Architecture & Cost Breakdown`, + hook: `Most teams wire up ${companyName} in a weekend prototype and hit scaling walls in production. Here is how to structure ${companyName} in 60 seconds without burning token or latency budgets.`, + angle: + "End-to-end production build on CodingCat.dev showing real SDK integration, latency benchmarks, and 4 vertical Shorts syndicated across 7 developer networks.", + targetPackage: "Full-Stack Launch Bundle ($3,500)", + }, + { + title: `Under the Hood of ${companyName}: Engineering Deep-Dive (${cleanSummary.slice(0, 60)})`, + hook: `What happens under the hood when you replace brittle glue code with ${companyName}? Let us walk through the live architecture.`, + angle: + "45-minute technical founder/staff-engineer podcast showcase + canonical AEO/WebMCP article on codingcat.dev syndicated to Dev.to and Hashnode.", + targetPackage: "Sponsored Podcast & Multi-Channel Showcase ($2,000)", + }, + ]; + + const outreachDraftSubject = sanitizeDashes( + `2 technical video concepts for ${companyName} x CodingCat.dev`, + ); + + const outreachDraftBody = sanitizeDashes( + [ + `Hi ${companyName} team,`, + "", + `I have been following ${companyName}'s momentum in ${cleanSummary} and put together two concrete technical video concepts we could build and distribute for your team on CodingCat.dev (61.2K+ YouTube subscribers, ~21% US/CA/UK/AU senior engineering audience, 7-platform Shorts syndication, and canonical AEO blog coverage):`, + "", + `1. "${concepts[0].title}"`, + ` Angle: ${concepts[0].angle}`, + "", + `2. "${concepts[1].title}"`, + ` Angle: ${concepts[1].angle}`, + "", + "Our Full-Stack Launch Bundle ($3,500 total, locked with a 50% upfront deposit and 50% on publish) includes the dedicated YouTube build, 4 vertical Shorts published across 7 platforms (28 native video posts), a canonical SEO + Agent-Ready (WebMCP / llms.txt) article on codingcat.dev syndicated to Dev.to and Hashnode, and a live ROI Deal Room.", + "", + "If you are open to a quick 15-minute scoping call this week, you can grab a time directly on my calendar here:", + "https://calendar.app.google/tyGiF6dGFQeaVrN66", + "", + "Best,", + "Alex Patterson", + "Founder, CodingCat.dev", + "https://codingcat.dev/sponsorships", + ].join("\n"), + ); + + return { concepts, outreachDraftSubject, outreachDraftBody }; +} + +async function discoverLaunchHnProspects(): Promise { + const prospects: OutboundProspect[] = []; + try { + const res = await fetch( + "https://hn.algolia.com/api/v1/search_by_date?query=%22Launch%20HN%22&tags=story&hitsPerPage=8", + ); + if (res.ok) { + const data = (await res.json()) as { + hits?: Array<{ + title?: string; + url?: string; + author?: string; + points?: number; + }>; + }; + for (const hit of data.hits ?? []) { + const rawTitle = hit.title ?? ""; + // Match "Launch HN: Company (YC W26) – Summary" + const cleaned = rawTitle.replace(/^Launch HN:\s*/i, ""); + const parts = cleaned.split(/[\u2013\u2014\-:]/); + const nameWithBatch = (parts[0] ?? "").trim(); + const summary = + parts.slice(1).join(" ").trim() || + "Developer Infrastructure & AI Tooling"; + const batchMatch = nameWithBatch.match(/\((YC [^)]+)\)/i); + const companyName = nameWithBatch + .replace(/\s*\([^)]*\)\s*/g, "") + .trim(); + if (!companyName || companyName.length < 2) { + continue; + } + + let domain = "example.com"; + if (hit.url) { + try { + domain = new URL(hit.url).hostname.replace(/^www\./, ""); + } catch { + // fallback + } + } + const website = hit.url || `https://${domain}`; + const { concepts, outreachDraftSubject, outreachDraftBody } = + buildTailoredConcepts(companyName, "AI / Developer Tools", summary); + + prospects.push({ + companyName, + website, + contactName: hit.author ? `${hit.author} (Founder)` : "Founding Team", + contactEmail: `founders@${domain}`, + category: sanitizeDashes(summary.slice(0, 80)), + fundingRound: batchMatch ? batchMatch[1] : "YC / Launch HN", + source: "yc", + relevanceScore: Math.min( + 98, + 75 + Math.round((hit.points ?? 20) / 10), + ), + videoConcepts: concepts, + outreachDraftSubject, + outreachDraftBody, + }); + } + } + } catch { + // Fallback curated prospects below + } + + if (prospects.length === 0) { + const fallbackSeeds = [ + { + companyName: "SuperPlane", + website: "https://superplane.com", + contactName: "Larisa (Growth)", + contactEmail: "larisa@superplane.com", + category: "Open-Source DevOps & Cloud Orchestration", + fundingRound: "GA Launch Oct 2026", + source: "curated" as const, + }, + { + companyName: "Tessen ZERO", + website: "https://tessen.ai", + contactName: "Yannick Parfait", + contactEmail: "hello@tessen.ai", + category: "Autonomous Security & AI Agent Guardrails", + fundingRound: "Seed Launch", + source: "curated" as const, + }, + ]; + for (const seed of fallbackSeeds) { + const { concepts, outreachDraftSubject, outreachDraftBody } = + buildTailoredConcepts(seed.companyName, seed.category, seed.category); + prospects.push({ + ...seed, + relevanceScore: 95, + videoConcepts: concepts, + outreachDraftSubject, + outreachDraftBody, + }); + } + } + + return prospects; +} + +export const GET: APIRoute = async () => { + const prospects = await discoverLaunchHnProspects(); + return new Response( + JSON.stringify({ + success: true, + count: prospects.length, + prospects, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); +}; + +export const POST: APIRoute = async ({ request }) => { + let body: { + action?: "discover" | "send_outreach"; + companyName?: string; + website?: string; + contactName?: string; + contactEmail?: string; + category?: string; + fundingRound?: string; + subject?: string; + message?: string; + } = {}; + + try { + body = await request.json(); + } catch { + // Default to discover + } + + if (body.action === "send_outreach" && body.contactEmail) { + const { concepts, outreachDraftSubject, outreachDraftBody } = + buildTailoredConcepts( + body.companyName || "Partner", + body.category || "Developer Tooling", + body.category || "Developer Tooling", + ); + + const subject = sanitizeDashes(body.subject || outreachDraftSubject); + const text = sanitizeDashes(body.message || outreachDraftBody); + + const sent = await sendCodingCatEmail({ + to: body.contactEmail, + subject, + text, + }); + + const { client, canWrite } = getSanityServerClient(); + if (canWrite && body.companyName) { + try { + const slugId = body.companyName + .toLowerCase() + .replace(/[^a-z0-9]+/g, "-"); + await client.createOrReplace({ + _id: `sponsorPool-${slugId}`, + _type: "sponsorPool", + companyName: body.companyName, + contactName: body.contactName || "Founding Team", + contactEmail: body.contactEmail, + ...(body.website ? { website: body.website } : {}), + category: body.category || "AI / DevTools", + fundingRound: body.fundingRound || "Seed / Launch", + source: "yc", + relevanceScore: 92, + videoConcepts: concepts.map((c, idx) => ({ + _key: `vc-${idx}`, + ...c, + })), + outreachDraftSubject: subject, + outreachDraftBody: text, + lastContactedAt: new Date().toISOString(), + optedOut: false, + }); + } catch { + // Non-fatal + } + } + + return new Response( + JSON.stringify({ + success: true, + emailSentViaCloudflare: sent, + subject, + text, + videoConcepts: concepts, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + + const prospects = await discoverLaunchHnProspects(); + const { client, canWrite } = getSanityServerClient(); + + if (canWrite) { + for (const p of prospects.slice(0, 6)) { + try { + const slugId = p.companyName.toLowerCase().replace(/[^a-z0-9]+/g, "-"); + await client.createOrReplace({ + _id: `sponsorPool-${slugId}`, + _type: "sponsorPool", + companyName: p.companyName, + contactName: p.contactName, + contactEmail: p.contactEmail, + website: p.website, + category: p.category, + fundingRound: p.fundingRound, + source: p.source, + relevanceScore: p.relevanceScore, + videoConcepts: p.videoConcepts.map((c, idx) => ({ + _key: `vc-${idx}`, + ...c, + })), + outreachDraftSubject: p.outreachDraftSubject, + outreachDraftBody: p.outreachDraftBody, + optedOut: false, + }); + } catch { + // Non-fatal + } + } + } + + return new Response( + JSON.stringify({ + success: true, + savedToSanity: canWrite, + count: prospects.length, + prospects, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); +}; diff --git a/apps/site/src/pages/api/sponsorship.ts b/apps/site/src/pages/api/sponsorship.ts index 6c771093..c2d6b054 100644 --- a/apps/site/src/pages/api/sponsorship.ts +++ b/apps/site/src/pages/api/sponsorship.ts @@ -1,5 +1,14 @@ import { env } from "cloudflare:workers"; import type { APIRoute } from "astro"; +import { sendCodingCatEmail } from "@/lib/email"; +import { + getSanityServerClient, + resolveSponsorTier, +} from "@/lib/sanity/write-client"; +import { + createStripeMilestoneCheckout, + createStripeMilestoneInvoice, +} from "@/lib/stripe"; interface SponsorshipPayload { name?: string; @@ -7,34 +16,14 @@ interface SponsorshipPayload { email?: string; company?: string; companyName?: string; + website?: string; tier?: string | string[]; sponsorshipTier?: string | string[]; + paymentPreference?: "checkout" | "invoice"; message?: string; } -function getTierBudgetCents(tier: string | string[] | undefined): number { - const raw = ( - Array.isArray(tier) ? tier.join(" ") : String(tier ?? "") - ).toLowerCase(); - if (raw.includes("dedicated")) { - return 400000; - } - if (raw.includes("mid-roll") || raw.includes("integrated")) { - return 180000; - } - if (raw.includes("shout-out")) { - return 90000; - } - if (raw.includes("newsletter") || raw.includes("blog")) { - return 50000; - } - if (raw.includes("series") || raw.includes("custom")) { - return 500000; - } - return 150000; -} - -export const POST: APIRoute = async ({ request }) => { +export const POST: APIRoute = async ({ request, url }) => { let data: SponsorshipPayload = {}; const contentType = request.headers.get("content-type") ?? ""; @@ -92,8 +81,10 @@ export const POST: APIRoute = async ({ request }) => { const name = (data.name ?? data.fullName)?.trim(); const email = data.email?.trim(); - const company = (data.company ?? data.companyName)?.trim(); - const tier = data.tier ?? data.sponsorshipTier; + const company = + (data.company ?? data.companyName)?.trim() || name || "Sponsor Partner"; + const website = data.website?.trim(); + const rawTier = data.tier ?? data.sponsorshipTier; const message = data.message?.trim(); if (!name) { @@ -120,8 +111,8 @@ export const POST: APIRoute = async ({ request }) => { } const hasTier = - (typeof tier === "string" && tier.trim().length > 0) || - (Array.isArray(tier) && tier.length > 0); + (typeof rawTier === "string" && rawTier.trim().length > 0) || + (Array.isArray(rawTier) && rawTier.length > 0); if (!hasTier) { return new Response( @@ -136,66 +127,128 @@ export const POST: APIRoute = async ({ request }) => { ); } - const targetPlacements = Array.isArray(tier) ? tier : [String(tier)]; - const proposedBudget = getTierBudgetCents(tier); + const selectedTier = resolveSponsorTier(rawTier); + const portalToken = `spt_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`; + const leadId = `sponsorLead-${portalToken}`; - const flipcoreUrl = - (env as unknown as Record)?.FLIPCORE_API_URL ?? - "https://api.flipfeeds.com"; - const flipcoreWebUrl = - (env as unknown as Record) - ?.PUBLIC_FLIPCORE_WEB_URL ?? "https://flipfeeds.com"; + const workerEnv = (env ?? {}) as unknown as Record< + string, + string | undefined + >; + const fallbackOrigin = url?.origin ?? new URL(request.url).origin; + const siteUrl = (workerEnv.SITE_URL || fallbackOrigin).replace(/\/$/, ""); - const fallbackToken = `spt_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`; - let portalToken = fallbackToken; - let portalUrl = `${flipcoreWebUrl}/codingcatdev/portal/${fallbackToken}`; - let dealId = `deal_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`; - let inquiryId = `inq_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`; + // 1. Optional Stripe 50% Upfront Deposit Checkout + Net-15 Invoice generation + let stripeDepositCheckoutUrl: string | undefined; + let stripeDepositInvoiceId: string | undefined; + let stripeDepositInvoiceUrl: string | undefined; try { - const flipcoreRes = await fetch(`${flipcoreUrl}/v1/deals/inquiries`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - sponsorName: name, - sponsorEmail: email, - companyName: company ?? undefined, - targetPlacements, - proposedBudget, - currency: "USD", - message: message ?? undefined, - slug: "codingcatdev", + const [checkout, invoice] = await Promise.all([ + createStripeMilestoneCheckout({ + leadId, + portalToken, + companyName: company, + contactEmail: email, + packageName: selectedTier.name, + milestone: "deposit", + amountCents: selectedTier.depositAmountCents, + siteUrl, }), - }); + createStripeMilestoneInvoice({ + leadId, + portalToken, + companyName: company, + contactEmail: email, + packageName: selectedTier.name, + milestone: "deposit", + amountCents: selectedTier.depositAmountCents, + siteUrl, + daysUntilDue: 15, + }), + ]); + stripeDepositCheckoutUrl = checkout?.checkoutUrl; + stripeDepositInvoiceId = invoice?.invoiceId; + stripeDepositInvoiceUrl = invoice?.hostedInvoiceUrl; + } catch { + // Non-fatal if Stripe is not configured in local dev + } - if (flipcoreRes.ok) { - const resData = (await flipcoreRes.json()) as any; - if (resData.portalUrl) { - portalUrl = resData.portalUrl; - } - if (resData.portalToken) { - portalToken = resData.portalToken; - } - if (resData.dealId) { - dealId = resData.dealId; - } - if (resData.inquiryId) { - inquiryId = resData.inquiryId; - } + // 2. Persist sponsorLead directly in Sanity + const { client, canWrite } = getSanityServerClient(); + if (canWrite) { + try { + await client.createOrReplace({ + _id: leadId, + _type: "sponsorLead", + companyName: company, + contactName: name, + contactEmail: email, + ...(website ? { website } : {}), + portalToken, + source: "inbound", + status: + stripeDepositCheckoutUrl || stripeDepositInvoiceUrl + ? "deposit_sent" + : "new", + rateCard: selectedTier.id, + totalAmountCents: selectedTier.totalAmountCents, + depositAmountCents: selectedTier.depositAmountCents, + balanceAmountCents: selectedTier.balanceAmountCents, + ...(stripeDepositCheckoutUrl ? { stripeDepositCheckoutUrl } : {}), + ...(stripeDepositInvoiceId ? { stripeDepositInvoiceId } : {}), + ...(stripeDepositInvoiceUrl ? { stripeDepositInvoiceUrl } : {}), + intent: + message || + `Selected package: ${selectedTier.name} ($${selectedTier.priceUsd})`, + deliverables: selectedTier.deliverables.map((d, idx) => ({ + _key: `deliv-${idx}`, + title: d.title, + channel: d.channel, + status: "pending", + })), + lastEmailAt: new Date().toISOString(), + }); + } catch { + // Fallback to stateless Deal Room params if write token lacks permissions } - } catch { - // Resilient fallback: uses pre-generated live Deal Room URL } + const queryFallback = new URLSearchParams({ + company, + name, + email, + tier: selectedTier.id, + }); + const portalUrl = `${siteUrl}/portal/${portalToken}?${queryFallback.toString()}`; + + // 3. Fire-and-forget notification email via Cloudflare Email Routing if bound + await sendCodingCatEmail({ + to: "alex.patterson@gmail.com", + subject: `[CodingCat.dev Sponsor Inquiry] ${company} (${selectedTier.name} - $${selectedTier.priceUsd})`, + text: [ + "New Sponsorship Inquiry on CodingCat.dev", + `Company: ${company}`, + `Contact: ${name} <${email}>`, + `Package: ${selectedTier.name} ($${selectedTier.priceUsd} total / $${selectedTier.depositAmountCents / 100} 50% deposit)`, + `Deal Room URL: ${portalUrl}`, + `Message: ${message || "(none)"}`, + ].join("\n"), + }); + return new Response( JSON.stringify({ success: true, message: "Inquiry received", - inquiryId, - dealId, + inquiryId: leadId, + dealId: leadId, portalToken, portalUrl, dealRoomUrl: portalUrl, + depositAmountUsd: selectedTier.depositAmountCents / 100, + totalAmountUsd: selectedTier.priceUsd, + stripeDepositCheckoutUrl, + stripeDepositInvoiceUrl, }), { status: 200, diff --git a/apps/site/src/pages/api/stripe/milestone.ts b/apps/site/src/pages/api/stripe/milestone.ts new file mode 100644 index 00000000..75d31ce1 --- /dev/null +++ b/apps/site/src/pages/api/stripe/milestone.ts @@ -0,0 +1,167 @@ +import { env } from "cloudflare:workers"; +import type { APIRoute } from "astro"; +import { + getSanityServerClient, + resolveSponsorTier, +} from "@/lib/sanity/write-client"; +import { + createStripeMilestoneCheckout, + createStripeMilestoneInvoice, +} from "@/lib/stripe"; + +export const POST: APIRoute = async ({ request, url }) => { + let body: { + portalToken?: string; + leadId?: string; + companyName?: string; + contactEmail?: string; + tier?: string; + milestone?: "deposit" | "balance"; + mode?: "checkout" | "invoice"; + daysUntilDue?: number; + } = {}; + + try { + body = await request.json(); + } catch { + return new Response( + JSON.stringify({ success: false, message: "Invalid JSON body" }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ); + } + + const portalToken = + body.portalToken || + `spt_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`; + const leadId = body.leadId || `sponsorLead-${portalToken}`; + const milestone = body.milestone || "deposit"; + const mode = body.mode || "checkout"; + const tier = resolveSponsorTier(body.tier); + const amountCents = + milestone === "deposit" ? tier.depositAmountCents : tier.balanceAmountCents; + + const companyName = body.companyName || "Sponsor Partner"; + const contactEmail = body.contactEmail || "alex@codingcat.dev"; + + const workerEnv = (env ?? {}) as unknown as Record< + string, + string | undefined + >; + const fallbackOrigin = url?.origin ?? new URL(request.url).origin; + const siteUrl = (workerEnv.SITE_URL || fallbackOrigin).replace(/\/$/, ""); + + if (mode === "invoice") { + const invoice = await createStripeMilestoneInvoice({ + leadId, + portalToken, + companyName, + contactEmail, + packageName: tier.name, + milestone, + amountCents, + siteUrl, + daysUntilDue: body.daysUntilDue ?? 15, + }); + + if (!invoice) { + return new Response( + JSON.stringify({ + success: false, + message: + "Stripe secret key not configured or invoice creation failed.", + }), + { status: 503, headers: { "Content-Type": "application/json" } }, + ); + } + + const { client, canWrite } = getSanityServerClient(); + if (canWrite) { + try { + await client + .patch(leadId) + .set( + milestone === "deposit" + ? { + stripeDepositInvoiceId: invoice.invoiceId, + stripeDepositInvoiceUrl: invoice.hostedInvoiceUrl, + status: "deposit_sent", + } + : { + stripeBalanceInvoiceId: invoice.invoiceId, + stripeBalanceInvoiceUrl: invoice.hostedInvoiceUrl, + status: "balance_sent", + }, + ) + .commit(); + } catch { + // Non-fatal + } + } + + return new Response( + JSON.stringify({ + success: true, + milestone, + mode: "invoice", + invoiceId: invoice.invoiceId, + url: invoice.hostedInvoiceUrl, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + + const checkout = await createStripeMilestoneCheckout({ + leadId, + portalToken, + companyName, + contactEmail, + packageName: tier.name, + milestone, + amountCents, + siteUrl, + }); + + if (!checkout) { + return new Response( + JSON.stringify({ + success: false, + message: + "Stripe secret key not configured or checkout creation failed.", + }), + { status: 503, headers: { "Content-Type": "application/json" } }, + ); + } + + const { client, canWrite } = getSanityServerClient(); + if (canWrite) { + try { + await client + .patch(leadId) + .set( + milestone === "deposit" + ? { + stripeDepositCheckoutUrl: checkout.checkoutUrl, + status: "deposit_sent", + } + : { + stripeBalanceCheckoutUrl: checkout.checkoutUrl, + status: "balance_sent", + }, + ) + .commit(); + } catch { + // Non-fatal + } + } + + return new Response( + JSON.stringify({ + success: true, + milestone, + mode: "checkout", + sessionId: checkout.sessionId, + url: checkout.checkoutUrl, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); +}; diff --git a/apps/site/src/pages/api/stripe/webhook.ts b/apps/site/src/pages/api/stripe/webhook.ts new file mode 100644 index 00000000..f1797d5f --- /dev/null +++ b/apps/site/src/pages/api/stripe/webhook.ts @@ -0,0 +1,111 @@ +import { env } from "cloudflare:workers"; +import type { APIRoute } from "astro"; +import { getSanityServerClient } from "@/lib/sanity/write-client"; +import { verifyStripeWebhookSignature } from "@/lib/stripe"; + +export const POST: APIRoute = async ({ request }) => { + const rawBody = await request.text(); + const sigHeader = request.headers.get("stripe-signature"); + + const workerEnv = (env ?? {}) as unknown as Record< + string, + string | undefined + >; + const webhookSecret = workerEnv.STRIPE_WEBHOOK_SECRET; + + if (webhookSecret) { + const valid = await verifyStripeWebhookSignature( + rawBody, + sigHeader, + webhookSecret, + ); + if (!valid) { + return new Response( + JSON.stringify({ error: "Invalid Stripe signature" }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ); + } + } + + let event: { + type?: string; + data?: { + object?: { + id?: string; + metadata?: { + leadId?: string; + portalToken?: string; + milestone?: "deposit" | "balance"; + companyName?: string; + }; + }; + }; + }; + + try { + event = JSON.parse(rawBody); + } catch { + return new Response(JSON.stringify({ error: "Invalid JSON" }), { + status: 400, + }); + } + + if ( + event.type === "checkout.session.completed" || + event.type === "invoice.paid" + ) { + const obj = event.data?.object; + const leadId = obj?.metadata?.leadId; + const milestone = obj?.metadata?.milestone || "deposit"; + const companyName = obj?.metadata?.companyName; + const nowIso = new Date().toISOString(); + + const { client, canWrite } = getSanityServerClient(); + if (canWrite && leadId) { + try { + if (milestone === "deposit") { + await client + .patch(leadId) + .set({ + status: "deposit_paid", + depositPaidAt: nowIso, + }) + .commit(); + } else { + // Final 50% balance paid -> mark paid in full and ensure public sponsor doc exists + const sponsorId = `sponsor-${leadId.replace(/^sponsorLead-/, "")}`; + if (companyName) { + await client.createIfNotExists({ + _id: sponsorId, + _type: "sponsor", + title: companyName, + description: `Official CodingCat.dev Launch Partner (${companyName})`, + }); + } + await client + .patch(leadId) + .set({ + status: "paid", + balancePaidAt: nowIso, + ...(companyName + ? { + sponsorDocId: { + _type: "reference", + _ref: sponsorId, + }, + } + : {}), + }) + .commit(); + } + } catch { + // Non-fatal + } + } + } + + return new Response(JSON.stringify({ received: true }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); +}; diff --git a/apps/site/src/pages/book.astro b/apps/site/src/pages/book.astro index a1ecb0ab..2ab24e13 100644 --- a/apps/site/src/pages/book.astro +++ b/apps/site/src/pages/book.astro @@ -2,102 +2,24 @@ import Breadcrumbs from "@/components/Breadcrumbs.astro"; import PodmatchBadge from "@/components/PodmatchBadge.astro"; import BaseLayout from "@/layouts/BaseLayout.astro"; -import { env } from "cloudflare:workers"; - -interface BookingPageDetails { - title: string; - description: string; - durationMinutes: number; - hostName: string; - slug: string; - slots: Array<{ - id: string; - startAt: string; - displayDate: string; - displayTime: string; - }>; -} - -let bookingDetails: BookingPageDetails = { - title: "CodingCat.dev Podcast Recording Session", - description: - "Join Alex Patterson for an in-depth conversation exploring web development, edge runtimes, developer tooling, and modern frameworks.", - durationMinutes: 45, - hostName: "Alex Patterson", - slug: "codingcatdev", - slots: [ - { - id: "slot-1", - startAt: "2026-10-15T14:00:00Z", - displayDate: "Thursday, Oct 15", - displayTime: "10:00 AM EDT (14:00 UTC)", - }, - { - id: "slot-2", - startAt: "2026-10-15T16:00:00Z", - displayDate: "Thursday, Oct 15", - displayTime: "12:00 PM EDT (16:00 UTC)", - }, - { - id: "slot-3", - startAt: "2026-10-20T14:00:00Z", - displayDate: "Tuesday, Oct 20", - displayTime: "10:00 AM EDT (14:00 UTC)", - }, - { - id: "slot-4", - startAt: "2026-10-20T16:00:00Z", - displayDate: "Tuesday, Oct 20", - displayTime: "12:00 PM EDT (16:00 UTC)", - }, - { - id: "slot-5", - startAt: "2026-10-22T15:00:00Z", - displayDate: "Thursday, Oct 22", - displayTime: "11:00 AM EDT (15:00 UTC)", - }, - ], -}; - -try { - const flipcoreUrl = - (env as unknown as Record)?.FLIPCORE_API_URL ?? - "https://api.flipfeeds.com"; - const res = await fetch(`${flipcoreUrl}/api/public/booking/codingcatdev`, { - headers: { Accept: "application/json" }, - }); - if (res.ok) { - const data = (await res.json()) as any; - if (data?.title) bookingDetails.title = data.title; - if (data?.description) bookingDetails.description = data.description; - if (data?.durationMinutes) bookingDetails.durationMinutes = data.durationMinutes; - if (Array.isArray(data?.slots) && data.slots.length > 0) { - bookingDetails.slots = data.slots.map((s: any, idx: number) => { - const date = new Date(s.startAt); - return { - id: `slot-api-${idx}`, - startAt: s.startAt, - displayDate: date.toLocaleDateString("en-US", { - weekday: "short", - month: "short", - day: "numeric", - }), - displayTime: date.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - timeZoneName: "short", - }), - }; - }); - } - } -} catch { - // Resilient fallback defaults used -} - -const title = "Book a Recording Session — CodingCat.dev"; +import { getMultiCalendarAvailableSlots } from "@/lib/google-calendar"; + +const searchParams = Astro.url.searchParams; +const initialTrack = + searchParams.get("track") === "sponsored" + ? "sponsored" + : searchParams.get("track") === "full-stack-launch" + ? "full-stack-launch" + : "editorial"; +const initialCompany = searchParams.get("company") || ""; +const initialName = searchParams.get("name") || ""; +const initialEmail = searchParams.get("email") || ""; + +const { slots: upcomingSlots, liveVerified } = await getMultiCalendarAvailableSlots(6); + +const title = "Book a Podcast & Showcase Session — CodingCat.dev"; const description = - "Schedule your guest appearance on the CodingCat.dev Podcast. Choose your recording slot, complete intake questions, and sign your digital release form."; + "Schedule your guest appearance or fast-track sponsored showcase on the CodingCat.dev Podcast. Choose your track, complete intake questions, and sign your E-SIGN digital media release."; --- @@ -107,11 +29,14 @@ const description =
+ + CodingCat.dev Studio & Google Calendar Sync +

- {bookingDetails.title} + CodingCat.dev Podcast Recording Session

- {bookingDetails.description} + Join Alex Patterson for an in-depth conversation exploring AI agents, web engineering, cloud infrastructure, and modern developer tooling.

@@ -119,38 +44,119 @@ const description =
Host: - {bookingDetails.hostName} + Alex Patterson
Duration: - {bookingDetails.durationMinutes} Minutes + 45 Minutes
- Studio Engine: - FlipFeeds 4K Local Ingest + Recording Window: + 8:00 AM – 5:00 PM PT / ET
- Two-Way Sync: - Google Calendar & Studio Room + Calendar Sync: + Google Calendar Appointment Schedule
- +

1 - Select an Available Recording Slot + Choose Your Session Track +

+ Editorial vs. Guaranteed Launch Showcase +
+ +
+ + + +
+
+ + +
+
+

+ 2 + Select Preferred Recording Window + {liveVerified && ( + + ✓ Live Multi-Calendar FreeBusy Verified + + )}

- All times normalized to UTC + + View full live Google Calendar schedule ↗ +
{ - bookingDetails.slots.map((slot, index) => ( + upcomingSlots.map((slot, index) => (
@@ -231,7 +239,8 @@ const description =
@@ -242,21 +251,21 @@ const description =

- 3 + 4 Digital Appearance & Media Release Agreement

- Terms v1.0 · E-SIGN Valid + Terms v1.0 · E-SIGN Valid
- FLIPCORE & CODINGCAT.DEV PODCAST APPEARANCE RELEASE AGREEMENT (v1.0)

- 1. GRANT OF RIGHTS: The Participant grants CodingCat.dev and FlipFeeds the irrevocable, worldwide, perpetual right to record, edit, broadcast, stream, and commercially distribute their voice, video likeness, conversation, and biographical materials across any digital distribution channels.

- 2. CLIPPING & TRANSCRIPTS: The Producer retains sole discretion to create derivative works, short-form clips (TikTok, Shorts, Reels), and word-by-word transcripts.

+ CODINGCAT.DEV PODCAST APPEARANCE RELEASE AGREEMENT (v1.0)

+ 1. GRANT OF RIGHTS: The Participant grants CodingCat.dev the irrevocable, worldwide, perpetual right to record, edit, broadcast, stream, and commercially distribute their voice, video likeness, conversation, and biographical materials across any digital distribution channels.

+ 2. CLIPPING & TRANSCRIPTS: The Producer retains sole discretion to create derivative works, short-form clips (YouTube Shorts, X, LinkedIn, Bluesky, Threads, Mastodon, TikTok, Reels), and word-by-word transcripts.

3. COMMERCIAL & SPONSORSHIP USE: Producer may monetize the show through sponsorships and dynamic ad insertion without financial compensation unless agreed separately.

- 4. ELECTRONIC SIGNATURE VALIDITY: Executing this release electronically carries the full legal force of a handwritten physical signature pursuant to 15 U.S.C. § 7001 (E-SIGN Act). + 4. ELECTRONIC SIGNATURE VALIDITY: Executing this release electronically carries the full legal force of a handwritten physical signature pursuant to 15 U.S.C. § 7001 (E-SIGN Act).
@@ -271,7 +280,7 @@ const description = @@ -345,30 +354,29 @@ const description =

- Recording Session Confirmed! + Intake & Media Release Recorded!

- Your technical interview with Alex Patterson has been reserved. Your calendar invite and Studio entry pass have been generated. + Your E-SIGN appearance release with Alex Patterson has been saved to Sanity. Lock in your exact Google Calendar invite below.

- +
- Your Studio Entry Pass -
- - https://flipfeeds.com/studio?room=srt_codingcatdev_preview - - -
+ + Step 2: Lock Your Google Calendar Invite + + + Open Google Calendar Appointment Schedule ↗ +

- Please join 5 minutes before scheduled time using Chrome, Edge, or Brave with headphones enabled. + Selecting your time on the Google Calendar schedule automatically sends calendar invites and recording links to both you and Alex.

@@ -392,24 +400,40 @@ const description =
PodMatch Network Certified - Powered by FlipFeeds Two-Sided Creator & Guest Matching + Powered by CodingCat.dev & Sanity Content Operating System
- diff --git a/apps/site/src/pages/portal/[token].astro b/apps/site/src/pages/portal/[token].astro new file mode 100644 index 00000000..a3f4d038 --- /dev/null +++ b/apps/site/src/pages/portal/[token].astro @@ -0,0 +1,533 @@ +--- +import Breadcrumbs from "@/components/Breadcrumbs.astro"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import { + getSanityServerClient, + resolveSponsorTier, +} from "@/lib/sanity/write-client"; + +const { token } = Astro.params; +const searchParams = Astro.url.searchParams; + +interface DeliverableItem { + title: string; + channel: string; + status: string; + url?: string; + views?: number; + publishedAt?: string; +} + +interface LeadPortalData { + _id: string; + portalToken: string; + companyName: string; + contactName: string; + contactEmail: string; + status: string; + rateCard: string; + packageName: string; + packageDescription: string; + totalAmountCents: number; + depositAmountCents: number; + balanceAmountCents: number; + depositPaidAt?: string; + balancePaidAt?: string; + stripeDepositCheckoutUrl?: string; + stripeDepositInvoiceUrl?: string; + stripeBalanceCheckoutUrl?: string; + stripeBalanceInvoiceUrl?: string; + deliverables: DeliverableItem[]; +} + +const fallbackTier = resolveSponsorTier(searchParams.get("tier") || "full-stack-launch"); +const fallbackCompany = searchParams.get("company") || "Sponsor Partner"; +const fallbackName = searchParams.get("name") || "Sponsor Team"; +const fallbackEmail = searchParams.get("email") || ""; +const paymentNotice = searchParams.get("payment"); + +let portalData: LeadPortalData = { + _id: `sponsorLead-${token}`, + portalToken: token || "demo", + companyName: fallbackCompany, + contactName: fallbackName, + contactEmail: fallbackEmail, + status: paymentNotice === "deposit_success" ? "deposit_paid" : "new", + rateCard: fallbackTier.id, + packageName: fallbackTier.name, + packageDescription: fallbackTier.description, + totalAmountCents: fallbackTier.totalAmountCents, + depositAmountCents: fallbackTier.depositAmountCents, + balanceAmountCents: fallbackTier.balanceAmountCents, + deliverables: fallbackTier.deliverables.map((d) => ({ + title: d.title, + channel: d.channel, + status: paymentNotice === "deposit_success" ? "in-progress" : "pending", + })), +}; + +try { + const { client } = getSanityServerClient(); + const doc = await client.fetch | null>( + `*[_type == "sponsorLead" && (portalToken == $portalToken || _id == $leadId)][0]`, + { portalToken: token ?? "", leadId: `sponsorLead-${token ?? ""}` }, + ); + if (doc) { + const tier = resolveSponsorTier(doc.rateCard || fallbackTier.id); + portalData = { + _id: doc._id, + portalToken: doc.portalToken || token || "demo", + companyName: doc.companyName || fallbackCompany, + contactName: doc.contactName || fallbackName, + contactEmail: doc.contactEmail || fallbackEmail, + status: + paymentNotice === "deposit_success" + ? "deposit_paid" + : paymentNotice === "balance_success" + ? "paid" + : doc.status || "new", + rateCard: tier.id, + packageName: tier.name, + packageDescription: tier.description, + totalAmountCents: doc.totalAmountCents || tier.totalAmountCents, + depositAmountCents: doc.depositAmountCents || tier.depositAmountCents, + balanceAmountCents: doc.balanceAmountCents || tier.balanceAmountCents, + depositPaidAt: doc.depositPaidAt, + balancePaidAt: doc.balancePaidAt, + stripeDepositCheckoutUrl: doc.stripeDepositCheckoutUrl, + stripeDepositInvoiceUrl: doc.stripeDepositInvoiceUrl, + stripeBalanceCheckoutUrl: doc.stripeBalanceCheckoutUrl, + stripeBalanceInvoiceUrl: doc.stripeBalanceInvoiceUrl, + deliverables: + Array.isArray(doc.deliverables) && doc.deliverables.length > 0 + ? doc.deliverables + : tier.deliverables.map((d) => ({ + title: d.title, + channel: d.channel, + status: "pending", + })), + }; + } +} catch { + // Uses resilient fallback portalData +} + +const formatUsd = (cents: number) => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format(cents / 100); + +const isDepositPaid = + Boolean(portalData.depositPaidAt) || + portalData.status === "deposit_paid" || + portalData.status === "booked" || + portalData.status === "balance_sent" || + portalData.status === "paid"; + +const isBalancePaid = + Boolean(portalData.balancePaidAt) || portalData.status === "paid"; +--- + + + + +
+ { + paymentNotice === "deposit_success" && ( +
+ 50% Deposit Confirmed! Your production slot is locked. Next step: pick your kickoff or recording slot below. +
+ ) + } + { + paymentNotice === "balance_success" && ( +
+ Final 50% Balance Paid! Thank you for partnering with CodingCat.dev. +
+ ) + } + + +
+
+
+ CodingCat.dev Sponsor Deal Room + · + Token: {portalData.portalToken} +
+

+ {portalData.companyName} × CodingCat.dev +

+

+ {portalData.packageName} — {portalData.packageDescription} +

+
+ +
+ + Total Campaign Value + + + {formatUsd(portalData.totalAmountCents)} + + + 50% Deposit ({formatUsd(portalData.depositAmountCents)}) + 50% on Publish + +
+
+ + +
+
+ YouTube Subscribers + 61.2K+ + ✓ Verified YouTube API +
+
+ US / CA / UK / AU Share + 21.1% + ✓ #1 Watch-Time Engagement +
+
+ Short-Form Syndication + 7 Platforms + ✓ YT, X, LI, BSky, Threads, Masto, IG/TT +
+
+ Canonical SEO + AEO + WebMCP + llms.txt + ✓ Dev.to & Hashnode Syndicated +
+
+ + +
+ +
+
+
+

+ 1. Milestone Billing (50% / 50%) +

+ + Stripe Verified + +
+

+ Lock your production and recording window with a 50% upfront deposit via instant card/ACH checkout or a Net-15 corporate invoice. The remaining 50% balance is billed only when your deliverables go live. +

+ + +
+
+
+ + Milestone 1 · Upfront Deposit (50%) + +

+ Lock Production & Calendar Slot +

+
+ + {formatUsd(portalData.depositAmountCents)} + +
+ + { + isDepositPaid ? ( +
+ ✓ 50% Deposit Paid {portalData.depositPaidAt ? `on ${new Date(portalData.depositPaidAt).toLocaleDateString()}` : ""} +
+ ) : ( +
+ {portalData.stripeDepositCheckoutUrl ? ( + + Pay {formatUsd(portalData.depositAmountCents)} Deposit Now → + + ) : ( + + )} + + {portalData.stripeDepositInvoiceUrl ? ( + + View Net-15 Corporate Invoice ↗ + + ) : ( + + )} +
+ ) + } +
+ + +
+
+
+ + Milestone 2 · Final Balance on Publish (50%) + +

+ Due Upon Campaign Publication +

+
+ + {formatUsd(portalData.balanceAmountCents)} + +
+ + { + isBalancePaid ? ( +
+ ✓ Paid in Full +
+ ) : ( +
+ + +
+ ) + } +
+ + +
+
+ + +
+ +
+
+ + +
+
+
+

+ 3. Multi-Channel Deliverables & Live ROI Tracking +

+

+ Every asset in your bundle is tracked here with live links and verified impression telemetry as it ships. +

+
+ + {portalData.deliverables.length} Deliverables + +
+ +
+ { + portalData.deliverables.map((item) => ( +
+
+ + {item.title} + + + Channel: {item.channel} + +
+
+ {typeof item.views === "number" && ( + + {item.views.toLocaleString()} views + + )} + {item.url && ( + + View Live ↗ + + )} + + {item.status.replace("-", " ")} + +
+
+ )) + } +
+
+
+
+ + diff --git a/apps/site/src/pages/sponsorships.astro b/apps/site/src/pages/sponsorships.astro index a9c812a8..145f3074 100644 --- a/apps/site/src/pages/sponsorships.astro +++ b/apps/site/src/pages/sponsorships.astro @@ -1,143 +1,134 @@ --- import Breadcrumbs from "@/components/Breadcrumbs.astro"; import BaseLayout from "@/layouts/BaseLayout.astro"; -import { env } from "cloudflare:workers"; +import { SPONSOR_PACKAGE_TIERS } from "@/lib/sanity/write-client"; -interface VerifiedMetrics { - subscribers: string; - monthlyViews: string; - reach: string; -} - -let metrics: VerifiedMetrics = { - subscribers: "61K+", - monthlyViews: "350K+", - reach: "150K+", +const metrics = { + subscribers: "61.2K+", + totalViews: "405K+", + tier1GeoShare: "21.1%", + shortPlatforms: "7 Platforms", }; -try { - const flipcoreUrl = - (env as unknown as Record)?.FLIPCORE_API_URL ?? - "https://api.flipfeeds.com"; - const kitRes = await fetch(`${flipcoreUrl}/api/public/media_kit/codingcatdev`, { - headers: { Accept: "application/json" }, - }); - if (kitRes.ok) { - const kitData = (await kitRes.json()) as any; - if (kitData?.stats && Array.isArray(kitData.stats)) { - for (const s of kitData.stats) { - const label = (s.label ?? s.name ?? "").toLowerCase(); - if (label.includes("sub") && s.displayValue) metrics.subscribers = s.displayValue; - if (label.includes("view") && s.displayValue) metrics.monthlyViews = s.displayValue; - if (label.includes("reach") && s.displayValue) metrics.reach = s.displayValue; - } - } - } -} catch { - // Resilient fallback defaults -} - -const sponsorshipTiers = [ - { - id: "dedicated-video", - name: "Dedicated Video", - price: "$4,000", - description: - "A full video dedicated to your product or service. Includes a permanent logo and link on our sponsors page.", - }, - { - id: "mid-roll-ad", - name: "Integrated Mid-Roll Ad", - price: "$1,800", - description: - "A 60-second ad integrated into the middle of a video. Includes a permanent logo and link on our sponsors page.", - }, - { - id: "shout-out", - name: "Quick Shout-Out", - price: "$900", - description: - "A 30-second shout-out at the beginning of a video. Includes a permanent logo and link on our sponsors page.", - }, - { - id: "blog-newsletter", - name: "Blog Post / Newsletter", - price: "$500", - description: - "Sponsor a blog post or our weekly newsletter. Your logo and a link will be featured.", - }, - { - id: "video-series", - name: "Video Series", - price: "Custom", - description: - "Sponsor a whole series of videos. Contact us for custom pricing and tailored packages.", - }, -]; +const formatUsd = (cents: number) => + new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: 0, + }).format(cents / 100); ---
-
+
+ + B2B Developer Marketing & Multi-Channel Distribution +

- Sponsor CodingCat.dev + Launch Your DevTool Across CodingCat.dev

-

- Reach an engaged global audience of developers, creators, and technology leaders. Choose from our standard packages or request custom collaboration. +

+ Stop buying isolated one-off shout-outs. Partner with Alex Patterson for end-to-end technical deep-dives, 7-platform vertical Shorts syndication, canonical SEO & Agent-Ready (AEO / WebMCP / llms.txt) articles, and live ROI Deal Room telemetry.

- -
+ +
YouTube Subscribers {metrics.subscribers} - ✓ Verified via FlipFeeds + ✓ Verified First-Party YouTube API +
+
+ US / CA / UK / AU Share + {metrics.tier1GeoShare} + ✓ #1 Watch-Time Engagement per Viewer
- Monthly Impressions - {metrics.reach} - ✓ Real-time Analytics + Short-Form Video Network + {metrics.shortPlatforms} + ✓ YT, X, LinkedIn, BSky, Threads, Masto, IG/TT
- Monthly Video Views - {metrics.monthlyViews} - ✓ First-Party Telemetry + Canonical SEO & AEO + WebMCP + llms.txt + ✓ Syndicated to Dev.to & Hashnode
-
-

Sponsorship Tiers

+
+
+
+

+ Multi-Channel Launch Bundles & Rate Card +

+

+ All packages use milestone billing: 50% upfront deposit to lock your production & calendar slot, and 50% upon publication. (Includes Dedicated Video, Integrated Mid-Roll Ad, Quick Shout-Out, Blog Post / Newsletter, and Video Series options.) +

+
+ + Prefer to talk first? Book a 15-min Sponsor Discovery Call ↗ + +
+
{ - sponsorshipTiers.map((tier) => ( -
+ SPONSOR_PACKAGE_TIERS.map((tier, idx) => ( +
-

+ + {tier.tagline} + +

{tier.name}

-

- {tier.price} -

+
+

+ {formatUsd(tier.totalAmountCents)} +

+ + ({formatUsd(tier.depositAmountCents)} 50% deposit) + +

{tier.description}

+
    + {tier.deliverables.map((d) => ( +
  • + ✓ + {d.title} +
  • + ))} +
-
+
@@ -146,13 +137,13 @@ const sponsorshipTiers = [
- +

- Inquire About Sponsorship + Open Your Sponsor Deal Room

- Fill out the form below with your sponsorship goals, and our team will get back to you promptly. + Select your package below to immediately open your dedicated Deal Room with 50% deposit Stripe Checkout, Net-15 corporate invoice generation, and direct Google Calendar scheduling.

-
- - -
+
+
+ + +
-
- - +
+ + +
-
- - +
+
+ + +
+ +
+ + +
@@ -261,9 +271,9 @@ const sponsorshipTiers = [
@@ -277,12 +287,11 @@ const sponsorshipTiers = [ const submitBtn = document.getElementById("submit-btn") as HTMLButtonElement | null; const tierSelect = document.getElementById("tier") as HTMLSelectElement | null; - // Click to auto-select tier from cards above document.querySelectorAll(".tier-select-btn").forEach((btn) => { btn.addEventListener("click", () => { - const tierName = btn.getAttribute("data-tier-name"); - if (tierSelect && tierName) { - tierSelect.value = tierName; + const tierId = btn.getAttribute("data-tier-id"); + if (tierSelect && tierId) { + tierSelect.value = tierId; tierSelect.scrollIntoView({ behavior: "smooth", block: "center" }); tierSelect.focus(); } @@ -300,12 +309,13 @@ const sponsorshipTiers = [ name: formData.get("name"), email: formData.get("email"), company: formData.get("company"), + website: formData.get("website"), tier: formData.get("tier"), message: formData.get("message"), }; submitBtn.disabled = true; - submitBtn.textContent = "Sending..."; + submitBtn.textContent = "Opening Deal Room..."; try { const res = await fetch("/api/sponsorship", { @@ -326,7 +336,7 @@ const sponsorshipTiers = [ const targetUrl = result.portalUrl || result.dealRoomUrl; if (targetUrl) { if (successEl) { - successEl.innerHTML = `Inquiry received! Opening your Deal Room portal...
Click here if not redirected automatically →`; + successEl.innerHTML = `Deal Room created! Opening your Deal Room portal...
Click here if not redirected automatically →`; successEl.classList.remove("hidden"); } window.location.href = targetUrl; @@ -339,14 +349,14 @@ const sponsorshipTiers = [ errorEl.classList.remove("hidden"); } } - } catch (err) { + } catch { if (errorEl) { errorEl.textContent = "A network error occurred. Please try again."; errorEl.classList.remove("hidden"); } } finally { submitBtn.disabled = false; - submitBtn.textContent = "Send Inquiry"; + submitBtn.textContent = "Create Deal Room & View Deposit Options →"; } }); } diff --git a/apps/site/wrangler.jsonc b/apps/site/wrangler.jsonc index 3a9fbe0e..e31058a5 100644 --- a/apps/site/wrangler.jsonc +++ b/apps/site/wrangler.jsonc @@ -12,6 +12,7 @@ "workers_dev": true, "preview_urls": true, "ai": { "binding": "AI" }, + "send_email": [{ "name": "EMAIL" }], // Uncomment after clicking "Enable Analytics Engine" once at: // https://dash.cloudflare.com/c46bd0f3b9ed47b8692cca098030edbf/workers/analytics-engine // "analytics_engine_datasets": [ @@ -58,6 +59,7 @@ } ], "ai": { "binding": "AI" }, + "send_email": [{ "name": "EMAIL" }], "d1_databases": [ { "binding": "DB", diff --git a/package.json b/package.json index b2bfac4f..16dbcbe2 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,10 @@ "scripts": { "dev": "pnpm --filter @codingcatdev/site dev", "dev:studio": "pnpm --filter @codingcatdev/sanity dev", + "dev:ops": "pnpm --filter @codingcatdev/ops dev", "build": "pnpm --filter @codingcatdev/site build", "build:studio": "pnpm --filter @codingcatdev/sanity build", + "build:ops": "pnpm --filter @codingcatdev/ops build", "deploy:studio": "pnpm --filter @codingcatdev/sanity deploy", "deploy:blueprints": "sanity blueprints deploy", "blueprints:deploy": "sanity blueprints deploy", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 30491ea4..dea8515b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,40 @@ importers: specifier: ^6.9.2 version: 6.9.2(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@noble/hashes@2.4.0)(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.2)(rolldown@1.2.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)))(@sanity/sdk@2.12.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))(xstate@5.32.5))(@types/node@25.5.0)(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(jiti@2.7.0)(prismjs@1.30.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.2.4)(styled-components@6.3.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(terser@5.48.0)(typescript@5.9.3) + apps/ops: + dependencies: + '@sanity/sdk': + specifier: ^1.0.0 + version: 1.0.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + '@sanity/sdk-react': + specifier: ^1.0.0 + version: 1.0.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + '@sanity/ui': + specifier: ^3.1.10 + version: 3.5.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(styled-components@6.3.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) + react: + specifier: ^19.2.0 + version: 19.2.8 + react-dom: + specifier: ^19.2.0 + version: 19.2.8(react@19.2.8) + sanity: + specifier: ^6.9.2 + version: 6.9.2(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@noble/hashes@2.4.0)(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.2)(rolldown@1.2.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)))(@sanity/sdk@1.0.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)))(@types/node@25.5.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(jiti@2.7.0)(prismjs@1.30.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.2.4)(styled-components@6.3.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(terser@5.48.0)(typescript@5.9.3) + styled-components: + specifier: ^6.3.11 + version: 6.3.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + devDependencies: + '@types/react': + specifier: ^19.2.0 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.0 + version: 19.2.3(@types/react@19.2.14) + typescript: + specifier: ^5.9.3 + version: 5.9.3 + apps/sanity: dependencies: '@portabletext/block-tools': @@ -2566,6 +2600,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + '@lezer/common@1.5.1': resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==} @@ -3246,6 +3283,14 @@ packages: resolution: {integrity: sha512-MBQ082q7GRvzSH7Xzx4ul1Kxm/AEKDgwTz5cd13oXIlenl3VAM0Un0QpeTjkC5IZTC7SHbnhwkgoVHTZDz9WKQ==} engines: {node: '>=18.0.0'} + '@sanity/comlink@2.0.5': + resolution: {integrity: sha512-6Rbg71hkeoGInk/9hBsCUBCZ33IHSs2fZynAR85ANkXDM+WYiwRDlker7OngBkfbK8TF9+G797VjNMQQgJINiQ==} + engines: {node: '>=18'} + + '@sanity/comlink@3.1.1': + resolution: {integrity: sha512-UyBJG4oWNs+VGVo5Yr5aKir5bgMzF/dnaNYjqxP2+5+iXnvhVOcI6dAtEXDj7kMmn5/ysHNKbLDlW6aVeBm7xg==} + engines: {node: '>=18'} + '@sanity/comlink@4.0.3': resolution: {integrity: sha512-gCnltbeB8BmXVLSr/6XHAA3tQmhJDMKXufOVRXYxAhIEv+5n9CtnxupjY5IzmdI+7v+G67TZV/8zCQhvaFlVww==} engines: {node: '>=20.19 <22 || >=22.12'} @@ -3337,6 +3382,10 @@ packages: '@sanity/media-library-types@1.6.0': resolution: {integrity: sha512-1Nqw8GSUr7E8AFue9luyTOM4Ev0ZlJ35SHcQrHZ6T68G5llfXw99e3GkLXzk6qYFk9r9fJaBTSpW5IyO2mObZQ==} + '@sanity/message-protocol@0.12.0': + resolution: {integrity: sha512-RMRWQG5yVkCZnnBHW3qxVbZGUOeXPBzFPdD9+pynQCTVZI7zYBEzjnY8lcSYjty+0unDHqeoqMPfBXhqs0rg2g==} + engines: {node: '>=20.0.0'} + '@sanity/message-protocol@0.23.0': resolution: {integrity: sha512-UfQDuWRzbK4dRTfLURGCZo7ZlR0sK+2lwT2QMAfqsM5kMq5GR31lbX4LMcSw27h7rwUv8ZSf1hBEbluVpgRmlg==} engines: {node: '>=20.0.0'} @@ -3349,6 +3398,10 @@ packages: resolution: {integrity: sha512-XeDbtq/YmpXwLd01wJU/mrNqXxPEKRZiPugt/ukXJoDdetjgBNmcIPkic6RnXpzHyLNf/Pi6+d5ktlrUvMYI2Q==} engines: {node: '>=22.12'} + '@sanity/mutate@0.12.6': + resolution: {integrity: sha512-Ai9Dy0C79yUALnuLe0ealwqgz11T+ngpWCzTyZv01xdjB6coQo+KoM8E0FeRTK5Zr/IAgKphYuYLU5DFCB9cGw==} + engines: {node: '>=18'} + '@sanity/mutate@0.16.1': resolution: {integrity: sha512-400OooNtiafgJEOCzj0E5atuWlaKp1z6LU/LB/xZUVVywNj3WuT52U6qeVOfGlZeWKhYMCdGFX2ZnMbIrME95w==} engines: {node: '>=18'} @@ -3395,6 +3448,17 @@ packages: '@sanity/schema@6.9.2': resolution: {integrity: sha512-k01v24uhByfKbOqgzlPlXVZ8yeW5qdzL3LtUUGgoikC1DEWBETY56Tc8u0R6eFmYPM8+I63L78TjbvtXYu2INQ==} + '@sanity/sdk-react@1.0.0': + resolution: {integrity: sha512-gZ4p5gh+2TaR22HZbrvXvPlR90onRyIVP84FRIgXcd2SDyPmqOfoCuZ/rpgYNhtU88ZwLPDWAh1+rMYJXxUNqg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@sanity/sdk@1.0.0': + resolution: {integrity: sha512-9IsaEboT+KIWnWASMZIBEarZP8E0NE+XSqQOUZ+0PZ3G5TRhgcLaq8D9hM5MRvNdVjxAY/QEFeGyduEs2hMSpw==} + engines: {node: '>=20.0.0'} + '@sanity/sdk@2.12.0': resolution: {integrity: sha512-WrQKzwuvpMA0Z/LKzrkpwisx+kdm+USNPiFJej54TWS7rTQPvf1JJhxr02rODcGKDbDgwCHGiOmV5GGr8cPAig==} @@ -3415,6 +3479,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + '@sanity/types@3.99.0': + resolution: {integrity: sha512-a766U9VSoyOSWq+RZz9wsEo/Nnn+inDkEcdGu+rHFuygdepullB/RZpF2MxNsfUMCSPnajgG1Tm9lhwbSmlySA==} + peerDependencies: + '@types/react': 18 || 19 + '@sanity/types@5.28.0': resolution: {integrity: sha512-kfLwYx/8q1l76Hqt2dMS/apoXskj6NB2LKnGIa+nDh0JfHArWror1j9Zwqpm/aF1ywS7R0I5xXBsiygCLeGszg==} peerDependencies: @@ -3425,6 +3494,14 @@ packages: peerDependencies: '@types/react': '*' + '@sanity/ui@3.5.5': + resolution: {integrity: sha512-5ehxYAXf8FlUKvTlMuEDMbz4keb8UhEnCyRnlp0lIcPF1EYODe6NdHjtgQqzQRKqS34OAVi2y+WAP/lLrx2+lg==} + engines: {node: '>=20.19 <22 || >=22.12'} + peerDependencies: + react: ^18 || >=19.0.0-0 + react-dom: ^18 || >=19.0.0-0 + styled-components: ^5.2 || ^6 + '@sanity/ui@4.0.3': resolution: {integrity: sha512-IXZ01joAs1YDKhzcvW5+F+3hL5BsDLH3HB7WqpBqfTitQ0jmvyXr8quHQnsP8br3JyxEREmTEAIJhS8ABEXPXw==} engines: {node: '>=22.12'} @@ -3739,6 +3816,12 @@ packages: '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.26': + resolution: {integrity: sha512-48NWfiY6oNcKOh47Z4h+sIvwcQR91h6KPhaYKgeMIrkT2jV82Gy90SErj60vW8jQbeMSOSbhIv3bZ7JoqPPVhA==} + '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} @@ -4908,6 +4991,17 @@ packages: react-dom: optional: true + framer-motion@13.5.1: + resolution: {integrity: sha512-HeWOaeHD3kyTSIEMT6+nw7fBn4saIfxxabZyAITbycAl4xr1kzGJKIvGt/OWghSlvIA9qTnZ1zTGelHAe6S8DA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -5790,9 +5884,15 @@ packages: motion-dom@13.0.0: resolution: {integrity: sha512-Xk+SJas70uMAUIApg+m3lZDShxI3LBFHq7mFGbBKoRXc2PVPDyAKmzN64Bbzt4CZdP/CItTiJxWtn4TA0v53Ng==} + motion-dom@13.5.1: + resolution: {integrity: sha512-/7CVCD20Mz1KobktorpdgvowV0V41J7UxqE+qWS3GkSJqrRr5ft+2RloOYtEKv+wvBp3JaSFBclhxt4ja8ovHg==} + motion-utils@13.0.0: resolution: {integrity: sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==} + motion-utils@13.5.0: + resolution: {integrity: sha512-Hqe/sqjIKliPZg8sVjWAkdySDPJNIeKH+euVkDOG8ri3DUUU7ZjndmHsGqEPz2iHpiyVaV5I22iguCMHU2he+Q==} + motion@13.1.0: resolution: {integrity: sha512-qtvscq59uCPdWnNW4SdSkrxR+BS/QYsa923bx7ocA+4p+ZGNbbVQwkSnG4aukB81QWjtl3AxX36plxNyZLmHCA==} peerDependencies: @@ -5804,6 +5904,17 @@ packages: react-dom: optional: true + motion@13.5.1: + resolution: {integrity: sha512-eV9SUem6KnzEa9+eT+dVBeDlhy5TTdn09YWR1WhUsKoZCJPw/n4TcYhjbxq2Gy5PTwDr//4H3r5vxkcZnNnO1w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + mrmime@2.0.1: resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} engines: {node: '>=10'} @@ -6185,6 +6296,16 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-compiler-runtime@1.0.0: + resolution: {integrity: sha512-rRfjYv66HlG8896yPUDONgKzG5BxZD1nV9U6rkm+7VCuvQc903C4MjcoZR4zPw53IKSOX9wMQVpA1IAbRtzQ7w==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental + + react-compiler-runtime@19.1.0-rc.1: + resolution: {integrity: sha512-wCt6g+cRh8g32QT18/9blfQHywGjYu+4FlEc3CW1mx3pPxYzZZl1y+VtqxRgnKKBCFLIGUYxog4j4rs5YS86hw==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -6206,6 +6327,11 @@ packages: peerDependencies: react: '>= 16.8' + react-error-boundary@5.0.0: + resolution: {integrity: sha512-tnjAxG+IkpLephNcePNA7v6F/QpWLH8He65+DmedchDwg162JZqx4NmbXj0mlAYVVEd81OW7aFhmbsScYfiAFQ==} + peerDependencies: + react: '>=16.13.1' + react-fast-compare@3.2.2: resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} @@ -7071,6 +7197,11 @@ packages: peerDependencies: react: ^18.3 || ^19.0.0-0 + use-effect-event@2.0.4: + resolution: {integrity: sha512-il5ezAwTIxg1dr+eXP+1SS1EpXe6IFy1oADvGxmiCNGCDX1X+ygERRDOFoCyzc+r6ktqag3VITmrw1ZPfoig1g==} + peerDependencies: + react: ^18.3 || ^19.0.0-0 + use-hot-module-reload@2.0.0: resolution: {integrity: sha512-RbL/OY1HjHNf5BYSFV3yDtQhIGKjCx9ntEjnUBYsOGc9fTo94nyFTcjtD42/twJkPgMljWpszUIpTGD3LuwHSg==} peerDependencies: @@ -9982,6 +10113,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@juggle/resize-observer@3.4.0': {} + '@lezer/common@1.5.1': {} '@lezer/css@1.3.1': @@ -10162,7 +10295,7 @@ snapshots: '@types/react': 19.2.14 '@types/react-dom': 19.2.3(@types/react@19.2.14) - '@mux/mux-player-react@3.13.2(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@mux/mux-player-react@3.13.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@mux/mux-player': 3.13.2(react@19.2.8) '@mux/playback-core': 0.35.2 @@ -10171,6 +10304,7 @@ snapshots: react-dom: 19.2.8(react@19.2.8) optionalDependencies: '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) '@mux/mux-player@3.13.2(react@19.2.4)': dependencies: @@ -11064,6 +11198,18 @@ snapshots: '@sanity/color@3.0.8': {} + '@sanity/comlink@2.0.5': + dependencies: + rxjs: 7.8.2 + uuid: 11.1.0 + xstate: 5.32.5 + + '@sanity/comlink@3.1.1': + dependencies: + rxjs: 7.8.2 + uuid: 11.1.0 + xstate: 5.32.5 + '@sanity/comlink@4.0.3': dependencies: rxjs: 7.8.2 @@ -11187,6 +11333,10 @@ snapshots: '@sanity/media-library-types@1.6.0': {} + '@sanity/message-protocol@0.12.0': + dependencies: + '@sanity/comlink': 2.0.5 + '@sanity/message-protocol@0.23.0': dependencies: '@sanity/comlink': 4.0.3 @@ -11211,6 +11361,17 @@ snapshots: - supports-color - xstate + '@sanity/mutate@0.12.6': + dependencies: + '@sanity/client': 7.26.2 + '@sanity/diff-match-patch': 3.2.1 + '@sanity/uuid': 3.0.3 + hotscript: 1.0.13 + lodash: 4.18.1 + mendoza: 3.0.8 + nanoid: 5.1.11 + rxjs: 7.8.2 + '@sanity/mutate@0.16.1(xstate@5.32.5)': dependencies: '@sanity/client': 7.26.2 @@ -11341,6 +11502,45 @@ snapshots: transitivePeerDependencies: - '@types/react' + '@sanity/sdk-react@1.0.0(@types/react@19.2.14)(immer@11.1.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))': + dependencies: + '@sanity/client': 7.26.2 + '@sanity/message-protocol': 0.12.0 + '@sanity/sdk': 1.0.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + '@sanity/types': 3.99.0(@types/react@19.2.14) + '@types/lodash-es': 4.17.12 + groq: 3.88.1-typegen-experimental.0 + lodash-es: 4.18.1 + react: 19.2.8 + react-compiler-runtime: 19.1.0-rc.1(react@19.2.8) + react-dom: 19.2.8(react@19.2.8) + react-error-boundary: 5.0.0(react@19.2.8) + rxjs: 7.8.2 + transitivePeerDependencies: + - '@types/react' + - immer + - use-sync-external-store + + '@sanity/sdk@1.0.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8))': + dependencies: + '@sanity/client': 7.26.2 + '@sanity/comlink': 3.1.1 + '@sanity/diff-match-patch': 3.2.1 + '@sanity/message-protocol': 0.12.0 + '@sanity/mutate': 0.12.6 + '@sanity/types': 3.99.0(@types/react@19.2.14) + '@types/lodash-es': 4.17.12 + groq: 3.88.1-typegen-experimental.0 + lodash-es: 4.18.1 + reselect: 5.2.0 + rxjs: 7.8.2 + zustand: 5.0.14(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + transitivePeerDependencies: + - '@types/react' + - immer + - react + - use-sync-external-store + '@sanity/sdk@2.12.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(xstate@5.32.5)': dependencies: '@sanity/bifur-client': 1.0.0 @@ -11420,6 +11620,12 @@ snapshots: '@actions/github': 9.0.0 yaml: 2.9.0 + '@sanity/types@3.99.0(@types/react@19.2.14)': + dependencies: + '@sanity/client': 7.26.2 + '@sanity/media-library-types': 1.6.0 + '@types/react': 19.2.14 + '@sanity/types@5.28.0(@types/react@19.2.14)': dependencies: '@sanity/client': 7.26.2 @@ -11432,6 +11638,22 @@ snapshots: '@sanity/media-library-types': 1.6.0 '@types/react': 19.2.14 + '@sanity/ui@3.5.5(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(styled-components@6.3.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8))': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@juggle/resize-observer': 3.4.0 + '@sanity/color': 3.0.8 + '@sanity/icons': 5.2.1(react@19.2.8) + csstype: 3.2.3 + motion: 13.5.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-compiler-runtime: 1.0.0(react@19.2.8) + react-dom: 19.2.8(react@19.2.8) + react-is: 19.2.8 + react-refractor: 4.0.0(react@19.2.8) + styled-components: 6.3.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + use-effect-event: 2.0.4(react@19.2.8) + '@sanity/ui@4.0.3(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(styled-components@6.3.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': dependencies: '@floating-ui/react-dom': 2.1.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -11600,6 +11822,21 @@ snapshots: - vue-tsc - yaml + '@sanity/workbench@0.1.0-alpha.36(@sanity/sdk@1.0.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)))(react@19.2.8)(xstate@5.32.5)': + dependencies: + '@module-federation/runtime': 2.8.2 + '@sanity/client': 7.26.2 + '@sanity/color': 3.0.8 + '@sanity/sdk': 1.0.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + '@sanity/telemetry': 1.1.0(react@19.2.8) + es-toolkit: 1.51.0 + rxjs: 7.8.2 + verkit: 0.3.2 + xstate: 5.32.5 + zod: 4.4.3 + transitivePeerDependencies: + - react + '@sanity/workbench@0.1.0-alpha.36(@sanity/sdk@2.12.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(xstate@5.32.5))(react@19.2.4)(xstate@5.32.5)': dependencies: '@module-federation/runtime': 2.8.2 @@ -11897,6 +12134,12 @@ snapshots: '@types/linkify-it@5.0.0': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.26 + + '@types/lodash@4.17.26': {} + '@types/markdown-it@14.1.2': dependencies: '@types/linkify-it': 5.0.0 @@ -13127,6 +13370,15 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + framer-motion@13.5.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + motion-dom: 13.5.1 + motion-utils: 13.5.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + fsevents@2.3.3: optional: true @@ -14036,8 +14288,14 @@ snapshots: dependencies: motion-utils: 13.0.0 + motion-dom@13.5.1: + dependencies: + motion-utils: 13.5.0 + motion-utils@13.0.0: {} + motion-utils@13.5.0: {} + motion@13.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: framer-motion: 13.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -14054,6 +14312,14 @@ snapshots: react: 19.2.8 react-dom: 19.2.8(react@19.2.8) + motion@13.5.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + framer-motion: 13.5.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + mrmime@2.0.1: {} ms@2.1.3: {} @@ -14401,6 +14667,14 @@ snapshots: '@babel/runtime': 7.29.2 react: 19.2.8 + react-compiler-runtime@1.0.0(react@19.2.8): + dependencies: + react: 19.2.8 + + react-compiler-runtime@19.1.0-rc.1(react@19.2.8): + dependencies: + react: 19.2.8 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -14423,6 +14697,11 @@ snapshots: prop-types: 15.8.1 react: 19.2.4 + react-error-boundary@5.0.0(react@19.2.8): + dependencies: + '@babel/runtime': 7.29.2 + react: 19.2.8 + react-fast-compare@3.2.2: {} react-file-icon@1.6.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): @@ -14779,6 +15058,154 @@ snapshots: - '@types/react' - supports-color + sanity@6.9.2(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@noble/hashes@2.4.0)(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.2)(rolldown@1.2.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)))(@sanity/sdk@1.0.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)))(@types/node@25.5.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(jiti@2.7.0)(prismjs@1.30.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(rolldown@1.2.4)(styled-components@6.3.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(terser@5.48.0)(typescript@5.9.3): + dependencies: + '@algorithm.ts/lcs': 4.0.6 + '@date-fns/tz': 1.5.0 + '@dnd-kit/core': 6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@dnd-kit/modifiers': 9.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + '@dnd-kit/sortable': 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) + '@dnd-kit/utilities': 3.2.2(react@19.2.8) + '@isaacs/ttlcache': 2.1.5 + '@mux/mux-player-react': 3.13.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@portabletext/editor': 7.11.0(@types/react@19.2.14)(react@19.2.8) + '@portabletext/html': 1.2.0 + '@portabletext/patches': 2.0.6 + '@portabletext/plugin-dnd': 1.0.32(@portabletext/editor@7.11.0(@types/react@19.2.14)(react@19.2.8))(react@19.2.8) + '@portabletext/plugin-list-index': 1.0.32(@portabletext/editor@7.11.0(@types/react@19.2.14)(react@19.2.8))(react@19.2.8) + '@portabletext/plugin-markdown-shortcuts': 8.0.48(@portabletext/editor@7.11.0(@types/react@19.2.14)(react@19.2.8))(@types/react@19.2.14)(react@19.2.8) + '@portabletext/plugin-one-line': 7.0.47(@portabletext/editor@7.11.0(@types/react@19.2.14)(react@19.2.8))(react@19.2.8) + '@portabletext/plugin-paste-link': 4.0.47(@portabletext/editor@7.11.0(@types/react@19.2.14)(react@19.2.8))(react@19.2.8) + '@portabletext/plugin-table': 1.3.17(@portabletext/editor@7.11.0(@types/react@19.2.14)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@portabletext/plugin-typography': 8.0.48(@portabletext/editor@7.11.0(@types/react@19.2.14)(react@19.2.8))(@types/react@19.2.14)(react@19.2.8) + '@portabletext/react': 7.0.1(react@19.2.8) + '@portabletext/sanity-bridge': 3.2.5(@types/react@19.2.14) + '@portabletext/to-html': 5.0.3 + '@portabletext/toolkit': 5.0.2 + '@rexxars/react-json-inspector': 9.0.1(react@19.2.8) + '@sanity-labs/ui-poc': 0.0.1-alpha.25(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@sanity/asset-utils': 2.3.0 + '@sanity/bifur-client': 1.0.0 + '@sanity/cli': 7.18.0(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@noble/hashes@2.4.0)(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.2)(rolldown@1.2.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)))(@types/node@25.5.0)(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(jiti@2.7.0)(rolldown@1.2.4)(terser@5.48.0)(typescript@5.9.3)(xstate@5.32.5) + '@sanity/client': 7.26.2 + '@sanity/color': 3.0.8 + '@sanity/comlink': 4.0.3 + '@sanity/diff': 6.9.2 + '@sanity/diff-match-patch': 3.2.0 + '@sanity/diff-patch': 5.0.0 + '@sanity/eventsource': 5.0.4 + '@sanity/generate-help-url': 4.0.0 + '@sanity/icons': 5.2.1(react@19.2.8) + '@sanity/id-utils': 1.0.0 + '@sanity/image-url': 2.1.1 + '@sanity/logos': 2.2.5(react@19.2.8) + '@sanity/media-library-types': 1.6.0 + '@sanity/message-protocol': 0.24.0 + '@sanity/migrate': 8.0.2(@types/react@19.2.14)(xstate@5.32.5) + '@sanity/mutate': 0.18.1(xstate@5.32.5) + '@sanity/mutator': 6.9.2(@types/react@19.2.14) + '@sanity/presentation-comlink': 2.2.3(@sanity/types@6.9.2(@types/react@19.2.14)) + '@sanity/preview-url-secret': 4.1.4(@sanity/client@7.26.2) + '@sanity/prism-groq': 1.1.2(prismjs@1.30.0) + '@sanity/schema': 6.9.2(@types/react@19.2.14) + '@sanity/telemetry': 1.1.0(react@19.2.8) + '@sanity/types': 6.9.2(@types/react@19.2.14) + '@sanity/ui': 4.0.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(styled-components@6.3.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8)) + '@sanity/util': 6.9.2(@types/react@19.2.14) + '@sanity/uuid': 3.0.3 + '@sanity/workbench': 0.1.0-alpha.36(@sanity/sdk@1.0.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)))(react@19.2.8)(xstate@5.32.5) + '@sentry/react': 10.70.0(react@19.2.8) + '@tanstack/react-table': 8.21.3(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@tanstack/react-virtual': 3.14.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@xstate/react': 6.1.0(@types/react@19.2.14)(react@19.2.8)(xstate@5.32.5) + clsx: 2.1.1 + color2k: 2.0.4 + dataloader: 2.2.3 + date-fns: 4.4.0 + debug: 4.4.3(supports-color@8.1.1) + exif-component: 1.0.1 + fast-deep-equal: 3.1.3 + groq-js: 2.0.0 + history: 5.3.0 + i18next: 26.3.6(typescript@5.9.3) + is-hotkey-esm: 1.0.0 + is-network-error: 1.3.2 + isomorphic-dompurify: 2.36.0(@noble/hashes@2.4.0) + json-reduce: 3.0.0 + json-stable-stringify: 1.3.0 + lodash-es: 4.18.1 + mendoza: 3.0.8 + motion: 13.1.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + nano-pubsub: 3.0.0 + nanoid: 6.0.1 + observable-callback: 1.0.3(rxjs@7.8.2) + path-to-regexp: 8.4.2 + player.style: 0.3.4(react@19.2.8) + polished: 4.3.1 + quick-lru: 7.3.0 + raf: 3.4.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-fast-compare: 3.2.2 + react-focus-lock: 2.13.7(@types/react@19.2.14)(react@19.2.8) + react-i18next: 17.0.11(i18next@26.3.6(typescript@5.9.3))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) + react-is: 19.2.8 + react-refractor: 4.0.0(react@19.2.8) + react-rx: 5.1.1(react@19.2.8)(rxjs@7.8.2) + refractor: 5.0.0 + rxjs: 7.8.2 + rxjs-exhaustmap-with-trailing: 2.1.1(rxjs@7.8.2) + rxjs-mergemap-array: 0.1.0(rxjs@7.8.2) + scroll-into-view-if-needed: 3.1.0 + scrollmirror: 1.2.4 + semver: 7.8.5 + shallow-equals: 1.0.0 + speakingurl: 14.0.1 + styled-components: 6.3.11(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + swr: 2.5.1(react@19.2.8) + urlpattern-polyfill: 10.1.0 + use-device-pixel-ratio: 1.1.2(react@19.2.8) + use-effect-event: 2.0.3(react@19.2.8) + use-hot-module-reload: 2.0.0(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + uuid: 14.0.1 + web-vitals: 6.1.1 + xstate: 5.32.5 + transitivePeerDependencies: + - '@babel/core' + - '@babel/plugin-transform-runtime' + - '@babel/runtime' + - '@noble/hashes' + - '@rolldown/plugin-babel' + - '@sanity/sdk' + - '@types/node' + - '@types/react' + - '@types/react-dom' + - '@vitejs/devtools' + - babel-plugin-react-compiler + - bare-abort-controller + - bare-buffer + - bufferutil + - canvas + - esbuild + - jiti + - less + - oxfmt + - prismjs + - react-native + - react-native-b4a + - rolldown + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - typescript + - utf-8-validate + - vitest + - vue-tsc + sanity@6.9.2(@babel/core@7.29.7)(@babel/runtime@7.29.2)(@noble/hashes@2.4.0)(@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/runtime@7.29.2)(rolldown@1.2.4)(vite@8.2.1(@types/node@25.5.0)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(tsx@4.23.12)(yaml@2.9.0)))(@sanity/sdk@2.12.0(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))(xstate@5.32.5))(@types/node@25.5.0)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.1)(jiti@2.7.0)(prismjs@1.30.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(rolldown@1.2.4)(styled-components@6.3.11(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(terser@5.48.0)(typescript@5.9.3): dependencies: '@algorithm.ts/lcs': 4.0.6 @@ -14936,7 +15363,7 @@ snapshots: '@dnd-kit/sortable': 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8) '@dnd-kit/utilities': 3.2.2(react@19.2.8) '@isaacs/ttlcache': 2.1.5 - '@mux/mux-player-react': 3.13.2(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@mux/mux-player-react': 3.13.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@portabletext/editor': 7.11.0(@types/react@19.2.14)(react@19.2.8) '@portabletext/html': 1.2.0 '@portabletext/patches': 2.0.6 @@ -15719,6 +16146,10 @@ snapshots: dependencies: react: 19.2.8 + use-effect-event@2.0.4(react@19.2.8): + dependencies: + react: 19.2.8 + use-hot-module-reload@2.0.0(react@19.2.4): dependencies: react: 19.2.4