feat: dashboard redesign — icon sidebar, KPI hierarchy, card gallery, seed contact#1
feat: dashboard redesign — icon sidebar, KPI hierarchy, card gallery, seed contact#1Fusion-Data-Company wants to merge 14 commits into
Conversation
Covers icon sidebar, KPI overflow fix, empty states with CTAs, card gallery upload/delete routes, and seed contact endpoint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t} queries work Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rdTemplate relation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reviewer's GuideDashboard redesign implementing an icon-only sidebar, improved KPI and schedule layout with actionable empty states, new card gallery upload/delete APIs with validation and tenant scoping, and a dev-only seed route plus schema/auth utilities to support test data and schedule relations. Sequence diagram for card upload via card gallerysequenceDiagram
actor User
participant CardGalleryUI
participant CardsUploadRoute as ApiCardsUpload
participant ClerkAuth as ClerkAuth
participant AuthUtil as AuthUtil_getTenantId
participant Database as DB
participant FileSystem as DiskStorage
User->>CardGalleryUI: Select card image and occasionType
CardGalleryUI->>CardsUploadRoute: POST /api/cards/upload (multipart formData)
CardsUploadRoute->>ClerkAuth: auth()
ClerkAuth-->>CardsUploadRoute: userId
alt user not authenticated
CardsUploadRoute-->>CardGalleryUI: 401 Unauthorized
else user authenticated
CardsUploadRoute->>AuthUtil: getTenantId(userId)
AuthUtil-->>CardsUploadRoute: tenantId or null
alt no tenantId
CardsUploadRoute-->>CardGalleryUI: 400 No tenant
else tenantId found
CardsUploadRoute->>CardsUploadRoute: Validate file size, extension, MIME
alt invalid file
CardsUploadRoute-->>CardGalleryUI: 4xx error (size/type)
else valid file
CardsUploadRoute->>FileSystem: mkdir uploadDir, writeFile
FileSystem-->>CardsUploadRoute: file persisted
CardsUploadRoute->>Database: insert cardTemplates (tenantId, occasionType, name, imageUrl)
Database-->>CardsUploadRoute: created cardTemplate
CardsUploadRoute-->>CardGalleryUI: 201 Created (cardTemplate)
CardGalleryUI-->>User: Show new card in gallery
end
end
end
Sequence diagram for card delete via card gallerysequenceDiagram
actor User
participant CardGalleryUI
participant CardsDeleteRoute as ApiCardsId
participant ClerkAuth as ClerkAuth
participant AuthUtil as AuthUtil_getTenantId
participant Database as DB
User->>CardGalleryUI: Click delete on card
CardGalleryUI->>CardsDeleteRoute: DELETE /api/cards/{id}
CardsDeleteRoute->>ClerkAuth: auth()
ClerkAuth-->>CardsDeleteRoute: userId
alt user not authenticated
CardsDeleteRoute-->>CardGalleryUI: 401 Unauthorized
else user authenticated
CardsDeleteRoute->>AuthUtil: getTenantId(userId)
AuthUtil-->>CardsDeleteRoute: tenantId or null
alt no tenantId
CardsDeleteRoute-->>CardGalleryUI: 400 No tenant
else tenantId found
CardsDeleteRoute->>Database: findFirst cardTemplates where id and tenantId
Database-->>CardsDeleteRoute: card or null
alt card not found
CardsDeleteRoute-->>CardGalleryUI: 404 Not found
else card is system card
CardsDeleteRoute-->>CardGalleryUI: 403 Cannot delete system cards
else deletable card
CardsDeleteRoute->>Database: delete cardTemplates where id and tenantId
Database-->>CardsDeleteRoute: delete success
CardsDeleteRoute-->>CardGalleryUI: 200 { success: true }
CardGalleryUI-->>User: Remove card from gallery
end
end
end
Sequence diagram for dev-only dashboard seed routesequenceDiagram
actor DevUser
participant DashboardPage
participant SeedRoute as ApiDevSeed
participant ClerkAuth as ClerkAuth
participant AuthUtil as AuthUtil_getTenantId
participant Database as DB
DevUser->>DashboardPage: Click Load test data
DashboardPage->>SeedRoute: POST /api/dev/seed (useMutation)
alt production environment or dev routes disabled
SeedRoute-->>DashboardPage: 403 Not available
DashboardPage-->>DevUser: Show error (cannot load test data)
else dev environment allowed
SeedRoute->>ClerkAuth: auth()
ClerkAuth-->>SeedRoute: userId
alt no userId
SeedRoute-->>DashboardPage: 401 Unauthorized
else user authenticated
SeedRoute->>AuthUtil: getTenantId(userId)
AuthUtil-->>SeedRoute: tenantId or null
alt no tenantId
SeedRoute-->>DashboardPage: 400 No tenant
else tenantId found
SeedRoute->>Database: delete contacts where tenantId and email alex.testfield@example.com
Database-->>SeedRoute: delete complete
SeedRoute->>Database: insert contacts Alex Testfield (all McKay fields)
Database-->>SeedRoute: created contact
SeedRoute->>Database: insert contactChildren records
SeedRoute->>Database: insert contactSportsTeams record
SeedRoute->>Database: insert scheduledSends birthday for tomorrow
Database-->>SeedRoute: inserts complete
SeedRoute-->>DashboardPage: 200 { success: true, contactId }
DashboardPage->>DashboardPage: queryClient.invalidateQueries()
DashboardPage-->>DevUser: Dashboard refresh shows Alex Testfield in Coming Up and Contacts
end
end
end
Entity relationship diagram for contacts, scheduled sends, and cardserDiagram
tenants {
string id PK
}
contacts {
string id PK
string tenantId FK
string email
string firstName
string lastName
}
contactChildren {
string id PK
string contactId FK
string tenantId FK
string name
}
contactSportsTeams {
string id PK
string contactId FK
string tenantId FK
string sport
string teamName
}
cardTemplates {
string id PK
string tenantId FK
string occasionType
string name
string imageUrl
boolean isSystem
boolean isActive
}
scheduledSends {
string id PK
string tenantId FK
string contactId FK
string cardTemplateId FK
string occasionType
string occasionLabel
date scheduledDate
string status
string emailSubject
}
tenants ||--o{ contacts : has
tenants ||--o{ contactChildren : has
tenants ||--o{ contactSportsTeams : has
tenants ||--o{ cardTemplates : has
tenants ||--o{ scheduledSends : has
contacts ||--o{ contactChildren : has
contacts ||--o{ contactSportsTeams : has
contacts ||--o{ scheduledSends : has
cardTemplates ||--o{ scheduledSends : used_by
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In the
DELETE /api/cards/[id]handler, theparamsargument is typed and treated as aPromise, but Next.js app router passes it synchronously, so you can simplify the signature to{ params }: { params: { id: string } }and drop theawait paramsto make the handler more idiomatic and type-safe. - For
StatCardcolor handling, consider tightening thecolortype to a union of the supported keys (e.g.'teal' | 'gold' | 'sky' | 'coral') instead ofstringso TypeScript can catch invalid values and ensure you always hit a defined accent inACCENT/ACCENT_RGB. - On the dashboard page you’re reading
process.env.NODE_ENVin a client component; although this is inlined at build time, using a dedicatedNEXT_PUBLIC_...flag (as mentioned in the spec) would make the dev-only button behavior clearer and avoid confusion if you later change build targets or environments.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In the `DELETE /api/cards/[id]` handler, the `params` argument is typed and treated as a `Promise`, but Next.js app router passes it synchronously, so you can simplify the signature to `{ params }: { params: { id: string } }` and drop the `await params` to make the handler more idiomatic and type-safe.
- For `StatCard` color handling, consider tightening the `color` type to a union of the supported keys (e.g. `'teal' | 'gold' | 'sky' | 'coral'`) instead of `string` so TypeScript can catch invalid values and ensure you always hit a defined accent in `ACCENT`/`ACCENT_RGB`.
- On the dashboard page you’re reading `process.env.NODE_ENV` in a client component; although this is inlined at build time, using a dedicated `NEXT_PUBLIC_...` flag (as mentioned in the spec) would make the dev-only button behavior clearer and avoid confusion if you later change build targets or environments.
## Individual Comments
### Comment 1
<location path="src/app/api/cards/[id]/route.ts" line_range="9-18" />
<code_context>
+
+export async function DELETE(
+ _req: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const { userId } = await auth()
+ if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
+
+ const tenantId = await getTenantId(userId)
+ if (!tenantId) return NextResponse.json({ error: "No tenant" }, { status: 400 })
+
+ const { id } = await params
+
+ const card = await db.query.cardTemplates.findFirst({
</code_context>
<issue_to_address>
**suggestion:** The `params` type and `await params` usage are inconsistent with Next.js route handler conventions.
In App Router route handlers, `params` is a plain object, not a promise. Typing it as `Promise<{ id: string }>` and `await`-ing it is misleading for both readers and TypeScript. Please update the signature to use a non-promise `params` object and access `id` directly:
```ts
export async function DELETE(
_req: Request,
{ params }: { params: { id: string } }
) {
const { id } = params
// ...
}
```
Suggested implementation:
```typescript
export async function DELETE(
_req: Request,
{ params }: { params: { id: string } }
) {
```
```typescript
const { id } = params
```
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| { params }: { params: Promise<{ id: string }> } | ||
| ) { | ||
| try { | ||
| const { userId } = await auth() | ||
| if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 }) | ||
|
|
||
| const tenantId = await getTenantId(userId) | ||
| if (!tenantId) return NextResponse.json({ error: "No tenant" }, { status: 400 }) | ||
|
|
||
| const { id } = await params |
There was a problem hiding this comment.
suggestion: The params type and await params usage are inconsistent with Next.js route handler conventions.
In App Router route handlers, params is a plain object, not a promise. Typing it as Promise<{ id: string }> and await-ing it is misleading for both readers and TypeScript. Please update the signature to use a non-promise params object and access id directly:
export async function DELETE(
_req: Request,
{ params }: { params: { id: string } }
) {
const { id } = params
// ...
}Suggested implementation:
export async function DELETE(
_req: Request,
{ params }: { params: { id: string } }
) { const { id } = params- ContactsTable: full 4-side cell borders on every th/td, column widths matched to content type, date formatting (May 7 1981), boolean Yes/No, zebra striping, font size 12px - Dashboard: 2-col mobile / 4-col desktop KPI grid, 1-col mobile / 3-col desktop content row, min-h-[280px] on panels, min-h-full page fill - Spec doc added for this rebuild
Summary
/api/cards/upload(POST, multipart, 10MB limit, image-type allowlist) and/api/cards/[id](DELETE, tenant-scoped)/api/dev/seedinserts Alex Testfield with all 66 McKay fields, 2 children, Cowboys sports team, birthday scheduled send for tomorrow; guarded byNODE_ENV+ENABLE_DEV_ROUTES=truescheduledSendsRelationssowith: { contact: true }in schedule routes resolves correctlygetTenantIdtosrc/lib/auth.tsTest Plan
localhost:3000Security
NODE_ENV !== 'production'ANDENABLE_DEV_ROUTES=true(double guard)id + tenantIdfirst, then delete with same conditions🤖 Generated with Claude Code
Summary by Sourcery
Redesign the dashboard and app shell while adding card gallery APIs and a dev-only seed contact for easier testing.
New Features:
Bug Fixes:
Enhancements:
Documentation:
Chores: