kyd-shared-badge 0.3.70 → 0.3.72
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 +243 -119
- package/src/connect/oauth.ts +13 -8
- package/src/connect/types.ts +5 -0
- package/src/ui/card.tsx +1 -1
- package/src/utils/index.ts +2 -0
- package/src/utils/utils.ts +9 -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.72",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -22,12 +22,14 @@
|
|
|
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",
|
|
26
27
|
"lucide-react": "^0.545.0",
|
|
27
28
|
"next-auth": "^4.24.11",
|
|
28
29
|
"react-hot-toast": "^2.6.0",
|
|
29
30
|
"react-icons": "^5.5.0",
|
|
30
31
|
"recharts": "^2.15.4",
|
|
32
|
+
"tailwind-merge": "^3.3.1",
|
|
31
33
|
"ulid": "^3.0.1"
|
|
32
34
|
},
|
|
33
35
|
"publishConfig": {
|
|
@@ -3,8 +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 } from 'lucide-react';
|
|
7
|
-
import {
|
|
6
|
+
import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink } from 'lucide-react';
|
|
7
|
+
import { AnimatePresence, motion } from 'framer-motion';
|
|
8
|
+
import { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter, CardTitle } from '../ui';
|
|
9
|
+
import Link from 'next/link';
|
|
8
10
|
|
|
9
11
|
function byPriority(a: string, b: string) {
|
|
10
12
|
const pr = (id: string) => (id === 'github' ? 0 : id === 'linkedin' ? 1 : 2);
|
|
@@ -28,6 +30,11 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
28
30
|
onDisconnected,
|
|
29
31
|
onError,
|
|
30
32
|
className,
|
|
33
|
+
oauthClientIds,
|
|
34
|
+
providerPickCallback,
|
|
35
|
+
handleBackButton,
|
|
36
|
+
headerTitle,
|
|
37
|
+
headerDescription,
|
|
31
38
|
} = props;
|
|
32
39
|
|
|
33
40
|
const router = useRouter();
|
|
@@ -89,6 +96,11 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
89
96
|
}
|
|
90
97
|
};
|
|
91
98
|
|
|
99
|
+
const setSelectedProviderIdAndCallback = (providerId: string | null) => {
|
|
100
|
+
setSelectedProviderId(providerId);
|
|
101
|
+
providerPickCallback(providerId);
|
|
102
|
+
};
|
|
103
|
+
|
|
92
104
|
const onSubmitLink = async (providerId: string) => {
|
|
93
105
|
const provider = providers.find(p => p.id.toLowerCase() === providerId.toLowerCase());
|
|
94
106
|
if (!provider || !provider.endpoint) return;
|
|
@@ -106,7 +118,8 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
106
118
|
});
|
|
107
119
|
const data = await res.json().catch(() => ({}));
|
|
108
120
|
if (!res.ok) throw new Error(data?.error || `Failed to sync ${provider.name} profile.`);
|
|
109
|
-
|
|
121
|
+
setSelectedProviderIdAndCallback(null);
|
|
122
|
+
|
|
110
123
|
setLinkUrl('');
|
|
111
124
|
if (onConnected) onConnected(providerId);
|
|
112
125
|
} catch (e) {
|
|
@@ -124,136 +137,247 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
124
137
|
// Lazy import builder to avoid circular deps
|
|
125
138
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
126
139
|
const { buildOAuthUrl } = require('./oauth');
|
|
127
|
-
const url = buildOAuthUrl({ providerId: id, redirectUri, state });
|
|
140
|
+
const url = buildOAuthUrl({ providerId: id, redirectUri, state, clientIds: oauthClientIds });
|
|
128
141
|
if (url) router.push(url);
|
|
129
142
|
};
|
|
130
143
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
|
|
144
|
+
const selectedProvider = useMemo(
|
|
145
|
+
() => (selectedProviderId ? providers.find(p => p.id.toLowerCase() === selectedProviderId.toLowerCase()) : null),
|
|
146
|
+
[selectedProviderId, providers]
|
|
147
|
+
);
|
|
136
148
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
const handleConnectBack = () => {
|
|
150
|
+
setSelectedProviderIdAndCallback(null);
|
|
151
|
+
setLinkUrl('');
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const cardVariants = {
|
|
155
|
+
initial: { opacity: 0, y: 20 },
|
|
156
|
+
animate: { opacity: 1, y: 0 },
|
|
157
|
+
exit: { opacity: 0, y: -20 },
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<>
|
|
162
|
+
{selectedProvider ? (
|
|
163
|
+
<AnimatePresence>
|
|
164
|
+
<motion.div
|
|
165
|
+
key="connect-card"
|
|
166
|
+
className="rounded-xl border max-w-xl w-full"
|
|
167
|
+
style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)'}}
|
|
168
|
+
initial="initial" animate="animate" exit="exit" variants={cardVariants}
|
|
169
|
+
>
|
|
170
|
+
<div className="sm:p-6 p-4">
|
|
171
|
+
<button onClick={handleConnectBack} className="flex items-center gap-2 text-sm text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors mb-4">
|
|
172
|
+
<ArrowLeft className="w-4 h-4" />
|
|
173
|
+
Back
|
|
174
|
+
</button>
|
|
175
|
+
<div className="text-center">
|
|
176
|
+
<div className="flex justify-center mb-4">
|
|
177
|
+
<ProviderIcon name={selectedProvider.id} className={`w-10 h-10 ${selectedProvider.iconColor || 'text-gray-500'}`} />
|
|
178
|
+
</div>
|
|
179
|
+
<h3 className="sm:text-lg text-base font-semibold" style={{ color: 'var(--text-main)'}}>
|
|
180
|
+
{selectedProvider.connectionType === 'url' || (selectedProvider.connectionType || 'url') === 'link'
|
|
181
|
+
? `Use Public ${selectedProvider.name} Profile`
|
|
182
|
+
: `Connect ${selectedProvider.name}`}
|
|
183
|
+
</h3>
|
|
184
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] mx-auto">
|
|
185
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link')
|
|
186
|
+
? (selectedProvider.placeholder || 'Enter your public profile URL.')
|
|
187
|
+
: `Authorize with ${selectedProvider.name} to connect your account.`}
|
|
188
|
+
</p>
|
|
156
189
|
</div>
|
|
157
190
|
|
|
158
|
-
{
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
191
|
+
{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? (
|
|
192
|
+
<motion.form
|
|
193
|
+
onSubmit={(e) => { e.preventDefault(); onSubmitLink(selectedProvider.id); }}
|
|
194
|
+
className="mt-6 space-y-4"
|
|
195
|
+
initial="initial" animate="animate" exit="exit" variants={cardVariants}
|
|
196
|
+
>
|
|
197
|
+
{selectedProvider.id === 'linkedin' && (
|
|
198
|
+
<p className="sm:text-xs items-center text-[10px] text-[var(--text-secondary)] leading-relaxed max-w-xs mx-auto -mt-2">
|
|
199
|
+
<Link
|
|
200
|
+
href="https://www.linkedin.com/public-profile/settings"
|
|
201
|
+
target="_blank"
|
|
202
|
+
rel="noopener noreferrer"
|
|
203
|
+
className="underline"
|
|
204
|
+
style={{ color: 'var(--icon-accent)' }}
|
|
205
|
+
>
|
|
206
|
+
LinkedIn <ExternalLink className="size-3 inline-block ml-1 underline-0" />
|
|
207
|
+
</Link>
|
|
208
|
+
. This opens your public profile settings (you’ll see your shareable URL if you’re signed in).
|
|
209
|
+
</p>
|
|
210
|
+
)}
|
|
211
|
+
<div className="relative">
|
|
212
|
+
<LinkIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-secondary)]" />
|
|
213
|
+
<Input
|
|
214
|
+
type="url"
|
|
215
|
+
value={linkUrl}
|
|
216
|
+
onChange={(e) => setLinkUrl(e.target.value)}
|
|
217
|
+
placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'}
|
|
218
|
+
required
|
|
219
|
+
className="w-full border bg-transparent p-2 pl-9"
|
|
220
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
221
|
+
onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
222
|
+
onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
223
|
+
/>
|
|
164
224
|
</div>
|
|
165
|
-
<div
|
|
166
|
-
<Button
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
225
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
226
|
+
<Button type="submit" className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors" disabled={isSubmitting}>
|
|
227
|
+
{isSubmitting ? (
|
|
228
|
+
<div className="flex items-center justify-center">
|
|
229
|
+
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
230
|
+
Connecting...
|
|
231
|
+
</div>
|
|
232
|
+
) : (
|
|
233
|
+
'Connect'
|
|
234
|
+
)}
|
|
173
235
|
</Button>
|
|
174
|
-
</div>
|
|
175
|
-
</
|
|
236
|
+
</motion.div>
|
|
237
|
+
</motion.form>
|
|
176
238
|
) : (
|
|
177
|
-
<div className="
|
|
178
|
-
{
|
|
179
|
-
<
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
onChange={(e) => setLinkUrl(e.target.value)}
|
|
185
|
-
placeholder={provider.placeholder || 'https://example.com/your-profile'}
|
|
186
|
-
required
|
|
187
|
-
className="w-72 border bg-transparent p-2 text-sm rounded"
|
|
188
|
-
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
189
|
-
onPaste={providerId === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
190
|
-
onBlur={providerId === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
191
|
-
/>
|
|
192
|
-
</div>
|
|
193
|
-
<Button type="submit" disabled={isSubmitting} className="px-3 py-2 rounded bg-[var(--icon-accent)] text-white">
|
|
194
|
-
{isSubmitting ? 'Connecting…' : 'Connect'}
|
|
195
|
-
</Button>
|
|
196
|
-
</form>
|
|
197
|
-
) : (
|
|
198
|
-
<>
|
|
199
|
-
<Button
|
|
200
|
-
onClick={() => (isOauth ? onOAuth(providerId) : setSelectedProviderId(providerId))}
|
|
201
|
-
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"
|
|
202
|
-
>
|
|
203
|
-
{/* OAuth/URL leading icon */}
|
|
204
|
-
{isOauth ? (
|
|
205
|
-
<Link2 className="size-3 sm:size-4" />
|
|
206
|
-
) : (
|
|
207
|
-
<LinkIcon className="size-3 sm:size-4" />
|
|
208
|
-
)}
|
|
209
|
-
<span className="sm:text-base text-sm">Connect</span>
|
|
210
|
-
</Button>
|
|
211
|
-
{needsReconnect && (
|
|
212
|
-
<Button
|
|
213
|
-
onClick={() => onDisconnect(providerId)}
|
|
214
|
-
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
215
|
-
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
216
|
-
>
|
|
217
|
-
{/* Unlink icon (inline SVG) */}
|
|
218
|
-
<Unlink className="size-3 sm:size-4" />
|
|
219
|
-
<span>Remove</span>
|
|
220
|
-
</Button>
|
|
221
|
-
)}
|
|
222
|
-
</>
|
|
223
|
-
)}
|
|
239
|
+
<div className="mt-6">
|
|
240
|
+
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
|
|
241
|
+
<Button onClick={() => onOAuth(selectedProvider.id)} className="w-full bg-[var(--icon-accent)] text-white hover:bg-[var(--icon-accent-hover)] transition-colors">
|
|
242
|
+
<ExternalLink className="w-4 h-4 mr-2" />
|
|
243
|
+
Connect with {selectedProvider.name}
|
|
244
|
+
</Button>
|
|
245
|
+
</motion.div>
|
|
224
246
|
</div>
|
|
225
247
|
)}
|
|
226
248
|
</div>
|
|
227
|
-
|
|
228
|
-
|
|
249
|
+
</motion.div>
|
|
250
|
+
</AnimatePresence>
|
|
251
|
+
) : (
|
|
252
|
+
<Card className="border-[var(--icon-button-secondary)] pt-2" style={{ backgroundColor: 'var(--content-card-background)'}}>
|
|
253
|
+
<AnimatePresence mode="wait">
|
|
254
|
+
<motion.div key="platform-list-shared" variants={cardVariants} initial="initial" animate="animate" exit="exit" transition={{ duration: 0.3 }}>
|
|
255
|
+
<CardHeader className="pb-4">
|
|
256
|
+
{handleBackButton && (
|
|
257
|
+
<button onClick={() => handleBackButton()} className="flex items-center gap-2 text-sm sm:mb-1 mb-4 text-[var(--text-secondary)] hover:text-[var(--text-main)] transition-colors">
|
|
258
|
+
<ArrowLeft className="w-4 h-4" /> Back
|
|
259
|
+
</button>
|
|
260
|
+
)}
|
|
261
|
+
<CardTitle className="sm:text-xl text-base font-semibold text-[var(--text-main)] mb-2">{headerTitle}</CardTitle>
|
|
262
|
+
<p className="sm:text-sm text-xs text-[var(--text-secondary)] leading-relaxed">{headerDescription}</p>
|
|
263
|
+
</CardHeader>
|
|
264
|
+
<CardContent className="space-y-2">
|
|
265
|
+
{(() => {
|
|
266
|
+
const oauthList = list.filter(p => p.connectionType === 'oauth');
|
|
267
|
+
const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
|
|
229
268
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
269
|
+
const Row = (provider: typeof list[number]) => {
|
|
270
|
+
const providerId = provider.id;
|
|
271
|
+
const isConnected = connectedIds.has(providerId.toLowerCase());
|
|
272
|
+
const needsReconnect = reconnectIds.has(providerId.toLowerCase());
|
|
273
|
+
const isOauth = provider.connectionType === 'oauth'
|
|
274
|
+
const connectedUrl = connected.find(c => c.id.toLowerCase() === providerId.toLowerCase())?.url;
|
|
275
|
+
const betaFlag = () => (
|
|
276
|
+
<span
|
|
277
|
+
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"
|
|
278
|
+
style={{ verticalAlign: 'middle', letterSpacing: '0.05em' }}
|
|
279
|
+
>
|
|
280
|
+
Beta
|
|
281
|
+
</span>
|
|
282
|
+
);
|
|
242
283
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
284
|
+
return (
|
|
285
|
+
<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">
|
|
286
|
+
<div className="flex items-center gap-3">
|
|
287
|
+
|
|
288
|
+
{isConnected && connectedUrl ? (
|
|
289
|
+
<Link href={connectedUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-3 hover:underline">
|
|
290
|
+
<ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
|
|
291
|
+
<span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
|
|
292
|
+
</Link>
|
|
293
|
+
) : (
|
|
294
|
+
<div className="flex items-center gap-3">
|
|
295
|
+
<ProviderIcon name={provider.id} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
|
|
296
|
+
<span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
{provider.beta ? betaFlag() : null}
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{isConnected ? (
|
|
303
|
+
<div className="relative flex items-center">
|
|
304
|
+
<div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
|
|
305
|
+
<CheckCircle className="size-3 sm:size-4" />
|
|
306
|
+
<span className="text-sm font-medium">Connected</span>
|
|
307
|
+
</div>
|
|
308
|
+
<div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
|
|
309
|
+
<button
|
|
310
|
+
onClick={() => onDisconnect(providerId)}
|
|
311
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
|
|
312
|
+
>
|
|
313
|
+
<Unlink className="size-3 sm:size-4" />
|
|
314
|
+
<span>Disconnect</span>
|
|
315
|
+
</button>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
) : (
|
|
319
|
+
<div className="flex items-center gap-2">
|
|
320
|
+
<>
|
|
321
|
+
<Button
|
|
322
|
+
onClick={() => setSelectedProviderIdAndCallback(providerId)}
|
|
323
|
+
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"
|
|
324
|
+
>
|
|
325
|
+
{isOauth ? (
|
|
326
|
+
<Link2 className="size-3 sm:size-4" />
|
|
327
|
+
) : (
|
|
328
|
+
<LinkIcon className="size-3 sm:size-4" />
|
|
329
|
+
)}
|
|
330
|
+
<span className="sm:text-base text-sm">Connect</span>
|
|
331
|
+
</Button>
|
|
332
|
+
{needsReconnect && (
|
|
333
|
+
<Button
|
|
334
|
+
onClick={() => onDisconnect(providerId)}
|
|
335
|
+
className="inline-flex items-center justify-center gap-1.5 px-4 py-2 text-sm rounded border"
|
|
336
|
+
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
337
|
+
>
|
|
338
|
+
<Unlink className="size-3 sm:size-4" />
|
|
339
|
+
<span>Remove</span>
|
|
340
|
+
</Button>
|
|
341
|
+
)}
|
|
342
|
+
</>
|
|
343
|
+
</div>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<>
|
|
351
|
+
{oauthList.length ? (
|
|
352
|
+
<>
|
|
353
|
+
<div className="flex items-center my-2">
|
|
354
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
355
|
+
<span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>OAuth</span>
|
|
356
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
357
|
+
</div>
|
|
358
|
+
{oauthList.map(p => Row(p))}
|
|
359
|
+
</>
|
|
360
|
+
) : null}
|
|
361
|
+
|
|
362
|
+
{urlList.length ? (
|
|
363
|
+
<>
|
|
364
|
+
<div className="flex items-center my-2">
|
|
365
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
366
|
+
<span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>Public Urls</span>
|
|
367
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
368
|
+
</div>
|
|
369
|
+
{urlList.map(p => Row(p))}
|
|
370
|
+
</>
|
|
371
|
+
) : null}
|
|
372
|
+
</>
|
|
373
|
+
);
|
|
374
|
+
})()}
|
|
375
|
+
</CardContent>
|
|
376
|
+
</motion.div>
|
|
377
|
+
</AnimatePresence>
|
|
378
|
+
</Card>
|
|
379
|
+
)}
|
|
380
|
+
</>
|
|
257
381
|
);
|
|
258
382
|
}
|
|
259
383
|
|
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,15 @@ 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;
|
|
36
|
+
handleBackButton?: () => void;
|
|
37
|
+
headerTitle: string;
|
|
38
|
+
headerDescription: string;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
|
package/src/ui/card.tsx
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { clsx, type ClassValue } from "clsx"
|
|
2
|
+
import { twMerge } from "tailwind-merge"
|
|
3
|
+
|
|
4
|
+
export function cn(...inputs: ClassValue[]) {
|
|
5
|
+
return twMerge(clsx(inputs))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const ROLE_NAME_MAX_LENGTH = 500;
|
|
9
|
+
export const ROLE_DESCRIPTION_MAX_LENGTH = 5500;
|