Skip to content

Commit c40ae78

Browse files
committed
feat: vercel integration step 2
1 parent 0bdd80f commit c40ae78

File tree

8 files changed

+1850
-556
lines changed

8 files changed

+1850
-556
lines changed

apps/dashboard/app/(main)/settings/integrations/page.tsx

Lines changed: 94 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import {
44
CheckCircleIcon,
5+
DotsThreeIcon,
56
LinkIcon,
67
PlusIcon,
78
WarningIcon,
@@ -13,6 +14,12 @@ import { useEffect, useState } from 'react';
1314
import { Badge } from '@/components/ui/badge';
1415
import { Button } from '@/components/ui/button';
1516
import { Card, CardContent } from '@/components/ui/card';
17+
import {
18+
DropdownMenu,
19+
DropdownMenuContent,
20+
DropdownMenuItem,
21+
DropdownMenuTrigger,
22+
} from '@/components/ui/dropdown-menu';
1623
import { Skeleton } from '@/components/ui/skeleton';
1724
import {
1825
type Integration,
@@ -190,67 +197,95 @@ export default function IntegrationsPage() {
190197
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
191198
{categoryIntegrations.map((integration) => (
192199
<Card
193-
className="group relative border-0 shadow-sm transition-all duration-200 hover:shadow-black/5 hover:shadow-md"
200+
className="group relative border-0 shadow-sm"
194201
key={integration.id}
195202
>
196-
<CardContent className="p-6">
197-
<div className="space-y-4">
198-
<div className="flex items-start justify-between">
199-
<div className="flex h-12 w-12 items-center justify-center rounded-lg border bg-white shadow-sm dark:bg-gray-800">
200-
<Image
201-
alt={`${integration.name} logo`}
202-
className="h-7 w-7 brightness-0 dark:brightness-100"
203-
height={28}
204-
src={integration.logo}
205-
width={28}
206-
/>
203+
{integration.connected ? (
204+
<Link href={`/settings/integrations/${integration.id}`}>
205+
<CardContent className="cursor-pointer p-6">
206+
<div className="space-y-4">
207+
<div className="flex items-start justify-between">
208+
<div className="flex h-12 w-12 items-center justify-center rounded-lg border bg-white shadow-sm dark:bg-gray-800">
209+
<Image
210+
alt={`${integration.name} logo`}
211+
className="h-7 w-7 brightness-0 dark:brightness-100"
212+
height={28}
213+
src={integration.logo}
214+
width={28}
215+
/>
216+
</div>
217+
<div className="flex items-center gap-2">
218+
<Badge
219+
className="bg-green-100 text-green-800 hover:bg-green-100 dark:bg-green-900 dark:text-green-100"
220+
variant="default"
221+
>
222+
Connected
223+
</Badge>
224+
<DropdownMenu>
225+
<DropdownMenuTrigger asChild>
226+
<Button
227+
className="h-8 w-8 p-0"
228+
onClick={(e) => e.preventDefault()}
229+
size="sm"
230+
variant="ghost"
231+
>
232+
<DotsThreeIcon className="h-4 w-4" />
233+
</Button>
234+
</DropdownMenuTrigger>
235+
<DropdownMenuContent align="end">
236+
<DropdownMenuItem
237+
className="text-destructive focus:text-destructive"
238+
disabled={disconnectMutation.isPending}
239+
onClick={(e) => {
240+
e.preventDefault();
241+
handleDisconnect(integration);
242+
}}
243+
>
244+
{disconnectMutation.isPending
245+
? 'Disconnecting...'
246+
: 'Disconnect'}
247+
</DropdownMenuItem>
248+
</DropdownMenuContent>
249+
</DropdownMenu>
250+
</div>
251+
</div>
252+
253+
<div className="space-y-2">
254+
<h3 className="font-semibold text-lg leading-none tracking-tight">
255+
{integration.name}
256+
</h3>
257+
<p className="text-muted-foreground text-sm leading-relaxed">
258+
{integration.description}
259+
</p>
260+
</div>
261+
</div>
262+
</CardContent>
263+
</Link>
264+
) : (
265+
<CardContent className="p-6">
266+
<div className="space-y-4">
267+
<div className="flex items-start justify-between">
268+
<div className="flex h-12 w-12 items-center justify-center rounded-lg border bg-white shadow-sm dark:bg-gray-800">
269+
<Image
270+
alt={`${integration.name} logo`}
271+
className="h-7 w-7 brightness-0 dark:brightness-100"
272+
height={28}
273+
src={integration.logo}
274+
width={28}
275+
/>
276+
</div>
207277
</div>
208-
{integration.connected && (
209-
<Badge
210-
className="bg-green-100 text-green-800 hover:bg-green-100 dark:bg-green-900 dark:text-green-100"
211-
variant="default"
212-
>
213-
Connected
214-
</Badge>
215-
)}
216-
</div>
217278

218-
<div className="space-y-2">
219-
<h3 className="font-semibold text-lg leading-none tracking-tight">
220-
{integration.name}
221-
</h3>
222-
<p className="text-muted-foreground text-sm leading-relaxed">
223-
{integration.description}
224-
</p>
225-
</div>
279+
<div className="space-y-2">
280+
<h3 className="font-semibold text-lg leading-none tracking-tight">
281+
{integration.name}
282+
</h3>
283+
<p className="text-muted-foreground text-sm leading-relaxed">
284+
{integration.description}
285+
</p>
286+
</div>
226287

227-
<div className="pt-2">
228-
{integration.connected ? (
229-
<div className="flex gap-2">
230-
<Link
231-
href={`/settings/integrations/${integration.id}`}
232-
>
233-
<Button
234-
className="flex-1"
235-
size="sm"
236-
variant="outline"
237-
>
238-
Configure
239-
</Button>
240-
</Link>
241-
<Button
242-
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
243-
disabled={disconnectMutation.isPending}
244-
onClick={() => handleDisconnect(integration)}
245-
size="sm"
246-
variant="ghost"
247-
>
248-
{disconnectMutation.isPending
249-
? 'Disconnecting...'
250-
: 'Disconnect'}
251-
</Button>
252-
</div>
253-
) : (
288+
<div className="pt-2">
254289
<Button
255290
className="w-full font-medium"
256291
disabled={connectingProvider === integration.id}
@@ -265,10 +300,10 @@ export default function IntegrationsPage() {
265300
</>
266301
)}
267302
</Button>
268-
)}
303+
</div>
269304
</div>
270-
</div>
271-
</CardContent>
305+
</CardContent>
306+
)}
272307
</Card>
273308
))}
274309
</div>

apps/dashboard/app/(main)/settings/integrations/vercel/_components/create-website-dialog.tsx

Lines changed: 76 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,7 @@ import {
2020
SheetTitle,
2121
} from '@/components/ui/sheet';
2222
import type { Domain, Project } from './types';
23-
import {
24-
generateWebsiteName,
25-
generateWebsitePlaceholder,
26-
inferTargetFromDomain,
27-
} from './utils';
23+
import { generateWebsiteName, generateWebsitePlaceholder } from './utils';
2824

2925
interface WebsiteConfig {
3026
domain: Domain;
@@ -54,13 +50,25 @@ export function CreateWebsiteDialog({
5450

5551
useEffect(() => {
5652
if (selectedDomains.length > 0) {
57-
const configs = selectedDomains.map((domain) => ({
58-
domain,
59-
name: '', // Start with empty name, will use placeholder
60-
target: isMultipleMode
61-
? inferTargetFromDomain(domain)
62-
: (['production', 'preview'] as string[]),
63-
}));
53+
const configs = selectedDomains.map((domain, index) => {
54+
let target: string[];
55+
56+
if (isMultipleMode) {
57+
if (index === 0) {
58+
target = ['production'];
59+
} else {
60+
target = ['preview'];
61+
}
62+
} else {
63+
target = ['production'];
64+
}
65+
66+
return {
67+
domain,
68+
name: '',
69+
target,
70+
};
71+
});
6472
setWebsiteConfigs(configs);
6573
}
6674
}, [selectedDomains, isMultipleMode]);
@@ -96,10 +104,22 @@ export function CreateWebsiteDialog({
96104
}, [onClose]);
97105

98106
const isFormValid = useMemo(() => {
99-
return (
100-
websiteConfigs.length > 0 &&
101-
websiteConfigs.every((config) => config.target.length > 0)
107+
if (websiteConfigs.length === 0) {
108+
return false;
109+
}
110+
111+
// Each domain must have exactly one target environment
112+
const hasValidTargets = websiteConfigs.every(
113+
(config) => config.target.length === 1
102114
);
115+
116+
// Only production environment can be used once, preview can be used multiple times
117+
const productionCount = websiteConfigs.filter((config) =>
118+
config.target.includes('production')
119+
).length;
120+
const hasValidProductionUsage = productionCount <= 1;
121+
122+
return hasValidTargets && hasValidProductionUsage;
103123
}, [websiteConfigs]);
104124

105125
if (selectedDomains.length === 0) {
@@ -195,32 +215,52 @@ export function CreateWebsiteDialog({
195215

196216
<div className="space-y-3">
197217
<Label className="font-medium text-sm">
198-
Target Environments
218+
Target Environment
199219
</Label>
200220
<div className="flex flex-wrap gap-2">
201-
{['production', 'preview'].map((env) => (
202-
<Button
203-
className="h-8 rounded text-xs transition-colors"
204-
key={env}
205-
onClick={() => {
206-
const newTarget = config.target.includes(env)
207-
? config.target.filter((t) => t !== env)
208-
: [...config.target, env];
209-
updateWebsiteConfig(index, { target: newTarget });
210-
}}
211-
size="sm"
212-
variant={
213-
config.target.includes(env) ? 'default' : 'outline'
214-
}
215-
>
216-
{env.charAt(0).toUpperCase() + env.slice(1)}
217-
</Button>
218-
))}
221+
{['production', 'preview'].map((env) => {
222+
const isUsedByOther =
223+
env === 'production' &&
224+
websiteConfigs.some(
225+
(otherConfig, otherIndex) =>
226+
otherIndex !== index &&
227+
otherConfig.target.includes(env)
228+
);
229+
const isSelected = config.target.includes(env);
230+
231+
return (
232+
<Button
233+
className="h-8 rounded text-xs transition-colors"
234+
disabled={isUsedByOther && !isSelected}
235+
key={env}
236+
onClick={() => {
237+
if (isSelected) {
238+
// Remove this environment
239+
updateWebsiteConfig(index, {
240+
target: config.target.filter((t) => t !== env),
241+
});
242+
} else {
243+
updateWebsiteConfig(index, { target: [env] });
244+
}
245+
}}
246+
size="sm"
247+
variant={isSelected ? 'default' : 'outline'}
248+
>
249+
{env.charAt(0).toUpperCase() + env.slice(1)}
250+
{isUsedByOther && !isSelected && ' (Used)'}
251+
</Button>
252+
);
253+
})}
219254
</div>
220255
<p className="text-muted-foreground text-xs">
221-
Select which Vercel environments should receive the
222-
DATABUDDY_CLIENT_ID variable
256+
Production can only be assigned to one domain. Preview can be
257+
used by multiple domains.
223258
</p>
259+
{config.target.length === 0 && (
260+
<p className="text-destructive text-xs">
261+
Please select an environment for this domain.
262+
</p>
263+
)}
224264
</div>
225265

226266
<div className="rounded border bg-muted/30 p-3">

0 commit comments

Comments
 (0)