11'use client' ;
22
3- import { CheckCircleIcon , LinkIcon , PlusIcon , WarningIcon } from '@phosphor-icons/react' ;
3+ import {
4+ CheckCircleIcon ,
5+ LinkIcon ,
6+ PlusIcon ,
7+ WarningIcon ,
8+ } from '@phosphor-icons/react' ;
49import Image from 'next/image' ;
510import { useSearchParams } from 'next/navigation' ;
611import { useEffect , useState } from 'react' ;
7- import { Card , CardContent , CardDescription , CardHeader , CardTitle } from '@/components/ui/card' ;
8- import { Button } from '@/components/ui/button' ;
912import { Badge } from '@/components/ui/badge' ;
13+ import { Button } from '@/components/ui/button' ;
14+ import {
15+ Card ,
16+ CardContent ,
17+ CardDescription ,
18+ CardHeader ,
19+ CardTitle ,
20+ } from '@/components/ui/card' ;
1021import { Skeleton } from '@/components/ui/skeleton' ;
11- import { useIntegrations , useDisconnectIntegration , type Integration } from '@/hooks/use-integrations' ;
22+ import {
23+ type Integration ,
24+ useDisconnectIntegration ,
25+ useIntegrations ,
26+ } from '@/hooks/use-integrations' ;
1227
1328const categoryLabels = {
1429 deployment : 'Deployment' ,
@@ -31,7 +46,7 @@ function LoadingSkeleton() {
3146 </ div >
3247 < div className = "grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" >
3348 { [ 1 , 2 , 3 , 4 ] . map ( ( num ) => (
34- < Card key = { num } className = "animate-pulse border-0 shadow-sm" >
49+ < Card className = "animate-pulse border-0 shadow-sm" key = { num } >
3550 < CardContent className = "p-6" >
3651 < div className = "space-y-4" >
3752 < div className = "flex items-start justify-between" >
@@ -65,7 +80,7 @@ function ErrorState({ onRetry }: { onRetry: () => void }) {
6580 < p className = "mt-1 text-muted-foreground text-sm" >
6681 There was an issue loading your integrations. Please try again.
6782 </ p >
68- < Button onClick = { onRetry } variant = "outline" className = "mt-4 ">
83+ < Button className = "mt-4" onClick = { onRetry } variant = "outline" >
6984 Try Again
7085 </ Button >
7186 </ div >
@@ -75,7 +90,9 @@ function ErrorState({ onRetry }: { onRetry: () => void }) {
7590
7691export default function IntegrationsPage ( ) {
7792 const searchParams = useSearchParams ( ) ;
78- const [ connectingProvider , setConnectingProvider ] = useState < string | null > ( null ) ;
93+ const [ connectingProvider , setConnectingProvider ] = useState < string | null > (
94+ null
95+ ) ;
7996 const [ showSuccessMessage , setShowSuccessMessage ] = useState ( false ) ;
8097 const { integrations, isLoading, isError, refetch } = useIntegrations ( ) ;
8198 const disconnectMutation = useDisconnectIntegration ( ) ;
@@ -85,17 +102,17 @@ export default function IntegrationsPage() {
85102 if ( searchParams . get ( 'vercel_integrated' ) === 'true' ) {
86103 setShowSuccessMessage ( true ) ;
87104 refetch ( ) ; // Refresh integrations to show the new connection
88-
105+
89106 // Remove the query parameter from URL
90107 const url = new URL ( window . location . href ) ;
91108 url . searchParams . delete ( 'vercel_integrated' ) ;
92109 window . history . replaceState ( { } , '' , url . toString ( ) ) ;
93-
110+
94111 // Hide success message after 5 seconds
95112 const timer = setTimeout ( ( ) => {
96113 setShowSuccessMessage ( false ) ;
97114 } , 5000 ) ;
98-
115+
99116 return ( ) => clearTimeout ( timer ) ;
100117 }
101118 } , [ searchParams , refetch ] ) ;
@@ -125,127 +142,143 @@ export default function IntegrationsPage() {
125142 return < ErrorState onRetry = { refetch } /> ;
126143 }
127144
128- const groupedIntegrations = integrations . reduce ( ( acc , integration ) => {
129- if ( ! acc [ integration . category ] ) {
130- acc [ integration . category ] = [ ] ;
131- }
132- acc [ integration . category ] . push ( integration ) ;
133- return acc ;
134- } , { } as Record < string , Integration [ ] > ) ;
145+ const groupedIntegrations = integrations . reduce (
146+ ( acc , integration ) => {
147+ if ( ! acc [ integration . category ] ) {
148+ acc [ integration . category ] = [ ] ;
149+ }
150+ acc [ integration . category ] . push ( integration ) ;
151+ return acc ;
152+ } ,
153+ { } as Record < string , Integration [ ] >
154+ ) ;
135155
136156 return (
137157 < div className = "space-y-8" >
138158 { showSuccessMessage && (
139159 < div className = "rounded-xl border border-green-200 bg-green-50 p-4 dark:border-green-800 dark:bg-green-950/50" >
140160 < div className = "flex items-start gap-3" >
141- < CheckCircleIcon className = "h-5 w-5 text-green-600 dark:text-green-400 mt-0.5 " />
161+ < CheckCircleIcon className = "mt-0.5 h-5 w-5 text-green-600 dark:text-green-400" />
142162 < div className = "flex-1" >
143163 < h3 className = "font-semibold text-green-900 dark:text-green-100" >
144164 Integration Connected Successfully
145165 </ h3 >
146- < p className = "text-green-700 text-sm dark:text-green-300 mt-1" >
147- Vercel has been connected to your account. You can now manage your deployments.
166+ < p className = "mt-1 text-green-700 text-sm dark:text-green-300" >
167+ Vercel has been connected to your account. You can now manage
168+ your deployments.
148169 </ p >
149170 </ div >
150171 < Button
151- variant = "ghost"
152- size = "sm"
172+ className = "-mt-1 text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-200"
153173 onClick = { ( ) => setShowSuccessMessage ( false ) }
154- className = "text-green-600 hover:text-green-800 dark:text-green-400 dark:hover:text-green-200 -mt-1"
174+ size = "sm"
175+ variant = "ghost"
155176 >
156177 ×
157178 </ Button >
158179 </ div >
159180 </ div >
160181 ) }
161-
162- { Object . entries ( groupedIntegrations ) . map ( ( [ category , categoryIntegrations ] ) => (
163- < div key = { category } className = "space-y-6" >
164- < div className = "flex items-center gap-3" >
165- < h2 className = "font-semibold text-xl text-foreground" >
166- { categoryLabels [ category as keyof typeof categoryLabels ] }
167- </ h2 >
168- < div className = "flex h-6 w-6 items-center justify-center rounded-full bg-muted text-muted-foreground text-xs font-medium" >
169- { categoryIntegrations . length }
182+
183+ { Object . entries ( groupedIntegrations ) . map (
184+ ( [ category , categoryIntegrations ] ) => (
185+ < div className = "space-y-6" key = { category } >
186+ < div className = "flex items-center gap-3" >
187+ < h2 className = "font-semibold text-foreground text-xl" >
188+ { categoryLabels [ category as keyof typeof categoryLabels ] }
189+ </ h2 >
190+ < div className = "flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-muted-foreground text-xs" >
191+ { categoryIntegrations . length }
192+ </ div >
170193 </ div >
171- </ div >
172194
173- < div className = "grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" >
174- { categoryIntegrations . map ( ( integration ) => (
175- < Card key = { integration . id } className = "group relative border-0 shadow-sm transition-all duration-200 hover:shadow-md hover:shadow-black/5" >
176- < CardContent className = "p-6" >
177- < div className = "space-y-4" >
178- < div className = "flex items-start justify-between" >
179- < div className = "flex h-12 w-12 items-center justify-center rounded-lg border bg-white shadow-sm dark:bg-gray-800" >
180- < Image
181- src = { integration . logo }
182- alt = { `${ integration . name } logo` }
183- width = { 28 }
184- height = { 28 }
185- className = "h-7 w-7 brightness-0 dark:brightness-100"
186- />
187- </ div >
188- { integration . connected && (
189- < Badge variant = "default" className = "bg-green-100 text-green-800 hover:bg-green-100 dark:bg-green-900 dark:text-green-100" >
190- Connected
191- </ Badge >
192- ) }
193- </ div >
194-
195- < div className = "space-y-2" >
196- < h3 className = "font-semibold text-lg leading-none tracking-tight" >
197- { integration . name }
198- </ h3 >
199- < p className = "text-muted-foreground text-sm leading-relaxed" >
200- { integration . description }
201- </ p >
202- </ div >
203-
204- < div className = "pt-2" >
205- { integration . connected ? (
206- < div className = "flex gap-2" >
207- < Button
208- variant = "outline"
209- size = "sm"
210- className = "flex-1"
211- disabled
195+ < div className = "grid gap-6 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4" >
196+ { categoryIntegrations . map ( ( integration ) => (
197+ < Card
198+ className = "group relative border-0 shadow-sm transition-all duration-200 hover:shadow-black/5 hover:shadow-md"
199+ key = { integration . id }
200+ >
201+ < CardContent className = "p-6" >
202+ < div className = "space-y-4" >
203+ < div className = "flex items-start justify-between" >
204+ < div className = "flex h-12 w-12 items-center justify-center rounded-lg border bg-white shadow-sm dark:bg-gray-800" >
205+ < Image
206+ alt = { `${ integration . name } logo` }
207+ className = "h-7 w-7 brightness-0 dark:brightness-100"
208+ height = { 28 }
209+ src = { integration . logo }
210+ width = { 28 }
211+ />
212+ </ div >
213+ { integration . connected && (
214+ < Badge
215+ className = "bg-green-100 text-green-800 hover:bg-green-100 dark:bg-green-900 dark:text-green-100"
216+ variant = "default"
212217 >
213- Configure
214- </ Button >
215- < Button
216- variant = "ghost"
217- size = "sm"
218- className = "text-destructive hover:text-destructive hover:bg-destructive/10"
219- onClick = { ( ) => handleDisconnect ( integration ) }
220- disabled = { disconnectMutation . isPending }
218+ Connected
219+ </ Badge >
220+ ) }
221+ </ div >
222+
223+ < div className = "space-y-2" >
224+ < h3 className = "font-semibold text-lg leading-none tracking-tight" >
225+ { integration . name }
226+ </ h3 >
227+ < p className = "text-muted-foreground text-sm leading-relaxed" >
228+ { integration . description }
229+ </ p >
230+ </ div >
231+
232+ < div className = "pt-2" >
233+ { integration . connected ? (
234+ < div className = "flex gap-2" >
235+ < Button
236+ className = "flex-1"
237+ onClick = { ( ) =>
238+ ( window . location . href = `/settings/integrations/${ integration . id } ` )
239+ }
240+ size = "sm"
241+ variant = "outline"
242+ >
243+ Configure
244+ </ Button >
245+ < Button
246+ className = "text-destructive hover:bg-destructive/10 hover:text-destructive"
247+ disabled = { disconnectMutation . isPending }
248+ onClick = { ( ) => handleDisconnect ( integration ) }
249+ size = "sm"
250+ variant = "ghost"
251+ >
252+ { disconnectMutation . isPending
253+ ? 'Disconnecting...'
254+ : 'Disconnect' }
255+ </ Button >
256+ </ div >
257+ ) : (
258+ < Button
259+ className = "w-full font-medium"
260+ disabled = { connectingProvider === integration . id }
261+ onClick = { ( ) => handleConnect ( integration ) }
221262 >
222- { disconnectMutation . isPending ? 'Disconnecting...' : 'Disconnect' }
263+ { connectingProvider === integration . id ? (
264+ 'Connecting...'
265+ ) : (
266+ < >
267+ < PlusIcon className = "mr-2 h-4 w-4" />
268+ Connect
269+ </ >
270+ ) }
223271 </ Button >
224- </ div >
225- ) : (
226- < Button
227- onClick = { ( ) => handleConnect ( integration ) }
228- className = "w-full font-medium"
229- disabled = { connectingProvider === integration . id }
230- >
231- { connectingProvider === integration . id ? (
232- 'Connecting...'
233- ) : (
234- < >
235- < PlusIcon className = "mr-2 h-4 w-4" />
236- Connect
237- </ >
238- ) }
239- </ Button >
240- ) }
272+ ) }
273+ </ div >
241274 </ div >
242- </ div >
243- </ CardContent >
244- </ Card >
245- ) ) }
275+ </ CardContent >
276+ </ Card >
277+ ) ) }
278+ </ div >
246279 </ div >
247- </ div >
248- ) ) }
280+ )
281+ ) }
249282
250283 { integrations . length === 0 && (
251284 < div className = "flex h-64 items-center justify-center" >
0 commit comments