When creating products, make Team option only available if Team app is installed

This commit is contained in:
Konstantin Wohlwend 2026-01-21 19:36:40 -08:00
parent 9c75c3572e
commit e400985754
2 changed files with 157 additions and 73 deletions

View File

@ -1,5 +1,6 @@
"use client";
import { Link } from "@/components/link";
import { ItemDialog } from "@/components/payments/item-dialog";
import { useRouter } from "@/components/router";
import {
@ -26,7 +27,7 @@ import {
} from "@/components/ui";
import { useUpdateConfig } from "@/lib/config-update";
import { cn } from "@/lib/utils";
import { ArrowLeftIcon, BuildingOfficeIcon, CaretDownIcon, ChatIcon, ClockIcon, CodeIcon, CopyIcon, GearIcon, HardDriveIcon, LightningIcon, PlusIcon, PuzzlePieceIcon, StackIcon, TrashIcon, UserIcon } from "@phosphor-icons/react";
import { ArrowLeftIcon, ArrowSquareOutIcon, BuildingOfficeIcon, CaretDownIcon, ChatIcon, ClockIcon, CodeIcon, CopyIcon, GearIcon, HardDriveIcon, LightningIcon, PlusIcon, PuzzlePieceIcon, StackIcon, TrashIcon, UserIcon } from "@phosphor-icons/react";
import { CompleteConfig } from "@stackframe/stack-shared/dist/config/schema";
import { getUserSpecifiedIdErrorMessage, isValidUserSpecifiedId, sanitizeUserSpecifiedId } from "@stackframe/stack-shared/dist/schema-fields";
import { typedEntries } from "@stackframe/stack-shared/dist/utils/objects";
@ -70,13 +71,48 @@ const CUSTOMER_TYPE_OPTIONS = [
},
] as const;
const COLOR_CLASSES = {
blue: {
hover: 'hover:border-blue-500/40 hover:shadow-[0_0_12px_rgba(59,130,246,0.1)]',
bg: 'bg-blue-500/10 dark:bg-blue-500/[0.15] group-hover:bg-blue-500/20',
icon: 'text-blue-600 dark:text-blue-400',
},
emerald: {
hover: 'hover:border-emerald-500/40 hover:shadow-[0_0_12px_rgba(16,185,129,0.1)]',
bg: 'bg-emerald-500/10 dark:bg-emerald-500/[0.15] group-hover:bg-emerald-500/20',
icon: 'text-emerald-600 dark:text-emerald-400',
},
amber: {
hover: 'hover:border-amber-500/40 hover:shadow-[0_0_12px_rgba(245,158,11,0.1)]',
bg: 'bg-amber-500/10 dark:bg-amber-500/[0.15] group-hover:bg-amber-500/20',
icon: 'text-amber-600 dark:text-amber-400',
},
gray: {
hover: '',
bg: 'bg-foreground/[0.05]',
icon: 'text-foreground/40',
},
} as const;
function CustomerTypeSelection({
onSelectCustomerType,
onCancel,
isTeamsEnabled,
projectId,
}: {
onSelectCustomerType: (type: 'user' | 'team' | 'custom') => void,
onCancel: () => void,
isTeamsEnabled: boolean,
projectId: string,
}) {
// Split options into available and unavailable
const availableOptions = CUSTOMER_TYPE_OPTIONS.filter(
(option) => option.value !== 'team' || isTeamsEnabled
);
const unavailableOptions = CUSTOMER_TYPE_OPTIONS.filter(
(option) => option.value === 'team' && !isTeamsEnabled
);
return (
<div className="flex flex-col h-full">
<div className="flex items-center gap-4 px-6 py-4 border-b border-border/40">
@ -97,26 +133,11 @@ function CustomerTypeSelection({
<Typography type="h2" className="text-2xl font-semibold">Who will this product be for?</Typography>
</div>
{/* Available options */}
<div className="grid gap-3">
{CUSTOMER_TYPE_OPTIONS.map((option) => {
{availableOptions.map((option) => {
const Icon = option.icon;
const colorClasses = {
blue: {
hover: 'hover:border-blue-500/40 hover:shadow-[0_0_12px_rgba(59,130,246,0.1)]',
bg: 'bg-blue-500/10 dark:bg-blue-500/[0.15] group-hover:bg-blue-500/20',
icon: 'text-blue-600 dark:text-blue-400',
},
emerald: {
hover: 'hover:border-emerald-500/40 hover:shadow-[0_0_12px_rgba(16,185,129,0.1)]',
bg: 'bg-emerald-500/10 dark:bg-emerald-500/[0.15] group-hover:bg-emerald-500/20',
icon: 'text-emerald-600 dark:text-emerald-400',
},
amber: {
hover: 'hover:border-amber-500/40 hover:shadow-[0_0_12px_rgba(245,158,11,0.1)]',
bg: 'bg-amber-500/10 dark:bg-amber-500/[0.15] group-hover:bg-amber-500/20',
icon: 'text-amber-600 dark:text-amber-400',
},
}[option.color];
const colorClasses = COLOR_CLASSES[option.color];
return (
<Card
@ -150,6 +171,59 @@ function CustomerTypeSelection({
);
})}
</div>
{/* Unavailable options section */}
{unavailableOptions.length > 0 && (
<div className="space-y-2 pt-8">
<Typography type="label" className="text-xs text-foreground/40 uppercase tracking-wider">
Unavailable options
</Typography>
<p className="text-xs text-muted-foreground mb-3">
These options require additional apps or configuration.
</p>
<div className="grid gap-3">
{unavailableOptions.map((option) => {
const Icon = option.icon;
const colorClasses = COLOR_CLASSES.gray;
return (
<Link
key={option.value}
href={`/projects/${projectId}/apps/teams`}
>
<Card
className={cn(
"cursor-pointer group",
"rounded-xl border border-border/30 dark:border-foreground/[0.05]",
"bg-foreground/[0.01] hover:bg-foreground/[0.03]",
"opacity-60 hover:opacity-80",
"transition-all duration-150 hover:transition-none"
)}
>
<CardHeader className="p-4">
<div className="flex items-center gap-3">
<div className={cn(
"p-2.5 rounded-xl",
colorClasses.bg
)}>
<Icon className={cn("h-5 w-5", colorClasses.icon)} />
</div>
<div className="flex-1">
<CardTitle className="text-base font-semibold text-foreground/50">{option.label}</CardTitle>
<CardDescription className="text-sm mt-1 text-muted-foreground/70">
Enable the Teams app to choose this customer type
</CardDescription>
</div>
<ArrowSquareOutIcon className="h-4 w-4 text-foreground/30" />
</div>
</CardHeader>
</Card>
</Link>
);
})}
</div>
</div>
)}
</div>
</div>
</div>
@ -458,12 +532,17 @@ export default function PageClient() {
}
};
// Check if Teams app is enabled
const isTeamsEnabled = config.apps.installed.teams?.enabled ?? false;
// Show customer type selection if not selected yet
if (!hasSelectedCustomerType) {
return (
<CustomerTypeSelection
onSelectCustomerType={handleSelectCustomerType}
onCancel={handleCancel}
isTeamsEnabled={isTeamsEnabled}
projectId={projectId}
/>
);
}

View File

@ -119,33 +119,17 @@ const CyclingPlaceholder = memo(function CyclingPlaceholder({
onSelectQuery?: (query: string) => void,
}) {
return (
<div className="h-full flex flex-col gap-4 items-center select-none px-6 pt-8 pb-4">
<div className="h-full flex flex-col items-center select-none px-6">
{/* Top spacer */}
<div className="flex-1" />
{/* Welcome header */}
<div className="relative text-center">
{/* Keybind reminder - like tape on the corner */}
<span className="absolute -top-4 -right-8 rotate-[30deg] flex items-center gap-0.5 text-[10px] text-muted-foreground/40">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">K</kbd>
</span>
<h2 className="relative text-base font-semibold text-foreground mb-1 inline-block">
Welcome to Control Center
</h2>
<p className="text-[11px] text-muted-foreground/50">
Your shortcut to everything
</p>
</div>
{/* Feature highlights with floating icons */}
{/* Welcome header + Feature highlights grouped together */}
<div className="relative w-fit">
{/* Floating decorative icons - left and right sides only */}
<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">
<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">
<MagnifyingGlassIcon className="h-4.5 w-4.5 text-blue-500" />
</div>
<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">
<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">
<SparkleIcon className="h-4 w-4 text-purple-500" />
</div>
<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({
<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">
<LayoutIcon className="h-4.5 w-4.5 text-cyan-500" />
</div>
<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">
<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">
<PlayIcon className="h-3.5 w-3.5 text-amber-500" />
</div>
<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">
<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">
<SparkleIcon className="h-4 w-4 text-rose-500" />
</div>
{/* Welcome header */}
<div className="relative text-center mb-4">
{/* Keybind reminder - like tape on the corner */}
<span className="absolute -top-4 -right-8 rotate-[30deg] flex items-center gap-0.5 text-[10px] text-muted-foreground/40">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">K</kbd>
</span>
<h2 className="relative text-base font-semibold text-foreground mb-1 inline-block">
Welcome to Control Center
</h2>
<p className="text-[11px] text-muted-foreground/50">
Your shortcut to everything
</p>
</div>
{/* Feature text content */}
<div className="flex flex-col justify-center space-y-4 py-4 px-6 items-center">
{FEATURE_HIGHLIGHTS.map((feature, index) => {
@ -185,40 +185,45 @@ const CyclingPlaceholder = memo(function CyclingPlaceholder({
</div>
</div>
<div className="w-full max-w-[max(50vw,320px)] pt-4 mt-2 border-t border-foreground/[0.06]"></div>
{/* Cycling example */}
<div className="w-full max-w-[max(50vw,320px)]">
<p className="text-[9px] text-muted-foreground/40 uppercase tracking-wider mb-2.5 text-center pointer-events-none">Try something like</p>
<div className="flex justify-center">
<CyclingExample onSelectQuery={onSelectQuery} />
</div>
</div>
{/* Bottom spacer */}
<div className="flex-1" />
{/* Keyboard hints footer */}
<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">
<div className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">K</kbd>
<span>open</span>
{/* Bottom section - separator, cycling example, and footer grouped together */}
<div className="w-full shrink-0 -mx-6 px-6">
{/* Separator */}
<div className="border-t border-foreground/[0.06]" />
{/* Cycling example */}
<div className="py-5">
<p className="text-[9px] text-muted-foreground/40 uppercase tracking-wider mb-3 text-center pointer-events-none">Try something like</p>
<div className="flex justify-center">
<CyclingExample onSelectQuery={onSelectQuery} />
</div>
</div>
<div className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<span>navigate</span>
</div>
<div className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<span>select</span>
</div>
<div className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">esc</kbd>
<span>close</span>
{/* Keyboard hints footer */}
<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">
<div className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
+
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">K</kbd>
<span>open</span>
</div>
<div className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<span>navigate</span>
</div>
<div className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono"></kbd>
<span>select</span>
</div>
<div className="flex items-center gap-1.5">
<kbd className="px-1.5 py-0.5 rounded bg-foreground/[0.06] font-mono">esc</kbd>
<span>close</span>
</div>
</div>
</div>
</div>