mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
When creating products, make Team option only available if Team app is installed
This commit is contained in:
parent
9c75c3572e
commit
e400985754
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user