kyd-shared-badge 0.3.121 → 0.3.123

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,9 +3,9 @@ import { useRouter } from 'next/navigation';
3
3
  import { ProviderIcon } from '../utils/provider';
4
4
  import { normalizeLinkedInInput } from './linkedin';
5
5
  import type { ConnectAccountsProps } from './types';
6
- import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink, Settings, Shield, InfoIcon } from 'lucide-react';
7
- import { AnimatePresence, motion } from 'framer-motion';
8
- import { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter, CardTitle } from '../ui';
6
+ import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink, Eye, Lock, InfoIcon } from 'lucide-react';
7
+ import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
8
+ import { Button, Input, Spinner, Card, CardHeader, CardContent, CardTitle, ConnectProgress } from '../ui';
9
9
  import Link from 'next/link';
10
10
  import { Tooltip, TooltipTrigger, TooltipProvider, TooltipContent } from '../ui/';
11
11
 
@@ -42,6 +42,9 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
42
42
  githubAppSlugId,
43
43
  userId,
44
44
  inviteId,
45
+ shouldShowContinueButton,
46
+ shouldDisableContinueButton,
47
+ onContinue,
45
48
  } = props;
46
49
 
47
50
  const router = useRouter();
@@ -49,8 +52,9 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
49
52
  const [linkUrl, setLinkUrl] = useState('');
50
53
  const [isSubmitting, setIsSubmitting] = useState(false);
51
54
  const [isDisconnecting, setIsDisconnecting] = useState<string | null>(null);
52
- const [showGithubManage, setShowGithubManage] = useState(false);
53
55
  const [showDataHandling, setShowDataHandling] = useState(false);
56
+ const [previewProviderId, setPreviewProviderId] = useState<string | null>(null);
57
+ const [previewAction, setPreviewAction] = useState<'connect' | 'disconnect' | null>(null);
54
58
 
55
59
  const apiBase = apiGatewayUrl || (typeof process !== 'undefined' ? (process.env.NEXT_PUBLIC_API_GATEWAY_URL as string) : '');
56
60
  const connectedIds = useMemo(
@@ -63,8 +67,6 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
63
67
  useEffect(() => {
64
68
  if (initialProviderId && initialProviderId !== selectedProviderId) {
65
69
  setSelectedProviderIdAndCallback(initialProviderId);
66
- // If we landed here from initialProviderId, show the existing card (not manage)
67
- if (initialProviderId === 'githubapp') setShowGithubManage(false);
68
70
  }
69
71
  // Do not clear selection if initialProviderId becomes falsy later
70
72
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -114,7 +116,6 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
114
116
  });
115
117
  const data = await res.json().catch(() => ({}));
116
118
  if (!res.ok) throw new Error(data?.error || `Failed to disconnect ${providerId}.`);
117
- setShowGithubManage(false);
118
119
  if (onDisconnected) onDisconnected(providerId);
119
120
  } catch (e) {
120
121
  const err = e as Error;
@@ -194,11 +195,21 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
194
195
  ) || null;
195
196
  }, [selectedProviderId, providers]);
196
197
 
198
+ const setPreview = (providerId: string, action: 'connect' | 'disconnect') => {
199
+ setPreviewProviderId(providerId.toLowerCase());
200
+ setPreviewAction(action);
201
+ };
202
+ const clearPreview = () => {
203
+ setPreviewProviderId(null);
204
+ setPreviewAction(null);
205
+ };
206
+
197
207
  const handleConnectBack = () => {
198
208
  setSelectedProviderIdAndCallback(null);
199
209
  setLinkUrl('');
200
- setShowGithubManage(false);
201
210
  setShowDataHandling(false);
211
+ setPreviewProviderId(null);
212
+ setPreviewAction(null);
202
213
  };
203
214
 
204
215
  const cardVariants = {
@@ -206,11 +217,18 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
206
217
  animate: { opacity: 1, y: 0 },
207
218
  exit: { opacity: 0, y: -20 },
208
219
  };
220
+ const shouldReduceMotion = useReducedMotion();
221
+ const ease = [0.22, 1, 0.36, 1] as const;
209
222
  const fadeOnly = {
210
223
  initial: { opacity: 0 },
211
224
  animate: { opacity: 1 },
212
225
  exit: { opacity: 0 },
213
226
  };
227
+ const fadeIn = {
228
+ hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 8 },
229
+ visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease } },
230
+ exit: { opacity: 0, y: shouldReduceMotion ? 0 : -4, transition: { duration: 0.2, ease } }
231
+ };
214
232
 
215
233
  // GitHub status helpers
216
234
  const githubConnectedAccount = useMemo(() => {
@@ -223,484 +241,455 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
223
241
  }, [githubConnectedAccount]);
224
242
 
225
243
  // Progress UI is outsourced to ConnectProgress
226
-
227
- return (
228
- <>
229
-
230
- <AnimatePresence initial={false} mode="wait">
231
- {showDataHandling ? (
232
- <motion.div
233
- key="data-handling-card"
234
- className="rounded-xl border max-w-xl w-full"
235
- initial="initial" animate="animate" exit="exit" variants={fadeOnly}
236
- style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
237
- >
238
- <div className="sm:p-6 p-4">
239
- <button onClick={() => setShowDataHandling(false)} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
240
- <ArrowLeft className="w-4 h-4" />
241
- Back
242
- </button>
243
- <div className="text-center">
244
- <div className="flex justify-center mb-4">
245
- <InfoIcon className="w-10 h-10 text-[var(--text-main)]" />
246
- </div>
247
- <h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>How your data is handled</h3>
248
- <p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md mt-2 leading-relaxed">
249
- We understand that giving access to your private repositories can be a bit scary. So here's the deal: We install the KYD GitHub App in your account. The app has
250
- <span className="mx-1"><Link href="https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-contents" target="_blank" rel="noopener noreferrer" className="underline" style={{ color: 'var(--icon-accent)'}}>Contents <ExternalLink className="size-3 inline-block" /></Link></span>
251
- read access - only to the repositories you select. Then, once you request a badge assessment, we read the repositories and analyze the code, then its deleted, forever. Your code is not accessible to anyone, not even us.
252
- </p>
253
- <p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md mt-3 leading-relaxed">
254
- For other details, see our{' '}
255
- <Link href="https://www.knowyourdeveloper.ai/privacy-policy" target="_blank" rel="noopener noreferrer" className="underline" style={{ color: 'var(--icon-accent)'}}>
256
- Privacy Policy <ExternalLink className="size-3 inline-block ml-1" />
257
- </Link>.
258
- </p>
259
- </div>
260
- </div>
261
- </motion.div>
262
- ) : selectedProvider && selectedProvider.id !== 'githubapp' ? (
263
- <motion.div
264
- key="connect-card"
265
- initial="initial" animate="animate" exit="exit" variants={fadeOnly}
266
- className="rounded-xl border max-w-xl w-full"
267
- style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
268
- >
269
- <div className="sm:p-6 p-4">
270
- <button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
271
- <ArrowLeft className="w-4 h-4" />
272
- Back
273
- </button>
274
- <div className="text-center">
275
- <div className="flex justify-center mb-4">
276
- <ProviderIcon name={selectedProvider.id} className={`w-10 h-10 ${selectedProvider.iconColor || 'text-gray-500'}`} />
277
- </div>
278
- <h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
279
- {selectedProvider.connectionType === 'url' || (selectedProvider.connectionType || 'url') === 'link'
280
- ? `Use Public ${selectedProvider.name} Profile`
281
- : `Connect ${selectedProvider.name}`}
282
- </h3>
283
- <p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
284
- {(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
285
- ? (selectedProvider.placeholder || 'Enter your public profile URL.')
286
- : `Authorize with ${selectedProvider.name} to connect your account.`}
287
- </p>
288
- </div>
289
-
290
- {(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? (
291
- <motion.form
292
- onSubmit={(e) => { e.preventDefault(); onSubmitLink(selectedProvider.id); }}
293
- className="mt-6 space-y-4"
294
- initial="initial" animate="animate" exit="exit" variants={cardVariants}
295
- >
296
- {selectedProvider.id === 'linkedin' && (
297
- <>
298
- <p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
299
- <Link
300
- href="https://www.linkedin.com/public-profile/settings"
301
- target="_blank"
302
- rel="noopener noreferrer"
303
- className="underline"
304
- style={{ color: 'var(--icon-accent)' }}
305
- >
306
- LinkedIn <ExternalLink className="size-3 inline-block ml-1 underline-0" />
307
- </Link>
308
- . This opens your public profile settings (you’ll see your shareable URL if you’re signed in).
309
- </p>
310
- <p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
311
- LinkedIn data is not used to contribute to your score.
312
- </p>
313
- </>
314
244
 
315
- )}
316
- <div className="relative">
317
- <LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
318
- <Input
319
- type="url"
320
- value={linkUrl}
321
- onChange={(e) => setLinkUrl(e.target.value)}
322
- placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
323
- required
324
- className="w-full border bg-transparent p-2 pl-9"
325
- style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
326
- onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
327
- onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
328
- />
329
- </div>
330
- <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
331
- <Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
332
- {isSubmitting ? (
333
- <div className="flex items-center justify-center">
334
- <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
335
- Connecting...
336
- </div>
337
- ) : (
338
- 'Connect'
339
- )}
340
- </Button>
341
- </motion.div>
342
- </motion.form>
343
- ) : (
344
- <div className="mt-6">
345
- <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
346
- <Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
347
- <ExternalLink className="w-4 h-4 mr-2" />
348
- Connect with {selectedProvider.name}
349
- </Button>
350
- </motion.div>
351
- </div>
352
- )}
245
+ return (
246
+ <div className="w-full grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_380px] lg:justify-center gap-6 items-start max-w-7xl">
247
+ <div className="w-full flex justify-end">
248
+ <div className="w-full max-w-lg">
249
+ {/* Mobile: show progress above accounts, left-aligned */}
250
+ <div className="lg:hidden mb-4">
251
+ <ConnectProgress
252
+ layout="inline"
253
+ providers={providers}
254
+ connectedIds={connectedIds}
255
+ previewProviderId={previewProviderId}
256
+ previewAction={previewAction}
257
+ selectedProviderId={selectedProvider?.id || null}
258
+ />
353
259
  </div>
354
- </motion.div>
355
- ) : selectedProvider && selectedProvider.id === 'githubapp' ? (
356
- (!showGithubManage && initialProviderId === 'githubapp') ? (
357
- <div
358
- key="github-card"
359
- className="rounded-xl border max-w-xl w-full"
360
- style={{
361
- backgroundColor: 'var(--content-card-background)',
362
- borderColor: 'var(--icon-button-secondary)',
363
- }}
364
- >
365
- <motion.div className="p-6 flex flex-col items-center" initial="initial" animate="animate" exit="exit" variants={fadeOnly}>
366
- <div className="w-full flex items-center gap-3 mb-2 justify-center">
367
- <ProviderIcon name="github" className="w-8 h-8 inline-block" />
368
- <span className="sm:text-xl text-base font-semibold text-[var(--text-main)]">Connect Private GitHub Repositories</span>
369
- </div>
370
- <p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed mt-1 mb-6 text-center max-w-md">
371
- You&apos;ve successfully linked your GitHub account!
372
- <br />
373
- To complete your profile, you can optionally allow access to your <b>private repositories</b>. This is useful if you&apos;d like to highlight private work or share additional contributions for verification.
374
- <br /><br />
375
- <span className="text-[var(--text-main)] font-medium">
376
- Would you like to connect your private repositories?
377
- </span>
378
- <button
379
- type="button"
380
- onClick={() => setShowDataHandling(true)}
381
- className="sm:text-sm text-xs underline text-[var(--icon-accent)] hover:text-[var(--icon-accent-hover)]"
382
- >
383
- How KYD Handles Your Data
260
+ {showDataHandling ? (
261
+ <motion.div
262
+ key="data-handling-card"
263
+ className="rounded-xl border max-w-xl w-full"
264
+ initial="initial" animate="animate" exit="exit" variants={fadeOnly}
265
+ style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
266
+ >
267
+ <div className="sm:p-6 p-4">
268
+ <button onClick={() => setShowDataHandling(false)} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
269
+ <ArrowLeft className="w-4 h-4" />
270
+ Back
384
271
  </button>
385
- </p>
386
- <div className="flex flex-col sm:flex-row w-full gap-3 mt-2 justify-center items-center">
387
- <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
388
- <Button
389
- className="w-full sm:w-auto text-[var(--text-main)] transition-colors border border-[var(--icon-button-secondary)]"
390
- variant="destructive"
391
- onClick={() => {
392
- handleConnectBack();
393
- }}
394
- >
395
- No, don&#39;t connect
396
- </Button>
397
- </motion.div>
398
- <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
399
- <Button
400
- className="w-full sm:w-auto bg-[var(--icon-accent)] text-white transition-colors font-semibold"
401
- onClick={onGithubAppInstall}
402
- >
403
- <span className="flex items-center justify-center">
404
- <ExternalLink className="w-4 h-4 mr-2" />
405
- Yes, connect my private repos
406
- </span>
407
- </Button>
408
- </motion.div>
272
+ <div className="text-center">
273
+ <div className="flex justify-center mb-4">
274
+ <InfoIcon className="w-10 h-10 text-[var(--text-main)]" />
275
+ </div>
276
+ <h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>How your data is handled</h3>
277
+ <p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md mt-2 leading-relaxed">
278
+ We understand that giving access to your private repositories can be a bit scary. So here's the deal: We install the KYD GitHub App in your account. The app has
279
+ <span className="mx-1"><Link href="https://docs.github.com/en/rest/authentication/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-contents" target="_blank" rel="noopener noreferrer" className="underline" style={{ color: 'var(--icon-accent)'}}>Contents <ExternalLink className="size-3 inline-block" /></Link></span>
280
+ read access - only to the repositories you select. Then, once you request a badge assessment, we read the repositories and analyze the code, then its deleted, forever. Your code is not accessible to anyone, not even us.
281
+ </p>
282
+ <p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md mt-3 leading-relaxed">
283
+ For other details, see our{' '}
284
+ <Link href="https://www.knowyourdeveloper.ai/privacy-policy" target="_blank" rel="noopener noreferrer" className="underline" style={{ color: 'var(--icon-accent)'}}>
285
+ Privacy Policy <ExternalLink className="size-3 inline-block ml-1" />
286
+ </Link>.
287
+ </p>
288
+ </div>
409
289
  </div>
410
290
  </motion.div>
411
- </div>
412
- ) : (
413
- <motion.div
414
- key="github-manage"
415
- className="rounded-xl border max-w-xl w-full"
416
- style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
417
- variants={cardVariants}
418
- initial="initial"
419
- animate="animate"
420
- exit="exit"
421
- >
422
- <div className="sm:p-6 p-4">
423
- <button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
424
- <ArrowLeft className="w-4 h-4" />
425
- Back
426
- </button>
427
- <div className="text-center">
428
- <div className="flex justify-center mb-4">
429
- <ProviderIcon name="github" className="w-10 h-10" />
291
+ ) : selectedProvider && selectedProvider.id !== 'githubapp' ? (
292
+ <motion.div
293
+ key="connect-card"
294
+ initial="initial" animate="animate" exit="exit" variants={fadeOnly}
295
+ className="rounded-xl border max-w-xl w-full"
296
+ style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
297
+ >
298
+ <div className="sm:p-6 p-4">
299
+ <button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
300
+ <ArrowLeft className="w-4 h-4" />
301
+ Back
302
+ </button>
303
+ <div className="text-center">
304
+ <div className="flex justify-center mb-4">
305
+ <ProviderIcon name={selectedProvider.id} className={`w-10 h-10 ${selectedProvider.iconColor || 'text-gray-500'}`} />
306
+ </div>
307
+ <h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
308
+ {selectedProvider.connectionType === 'url' || (selectedProvider.connectionType || 'url') === 'link'
309
+ ? `Use Public ${selectedProvider.name} Profile`
310
+ : `Connect ${selectedProvider.name}`}
311
+ </h3>
312
+ <p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
313
+ {(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
314
+ ? (selectedProvider.placeholder || 'Enter your public profile URL.')
315
+ : `Authorize with ${selectedProvider.name} to connect your account.`}
316
+ </p>
430
317
  </div>
431
- <h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>Manage GitHub Connections</h3>
432
- <p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto max-w-md">
433
- Connect or disconnect your GitHub OAuth account and optional GitHub App for private repositories.
434
- </p>
435
- </div>
436
318
 
437
- <div className="mt-6 space-y-4">
438
- <div className="rounded-lg border p-4" style={{ borderColor: 'var(--icon-button-secondary)'}}>
439
- <div className="flex items-center justify-between">
440
- <div>
441
- <div className="font-semibold sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>GitHub OAuth</div>
442
- <div className="sm:text-sm text-xs" style={{ color: 'var(--text-secondary)'}}>{isGithubConnected ? 'Connected' : 'Not connected'}</div>
443
- </div>
444
- <div className="flex items-center gap-2">
445
- {isGithubConnected ? (
446
- <Button
447
- onClick={() => onDisconnect('github')}
448
- className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
449
- style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
450
- variant="destructive"
451
- disabled={isDisconnecting === 'github'}
452
- >
453
- {isDisconnecting === 'github' ? <Spinner /> : <Unlink className="size-3 sm:size-4" />}
454
- <span>Disconnect</span>
455
- </Button>
456
- ) : (
457
- <Button
458
- onClick={() => onOAuth('github')}
459
- className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
460
- >
461
- <ExternalLink className="w-4 h-4 mr-2" />
462
- Connect
463
- </Button>
464
- )}
319
+ {(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? (
320
+ <motion.form
321
+ onSubmit={(e) => { e.preventDefault(); onSubmitLink(selectedProvider.id); }}
322
+ className="mt-6 space-y-4"
323
+ initial="initial" animate="animate" exit="exit" variants={cardVariants}
324
+ >
325
+ {selectedProvider.id === 'linkedin' && (
326
+ <>
327
+ <p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
328
+ <Link
329
+ href="https://www.linkedin.com/public-profile/settings"
330
+ target="_blank"
331
+ rel="noopener noreferrer"
332
+ className="underline"
333
+ style={{ color: 'var(--icon-accent)' }}
334
+ >
335
+ LinkedIn <ExternalLink className="size-3 inline-block ml-1 underline-0" />
336
+ </Link>
337
+ . This opens your public profile settings (you’ll see your shareable URL if you’re signed in).
338
+ </p>
339
+ <p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
340
+ LinkedIn data is not used to contribute to your score.
341
+ </p>
342
+ </>
343
+
344
+ )}
345
+ <div className="relative">
346
+ <LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
347
+ <Input
348
+ type="url"
349
+ value={linkUrl}
350
+ onChange={(e) => setLinkUrl(e.target.value)}
351
+ placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
352
+ required
353
+ className="w-full border bg-transparent p-2 pl-9"
354
+ style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
355
+ onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
356
+ onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
357
+ />
465
358
  </div>
359
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
360
+ <Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
361
+ {isSubmitting ? (
362
+ <div className="flex items-center justify-center">
363
+ <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
364
+ Connecting...
365
+ </div>
366
+ ) : (
367
+ 'Connect'
368
+ )}
369
+ </Button>
370
+ </motion.div>
371
+ </motion.form>
372
+ ) : (
373
+ <div className="mt-6">
374
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
375
+ <Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
376
+ <ExternalLink className="w-4 h-4 mr-2" />
377
+ Connect with {selectedProvider.name}
378
+ </Button>
379
+ </motion.div>
466
380
  </div>
467
- </div>
468
-
469
- <div className="rounded-lg border p-4" style={{ borderColor: 'var(--icon-button-secondary)'}}>
470
- <div className="flex items-center justify-between">
471
- <div>
472
- <div className="font-semibold sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>GitHub App (Private Repos)</div>
381
+ )}
382
+ </div>
383
+ </motion.div>
384
+ ) : selectedProvider && selectedProvider.id === 'githubapp' ? (
385
+ <div
386
+ key="github-card"
387
+ className="rounded-xl border max-w-xl w-full"
388
+ style={{
389
+ backgroundColor: 'var(--content-card-background)',
390
+ borderColor: 'var(--icon-button-secondary)',
391
+ }}
392
+ >
393
+ <motion.div className="p-6 flex flex-col items-center" initial="initial" animate="animate" exit="exit" variants={fadeOnly}>
394
+ <div className="w-full flex items-center gap-3 mb-2 justify-center">
395
+ <ProviderIcon name="github" className="w-8 h-8 inline-block" />
396
+ <span className="sm:text-xl text-base font-semibold text-[var(--text-main)]">Connect Private GitHub Repositories</span>
397
+ </div>
398
+ <p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed mt-1 mb-6 text-center max-w-md">
399
+ You&apos;ve successfully linked your GitHub account!
400
+ <br />
401
+ To complete your profile, you can optionally allow access to your <b>private repositories</b>. This is useful if you&apos;d like to highlight private work or share additional contributions for verification.
402
+ <br /><br />
403
+ <span className="text-[var(--text-main)] font-medium">
404
+ Would you like to connect your private repositories?
405
+ </span>
406
+ <button
407
+ type="button"
408
+ onClick={() => setShowDataHandling(true)}
409
+ className="sm:text-sm text-xs underline text-[var(--icon-accent)] hover:text-[var(--icon-accent-hover)]"
410
+ >
411
+ How KYD Handles Your Data
412
+ </button>
413
+ </p>
414
+ <div className="flex flex-col sm:flex-row w-full gap-3 mt-2 justify-center items-center">
415
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
473
416
  <Button
474
- className="inline-flex items-center text-xs font-medium text-[var(--icon-accent)] hover:text-[var(--icon-accent-hover)] transition-colors underline px-0 py-0 h-auto"
475
- onClick={() => setShowDataHandling(true)}
476
- variant="link"
417
+ className="w-full sm:w-auto text-[var(--text-main)] transition-colors border border-[var(--icon-button-secondary)]"
418
+ variant="destructive"
419
+ onClick={() => {
420
+ handleConnectBack();
421
+ }}
477
422
  >
478
- How is my data handled?
423
+ No, don&#39;t connect
479
424
  </Button>
480
- </div>
481
- <div className="flex items-center gap-2">
482
- {isGithubAppInstalled ? (
483
- <Button
484
- onClick={() => { window.location.href = 'https://github.com/settings/installations'; }}
485
- className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
486
- style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
487
- variant="destructive"
488
- >
489
- <Unlink className="size-3 sm:size-4" />
490
- <span>Uninstall</span>
491
- </Button>
492
- ) : (
493
- <Button
494
- onClick={onGithubAppInstall}
495
- className="bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)]"
496
- >
425
+ </motion.div>
426
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
427
+ <Button
428
+ className="w-full sm:w-auto bg-[var(--icon-accent)] text-white transition-colors font-semibold"
429
+ onClick={onGithubAppInstall}
430
+ >
431
+ <span className="flex items-center justify-center">
497
432
  <ExternalLink className="w-4 h-4 mr-2" />
498
- Install
499
- </Button>
500
- )}
501
- </div>
433
+ Yes, connect my private repos
434
+ </span>
435
+ </Button>
436
+ </motion.div>
502
437
  </div>
503
- </div>
438
+ </motion.div>
504
439
  </div>
505
- </div>
506
- </motion.div>
507
- )
508
- ) : (
509
- <Card className="border-[var(--icon-button-secondary)] pt-2" style={{ backgroundColor: 'var(--content-card-background)'}}>
510
- <AnimatePresence mode="wait">
511
- <motion.div key="platform-list-shared" variants={cardVariants} initial="initial" animate="animate" exit="exit" transition={{ duration: 0.3 }}>
512
- <CardHeader className="pb-4">
513
- {handleBackButton && (
514
- <button onClick={() => handleBackButton()} className="flex items-center gap-2 text-sm mb-4 text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors">
515
- <ArrowLeft className="w-4 h-4" /> Back
516
- </button>
517
- )}
518
- <CardTitle className="sm:text-xl text-base font-semibold text-[var(--text-main)] mb-2">{headerTitle}</CardTitle>
519
- <p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed">{headerDescription}</p>
520
- </CardHeader>
521
- <CardContent className="space-y-2">
522
- {(() => {
523
- const oauthList = list.filter(p => p.connectionType === 'oauth');
524
- const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
525
-
526
- const Row = (provider: typeof list[number]) => {
527
- const providerId = provider.id;
528
- const isRequired = requiredProviders?.includes(providerId);
529
- const isConnected = connectedIds.has(providerId.toLowerCase());
530
- const needsReconnect = reconnectIds.has(providerId.toLowerCase());
531
- const isOauth = provider.connectionType === 'oauth'
532
- const connectedUrl = connected.find(c => c.name.toLowerCase() === providerId.toLowerCase())?.url;
533
- const betaFlag = () => (
534
- <span
535
- className="ml-2 inline-block rounded-full px-2 py-0.5 text-xs font-semibold bg-blue-100 text-blue-700 border border-blue-200"
536
- style={{ verticalAlign: 'middle', letterSpacing: '0.05em' }}
537
- >
538
- Beta
539
- </span>
540
- );
541
-
542
- const requiredFlag = () => (
543
- <TooltipProvider>
544
- <Tooltip>
545
- <TooltipTrigger>
546
- <div className="flex items-center justify-center size-4 sm:size-5 rounded-full bg-[var(--icon-accent)] cursor-help">
547
- <span className="text-white text-xs font-bold -translate-y-px select-none">!</span>
548
- </div>
549
- </TooltipTrigger>
550
- <TooltipContent>
551
- <p>Required by <span className="font-semibold">{companyName || 'your recruiter'}</span></p>
552
- </TooltipContent>
553
- </Tooltip>
554
- </TooltipProvider>
555
- );
556
-
557
- return (
558
- <div key={providerId} className="group flex items-center justify-between p-2 rounded-lg transition-colors hover:bg-[var(--icon-button-secondary)]/10 transform transition-transform hover:-translate-y-px">
559
- <div className="flex items-center gap-3">
560
-
561
- {isConnected && connectedUrl ? (
562
- <Link href={connectedUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-3 hover:underline">
563
- <ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
564
- <span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
565
- {isRequired && !isConnected ? (requiredFlag()) : null}
566
- </Link>
567
- ) : (
440
+ ) : (
441
+ <Card className="border-[var(--icon-button-secondary)] pt-2" style={{ backgroundColor: 'var(--content-card-background)'}}>
442
+ <AnimatePresence mode="wait">
443
+ <motion.div key="platform-list-shared" variants={cardVariants} initial="initial" animate="animate" exit="exit" transition={{ duration: 0.3 }}>
444
+ <CardHeader className="pb-4">
445
+ {handleBackButton && (
446
+ <button onClick={() => handleBackButton()} className="flex items-center gap-2 text-sm mb-4 text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors">
447
+ <ArrowLeft className="w-4 h-4" /> Back
448
+ </button>
449
+ )}
450
+ <CardTitle className="sm:text-xl text-base font-semibold text-[var(--text-main)] mb-2">{headerTitle}</CardTitle>
451
+ <p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed">{headerDescription}</p>
452
+ </CardHeader>
453
+ <CardContent className="space-y-2">
454
+ {(() => {
455
+ const oauthList = list.filter(p => p.connectionType === 'oauth');
456
+ const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
457
+
458
+ const Row = (provider: typeof list[number]) => {
459
+ const providerId = provider.id;
460
+ const isRequired = requiredProviders?.includes(providerId);
461
+ const isConnected = connectedIds.has(providerId.toLowerCase());
462
+ const needsReconnect = reconnectIds.has(providerId.toLowerCase());
463
+ const isOauth = provider.connectionType === 'oauth'
464
+ const connectedUrl = connected.find(c => c.name.toLowerCase() === providerId.toLowerCase())?.url;
465
+ const betaFlag = () => (
466
+ <span
467
+ className="ml-2 inline-block rounded-full px-2 py-0.5 text-xs font-semibold bg-blue-100 text-blue-700 border border-blue-200"
468
+ style={{ verticalAlign: 'middle', letterSpacing: '0.05em' }}
469
+ >
470
+ Beta
471
+ </span>
472
+ );
473
+
474
+ const requiredFlag = () => (
475
+ <TooltipProvider>
476
+ <Tooltip>
477
+ <TooltipTrigger>
478
+ <div className="flex items-center justify-center size-4 sm:size-5 rounded-full bg-[var(--icon-accent)] cursor-help">
479
+ <span className="text-white text-xs font-bold -translate-y-px select-none">!</span>
480
+ </div>
481
+ </TooltipTrigger>
482
+ <TooltipContent>
483
+ <p>Required by <span className="font-semibold">{companyName || 'your recruiter'}</span></p>
484
+ </TooltipContent>
485
+ </Tooltip>
486
+ </TooltipProvider>
487
+ );
488
+
489
+ return (
490
+ <div key={providerId} className="group flex items-center justify-between p-2 rounded-lg transition-colors hover:bg-[var(--icon-button-secondary)]/10 transform transition-transform hover:-translate-y-px">
568
491
  <div className="flex items-center gap-3">
569
- <ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
570
- <span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
571
- {isRequired && !isConnected ? (requiredFlag()) : null}
492
+
493
+ {isConnected && connectedUrl ? (
494
+ <Link href={connectedUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-3 hover:underline">
495
+ <ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
496
+ <span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
497
+ {isRequired && !isConnected ? (requiredFlag()) : null}
498
+ </Link>
499
+ ) : (
500
+ <div className="flex items-center gap-3">
501
+ <ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
502
+ <span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
503
+ {isRequired && !isConnected ? (requiredFlag()) : null}
504
+ </div>
505
+ )}
506
+ {provider.beta ? betaFlag() : null}
572
507
  </div>
573
- )}
574
- {provider.beta ? betaFlag() : null}
575
- </div>
576
-
577
- {providerId.toLowerCase() === 'github' ? (
578
- <div className="flex items-center gap-2">
579
- {(!isConnected && !isGithubAppInstalled) && (
580
- <Button
581
- onClick={() => setSelectedProviderIdAndCallback(providerId)}
582
- className="bg-[var(--icon-button-secondary)] text-[var(--text-main)] hover:bg-[var(--icon-accent)] hover:text-white border-0 sm:px-4 px-3 py-1 sm:py-2 rounded-lg flex items-center gap-2"
583
- >
584
- <Link2 className="size-3 sm:size-4" />
585
- <span className="sm:text-base text-sm">Connect</span>
586
- </Button>
587
- )}
588
- {(isConnected && !isGithubAppInstalled) && (
589
- <Button
590
- onClick={() => { setShowGithubManage(true); setSelectedProviderIdAndCallback('githubapp'); }}
591
- className="bg-[var(--icon-button-secondary)] text-[var(--text-main)] hover:bg-[var(--icon-accent)] hover:text-white border-0 sm:px-4 px-3 py-1 sm:py-2 rounded-lg flex items-center gap-2"
592
- >
593
- <Settings className="size-3 sm:size-4" />
594
- <span className="sm:text-base text-sm">Manage</span>
595
- </Button>
596
- )}
597
- {(isConnected && isGithubAppInstalled) && (
598
- <div
599
- className="relative flex items-center group"
600
- onClick={() => { setShowGithubManage(true); setSelectedProviderIdAndCallback('githubapp'); }}
601
- style={{ cursor: 'pointer' }}
602
- >
508
+
509
+ {providerId.toLowerCase() === 'github' ? (
510
+ <div className="flex items-center gap-2">
511
+ {/* Public (OAuth) */}
512
+ {isGithubConnected ? (
513
+ <div className="relative flex items-center">
514
+ <div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
515
+ <CheckCircle className="size-3 sm:size-4" />
516
+ <span className="text-sm font-medium">Connected</span>
517
+ </div>
518
+ <div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
519
+ <button
520
+ onClick={() => onDisconnect('github')}
521
+ onMouseEnter={() => setPreview('github', 'disconnect')}
522
+ onMouseLeave={clearPreview}
523
+ className="inline-flex items-center gap-1.5 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
524
+ disabled={isDisconnecting === 'github'}
525
+ >
526
+ {isDisconnecting === 'github' ? <Spinner /> : <Unlink className="size-3 sm:size-4" />}
527
+ <span>Disconnect</span>
528
+ </button>
529
+ </div>
530
+ </div>
531
+ ) : (
532
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
533
+ <Button
534
+ onClick={() => setSelectedProviderIdAndCallback('github')}
535
+ onMouseEnter={() => setPreview('github', 'connect')}
536
+ onMouseLeave={clearPreview}
537
+ className="bg-[var(--icon-button-secondary)] text-[var(--text-main)] hover:bg-[var(--icon-accent)] hover:text-white border-0 sm:px-4 px-3 py-1 sm:py-2 rounded-lg flex items-center gap-2"
538
+ >
539
+ <Eye className="size-3 sm:size-4" />
540
+ <span className="sm:text-base text-sm">Connect</span>
541
+ </Button>
542
+ </motion.div>
543
+ )}
544
+
545
+ {/* Private (GitHub App) */}
546
+ {isGithubAppInstalled ? (
547
+ <div className="relative flex items-center">
548
+ <div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
549
+ <CheckCircle className="size-3 sm:size-4" />
550
+ <span className="text-sm font-medium">Connected</span>
551
+ </div>
552
+ <div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
553
+ <button
554
+ onClick={() => { window.location.href = 'https://github.com/settings/installations'; }}
555
+ className="inline-flex items-center gap-1.5 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
556
+ >
557
+ <Unlink className="size-3 sm:size-4" />
558
+ <span>Uninstall</span>
559
+ </button>
560
+ </div>
561
+ </div>
562
+ ) : (
563
+ <motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
564
+ <Button
565
+ onClick={() => setSelectedProviderIdAndCallback('githubapp')}
566
+ onMouseEnter={() => setPreview('githubapp', 'connect')}
567
+ onMouseLeave={clearPreview}
568
+ className="bg-[var(--icon-button-secondary)] text-[var(--text-main)] hover:bg-[var(--icon-accent)] hover:text-white border-0 sm:px-4 px-3 py-1 sm:py-2 rounded-lg flex items-center gap-2"
569
+ >
570
+ <Lock className="size-3 sm:size-4" />
571
+ <span className="sm:text-base text-sm">Connect</span>
572
+ </Button>
573
+ </motion.div>
574
+ )}
575
+ </div>
576
+ ) : isConnected && isDisconnecting !== providerId ? (
577
+ <div className="relative flex items-center">
603
578
  <div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
604
579
  <CheckCircle className="size-3 sm:size-4" />
605
580
  <span className="text-sm font-medium">Connected</span>
606
581
  </div>
607
582
  <div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
608
- <div className="flex items-center gap-1.5 text-red-600 text-sm hover:underline">
583
+ <button
584
+ onClick={() => onDisconnect(providerId)}
585
+ onMouseEnter={() => setPreview(providerId, 'disconnect')}
586
+ onMouseLeave={clearPreview}
587
+ className="inline-flex items-center gap-1.5 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
588
+ >
609
589
  <Unlink className="size-3 sm:size-4" />
610
590
  <span>Disconnect</span>
611
- </div>
591
+ </button>
612
592
  </div>
613
593
  </div>
594
+ ) : isDisconnecting === providerId ? (
595
+ <div className="relative flex items-center">
596
+ <Spinner />
597
+ </div>
598
+ ) : (
599
+ <div className="flex items-center gap-2">
600
+ <>
601
+ <Button
602
+ onClick={() => setSelectedProviderIdAndCallback(providerId)}
603
+ onMouseEnter={() => setPreview(providerId, 'connect')}
604
+ onMouseLeave={clearPreview}
605
+ className="bg-[var(--icon-button-secondary)] text-[var(--text-main)] hover:bg-[var(--icon-accent)] hover:text-white border-0 sm:px-4 px-3 py-1 sm:py-2 rounded-lg flex items-center gap-2"
606
+ >
607
+ {isOauth ? (
608
+ <Link2 className="size-3 sm:size-4" />
609
+ ) : (
610
+ <LinkIcon className="size-3 sm:size-4" />
611
+ )}
612
+ <span className="sm:text-base text-sm">Connect</span>
613
+ </Button>
614
+ {needsReconnect && !isRequired && (
615
+ <Button
616
+ onClick={() => onDisconnect(providerId)}
617
+ onMouseEnter={() => setPreview(providerId, 'disconnect')}
618
+ onMouseLeave={clearPreview}
619
+ className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
620
+ style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
621
+ variant="destructive"
622
+ >
623
+ <Unlink className="size-3 sm:size-4" />
624
+ <span>Remove</span>
625
+ </Button>
626
+ )}
627
+ </>
628
+ </div>
614
629
  )}
615
- {isDisconnecting === providerId && (
616
- <Spinner />
617
- )}
618
- </div>
619
- ) : isConnected && isDisconnecting !== providerId ? (
620
- <div className="relative flex items-center">
621
- <div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
622
- <CheckCircle className="size-3 sm:size-4" />
623
- <span className="text-sm font-medium">Connected</span>
624
- </div>
625
- <div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
626
- <button
627
- onClick={() => onDisconnect(providerId)}
628
- className="inline-flex items-center gap-1.5 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
629
- >
630
- <Unlink className="size-3 sm:size-4" />
631
- <span>Disconnect</span>
632
- </button>
633
- </div>
634
630
  </div>
635
- ) : isDisconnecting === providerId ? (
636
- <div className="relative flex items-center">
637
- <Spinner />
638
- </div>
639
- ) : (
640
- <div className="flex items-center gap-2">
641
- <>
642
- <Button
643
- onClick={() => setSelectedProviderIdAndCallback(providerId)}
644
- className="bg-[var(--icon-button-secondary)] text-[var(--text-main)] hover:bg-[var(--icon-accent)] hover:text-white border-0 sm:px-4 px-3 py-1 sm:py-2 rounded-lg flex items-center gap-2"
645
- >
646
- {isOauth ? (
647
- <Link2 className="size-3 sm:size-4" />
648
- ) : (
649
- <LinkIcon className="size-3 sm:size-4" />
650
- )}
651
- <span className="sm:text-base text-sm">Connect</span>
652
- </Button>
653
- {needsReconnect && !isRequired && (
654
- <Button
655
- onClick={() => onDisconnect(providerId)}
656
- className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
657
- style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
658
- variant="destructive"
659
- >
660
- <Unlink className="size-3 sm:size-4" />
661
- <span>Remove</span>
662
- </Button>
663
- )}
664
- </>
665
- </div>
666
- )}
667
- </div>
668
- );
669
- };
631
+ );
632
+ };
670
633
 
671
- return (
672
- <>
673
- {oauthList.length ? (
634
+ return (
674
635
  <>
675
- <div className="flex items-center my-2">
676
- <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
677
- <span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>OAuth</span>
678
- <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
679
- </div>
680
- {oauthList.map(p => Row(p))}
681
- </>
682
- ) : null}
636
+ {oauthList.length ? (
637
+ <>
638
+ <div className="flex items-center my-2">
639
+ <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
640
+ <span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>OAuth</span>
641
+ <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
642
+ </div>
643
+ {oauthList.map(p => Row(p))}
644
+ </>
645
+ ) : null}
683
646
 
684
- {urlList.length ? (
685
- <>
686
- <div className="flex items-center my-2">
687
- <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
688
- <span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>Public Urls</span>
689
- <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
690
- </div>
691
- {urlList.map(p => Row(p))}
647
+ {urlList.length ? (
648
+ <>
649
+ <div className="flex items-center my-2">
650
+ <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
651
+ <span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>Public Urls</span>
652
+ <div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
653
+ </div>
654
+ {urlList.map(p => Row(p))}
655
+ </>
656
+ ) : null}
692
657
  </>
693
- ) : null}
694
- </>
695
- );
696
- })()}
697
- </CardContent>
658
+ );
659
+ })()}
660
+ </CardContent>
661
+ </motion.div>
662
+ </AnimatePresence>
663
+ </Card>
664
+ )}
665
+ {shouldShowContinueButton && (
666
+ <motion.div className="mt-6 w-full" initial="hidden" animate="visible" variants={fadeIn}>
667
+ <motion.div whileHover={{ scale: 1.01 }} whileTap={{ scale: 0.99 }}>
668
+ <Button
669
+ onClick={onContinue}
670
+ disabled={shouldDisableContinueButton}
671
+ className="w-full text-white font-medium py-2.5 bg-[var(--icon-accent)] disabled:opacity-50"
672
+ >
673
+ Continue
674
+ </Button>
675
+ </motion.div>
698
676
  </motion.div>
699
- </AnimatePresence>
700
- </Card>
701
- )}
702
- </AnimatePresence>
703
- </>
677
+ )}
678
+ </div>
679
+ </div>
680
+
681
+ {/* Desktop: progress card to the right, sticky while scrolling */}
682
+ <div className="hidden lg:block lg:sticky lg:top-6">
683
+ <ConnectProgress
684
+ layout="inline"
685
+ providers={providers}
686
+ connectedIds={connectedIds}
687
+ previewProviderId={previewProviderId}
688
+ previewAction={previewAction}
689
+ selectedProviderId={selectedProvider?.id || null}
690
+ />
691
+ </div>
692
+ </div>
704
693
  );
705
694
  }
706
695