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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .claude/memory/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,27 @@ _Append only. Never delete entries._

---

## 2026-03-20 — revamp/phase-2: Hardcoded auth IDs (no Clerk)
**Decision**: Phase 2 uses hardcoded `test_agency_001`, `test_brand_001`, `test_creator_001` ClerkIDs in `lib/auth-helpers.ts`. No Clerk imports anywhere in Phase 2 code.
**Reason**: Eliminates auth service dependency during backend integration sprint; allows full API + DB testing without live Clerk session. All functions are tagged `// TODO(phase3): replace with real Clerk auth()`.
**Alternatives considered**: Mock Clerk session (rejected — more complex, still blocks headless testing).

---

## 2026-03-20 — revamp/phase-2: Monetary convention (cents stored, dollars returned)
**Decision**: DB stores all monetary values as `Decimal(10,2)` in cents (e.g., $35 → `3500`). API responses divide by 100 before returning (dollars). `creatorPayout` is always server-computed: `Math.round(dealValueCents * (1 - commissionPct/100))`.
**Reason**: Standard financial precision practice; existing frontend components expect dollar values (e.g., `deal.dealValue.toLocaleString()`); no frontend display changes needed.
**Alternatives considered**: Store and return cents everywhere (rejected — requires frontend display changes).

---

## 2026-03-20 — revamp/phase-2: Brief→Deal conversion placeholder brand
**Decision**: `PATCH /briefs/[id]` with `status: CONVERTED` creates a Deal with a placeholder "Unassigned" brand if no `brandId` is present on the brief. Agency is expected to update `brandId` after conversion.
**Reason**: DB `Deal.brandId` is a required FK. Brief has no `brandId` field. Placeholder avoids blocking the conversion workflow.
**Alternatives considered**: Make `Deal.brandId` nullable (rejected — too many downstream changes to schema + components).

---

## 2026-03-20 — revamp/phase-1: Mock data over real API calls
**Decision**: Phase 1 uses static mock data in `lib/mock/` with no API calls, no Clerk, no Prisma.
**Reason**: Allows rapid UI iteration without backend service dependencies; all values use dollars (not cents) for Phase 1 display simplicity; Prisma enums replaced with string literals throughout client code.
Expand Down
24 changes: 24 additions & 0 deletions .claude/memory/iterations.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@ _Append only. One entry per session or PR. Never delete._

---

## 2026-03-20 — revamp/phase-2: Backend Integration
**Type**: Feature
**Branch**: revamp/phase-2

### What changed
- **Prisma schema** (`prisma/schema.prisma`): 6 models (Creator, Brand, Deal, ContentSubmission, Brief, PartnershipRequest), 6 enums. `ContractStatus.NOT_SENT` (not `PENDING`). Migration applied to Neon DB.
- **Core lib files created**: `lib/db.ts` (singleton), `lib/api-response.ts` (ok/created/err/notFound/etc.), `lib/stage-transitions.ts` (Prisma-typed, server-side), `lib/auth-helpers.ts` (hardcoded test IDs for Phase 2), `lib/api.ts` (apiUrl with port 3001)
- **Zod v4 schemas**: `lib/validations/` — deal, brand, roster, brief, submission, partnership
- **15 REST API routes** under `app/api/v1/`: deals CRUD + stage + reopen + submissions, brands, roster, creators, briefs, partnerships
- **Database seed**: `prisma/seed.ts` — 4 brands, 5 creators, 8 deals (one per stage), 2 submissions, 3 briefs
- **12 pages wired** to real API (replaced all `lib/mock/` imports with `fetch()` calls)
- **Unit tests**: `lib/__tests__/stage-transitions.test.ts` + `lib/__tests__/api-response.test.ts` (19 new tests; 120 total)
- **CI/CD**: Removed 4 Clerk env vars from ci.yml + prod.yml quality job; added `NEXT_PUBLIC_APP_URL`
- **Port change**: Dev server and API_BASE use :3001 throughout
- **ESLint errors fixed**: 6 pre-existing React Compiler purity errors suppressed with targeted disable comments

### Quality gates
- `npm run typecheck` → 0 errors
- `npm run lint` → 0 errors (13 warnings, pre-existing)
- `npm run test` → 120/120 passing
- `npm run build` → successful (all 15 API routes + 18 pages compile)

---

## 2026-03-19 — fix/pre-m3-landing-auth
- Landing page at / — server-side auth redirect, hero + benefits + role cards
- /signup role picker with SSO hash guard (Clerk flow detected via window.location.hash via lazy useState initializer)
Expand Down
7 changes: 4 additions & 3 deletions .claude/memory/memory.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
_Last updated: 2026-03-20_

## Revamp (active)
- Branch: revamp/phase-1
- Branch: revamp/phase-2 (branched from revamp/phase-1)
- Approach: Frontend-first — UI shell with mock data → real API → auth
- Spec: /revamp/ directory (README, PRODUCT, ARCHITECTURE, ROADMAP, FRONTEND, BACKEND, MOCKDATA)
- Old M1+M2 code archived in _archive/
- Phase 1 complete: 2026-03-20 — all 18 routes with mock data, no DB, no Clerk

## Current state
- Status: **Phase 1 UI Shell complete — ready for Phase 2**
- Active milestone: Phase 2 — Backend Integration (replace mock data with real fetch() calls + Prisma)
- Status: **Phase 2 Backend Integration complete — ready for PR review**
- Branch: revamp/phase-2 (off revamp/phase-1)
- Active milestone: Phase 2 — Backend Integration ✅ complete

## Pre-M3 fixes in progress — 2 PRs open
- **PR #8** `fix/pre-m3-proxy-loop` — proxy.ts redirect loop guard (stale JWT)
Expand Down
3 changes: 2 additions & 1 deletion .claude/settings.local.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"Bash(git checkout:*)",
"Bash(ls /c/Users/Hp/AwesomeClaudeSkills/ProjectAlpha/.claude/worktrees/agent-af54c613/app/\\\\\\(agency\\\\\\)/dashboard/)",
"Bash(cat /c/Users/Hp/AwesomeClaudeSkills/ProjectAlpha/.claude/worktrees/agent-af54c613/app/\\\\\\(agency\\\\\\)/dashboard/page.tsx)",
"Bash(ls /c/Users/Hp/AwesomeClaudeSkills/ProjectAlpha/.claude/worktrees/agent-af54c613/app/\\\\\\(agency\\\\\\)/)"
"Bash(ls /c/Users/Hp/AwesomeClaudeSkills/ProjectAlpha/.claude/worktrees/agent-af54c613/app/\\\\\\(agency\\\\\\)/)",
"Bash(ls /c/Users/Hp/AwesomeClaudeSkills/ProjectAlpha/.env*)"
]
}
}
5 changes: 1 addition & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ jobs:
env:
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_URL_UNPOOLED: ${{ secrets.CI_DATABASE_URL_UNPOOLED }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
NEXT_PUBLIC_CLERK_SIGN_IN_URL: /login
NEXT_PUBLIC_CLERK_SIGN_UP_URL: /signup
NEXT_PUBLIC_APP_URL: http://localhost:3001

steps:
- name: Checkout
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@ jobs:
env:
DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
DATABASE_URL_UNPOOLED: ${{ secrets.CI_DATABASE_URL_UNPOOLED }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
NEXT_PUBLIC_CLERK_SIGN_IN_URL: /login
NEXT_PUBLIC_CLERK_SIGN_UP_URL: /signup
NEXT_PUBLIC_APP_URL: http://localhost:3001

steps:
- name: Checkout
Expand Down
11 changes: 6 additions & 5 deletions app/(agency)/brands/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { mockBrands } from '@/lib/mock/brands'
import { mockDeals } from '@/lib/mock/deals'
import { apiUrl } from '@/lib/api'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
Expand Down Expand Up @@ -37,10 +36,12 @@ export default async function BrandDetailPage({
params: Promise<{ id: string }>
}) {
const { id } = await params
const brand = mockBrands.find(b => b.id === id)
const res = await fetch(apiUrl(`/api/v1/brands/${id}`), { cache: 'no-store' })
if (!res.ok) notFound()
const { data: brand } = await res.json()
if (!brand) notFound()

const brandDeals = mockDeals.filter(d => d.brandId === id)
const brandDeals = brand.deals ?? []

return (
<div className="p-6 space-y-6">
Expand Down Expand Up @@ -81,7 +82,7 @@ export default async function BrandDetailPage({
</TableRow>
</TableHeader>
<TableBody>
{brandDeals.map(deal => (
{(brandDeals as Array<{ id: string; title: string; stage: string; creator: { name: string } | null; dealValue: number; deadline: string }>).map(deal => (
<TableRow key={deal.id}>
<TableCell>
<Link
Expand Down
14 changes: 10 additions & 4 deletions app/(agency)/brands/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { mockBrands } from '@/lib/mock/brands'
import { mockDeals } from '@/lib/mock/deals'
import { apiUrl } from '@/lib/api'
import { BrandsTable } from '@/components/brands/BrandsTable'

export default function BrandsPage() {
export default async function BrandsPage() {
const [brandsRes, dealsRes] = await Promise.all([
fetch(apiUrl('/api/v1/brands'), { cache: 'no-store' }),
fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' }),
])
const { data: brands } = await brandsRes.json()
const { data: deals } = await dealsRes.json()

return (
<div className="p-6">
<BrandsTable initialBrands={mockBrands} deals={mockDeals} />
<BrandsTable initialBrands={brands ?? []} deals={deals ?? []} />
</div>
)
}
6 changes: 4 additions & 2 deletions app/(agency)/briefs/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { notFound } from 'next/navigation'
import { mockBriefs } from '@/lib/mock/briefs'
import { apiUrl } from '@/lib/api'
import { BriefDetail } from '@/components/briefs/BriefDetail'

export default async function BriefDetailPage({
Expand All @@ -8,7 +8,9 @@ export default async function BriefDetailPage({
params: Promise<{ id: string }>
}) {
const { id } = await params
const brief = mockBriefs.find(b => b.id === id)
const res = await fetch(apiUrl(`/api/v1/briefs/${id}`), { cache: 'no-store' })
if (!res.ok) notFound()
const { data: brief } = await res.json()
if (!brief) notFound()
return (
<div className="p-6">
Expand Down
9 changes: 6 additions & 3 deletions app/(agency)/briefs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { mockBriefs } from '@/lib/mock/briefs'
import { apiUrl } from '@/lib/api'
import { BriefsTable } from '@/components/briefs/BriefsTable'

export default function BriefsPage() {
export default async function BriefsPage() {
const res = await fetch(apiUrl('/api/v1/briefs'), { cache: 'no-store' })
const { data: briefs } = await res.json()

return (
<div className="p-6">
<h1 className="text-2xl font-bold tracking-tight mb-6">Brief Inbox</h1>
<BriefsTable initialBriefs={mockBriefs} />
<BriefsTable initialBriefs={briefs ?? []} />
</div>
)
}
9 changes: 6 additions & 3 deletions app/(agency)/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { mockDeals } from '@/lib/mock/deals'
import { apiUrl } from '@/lib/api'
import { KanbanBoard } from '@/components/kanban/KanbanBoard'

export default function DashboardPage() {
return <KanbanBoard initialDeals={mockDeals} />
export default async function DashboardPage() {
const res = await fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' })
const { data: deals } = await res.json()

return <KanbanBoard initialDeals={deals ?? []} />
}
12 changes: 6 additions & 6 deletions app/(agency)/deals/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { notFound } from 'next/navigation'
import { mockDeals } from '@/lib/mock/deals'
import { mockSubmissions } from '@/lib/mock/submissions'
import { apiUrl } from '@/lib/api'
import { DealDetail } from '@/components/deals/DealDetail'

export default async function DealDetailPage({
Expand All @@ -9,14 +8,15 @@ export default async function DealDetailPage({
params: Promise<{ id: string }>
}) {
const { id } = await params
const deal = mockDeals.find((d) => d.id === id)
if (!deal) notFound()

const submissions = mockSubmissions.filter((s) => s.dealId === id)
const res = await fetch(apiUrl(`/api/v1/deals/${id}`), { cache: 'no-store' })
if (!res.ok) notFound()
const { data: deal } = await res.json()
if (!deal) notFound()

return (
<div className="p-6">
<DealDetail initialDeal={deal} initialSubmissions={submissions} />
<DealDetail initialDeal={deal} initialSubmissions={deal.submissions ?? []} />
</div>
)
}
14 changes: 10 additions & 4 deletions app/(agency)/deals/new/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { mockBrands } from '@/lib/mock/brands'
import { mockRoster } from '@/lib/mock/creators'
import { apiUrl } from '@/lib/api'
import { DealNewForm } from '@/components/forms/DealNewForm'

export default function NewDealPage() {
export default async function NewDealPage() {
const [brandsRes, rosterRes] = await Promise.all([
fetch(apiUrl('/api/v1/brands'), { cache: 'no-store' }),
fetch(apiUrl('/api/v1/roster'), { cache: 'no-store' }),
])
const { data: brands } = await brandsRes.json()
const { data: creators } = await rosterRes.json()

return (
<div className="p-6 max-w-2xl">
<h1 className="text-2xl font-bold mb-6 tracking-tight">New Deal</h1>
<DealNewForm brands={mockBrands} creators={mockRoster} />
<DealNewForm brands={brands ?? []} creators={creators ?? []} />
</div>
)
}
9 changes: 6 additions & 3 deletions app/(agency)/deals/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { mockDeals } from '@/lib/mock/deals'
import { apiUrl } from '@/lib/api'
import { DealsTable } from '@/components/deals/DealsTable'

export default function DealsPage() {
export default async function DealsPage() {
const res = await fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' })
const { data: deals } = await res.json()

return (
<div className="p-6">
<DealsTable initialDeals={mockDeals} />
<DealsTable initialDeals={deals ?? []} />
</div>
)
}
14 changes: 10 additions & 4 deletions app/(agency)/roster/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { mockRoster } from '@/lib/mock/creators'
import { mockDeals } from '@/lib/mock/deals'
import { apiUrl } from '@/lib/api'
import { RosterTable } from '@/components/roster/RosterTable'

export default function RosterPage() {
export default async function RosterPage() {
const [rosterRes, dealsRes] = await Promise.all([
fetch(apiUrl('/api/v1/roster'), { cache: 'no-store' }),
fetch(apiUrl('/api/v1/deals'), { cache: 'no-store' }),
])
const { data: creators } = await rosterRes.json()
const { data: deals } = await dealsRes.json()

return (
<div className="p-6">
<RosterTable initialCreators={mockRoster} deals={mockDeals} />
<RosterTable initialCreators={creators ?? []} deals={deals ?? []} />
</div>
)
}
12 changes: 5 additions & 7 deletions app/(creator)/creator/deals/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
import { notFound } from 'next/navigation'
import { mockDeals } from '@/lib/mock/deals'
import { mockSubmissions } from '@/lib/mock/submissions'
import { apiUrl } from '@/lib/api'
import { CreatorDealDetail } from '@/components/creator/CreatorDealDetail'

const MOCK_CREATOR_ID = 'creator_001'

export default async function CreatorDealDetailPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const deal = mockDeals.find(d => d.id === id && d.creatorId === MOCK_CREATOR_ID)
const res = await fetch(apiUrl(`/api/v1/deals/${id}`), { cache: 'no-store' })
if (!res.ok) notFound()
const { data: deal } = await res.json()
if (!deal) notFound()
const submissions = mockSubmissions.filter(s => s.dealId === id)
return (
<div className="p-6">
<CreatorDealDetail initialDeal={deal} initialSubmissions={submissions} />
<CreatorDealDetail initialDeal={deal} initialSubmissions={deal.submissions ?? []} />
</div>
)
}
12 changes: 6 additions & 6 deletions app/(creator)/creator/deals/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { mockDeals } from '@/lib/mock/deals'
import { apiUrl } from '@/lib/api'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import Link from 'next/link'
import { isOverdue } from '@/lib/overdue.client'

const MOCK_CREATOR_ID = 'creator_001'

const STAGE_LABELS: Record<string, string> = {
BRIEF_RECEIVED: 'Brief Received',
CREATOR_ASSIGNED: 'Creator Assigned',
Expand All @@ -17,8 +15,10 @@ const STAGE_LABELS: Record<string, string> = {
CLOSED: 'Closed',
}

export default function CreatorDealsPage() {
const myDeals = mockDeals.filter(d => d.creatorId === MOCK_CREATOR_ID)
export default async function CreatorDealsPage() {
const res = await fetch(apiUrl('/api/v1/deals?creatorId=creator_seed_001'), { cache: 'no-store' })
const { data } = await res.json()
const myDeals = data ?? []

if (myDeals.length === 0) {
return (
Expand All @@ -32,7 +32,7 @@ export default function CreatorDealsPage() {
<div className="p-6 space-y-4">
<h1 className="text-2xl font-bold tracking-tight">My Deals</h1>
<div className="grid gap-4">
{myDeals.map(deal => (
{(myDeals as Array<{ id: string; brand: { name: string }; title: string; stage: string; deadline: string; creatorPayout: number }>).map(deal => (
<Link key={deal.id} href={`/creator/deals/${deal.id}`}>
<Card className="hover:bg-muted/50 transition-colors cursor-pointer">
<CardContent className="pt-6">
Expand Down
Loading
Loading