kyd-shared-badge 0.3.69 → 0.3.70
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 +2 -1
- package/src/connect/ConnectAccounts.tsx +119 -78
- 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.70",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"module": "./src/index.ts",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
|
24
24
|
"ai": "5.0.47",
|
|
25
25
|
"i18n-iso-countries": "^7.14.0",
|
|
26
|
+
"lucide-react": "^0.545.0",
|
|
26
27
|
"next-auth": "^4.24.11",
|
|
27
28
|
"react-hot-toast": "^2.6.0",
|
|
28
29
|
"react-icons": "^5.5.0",
|
|
@@ -3,6 +3,8 @@ 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 { Button, Input, Spinner, Card, CardHeader, CardContent, CardFooter } from '../ui';
|
|
6
8
|
|
|
7
9
|
function byPriority(a: string, b: string) {
|
|
8
10
|
const pr = (id: string) => (id === 'github' ? 0 : id === 'linkedin' ? 1 : 2);
|
|
@@ -128,90 +130,129 @@ export function ConnectAccounts(props: ConnectAccountsProps) {
|
|
|
128
130
|
|
|
129
131
|
return (
|
|
130
132
|
<div className={className}>
|
|
131
|
-
{
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const needsReconnect = reconnectIds.has(providerId.toLowerCase());
|
|
135
|
-
const isUrl = (provider.connectionType || 'url') === 'url' || provider.connectionType === 'link';
|
|
136
|
-
const isOauth = provider.connectionType === 'oauth';
|
|
137
|
-
return (
|
|
138
|
-
<div key={providerId} className="group flex items-center justify-between p-2 rounded-lg transition-colors hover:bg-[var(--icon-button-secondary)]/10">
|
|
139
|
-
<div className="flex items-center gap-3">
|
|
140
|
-
<ProviderIcon name={providerId} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
|
|
141
|
-
<span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
|
|
142
|
-
{provider.beta ? (
|
|
143
|
-
<span
|
|
144
|
-
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"
|
|
145
|
-
style={{ verticalAlign: 'middle', letterSpacing: '0.05em' }}
|
|
146
|
-
>
|
|
147
|
-
Beta
|
|
148
|
-
</span>
|
|
149
|
-
) : null}
|
|
150
|
-
</div>
|
|
133
|
+
{(() => {
|
|
134
|
+
const oauthList = list.filter(p => p.connectionType === 'oauth');
|
|
135
|
+
const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link');
|
|
151
136
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
137
|
+
const Row = (provider: typeof list[number]) => {
|
|
138
|
+
const providerId = provider.id;
|
|
139
|
+
const isConnected = connectedIds.has(providerId.toLowerCase());
|
|
140
|
+
const needsReconnect = reconnectIds.has(providerId.toLowerCase());
|
|
141
|
+
const isUrl = (provider.connectionType || 'url') === 'url' || provider.connectionType === 'link';
|
|
142
|
+
const isOauth = provider.connectionType === 'oauth';
|
|
143
|
+
return (
|
|
144
|
+
<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">
|
|
145
|
+
<div className="flex items-center gap-3">
|
|
146
|
+
<ProviderIcon name={providerId} className={`sm:size-7 size-5 ${provider.iconColor || 'text-gray-500'}`} />
|
|
147
|
+
<span className="font-medium sm:text-base text-sm" style={{ color: 'var(--text-main)'}}>{provider.name}</span>
|
|
148
|
+
{provider.beta ? (
|
|
149
|
+
<span
|
|
150
|
+
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"
|
|
151
|
+
style={{ verticalAlign: 'middle', letterSpacing: '0.05em' }}
|
|
163
152
|
>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
</div>
|
|
153
|
+
Beta
|
|
154
|
+
</span>
|
|
155
|
+
) : null}
|
|
168
156
|
</div>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}}
|
|
182
|
-
onPaste={providerId === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined}
|
|
183
|
-
onBlur={providerId === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined}
|
|
184
|
-
/>
|
|
185
|
-
</div>
|
|
186
|
-
<button type="submit" disabled={isSubmitting} className="px-3 py-2 rounded bg-[var(--icon-accent)] text-white">
|
|
187
|
-
{isSubmitting ? 'Connecting…' : 'Connect'}
|
|
188
|
-
</button>
|
|
189
|
-
</form>
|
|
190
|
-
) : (
|
|
191
|
-
<>
|
|
192
|
-
<button
|
|
193
|
-
onClick={() => (isOauth ? onOAuth(providerId) : setSelectedProviderId(providerId))}
|
|
194
|
-
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"
|
|
157
|
+
|
|
158
|
+
{isConnected ? (
|
|
159
|
+
<div className="relative flex items-center">
|
|
160
|
+
<div className="flex items-center gap-2 transition-opacity group-hover:opacity-0" style={{ color: 'var(--success-green)'}}>
|
|
161
|
+
{/* CheckCircle icon (inline SVG) */}
|
|
162
|
+
<CheckCircle className="size-3 sm:size-4" />
|
|
163
|
+
<span className="text-sm font-medium">Connected</span>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
|
|
166
|
+
<Button
|
|
167
|
+
onClick={() => onDisconnect(providerId)}
|
|
168
|
+
className="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm text-red-600 hover:text-red-700 hover:underline"
|
|
195
169
|
>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
170
|
+
{/* Unlink icon (inline SVG) */}
|
|
171
|
+
<Unlink className="size-3 sm:size-4" />
|
|
172
|
+
<span>Disconnect</span>
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
) : (
|
|
177
|
+
<div className="flex items-center gap-2">
|
|
178
|
+
{selectedProviderId === providerId && isUrl ? (
|
|
179
|
+
<form onSubmit={(e) => { e.preventDefault(); onSubmitLink(providerId); }} className="flex items-center gap-2">
|
|
180
|
+
<div className="relative">
|
|
181
|
+
<Input
|
|
182
|
+
type="url"
|
|
183
|
+
value={linkUrl}
|
|
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"
|
|
203
202
|
>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return (
|
|
231
|
+
<>
|
|
232
|
+
{oauthList.length ? (
|
|
233
|
+
<>
|
|
234
|
+
<div className="flex items-center my-2">
|
|
235
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
236
|
+
<span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>OAuth</span>
|
|
237
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
238
|
+
</div>
|
|
239
|
+
{oauthList.map(p => Row(p))}
|
|
240
|
+
</>
|
|
241
|
+
) : null}
|
|
242
|
+
|
|
243
|
+
{urlList.length ? (
|
|
244
|
+
<>
|
|
245
|
+
<div className="flex items-center my-2">
|
|
246
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
247
|
+
<span className="mx-3 text-[10px] sm:text-xs uppercase tracking-wide" style={{ color: 'var(--text-secondary)'}}>Public Urls</span>
|
|
248
|
+
<div className="flex-1 border-t" style={{ borderColor: 'var(--icon-button-secondary)', opacity: 0.3 }} />
|
|
249
|
+
</div>
|
|
250
|
+
{urlList.map(p => Row(p))}
|
|
251
|
+
</>
|
|
252
|
+
) : null}
|
|
253
|
+
</>
|
|
213
254
|
);
|
|
214
|
-
})}
|
|
255
|
+
})()}
|
|
215
256
|
</div>
|
|
216
257
|
);
|
|
217
258
|
}
|
|
@@ -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 }
|