168 lines
7.3 KiB
TypeScript
168 lines
7.3 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState } from 'react';
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { Plus, Loader2, AlertCircle, Users, Trash2 } from 'lucide-react';
|
|
import { FaYoutube } from 'react-icons/fa';
|
|
import { SiTwitch, SiInstagram, SiTiktok, SiX, SiBluesky, SiDiscord } from 'react-icons/si';
|
|
import { fetchCollaborators, deleteCollaborator, type Collaborator } from '@/lib/api';
|
|
import CollaboratorModal, { PLATFORMS, type PlatformKey } from '@/components/shared/CollaboratorModal';
|
|
import styles from './page.module.css';
|
|
|
|
// ─── Platforms cell ───────────────────────────────────────────────────────────
|
|
|
|
const PLATFORM_ICON: Record<PlatformKey, React.ReactElement> = {
|
|
youtubeLink: <FaYoutube size={13} />,
|
|
twitchLink: <SiTwitch size={13} />,
|
|
instagramLink: <SiInstagram size={13} />,
|
|
tiktokLink: <SiTiktok size={13} />,
|
|
twitterLink: <SiX size={13} />,
|
|
blueskyLink: <SiBluesky size={13} />,
|
|
discordHandle: <SiDiscord size={13} />,
|
|
};
|
|
|
|
function PlatformsCell({ c }: { c: Collaborator }) {
|
|
const links = PLATFORMS.filter((p) => !!c[p.key]);
|
|
if (links.length === 0) return <span className={styles.empty2}>—</span>;
|
|
return (
|
|
<div className={styles.platformBadges}>
|
|
{links.map((p) => {
|
|
const val = c[p.key];
|
|
if (p.urlField && val) {
|
|
return (
|
|
<a key={p.key} href={val} target="_blank" rel="noopener noreferrer" className={styles.platformBadge} title={p.label}>
|
|
{PLATFORM_ICON[p.key]}
|
|
</a>
|
|
);
|
|
}
|
|
return (
|
|
<span key={p.key} className={styles.platformBadge} title={`${p.label}: ${val}`}>
|
|
{PLATFORM_ICON[p.key]}
|
|
</span>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// ─── Page ─────────────────────────────────────────────────────────────────────
|
|
|
|
export default function CollaboratorsPage() {
|
|
const qc = useQueryClient();
|
|
const [modal, setModal] = useState<'create' | Collaborator | null>(null);
|
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null);
|
|
const [deleteError, setDeleteError] = useState<string | null>(null);
|
|
|
|
const deleteMut = useMutation({
|
|
mutationFn: deleteCollaborator,
|
|
onSuccess: () => { qc.invalidateQueries({ queryKey: ['collaborators'] }); setConfirmDelete(null); setDeleteError(null); },
|
|
onError: (e: unknown) => setDeleteError((e as { response?: { data?: { message?: string } } })?.response?.data?.message ?? 'Delete failed'),
|
|
});
|
|
|
|
const { data: collaborators = [], isLoading, isError } = useQuery<Collaborator[]>({
|
|
queryKey: ['collaborators'],
|
|
queryFn: fetchCollaborators,
|
|
});
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
<header className={styles.header}>
|
|
<div className={styles.headerLeft}>
|
|
<span className={styles.eyebrow}>People & Partners</span>
|
|
<h1>
|
|
Collaborators
|
|
{collaborators.length > 0 && <span className={styles.count}>{collaborators.length}</span>}
|
|
</h1>
|
|
</div>
|
|
<div className={styles.headerRight}>
|
|
<button className="btn btn-primary" onClick={() => setModal('create')}>
|
|
<Plus size={18} />
|
|
<span>Add Collaborator</span>
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{isLoading && <div className={styles.state}><Loader2 size={24} className={styles.spinner} /><span>Loading collaborators…</span></div>}
|
|
{isError && <div className={styles.state}><AlertCircle size={20} /><span>Failed to load collaborators — is the backend running?</span></div>}
|
|
{!isLoading && !isError && collaborators.length === 0 && (
|
|
<div className={styles.empty}>
|
|
<Users size={40} className={styles.emptyIcon} />
|
|
<p>No collaborators yet.</p>
|
|
<p className={styles.emptyHint}>Add guest creators and partners to use them in your description blocks.</p>
|
|
<button className="btn btn-primary" onClick={() => setModal('create')}>
|
|
<Plus size={18} />
|
|
<span>Add your first collaborator</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{!isLoading && !isError && collaborators.length > 0 && (
|
|
<div className={styles.tableContainer}>
|
|
<table className={styles.table}>
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Platforms</th>
|
|
<th>Aliases</th>
|
|
<th>Status</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{collaborators.map((c) => (
|
|
<tr key={c.id}>
|
|
<td>
|
|
<div className={styles.nameCell}>
|
|
<div className={styles.avatar}>{c.name.charAt(0).toUpperCase()}</div>
|
|
<div>
|
|
<span className={styles.name}>{c.name}</span>
|
|
{c.notes && <span className={styles.notes}>{c.notes}</span>}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<PlatformsCell c={c} />
|
|
</td>
|
|
<td>
|
|
<div className={styles.aliases}>
|
|
{c.aliases.slice(0, 2).map((a) => <span key={a} className={styles.alias}>{a}</span>)}
|
|
{c.aliases.length > 2 && <span className={styles.alias}>+{c.aliases.length - 2}</span>}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{c.active ? <span className="pill pill-primary">Active</span> : <span className="pill">Inactive</span>}
|
|
</td>
|
|
<td>
|
|
{confirmDelete === c.id ? (
|
|
<div className={styles.confirmDelete}>
|
|
{deleteError
|
|
? <span className={styles.deleteErr}>{deleteError}</span>
|
|
: <span className={styles.confirmText}>Delete?</span>}
|
|
<button className={styles.confirmYes} onClick={() => deleteMut.mutate(c.id)} disabled={deleteMut.isPending}>
|
|
{deleteMut.isPending ? <Loader2 size={12} /> : 'Yes'}
|
|
</button>
|
|
<button className={styles.confirmNo} onClick={() => { setConfirmDelete(null); setDeleteError(null); }}>
|
|
{deleteError ? 'OK' : 'No'}
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'flex', gap: 'var(--space-3)', alignItems: 'center' }}>
|
|
<button className={styles.editBtn} onClick={() => setModal(c)}>Edit</button>
|
|
<button className={styles.trashBtn} onClick={() => { setConfirmDelete(c.id); setDeleteError(null); }}><Trash2 size={14} /></button>
|
|
</div>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{modal !== null && (
|
|
<CollaboratorModal initial={modal === 'create' ? undefined : modal} onClose={() => setModal(null)} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|