Files
youtube-studio-flow/frontend/src/app/(dashboard)/collaborators/page.tsx
T

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>
);
}