Skip to content

Commit e400985

Browse files
committed
When creating products, make Team option only available if Team app is installed
1 parent 9c75c35 commit e400985

2 files changed

Lines changed: 157 additions & 73 deletions

File tree

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/payments/products/new/page-client.tsx

Lines changed: 98 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"use client";
22

3+
import { Link } from "@/components/link";
34
import { ItemDialog } from "@/components/payments/item-dialog";
45
import { useRouter } from "@/components/router";
56
import {
@@ -26,7 +27,7 @@ import {
2627
} from "@/components/ui";
2728
import { useUpdateConfig } from "@/lib/config-update";
2829
import { cn } from "@/lib/utils";
29-
import { ArrowLeftIcon, BuildingOfficeIcon, CaretDownIcon, ChatIcon, ClockIcon, CodeIcon, CopyIcon, GearIcon, HardDriveIcon, LightningIcon, PlusIcon, PuzzlePieceIcon, StackIcon, TrashIcon, UserIcon } from "@phosphor-icons/react";
30+
import { ArrowLeftIcon, ArrowSquareOutIcon, BuildingOfficeIcon, CaretDownIcon, ChatIcon, ClockIcon, CodeIcon, CopyIcon, GearIcon, HardDriveIcon, LightningIcon, PlusIcon, PuzzlePieceIcon, StackIcon, TrashIcon, UserIcon } from "@phosphor-icons/react";
3031
import { CompleteConfig } from "@stackframe/stack-shared/dist/config/schema";
3132
import { getUserSpecifiedIdErrorMessage, isValidUserSpecifiedId, sanitizeUserSpecifiedId } from "@stackframe/stack-shared/dist/schema-fields";
3233
import { typedEntries } from "@stackframe/stack-shared/dist/utils/objects";
@@ -70,13 +71,48 @@ const CUSTOMER_TYPE_OPTIONS = [
7071
},
7172
] as const;
7273

74+
const COLOR_CLASSES = {
75+
blue: {
76+
hover: 'hover:border-blue-500/40 hover:shadow-[0_0_12px_rgba(59,130,246,0.1)]',
77+
bg: 'bg-blue-500/10 dark:bg-blue-500/[0.15] group-hover:bg-blue-500/20',
78+
icon: 'text-blue-600 dark:text-blue-400',
79+
},
80+
emerald: {
81+
hover: 'hover:border-emerald-500/40 hover:shadow-[0_0_12px_rgba(16,185,129,0.1)]',
82+
bg: 'bg-emerald-500/10 dark:bg-emerald-500/[0.15] group-hover:bg-emerald-500/20',
83+
icon: 'text-emerald-600 dark:text-emerald-400',
84+
},
85+
amber: {
86+
hover: 'hover:border-amber-500/40 hover:shadow-[0_0_12px_rgba(245,158,11,0.1)]',
87+
bg: 'bg-amber-500/10 dark:bg-amber-500/[0.15] group-hover:bg-amber-500/20',
88+
icon: 'text-amber-600 dark:text-amber-400',
89+
},
90+
gray: {
91+
hover: '',
92+
bg: 'bg-foreground/[0.05]',
93+
icon: 'text-foreground/40',
94+
},
95+
} as const;
96+
7397
function CustomerTypeSelection({
7498
onSelectCustomerType,
7599
onCancel,
100+
isTeamsEnabled,
101+
projectId,
76102
}: {
77103
onSelectCustomerType: (type: 'user' | 'team' | 'custom') => void,
78104
onCancel: () => void,
105+
isTeamsEnabled: boolean,
106+
projectId: string,
79107
}) {
108+
// Split options into available and unavailable
109+
const availableOptions = CUSTOMER_TYPE_OPTIONS.filter(
110+
(option) => option.value !== 'team' || isTeamsEnabled
111+
);
112+
const unavailableOptions = CUSTOMER_TYPE_OPTIONS.filter(
113+
(option) => option.value === 'team' && !isTeamsEnabled
114+
);
115+
80116
return (
81117
<div className="flex flex-col h-full">
82118
<div className="flex items-center gap-4 px-6 py-4 border-b border-border/40">
@@ -97,26 +133,11 @@ function CustomerTypeSelection({
97133
<Typography type="h2" className="text-2xl font-semibold">Who will this product be for?</Typography>
98134
</div>
99135

136+
{/* Available options */}
100137
<div className="grid gap-3">
101-
{CUSTOMER_TYPE_OPTIONS.map((option) => {
138+
{availableOptions.map((option) => {
102139
const Icon = option.icon;
103-
const colorClasses = {
104-
blue: {
105-
hover: 'hover:border-blue-500/40 hover:shadow-[0_0_12px_rgba(59,130,246,0.1)]',
106-
bg: 'bg-blue-500/10 dark:bg-blue-500/[0.15] group-hover:bg-blue-500/20',
107-
icon: 'text-blue-600 dark:text-blue-400',
108-
},
109-
emerald: {
110-
hover: 'hover:border-emerald-500/40 hover:shadow-[0_0_12px_rgba(16,185,129,0.1)]',
111-
bg: 'bg-emerald-500/10 dark:bg-emerald-500/[0.15] group-hover:bg-emerald-500/20',
112-
icon: 'text-emerald-600 dark:text-emerald-400',
113-
},
114-
amber: {
115-
hover: 'hover:border-amber-500/40 hover:shadow-[0_0_12px_rgba(245,158,11,0.1)]',
116-
bg: 'bg-amber-500/10 dark:bg-amber-500/[0.15] group-hover:bg-amber-500/20',
117-
icon: 'text-amber-600 dark:text-amber-400',
118-
},
119-
}[option.color];
140+
const colorClasses = COLOR_CLASSES[option.color];
120141

121142
return (
122143
<Card
@@ -150,6 +171,59 @@ function CustomerTypeSelection({
150171
);
151172
})}
152173
</div>
174+
175+
{/* Unavailable options section */}
176+
{unavailableOptions.length > 0 && (
177+
<div className="space-y-2 pt-8">
178+
<Typography type="label" className="text-xs text-foreground/40 uppercase tracking-wider">
179+
Unavailable options
180+
</Typography>
181+
<p className="text-xs text-muted-foreground mb-3">
182+
These options require additional apps or configuration.
183+
</p>
184+
<div className="grid gap-3">
185+
{unavailableOptions.map((option) => {
186+
const Icon = option.icon;
187+
const colorClasses = COLOR_CLASSES.gray;
188+
189+
return (
190+
<Link
191+
key={option.value}
192+
href={`/projects/${projectId}/apps/teams`}
193+
>
194+
<Card
195+
className={cn(
196+
"cursor-pointer group",
197+
"rounded-xl border border-border/30 dark:border-foreground/[0.05]",
198+
"bg-foreground/[0.01] hover:bg-foreground/[0.03]",
199+
"opacity-60 hover:opacity-80",
200+
"transition-all duration-150 hover:transition-none"
201+
)}
202+
>
203+
<CardHeader className="p-4">
204+
<div className="flex items-center gap-3">
205+
<div className={cn(
206+
"p-2.5 rounded-xl",
207+
colorClasses.bg
208+
)}>
209+
<Icon className={cn("h-5 w-5", colorClasses.icon)} />
210+
</div>
211+
<div className="flex-1">
212+
<CardTitle className="text-base font-semibold text-foreground/50">{option.label}</CardTitle>
213+
<CardDescription className="text-sm mt-1 text-muted-foreground/70">
214+
Enable the Teams app to choose this customer type
215+
</CardDescription>
216+
</div>
217+
<ArrowSquareOutIcon className="h-4 w-4 text-foreground/30" />
218+
</div>
219+
</CardHeader>
220+
</Card>
221+
</Link>
222+
);
223+
})}
224+
</div>
225+
</div>
226+
)}
153227
</div>
154228
</div>
155229
</div>
@@ -458,12 +532,17 @@ export default function PageClient() {
458532
}
459533
};
460534

535+
// Check if Teams app is enabled
536+
const isTeamsEnabled = config.apps.installed.teams?.enabled ?? false;
537+
461538
// Show customer type selection if not selected yet
462539
if (!hasSelectedCustomerType) {
463540
return (
464541
<CustomerTypeSelection
465542
onSelectCustomerType={handleSelectCustomerType}
466543
onCancel={handleCancel}
544+
isTeamsEnabled={isTeamsEnabled}
545+
projectId={projectId}
467546
/>
468547
);
469548
}

apps/dashboard/src/components/cmdk-search.tsx

Lines changed: 59 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -119,33 +119,17 @@ const CyclingPlaceholder = memo(function CyclingPlaceholder({
119119
onSelectQuery?: (query: string) => void,
120120
}) {
121121
return (
122-
<div className="h-full flex flex-col gap-4 items-center select-none px-6 pt-8 pb-4">
123-
122+
<div className="h-full flex flex-col items-center select-none px-6">
123+
{/* Top spacer */}
124124
<div className="flex-1" />
125125

126-
{/* Welcome header */}
127-
<div className="relative text-center">
128-
{/* Keybind reminder - like tape on the corner */}
129-
<span className="absolute -top-4 -right-8 rotate-[30deg] flex items-center gap-0.5 text-[10px] text-muted-foreground/40">
130-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
131-
+
132-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">K</kbd>
133-
</span>
134-
<h2 className="relative text-base font-semibold text-foreground mb-1 inline-block">
135-
Welcome to Control Center
136-
</h2>
137-
<p className="text-[11px] text-muted-foreground/50">
138-
Your shortcut to everything
139-
</p>
140-
</div>
141-
142-
{/* Feature highlights with floating icons */}
126+
{/* Welcome header + Feature highlights grouped together */}
143127
<div className="relative w-fit">
144128
{/* Floating decorative icons - left and right sides only */}
145-
<div className="absolute -left-6 top-0 w-9 h-9 rounded-xl bg-blue-500/10 flex items-center justify-center rotate-[-12deg] opacity-70">
129+
<div className="absolute -left-6 top-12 w-9 h-9 rounded-xl bg-blue-500/10 flex items-center justify-center rotate-[-12deg] opacity-70">
146130
<MagnifyingGlassIcon className="h-4.5 w-4.5 text-blue-500" />
147131
</div>
148-
<div className="absolute -right-6 top-2 w-8 h-8 rounded-xl bg-purple-500/10 flex items-center justify-center rotate-[15deg] opacity-60">
132+
<div className="absolute -right-6 top-14 w-8 h-8 rounded-xl bg-purple-500/10 flex items-center justify-center rotate-[15deg] opacity-60">
149133
<SparkleIcon className="h-4 w-4 text-purple-500" />
150134
</div>
151135
<div className="absolute -left-16 top-1/2 -translate-y-1/2 w-7 h-7 rounded-lg bg-green-500/10 flex items-center justify-center rotate-[20deg] opacity-50">
@@ -154,13 +138,29 @@ const CyclingPlaceholder = memo(function CyclingPlaceholder({
154138
<div className="absolute -right-20 top-1/2 -translate-y-1/2 w-9 h-9 rounded-xl bg-cyan-500/10 flex items-center justify-center rotate-[-8deg] opacity-60">
155139
<LayoutIcon className="h-4.5 w-4.5 text-cyan-500" />
156140
</div>
157-
<div className="absolute -left-7 bottom-0 w-7 h-7 rounded-lg bg-amber-500/10 flex items-center justify-center rotate-[8deg] opacity-50">
141+
<div className="absolute -left-7 bottom-4 w-7 h-7 rounded-lg bg-amber-500/10 flex items-center justify-center rotate-[8deg] opacity-50">
158142
<PlayIcon className="h-3.5 w-3.5 text-amber-500" />
159143
</div>
160-
<div className="absolute -right-5 bottom-2 w-8 h-8 rounded-xl bg-rose-500/10 flex items-center justify-center rotate-[-18deg] opacity-50">
144+
<div className="absolute -right-5 bottom-6 w-8 h-8 rounded-xl bg-rose-500/10 flex items-center justify-center rotate-[-18deg] opacity-50">
161145
<SparkleIcon className="h-4 w-4 text-rose-500" />
162146
</div>
163147

148+
{/* Welcome header */}
149+
<div className="relative text-center mb-4">
150+
{/* Keybind reminder - like tape on the corner */}
151+
<span className="absolute -top-4 -right-8 rotate-[30deg] flex items-center gap-0.5 text-[10px] text-muted-foreground/40">
152+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
153+
+
154+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">K</kbd>
155+
</span>
156+
<h2 className="relative text-base font-semibold text-foreground mb-1 inline-block">
157+
Welcome to Control Center
158+
</h2>
159+
<p className="text-[11px] text-muted-foreground/50">
160+
Your shortcut to everything
161+
</p>
162+
</div>
163+
164164
{/* Feature text content */}
165165
<div className="flex flex-col justify-center space-y-4 py-4 px-6 items-center">
166166
{FEATURE_HIGHLIGHTS.map((feature, index) => {
@@ -185,40 +185,45 @@ const CyclingPlaceholder = memo(function CyclingPlaceholder({
185185
</div>
186186
</div>
187187

188-
<div className="w-full max-w-[max(50vw,320px)] pt-4 mt-2 border-t border-foreground/[0.06]"></div>
189-
190-
{/* Cycling example */}
191-
<div className="w-full max-w-[max(50vw,320px)]">
192-
<p className="text-[9px] text-muted-foreground/40 uppercase tracking-wider mb-2.5 text-center pointer-events-none">Try something like</p>
193-
<div className="flex justify-center">
194-
<CyclingExample onSelectQuery={onSelectQuery} />
195-
</div>
196-
</div>
197-
188+
{/* Bottom spacer */}
198189
<div className="flex-1" />
199190

200-
{/* Keyboard hints footer */}
201-
<div className="pt-4 mt-4 -mx-6 px-6 border-t border-foreground/[0.06] w-full flex items-center justify-center gap-5 text-[10px] text-muted-foreground/40">
202-
<div className="flex items-center gap-1.5">
203-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
204-
+
205-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">K</kbd>
206-
<span>open</span>
207-
</div>
208-
<div className="flex items-center gap-1.5">
209-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
210-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
211-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
212-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
213-
<span>navigate</span>
214-
</div>
215-
<div className="flex items-center gap-1.5">
216-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
217-
<span>select</span>
191+
{/* Bottom section - separator, cycling example, and footer grouped together */}
192+
<div className="w-full shrink-0 -mx-6 px-6">
193+
{/* Separator */}
194+
<div className="border-t border-foreground/[0.06]" />
195+
196+
{/* Cycling example */}
197+
<div className="py-5">
198+
<p className="text-[9px] text-muted-foreground/40 uppercase tracking-wider mb-3 text-center pointer-events-none">Try something like</p>
199+
<div className="flex justify-center">
200+
<CyclingExample onSelectQuery={onSelectQuery} />
201+
</div>
218202
</div>
219-
<div className="flex items-center gap-1.5">
220-
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">esc</kbd>
221-
<span>close</span>
203+
204+
{/* Keyboard hints footer */}
205+
<div className="py-3 border-t border-foreground/[0.06] w-full flex items-center justify-center gap-5 text-[10px] text-muted-foreground/40">
206+
<div className="flex items-center gap-1.5">
207+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
208+
+
209+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">K</kbd>
210+
<span>open</span>
211+
</div>
212+
<div className="flex items-center gap-1.5">
213+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
214+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
215+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
216+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
217+
<span>navigate</span>
218+
</div>
219+
<div className="flex items-center gap-1.5">
220+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
221+
<span>select</span>
222+
</div>
223+
<div className="flex items-center gap-1.5">
224+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">esc</kbd>
225+
<span>close</span>
226+
</div>
222227
</div>
223228
</div>
224229
</div>

0 commit comments

Comments
 (0)