kyd-shared-badge 0.3.69 → 0.3.71
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.
- package/package.json +3 -1
- package/src/connect/ConnectAccounts.tsx +239 -82
- package/src/connect/oauth.ts +13 -8
- package/src/connect/types.ts +2 -0
- package/src/ui/button.tsx +57 -0
- package/src/ui/card.tsx +76 -0
- package/src/ui/index.ts +4 -0
- package/src/ui/input.tsx +22 -0
- package/src/ui/spinner.tsx +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kyd-shared-badge",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.71",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"@chatscope/chat-ui-kit-react": "^2.1.1",
|
|
23
23
|
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
|
24
24
|
"ai": "5.0.47",
|
|
25
|
+
"framer-motion": "^12.23.24",
|
|
25
26
|
"i18n-iso-countries": "^7.14.0",
|
|
27
|
+
"lucide-react": "^0.545.0",
|
|
26
28
|
"next-auth": "^4.24.11",
|
|
27
29
|
"react-hot-toast": "^2.6.0",
|
|
28
30
|
"react-icons": "^5.5.0",
|
|
@@ -3,6 +3,10 @@ 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 } from 'lucide-react';
|
|
7
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
8
|
+
import { Button, Input, Spinner } from '../ui';
|
|
9
|
+
import Link from 'next/link';
|
|
6
10
|
|
|
7
11
|
function byPriority(a: string, b: string) {
|
|
8
12
|
const pr = (id: string) => (id === 'github' ? 0 : id === 'linkedin' ? 1 : 2);
|
|
@@ -26,6 +30,8 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
26
30
|
onDisconnected,
|
|
27
31
|
onError,
|
|
28
32
|
className,
|
|
33
|
+
oauthClientIds,
|
|
34
|
+
providerPickCallback,
|
|
29
35
|
} = props;
|
|
30
36
|
|
|
31
37
|
const router = useRouter();
|
|
@@ -87,6 +93,11 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
87
93
|
}
|
|
88
94
|
};
|
|
89
95
|
|
|
96
|
+
const setSelectedProviderIdAndCallback = (providerId: string | null) => {
|
|
97
|
+
setSelectedProviderId(providerId);
|
|
98
|
+
providerPickCallback(providerId);
|
|
99
|
+
};
|
|
100
|
+
|
|
90
101
|
const onSubmitLink = async (providerId: string) => {
|
|
91
102
|
const provider = providers.find(p => p.id.toLowerCase() === providerId.toLowerCase());
|
|
92
103
|
if (!provider || !provider.endpoint) return;
|
|
@@ -104,7 +115,8 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
104
115
|
});
|
|
105
116
|
const data = await res.json().catch(() => ({}));
|
|
106
117
|
if (!res.ok) throw new Error(data?.error || `Failed to sync ${provider.name} profile.`);
|
|
107
|
-
|
|
118
|
+
setSelectedProviderIdAndCallback(null);
|
|
119
|
+
|
|
108
120
|
setLinkUrl('');
|
|
109
121
|
if (onConnected) onConnected(providerId);
|
|
110
122
|
} catch (e) {
|
|
@@ -122,97 +134,242 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
122
134
|
// Lazy import builder to avoid circular deps
|
|
123
135
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
124
136
|
const { buildOAuthUrl } = require('./oauth');
|
|
125
|
-
const url = buildOAuthUrl({ providerId: id, redirectUri, state });
|
|
137
|
+
const url = buildOAuthUrl({ providerId: id, redirectUri, state, clientIds: oauthClientIds });
|
|
126
138
|
if (url) router.push(url);
|
|
127
139
|
};
|
|
128
140
|
|
|
141
|
+
const selectedProvider = useMemo(
|
|
142
|
+
() => (selectedProviderId ? providers.find(p => p.id.toLowerCase() === selectedProviderId.toLowerCase()) : null),
|
|
143
|
+
[selectedProviderId, providers]
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const handleConnectBack = () => {
|
|
147
|
+
setSelectedProviderIdAndCallback(null);
|
|
148
|
+
setLinkUrl('');
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const fadeIn = {
|
|
152
|
+
hidden: { opacity: 0, y: 6 },
|
|
153
|
+
visible: { opacity: 1, y: 0, transition: { duration: 0.15 } },
|
|
154
|
+
exit: { opacity: 0, y: 6, transition: { duration: 0.1 } },
|
|
155
|
+
} as const;
|
|
156
|
+
|
|
129
157
|
return (
|
|
130
|
-
|
|
131
|
-
{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<div className="
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
<>
|
|
159
|
+
{selectedProvider ? (
|
|
160
|
+
<AnimatePresence>
|
|
161
|
+
<motion.div
|
|
162
|
+
key="connect-card"
|
|
163
|
+
className="rounded-xl border max-w-xl w-full"
|
|
164
|
+
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
165
|
+
initial="hidden" animate="visible" exit="exit" variants={fadeIn}
|
|
166
|
+
>
|
|
167
|
+
<div className="sm:p-6 p-4">
|
|
168
|
+
<button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
169
|
+
<ArrowLeft className="w-4 h-4" />
|
|
170
|
+
Back
|
|
171
|
+
</button>
|
|
172
|
+
<div className="text-center">
|
|
173
|
+
<div className="flex justify-center mb-4">
|
|
174
|
+
<ProviderIcon name={selectedProvider.id} className={`w-10 h-10 ${selectedProvider.iconColor || 'text-gray-500'}`} />
|
|
175
|
+
</div>
|
|
176
|
+
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
|
|
177
|
+
{selectedProvider.connectionType === 'url' || (selectedProvider.connectionType || 'url') === 'link'
|
|
178
|
+
? `Use Public ${selectedProvider.name} Profile`
|
|
179
|
+
: `Connect ${selectedProvider.name}`}
|
|
180
|
+
</h3>
|
|
181
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
|
|
182
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
|
|
183
|
+
? (selectedProvider.placeholder || 'Enter your public profile URL.')
|
|
184
|
+
: `Authorize with ${selectedProvider.name} to connect your account.`}
|
|
185
|
+
</p>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? (
|
|
189
|
+
<motion.form
|
|
190
|
+
onSubmit={(e) => { e.preventDefault(); onSubmitLink(selectedProvider.id); }}
|
|
191
|
+
className="mt-6 space-y-4"
|
|
192
|
+
initial="hidden" animate="visible" variants={fadeIn}
|
|
146
193
|
>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
194
|
+
{selectedProvider.id === 'linkedin' && (
|
|
195
|
+
<p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
|
|
196
|
+
<Link
|
|
197
|
+
href="https://www.linkedin.com/public-profile/settings"
|
|
198
|
+
target="_blank"
|
|
199
|
+
rel="noopener noreferrer"
|
|
200
|
+
className="underline"
|
|
201
|
+
style={{ color: 'var(--icon-accent)' }}
|
|
202
|
+
>
|
|
203
|
+
LinkedIn <ExternalLink className="size-3 inline-block ml-1 underline-0" />
|
|
204
|
+
</Link>
|
|
205
|
+
. This opens your public profile settings (you’ll see your shareable URL if you’re signed in).
|
|
206
|
+
</p>
|
|
207
|
+
)}
|
|
208
|
+
<div className="relative">
|
|
209
|
+
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
|
|
210
|
+
<Input
|
|
211
|
+
type="url"
|
|
212
|
+
value={linkUrl}
|
|
213
|
+
onChange={(e) => setLinkUrl(e.target.value)}
|
|
214
|
+
placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
|
|
215
|
+
required
|
|
216
|
+
className="w-full border bg-transparent p-2 pl-9"
|
|
217
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
218
|
+
onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
219
|
+
onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
220
|
+
/>
|
|
221
|
+
</div>
|
|
222
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
223
|
+
<Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
|
|
224
|
+
{isSubmitting ? (
|
|
225
|
+
<div className="flex items-center justify-center">
|
|
226
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
227
|
+
Connecting...
|
|
228
|
+
</div>
|
|
229
|
+
) : (
|
|
230
|
+
'Connect'
|
|
231
|
+
)}
|
|
232
|
+
</Button>
|
|
233
|
+
</motion.div>
|
|
234
|
+
</motion.form>
|
|
235
|
+
) : (
|
|
236
|
+
<div className="mt-6">
|
|
237
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
238
|
+
<Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
|
|
239
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
240
|
+
Connect with {selectedProvider.name}
|
|
241
|
+
</Button>
|
|
242
|
+
</motion.div>
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
150
245
|
</div>
|
|
246
|
+
</motion.div>
|
|
247
|
+
</AnimatePresence>
|
|
248
|
+
) : (
|
|
249
|
+
<motion.div className="rounded-xl border max-w-lg" style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}} initial="hidden" animate="visible" variants={fadeIn}>
|
|
250
|
+
<div className="sm:p-6 p-4">
|
|
251
|
+
<h2 className="sm:text-xl text-base font-semibold" style={{ color: 'var(--text-main)'}}>Connect accounts</h2>
|
|
252
|
+
<p className="mt-2 sm:text-sm text-xs" style={{ color: 'var(--text-secondary)'}}>
|
|
253
|
+
Each connected account adds verified signals to your report. Connect more accounts to strengthen your score—then disconnect anytime.
|
|
254
|
+
</p>
|
|
255
|
+
</div>
|
|
256
|
+
<motion.div className="sm:px-6 px-4 sm:pb-6 pb-4 space-y-2" initial="hidden" animate="visible" variants={fadeIn}>
|
|
257
|
+
<div className="space-y-2">
|
|
258
|
+
{(() => {
|
|
259
|
+
const oauthList = list.filter(p => p.connectionType === 'oauth');
|
|
260
|
+
const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
|
|
151
261
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
262
|
+
const Row = (provider: typeof list[number]) => {
|
|
263
|
+
const providerId = provider.id;
|
|
264
|
+
const isConnected = connectedIds.has(providerId.toLowerCase());
|
|
265
|
+
const needsReconnect = reconnectIds.has(providerId.toLowerCase());
|
|
266
|
+
const isOauth = provider.connectionType === 'oauth'
|
|
267
|
+
const connectedUrl = connected.find(c => c.id.toLowerCase() === providerId.toLowerCase())?.url;
|
|
268
|
+
const betaFlag = () => (
|
|
269
|
+
<span
|
|
270
|
+
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"
|
|
271
|
+
style={{ verticalAlign: 'middle', letterSpacing: '0.05em' }}
|
|
272
|
+
>
|
|
273
|
+
Beta
|
|
274
|
+
</span>
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<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">
|
|
279
|
+
<div className="flex items-center gap-3">
|
|
280
|
+
|
|
281
|
+
{isConnected && connectedUrl ? (
|
|
282
|
+
<Link href={connectedUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-3 hover:underline">
|
|
283
|
+
<ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
|
|
284
|
+
<span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
|
|
285
|
+
</Link>
|
|
286
|
+
) : (
|
|
287
|
+
<div className="flex items-center gap-3">
|
|
288
|
+
<ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
|
|
289
|
+
<span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
{provider.beta ? betaFlag() : null}
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
{isConnected ? (
|
|
296
|
+
<div className="relative flex items-center">
|
|
297
|
+
<div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
|
|
298
|
+
<CheckCircle className="size-3 sm:size-4" />
|
|
299
|
+
<span className="text-sm font-medium">Connected</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
|
|
302
|
+
<button
|
|
303
|
+
onClick={() => onDisconnect(providerId)}
|
|
304
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
|
|
305
|
+
>
|
|
306
|
+
<Unlink className="size-3 sm:size-4" />
|
|
307
|
+
<span>Disconnect</span>
|
|
308
|
+
</button>
|
|
309
|
+
</div>
|
|
310
|
+
</div>
|
|
311
|
+
) : (
|
|
312
|
+
<div className="flex items-center gap-2">
|
|
313
|
+
<>
|
|
314
|
+
<Button
|
|
315
|
+
onClick={() => setSelectedProviderIdAndCallback(providerId)}
|
|
316
|
+
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"
|
|
317
|
+
>
|
|
318
|
+
{isOauth ? (
|
|
319
|
+
<Link2 className="size-3 sm:size-4" />
|
|
320
|
+
) : (
|
|
321
|
+
<LinkIcon className="size-3 sm:size-4" />
|
|
322
|
+
)}
|
|
323
|
+
<span className="sm:text-base text-sm">Connect</span>
|
|
324
|
+
</Button>
|
|
325
|
+
{needsReconnect && (
|
|
326
|
+
<Button
|
|
327
|
+
onClick={() => onDisconnect(providerId)}
|
|
328
|
+
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
329
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
330
|
+
>
|
|
331
|
+
<Unlink className="size-3 sm:size-4" />
|
|
332
|
+
<span>Remove</span>
|
|
333
|
+
</Button>
|
|
334
|
+
)}
|
|
335
|
+
</>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
185
338
|
</div>
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
) : (
|
|
339
|
+
);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
return (
|
|
191
343
|
<>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
344
|
+
{oauthList.length ? (
|
|
345
|
+
<>
|
|
346
|
+
<div className="flex items-center my-2">
|
|
347
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
348
|
+
<span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>OAuth</span>
|
|
349
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
350
|
+
</div>
|
|
351
|
+
{oauthList.map(p => Row(p))}
|
|
352
|
+
</>
|
|
353
|
+
) : null}
|
|
354
|
+
|
|
355
|
+
{urlList.length ? (
|
|
356
|
+
<>
|
|
357
|
+
<div className="flex items-center my-2">
|
|
358
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
359
|
+
<span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>Public Urls</span>
|
|
360
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
361
|
+
</div>
|
|
362
|
+
{urlList.map(p => Row(p))}
|
|
363
|
+
</>
|
|
364
|
+
) : null}
|
|
208
365
|
</>
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
366
|
+
);
|
|
367
|
+
})()}
|
|
368
|
+
</div>
|
|
369
|
+
</motion.div>
|
|
370
|
+
</motion.div>
|
|
371
|
+
)}
|
|
372
|
+
</>
|
|
216
373
|
);
|
|
217
374
|
}
|
|
218
375
|
|
package/src/connect/oauth.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
type KnownProvider = 'github' | 'gitlab' | 'stackoverflow';
|
|
2
2
|
|
|
3
3
|
const env = (key: string): string | undefined => {
|
|
4
|
-
//
|
|
5
|
-
try {
|
|
4
|
+
// Use globalThis to avoid hard dependency on Node 'process' types/environ
|
|
5
|
+
try {
|
|
6
|
+
const g = globalThis as unknown as { process?: { env?: Record<string, string | undefined> } };
|
|
7
|
+
return g?.process?.env?.[key];
|
|
8
|
+
} catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
6
11
|
};
|
|
7
12
|
|
|
8
|
-
|
|
9
|
-
github: env('NEXT_PUBLIC_GITHUB_CLIENT_ID'),
|
|
10
|
-
gitlab: env('NEXT_PUBLIC_GITLAB_CLIENT_ID'),
|
|
11
|
-
stackoverflow: env('NEXT_PUBLIC_STACKOVERFLOW_CLIENT_ID'),
|
|
12
|
-
};
|
|
13
|
+
export type OAuthClientIds = Partial<Record<KnownProvider, string>>;
|
|
13
14
|
|
|
14
15
|
const DEFAULT_SCOPES: Record<KnownProvider, string> = {
|
|
15
16
|
github: 'read:user,repo',
|
|
@@ -22,9 +23,13 @@ export function buildOAuthUrl(params: {
|
|
|
22
23
|
redirectUri: string;
|
|
23
24
|
state?: string;
|
|
24
25
|
scopeOverride?: string;
|
|
26
|
+
clientIds?: OAuthClientIds;
|
|
25
27
|
}): string {
|
|
26
28
|
const id = (params.providerId || '').toLowerCase() as KnownProvider;
|
|
27
|
-
const clientId =
|
|
29
|
+
const clientId = (params.clientIds && params.clientIds[id]) || env(
|
|
30
|
+
id === 'github' ? 'NEXT_PUBLIC_GITHUB_CLIENT_ID' : id === 'gitlab' ? 'NEXT_PUBLIC_GITLAB_CLIENT_ID' : 'NEXT_PUBLIC_STACKOVERFLOW_CLIENT_ID'
|
|
31
|
+
);
|
|
32
|
+
// clientId should be inlined at build-time for NEXT_PUBLIC_*
|
|
28
33
|
const scope = params.scopeOverride ?? DEFAULT_SCOPES[id as KnownProvider] ?? '';
|
|
29
34
|
const redirect = encodeURIComponent(params.redirectUri);
|
|
30
35
|
const state = params.state ? `&state=${encodeURIComponent(params.state)}` : '';
|
package/src/connect/types.ts
CHANGED
|
@@ -27,10 +27,12 @@ export interface ConnectAccountsProps {
|
|
|
27
27
|
buildOAuthState?: () => Record<string, string | number | boolean> | string | undefined;
|
|
28
28
|
stateFormat?: StateFormat;
|
|
29
29
|
redirectUriOverrides?: RedirectUriOverrides;
|
|
30
|
+
oauthClientIds?: Partial<Record<'github' | 'gitlab' | 'stackoverflow', string>>;
|
|
30
31
|
onConnected?: (providerId: string) => void;
|
|
31
32
|
onDisconnected?: (providerId: string) => void;
|
|
32
33
|
onError?: (message: string) => void;
|
|
33
34
|
className?: string;
|
|
35
|
+
providerPickCallback: (providerId: string | null) => void;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Slot } from "@radix-ui/react-slot"
|
|
3
|
+
import { cva, type VariantProps } from "class-variance-authority"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default:
|
|
13
|
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
14
|
+
destructive:
|
|
15
|
+
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
|
16
|
+
outline:
|
|
17
|
+
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
|
20
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
21
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default: "h-9 px-4 py-2",
|
|
25
|
+
sm: "h-8 rounded-md px-3 text-xs",
|
|
26
|
+
lg: "h-10 rounded-md px-8",
|
|
27
|
+
icon: "h-9 w-9",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: "default",
|
|
32
|
+
size: "default",
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export interface ButtonProps
|
|
38
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
|
39
|
+
VariantProps<typeof buttonVariants> {
|
|
40
|
+
asChild?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
44
|
+
({ className, variant, size, asChild = false, ...props }, ref) => {
|
|
45
|
+
const Comp = asChild ? Slot : "button"
|
|
46
|
+
return (
|
|
47
|
+
<Comp
|
|
48
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
49
|
+
ref={ref}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
Button.displayName = "Button"
|
|
56
|
+
|
|
57
|
+
export { Button, buttonVariants }
|
package/src/ui/card.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const Card = React.forwardRef<
|
|
6
|
+
HTMLDivElement,
|
|
7
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<div
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn(
|
|
12
|
+
"rounded-lg border bg-card text-card-foreground shadow",
|
|
13
|
+
className
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
))
|
|
18
|
+
Card.displayName = "Card"
|
|
19
|
+
|
|
20
|
+
const CardHeader = React.forwardRef<
|
|
21
|
+
HTMLDivElement,
|
|
22
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
23
|
+
>(({ className, ...props }, ref) => (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn("flex flex-col sm:p-4 p-4", className)}
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
))
|
|
30
|
+
CardHeader.displayName = "CardHeader"
|
|
31
|
+
|
|
32
|
+
const CardTitle = React.forwardRef<
|
|
33
|
+
HTMLDivElement,
|
|
34
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
35
|
+
>(({ className, ...props }, ref) => (
|
|
36
|
+
<div
|
|
37
|
+
ref={ref}
|
|
38
|
+
className={cn("font-semibold leading-none tracking-tight", className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
))
|
|
42
|
+
CardTitle.displayName = "CardTitle"
|
|
43
|
+
|
|
44
|
+
const CardDescription = React.forwardRef<
|
|
45
|
+
HTMLDivElement,
|
|
46
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
47
|
+
>(({ className, ...props }, ref) => (
|
|
48
|
+
<div
|
|
49
|
+
ref={ref}
|
|
50
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
))
|
|
54
|
+
CardDescription.displayName = "CardDescription"
|
|
55
|
+
|
|
56
|
+
const CardContent = React.forwardRef<
|
|
57
|
+
HTMLDivElement,
|
|
58
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
59
|
+
>(({ className, ...props }, ref) => (
|
|
60
|
+
<div ref={ref} className={cn("sm:pt-2 sm:pb-4 sm:px-4 pb-3 px-4 pt-0", className)} {...props} />
|
|
61
|
+
))
|
|
62
|
+
CardContent.displayName = "CardContent"
|
|
63
|
+
|
|
64
|
+
const CardFooter = React.forwardRef<
|
|
65
|
+
HTMLDivElement,
|
|
66
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
67
|
+
>(({ className, ...props }, ref) => (
|
|
68
|
+
<div
|
|
69
|
+
ref={ref}
|
|
70
|
+
className={cn("flex items-center sm:p-6 p-2 pt-0", className)}
|
|
71
|
+
{...props}
|
|
72
|
+
/>
|
|
73
|
+
))
|
|
74
|
+
CardFooter.displayName = "CardFooter"
|
|
75
|
+
|
|
76
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
package/src/ui/index.ts
ADDED
package/src/ui/input.tsx
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/utils"
|
|
4
|
+
|
|
5
|
+
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
|
6
|
+
({ className, type, ...props }, ref) => {
|
|
7
|
+
return (
|
|
8
|
+
<input
|
|
9
|
+
type={type}
|
|
10
|
+
className={cn(
|
|
11
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm text-xs",
|
|
12
|
+
className
|
|
13
|
+
)}
|
|
14
|
+
ref={ref}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
)
|
|
20
|
+
Input.displayName = "Input"
|
|
21
|
+
|
|
22
|
+
export { Input }
|