Skip to content

Commit bd24575

Browse files
authored
[pro-web] feat: implement brand kit wizard with multi-step creation flow (#597)
* refactor(media-tab): extract WorkspaceDialog reusable component - Create WorkspaceDialog component for standardized dialog structure - Refactor version-history-dialog to use new component - Improve code reusability across media tab wizards * feat(media-tab): add BrandKitEntry component for brand creation and import * feat(media-tab): implement BrandKitWizard with steps for brand creation * feat(media-tab): add BrandKitDialog and integrate with MediaWizards and MediaSidebar * feat(media-tab): implement brand kit wizard with vibe selection step - Add dynamic subtitle showing org name + "Logo & Style" in wizard mode - Implement sticky wizard stepper and navigation buttons layout - Create VibeStep with 12 brand vibes, 2-selection limit, and color preview - Add proper scroll container for wizard content with fixed header/footer - Style BadgeCheck icon and color dots with proper borders for dark mode - Update wizard container to use viewport-based height for proper layout * feat(media-tab): implement fonts step with vibe-based recommendations - Add FontsStep with primary/secondary font selection - Create Google Fonts integration with 70+ font families - Implement vibe-based font recommendations system - Add role badges (primary/secondary) for selected fonts - Include font preview with multiple samples - Update Brand Kit state management in useWorkspaceMedia hook - Adjust wizard and dialog padding for better spacing - Fix VibeStep to use shared state from context * refactor(media-tab): improve fonts step with sticky selection bar and scroll optimization - Add sticky bar showing selected fonts with removal capability - Implement vertical scroll snap carousel for font grid - Update font state management with role-based structure - Optimize scroll behavior in VibeStep and wizard stepper - Remove border from stepper for cleaner visual separation * feat(media-tab): implement colors step with palette selection and live preview - Add ColorsStep with 8 curated color palettes across multiple categories - Implement live preview card with selected fonts and organization name - Add AA contrast ratio validation with gentle auto-adjustment - Create shuffle functionality for random palette selection - Build carousel navigation for palette browsing - Display color strips with primary, accent, bg, and text tokens - Add palette state management to useWorkspaceMedia hook - Integrate selected fonts from previous step into color preview * feat(media-tab): enhance ColorsStep layout with improved scrolling and responsive design * feat(media-tab): refactor ColorsStep for theme-based color adjustments and improve contrast handling * feat(media-tab): add checkpoint step to brand kit wizard - Create CheckpointStep component to review selected brand elements - Display color scheme with palette circles and category information - Show font family preview with primary and secondary fonts - Add font switching functionality between primary and secondary roles - Integrate organization name and vibe label in checkpoint UI - Replace placeholder LogoStep with functional CheckpointStep - Add flag icon to final step in wizard stepper for visual clarity * fix(media-tab): adapt checkpoint step for single source and validate sources by vibes * feat(media-tab): add persistence with IDB sync and checkpoint handlers * fix(media-tab): restore wizard step when changing organizations * feat(hasura): add brand_kit jsonb column to organization * fix(media-tab): improve validation and error handling for brand kit flow
1 parent a0720d9 commit bd24575

File tree

28 files changed

+6796
-3706
lines changed

28 files changed

+6796
-3706
lines changed

apps/hasura/metadata/databases/masterbots/tables/public_organization.yaml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ insert_permissions:
2727
permission:
2828
check: {}
2929
columns:
30+
- brand_kit
3031
- name
3132
- user_id
3233
comment: ""
@@ -36,23 +37,26 @@ insert_permissions:
3637
user_id:
3738
_eq: X-Hasura-User-Id
3839
columns:
40+
- brand_kit
3941
- name
4042
- user_id
4143
comment: ""
4244
select_permissions:
4345
- role: moderator
4446
permission:
4547
columns:
46-
- organization_id
48+
- brand_kit
4749
- name
50+
- organization_id
4851
- user_id
4952
filter: {}
5053
comment: ""
5154
- role: user
5255
permission:
5356
columns:
54-
- organization_id
57+
- brand_kit
5558
- name
59+
- organization_id
5660
- user_id
5761
filter:
5862
user_id:
@@ -62,13 +66,15 @@ update_permissions:
6266
- role: moderator
6367
permission:
6468
columns:
69+
- brand_kit
6570
- name
6671
filter: {}
6772
check: null
6873
comment: ""
6974
- role: user
7075
permission:
7176
columns:
77+
- brand_kit
7278
- name
7379
filter:
7480
user_id:
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
alter table "public"."organization"
2+
drop column "brand_kit";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
alter table "public"."organization" add column "brand_kit" jsonb
2+
not null default '{}'::jsonb;

apps/pro-web/app/actions/thread.actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,7 @@ export async function getUserOrganizations(
554554
organizationId: true,
555555
name: true,
556556
userId: true,
557+
brandKit: true,
557558
organization_chatbots: {
558559
chatbotId: true,
559560
isActive: true,
@@ -575,6 +576,7 @@ export async function getUserOrganizations(
575576
(org): OrganizationData => ({
576577
id: org.organizationId?.toString() || '',
577578
name: org.name || '',
579+
brandKit: org.brandKit || null,
578580
chatbots: org.organization_chatbots?.map((oc) => ({
579581
chatbotId: oc.chatbotId || 0,
580582
isActive: oc.isActive || false,

apps/pro-web/app/api/organizations/[id]/route.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { auth } from '@/auth'
22
import { logErrorToSentry } from '@/lib/sentry'
3+
import type { BrandKitData } from '@/types/media.types'
34
import type { OrganizationChatbot } from '@/types/thread.types'
45
import { getHasuraClient } from 'mb-lib'
56
import type { NextRequest } from 'next/server'
@@ -8,6 +9,7 @@ import { NextResponse } from 'next/server'
89
interface UpdateOrganizationBody {
910
name?: string
1011
chatbots?: OrganizationChatbot[]
12+
brandKit?: BrandKitData
1113
}
1214

1315
/**
@@ -65,6 +67,17 @@ export async function PATCH(
6567
)
6668
}
6769

70+
// Validate brandKit if provided
71+
if (
72+
body.brandKit !== undefined &&
73+
(typeof body.brandKit !== 'object' || body.brandKit === null)
74+
) {
75+
return NextResponse.json(
76+
{ error: 'Brand Kit must be an object' },
77+
{ status: 422 },
78+
)
79+
}
80+
6881
// Initialize Hasura client
6982
const client = getHasuraClient()
7083

@@ -163,6 +176,20 @@ export async function PATCH(
163176
}
164177
}
165178

179+
// Update brandKit if provided
180+
if (body.brandKit !== undefined) {
181+
await client.mutation({
182+
updateOrganizationByPk: {
183+
__args: {
184+
pkColumns: { organizationId: id },
185+
_set: { brandKit: body.brandKit },
186+
},
187+
organizationId: true,
188+
brandKit: true,
189+
},
190+
})
191+
}
192+
166193
// Fetch and return the updated organization
167194
const { organizationByPk } = await client.query({
168195
organizationByPk: {
@@ -172,6 +199,7 @@ export async function PATCH(
172199
organizationId: true,
173200
name: true,
174201
userId: true,
202+
brandKit: true,
175203
organization_chatbots: {
176204
chatbotId: true,
177205
isActive: true,
@@ -188,6 +216,7 @@ export async function PATCH(
188216
id: organizationByPk?.organizationId,
189217
name: organizationByPk?.name,
190218
userId: organizationByPk?.userId,
219+
brandKit: organizationByPk?.brandKit,
191220
chatbots: organizationByPk?.organization_chatbots?.map((oc) => ({
192221
chatbotId: oc.chatbotId,
193222
isActive: oc.isActive,

apps/pro-web/app/api/organizations/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export async function GET() {
4040
organizationId: true,
4141
name: true,
4242
userId: true,
43+
brandKit: true,
4344
organization_chatbots: {
4445
chatbotId: true,
4546
isActive: true,
@@ -59,6 +60,7 @@ export async function GET() {
5960
id: org.organizationId,
6061
name: org.name,
6162
userId: org.userId,
63+
brandKit: org.brandKit,
6264
chatbots: org.organization_chatbots?.map((oc) => ({
6365
chatbotId: oc.chatbotId,
6466
isActive: oc.isActive,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use client'
2+
3+
import { WorkspaceDialog } from '@/components/ui/workspace-dialog'
4+
import { useWorkspace } from '@/lib/hooks/use-workspace'
5+
import { useMemo, useState } from 'react'
6+
import { BrandKitEntry } from './brand-kit-entry'
7+
import { BrandKitWizard } from './wizard/brand-kit-wizard'
8+
9+
interface BrandKitDialogProps {
10+
open: boolean
11+
onOpenChange: (open: boolean) => void
12+
}
13+
14+
type BrandKitView = 'entry' | 'wizard' | 'summary'
15+
16+
export function BrandKitDialog({ open, onOpenChange }: BrandKitDialogProps) {
17+
const [currentView, setCurrentView] = useState<BrandKitView>('entry')
18+
const { activeOrganization } = useWorkspace()
19+
20+
const subtitle = useMemo(() => {
21+
if (currentView === 'wizard') {
22+
if (!activeOrganization) return 'Logo & Style'
23+
const endsWithS = activeOrganization.endsWith('s')
24+
return `${activeOrganization}${endsWithS ? "'" : "'s"} Logo & Style`
25+
}
26+
return 'Brand Kit'
27+
}, [currentView, activeOrganization])
28+
29+
const handleNewBrand = () => {
30+
setCurrentView('wizard')
31+
}
32+
33+
const handleImportBrand = () => {
34+
// TODO: Open brand import flow
35+
// This could trigger a file picker or navigate to import wizard
36+
console.log('Importing brand...')
37+
}
38+
39+
const handleWizardFinish = () => {
40+
// TODO: Navigate to summary view
41+
console.log('Wizard finished, navigating to summary...')
42+
setCurrentView('summary')
43+
}
44+
45+
const handleWizardCancel = () => {
46+
setCurrentView('entry')
47+
}
48+
49+
return (
50+
<WorkspaceDialog
51+
open={open}
52+
onOpenChange={onOpenChange}
53+
title="Media Mode"
54+
subtitle={subtitle}
55+
>
56+
{currentView === 'entry' && (
57+
<BrandKitEntry
58+
onNewBrand={handleNewBrand}
59+
onImportBrand={handleImportBrand}
60+
/>
61+
)}
62+
63+
{currentView === 'wizard' && (
64+
<BrandKitWizard
65+
onFinish={handleWizardFinish}
66+
onCancel={handleWizardCancel}
67+
/>
68+
)}
69+
70+
{/* TODO: Add summary view */}
71+
{/* {currentView === 'summary' && <BrandKitSummary ... />} */}
72+
</WorkspaceDialog>
73+
)
74+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
'use client'
2+
3+
import { Button } from '@/components/ui/button'
4+
import { ImageDown, ImagePlus } from 'lucide-react'
5+
6+
interface BrandKitEntryProps {
7+
onNewBrand: () => void
8+
onImportBrand: () => void
9+
}
10+
11+
export function BrandKitEntry({
12+
onNewBrand,
13+
onImportBrand,
14+
}: BrandKitEntryProps) {
15+
return (
16+
<div className="flex items-center justify-center min-h-[380px] py-10">
17+
<div className="w-full max-w-xs sm:max-w-sm rounded-3xl border border-border/60 bg-zinc-100/50 dark:bg-zinc-900/50 px-8 py-7 sm:px-10 sm:py-8 shadow-sm">
18+
<p className="text-center text-sm text-black dark:text-white mb-3">
19+
Start from scratch and create
20+
</p>
21+
22+
{/* New Brand Button */}
23+
<Button
24+
onClick={onNewBrand}
25+
className="w-full h-16 sm:h-20 rounded-2xl px-6 text-lg font-normal flex items-center justify-center gap-3"
26+
>
27+
<ImagePlus className="w-5 h-5" />
28+
<span>New Brand</span>
29+
</Button>
30+
31+
{/* Separator with "or" */}
32+
<div className="flex items-center my-6 text-xs text-black dark:text-white">
33+
<div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
34+
<span className="text-xs sm:text-sm px-3">or</span>
35+
<div className="flex-1 h-px bg-zinc-200 dark:bg-zinc-700" />
36+
</div>
37+
38+
{/* Import Brand Button */}
39+
<Button
40+
onClick={onImportBrand}
41+
className="w-full h-16 sm:h-20 rounded-2xl bg-accent text-accent-foreground hover:bg-accent/90 px-6 text-lg font-normal flex items-center justify-center gap-3"
42+
>
43+
<ImageDown className="w-5 h-5" />
44+
<span>Import Brand</span>
45+
</Button>
46+
47+
<p className="text-center text-xs sm:text-sm text-black dark:text-white mt-3">
48+
from your existing brand assets
49+
</p>
50+
</div>
51+
</div>
52+
)
53+
}

0 commit comments

Comments
 (0)