Initial commit: YouTube Studio Flow (backend, frontend, infrastructure, docs)

This commit is contained in:
2026-08-11 12:27:44 +02:00
commit d5af006443
304 changed files with 74604 additions and 0 deletions
@@ -0,0 +1,218 @@
'use client';
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Loader2 } from 'lucide-react';
import {
fetchCollaboratorVideos, createCollaborator, updateCollaborator,
type Collaborator, type CollaboratorPayload,
} from '@/lib/api';
import UsagePanel from './UsagePanel';
import Modal from './Modal';
import f from './FormField.module.css';
export const PLATFORMS = [
{ key: 'youtubeLink', prefix: 'https://www.youtube.com/@', display: 'youtube.com/@', label: 'YouTube', urlField: true },
{ key: 'twitchLink', prefix: 'https://twitch.tv/', display: 'twitch.tv/', label: 'Twitch', urlField: true },
{ key: 'instagramLink', prefix: 'https://instagram.com/', display: 'instagram.com/', label: 'Instagram', urlField: true },
{ key: 'tiktokLink', prefix: 'https://tiktok.com/@', display: 'tiktok.com/@', label: 'TikTok', urlField: true },
{ key: 'twitterLink', prefix: 'https://x.com/', display: 'x.com/', label: 'Twitter / X', urlField: true },
{ key: 'blueskyLink', prefix: 'https://bsky.app/profile/', display: 'bsky.app/profile/', label: 'Bluesky', urlField: true },
{ key: 'discordHandle', prefix: '', display: '', label: 'Discord', urlField: false },
] as const;
export type PlatformKey = typeof PLATFORMS[number]['key'];
export function toUsername(url: string | null | undefined, prefix: string): string {
if (!url) return '';
return prefix ? url.replace(prefix, '').replace(/^\/+/, '') : url;
}
export function toValue(username: string, prefix: string): string {
if (!username.trim()) return '';
return prefix ? prefix + username.trim().replace(/^\/+/, '') : username.trim();
}
const EMPTY_FORM: CollaboratorPayload = {
name: '',
youtubeLink: '', twitchLink: '', instagramLink: '', tiktokLink: '',
twitterLink: '', blueskyLink: '', discordHandle: '',
aliases: [], notes: '', active: true,
};
interface Props {
initial?: Collaborator;
onClose: () => void;
/** Called after a successful create, with the new collaborator id */
onCreated?: (id: string) => void;
}
export default function CollaboratorModal({ initial, onClose, onCreated }: Props) {
const qc = useQueryClient();
const [form, setForm] = useState<CollaboratorPayload>(
initial
? {
name: initial.name,
youtubeLink: initial.youtubeLink ?? '',
twitchLink: initial.twitchLink ?? '',
instagramLink: initial.instagramLink ?? '',
tiktokLink: initial.tiktokLink ?? '',
twitterLink: initial.twitterLink ?? '',
blueskyLink: initial.blueskyLink ?? '',
discordHandle: initial.discordHandle ?? '',
aliases: initial.aliases,
notes: initial.notes ?? '',
active: initial.active,
}
: EMPTY_FORM,
);
const [aliasInput, setAliasInput] = useState(initial?.aliases.join(', ') ?? '');
const { data: usageVideos = [], isLoading: usageLoading } = useQuery({
queryKey: ['collaborator-videos', initial?.id],
queryFn: () => fetchCollaboratorVideos(initial!.id),
enabled: !!initial,
});
const [usernames, setUsernames] = useState<Record<PlatformKey, string>>(() => {
const src = initial ?? {} as Collaborator;
return Object.fromEntries(
PLATFORMS.map((p) => [p.key, toUsername(src[p.key as keyof Collaborator] as string | null, p.prefix)])
) as Record<PlatformKey, string>;
});
const setUsername = (key: PlatformKey, val: string) =>
setUsernames((prev) => ({ ...prev, [key]: val }));
const mutation = useMutation({
mutationFn: () => {
const platformValues = Object.fromEntries(
PLATFORMS.map((p) => [p.key, toValue(usernames[p.key], p.prefix)])
);
const payload: CollaboratorPayload = {
...form,
...platformValues,
aliases: aliasInput.split(',').map((a) => a.trim()).filter(Boolean),
};
return initial ? updateCollaborator(initial.id, payload) : createCollaborator(payload);
},
onSuccess: (saved) => {
qc.invalidateQueries({ queryKey: ['collaborators'] });
if (!initial && onCreated) onCreated((saved as Collaborator).id);
onClose();
},
});
const set = (key: keyof CollaboratorPayload, val: unknown) =>
setForm((p) => ({ ...p, [key]: val }));
return (
<Modal title={initial ? 'Edit Collaborator' : 'Add Collaborator'} onClose={onClose} width={600}>
<div className={f.field}>
<label className={f.label}>Name</label>
<input
className={f.input}
value={form.name}
onChange={(e) => set('name', e.target.value)}
placeholder="Full name or channel name"
autoFocus
/>
</div>
{([
PLATFORMS.slice(0, 2),
PLATFORMS.slice(2, 4),
PLATFORMS.slice(4, 6),
PLATFORMS.slice(6, 7),
] as const).map((row, ri) => (
<div key={ri} className={f.row}>
{row.map((p) => (
<div key={p.key} className={f.field}>
<label className={f.label}>
{p.label}
{p.key === 'discordHandle' && (
<span style={{ fontWeight: 400, color: 'var(--color-text-faint)' }}> (handle)</span>
)}
</label>
{p.display ? (
<div className={f.inputPrefix}>
<span className={f.prefix}>{p.display}</span>
<input
className={f.input}
value={usernames[p.key]}
onChange={(e) =>
setUsername(
p.key,
e.target.value.replace(
new RegExp(`^https?://(www\\.)?${p.display.replace(/\//g, '\\/')}`, 'i'),
'',
),
)
}
placeholder="username"
/>
</div>
) : (
<input
className={f.input}
value={usernames[p.key]}
onChange={(e) => setUsername(p.key, e.target.value)}
placeholder="username"
/>
)}
</div>
))}
</div>
))}
<div className={f.field}>
<label className={f.label}>Aliases (comma-separated)</label>
<input
className={f.input}
value={aliasInput}
onChange={(e) => setAliasInput(e.target.value)}
placeholder="alias1, alias2"
/>
</div>
<div className={f.field}>
<label className={f.label}>Notes</label>
<textarea
className={f.textarea}
value={form.notes ?? ''}
onChange={(e) => set('notes', e.target.value)}
placeholder="Optional notes about this collaborator"
/>
</div>
<label className={f.toggle}>
<input type="checkbox" checked={form.active} onChange={(e) => set('active', e.target.checked)} />
Active
</label>
{initial && (
<UsagePanel
isLoading={usageLoading}
groups={[
{
heading: 'Videos',
items: usageVideos.map((v) => ({ id: v.id, label: v.title, href: `/videos/${v.id}` })),
},
]}
/>
)}
{mutation.isError && (
<p style={{ color: 'var(--color-error)', fontSize: 'var(--text-xs)' }}>
Save failed check the backend logs.
</p>
)}
<div className={f.actions}>
<button className="btn btn-secondary" onClick={onClose}>Cancel</button>
<button
className="btn btn-primary"
onClick={() => mutation.mutate()}
disabled={mutation.isPending || !form.name.trim()}
>
{mutation.isPending ? <Loader2 size={14} /> : null}
{initial ? 'Save Changes' : 'Add Collaborator'}
</button>
</div>
</Modal>
);
}