| import React, { useCallback, useEffect, useState } from 'react'; |
| import { Link2, RefreshCw, Loader2 } from 'lucide-react'; |
| import { Button } from '@/components/ui/button'; |
| import { apiFetch } from '@/lib/api'; |
| import { cn } from '@/lib/utils'; |
|
|
| export default function LinkedInConnectSettings() { |
| const [loading, setLoading] = useState(true); |
| const [busy, setBusy] = useState(false); |
| const [accounts, setAccounts] = useState([]); |
| const [defaultRefId, setDefaultRefId] = useState(null); |
|
|
| const load = useCallback(async () => { |
| setLoading(true); |
| try { |
| const r = await apiFetch('/api/unipile/linkedin/campaign-defaults'); |
| const d = r.ok ? await r.json() : {}; |
| setAccounts(d.accounts || []); |
| setDefaultRefId(d.default_unipile_account_ref_id ?? null); |
| } catch { |
| setAccounts([]); |
| } finally { |
| setLoading(false); |
| } |
| }, []); |
|
|
| useEffect(() => { |
| load(); |
| }, [load]); |
|
|
| const connectAccount = async () => { |
| setBusy(true); |
| try { |
| const res = await apiFetch('/api/unipile/linkedin/hosted-link', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| label: 'LinkedIn account', |
| }), |
| }); |
| const data = await res.json().catch(() => ({})); |
| if (!res.ok) throw new Error(data.detail || 'Could not start LinkedIn connect'); |
| if (data.url) window.location.href = data.url; |
| } catch (e) { |
| alert(e.message || 'Connect failed'); |
| } finally { |
| setBusy(false); |
| } |
| }; |
|
|
| return ( |
| <section className="rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"> |
| <div className="flex flex-wrap items-start justify-between gap-3"> |
| <div className="flex items-center gap-2 font-semibold text-slate-800"> |
| <Link2 className="h-5 w-5 text-violet-600" /> |
| Connect LinkedIn |
| </div> |
| <Button type="button" variant="outline" size="sm" disabled={loading || busy} onClick={load}> |
| <RefreshCw className={cn('mr-2 h-4 w-4', loading && 'animate-spin')} /> |
| Refresh |
| </Button> |
| </div> |
| <div className="mt-4 flex flex-col gap-3 md:flex-row md:items-end"> |
| <Button type="button" disabled={busy} onClick={connectAccount} className="bg-slate-900 text-white"> |
| <Link2 className="mr-2 h-4 w-4" /> |
| Connect account |
| </Button> |
| </div> |
| |
| {loading ? ( |
| <div className="mt-6 flex justify-center py-8 text-slate-400"> |
| <Loader2 className="h-8 w-8 animate-spin" /> |
| </div> |
| ) : accounts.length === 0 ? ( |
| <p className="mt-6 text-sm text-slate-500">Connected accounts: 0</p> |
| ) : ( |
| <div className="mt-6 space-y-3"> |
| <p className="text-sm font-medium text-slate-700"> |
| Connected accounts ({accounts.length}) — default for LinkedIn sequence steps |
| </p> |
| <ul className="space-y-2"> |
| {accounts.map((a) => { |
| const name = a.display_name || a.label || 'LinkedIn'; |
| const initials = name |
| .split(/\s+/) |
| .slice(0, 2) |
| .map((s) => s[0] || '') |
| .join('') |
| .toUpperCase(); |
| const checked = defaultRefId != null && Number(defaultRefId) === Number(a.id); |
| return ( |
| <li |
| key={a.id} |
| className={cn( |
| 'flex cursor-pointer items-center gap-3 rounded-xl border px-3 py-2 transition', |
| checked ? 'border-violet-300 bg-violet-50/60' : 'border-slate-200 hover:bg-slate-50' |
| )} |
| onClick={() => setDefaultRefId(a.id)} |
| > |
| <input |
| type="radio" |
| className="h-4 w-4 accent-violet-600" |
| checked={checked} |
| onChange={() => setDefaultRefId(a.id)} |
| /> |
| {a.avatar_url ? ( |
| <img |
| src={a.avatar_url} |
| alt="" |
| className="h-9 w-9 rounded-full object-cover" |
| /> |
| ) : ( |
| <span className="flex h-9 w-9 items-center justify-center rounded-full bg-violet-100 text-xs font-semibold text-violet-800"> |
| {initials || 'LI'} |
| </span> |
| )} |
| <div className="min-w-0 flex-1"> |
| <p className="truncate font-medium text-slate-900">{name}</p> |
| <p className="truncate text-xs text-slate-500">{a.label}</p> |
| </div> |
| <span className="text-[10px] font-semibold uppercase text-slate-400"> |
| {a.status || '—'} |
| </span> |
| </li> |
| ); |
| })} |
| </ul> |
| <div className="flex flex-wrap items-center gap-3"> |
| <button |
| type="button" |
| className="text-xs font-medium text-violet-700 hover:underline" |
| onClick={async () => { |
| setBusy(true); |
| try { |
| const res = await apiFetch('/api/me/linkedin-prefs', { |
| method: 'PATCH', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ default_unipile_account_ref_id: 0 }), |
| }); |
| if (!res.ok) throw new Error('Could not clear'); |
| setDefaultRefId(null); |
| await load(); |
| } catch (e) { |
| alert(e.message || 'Failed'); |
| } finally { |
| setBusy(false); |
| } |
| }} |
| > |
| Clear default |
| </button> |
| </div> |
| </div> |
| )} |
| </section> |
| ); |
| } |
|
|