Files
2026-07-17 02:23:59 +02:00

196 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useEffect, useState } from "react";
import { UpgradeProLink } from "./UpgradeProLink";
type Playlist = {
id: string;
title: string;
itemCount: number;
};
type Props = {
value: string;
onChange: (playlistId: string) => void;
enabled: boolean;
};
export function PlaylistSelect({ value, onChange, enabled }: Props) {
const [playlists, setPlaylists] = useState<Playlist[]>([]);
const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showCreate, setShowCreate] = useState(false);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [privacy, setPrivacy] = useState<"public" | "unlisted" | "private">("private");
const loadPlaylists = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch("/api/youtube/playlists");
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to load playlists");
setPlaylists(data.playlists ?? []);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load playlists");
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (!enabled) return;
void loadPlaylists();
}, [enabled, loadPlaylists]);
async function handleCreate() {
if (!title.trim()) {
setError("Playlist title is required");
return;
}
setCreating(true);
setError(null);
try {
const res = await fetch("/api/youtube/playlists", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
title: title.trim(),
description: description.trim() || undefined,
privacy,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Failed to create playlist");
const playlist = data.playlist as Playlist;
setPlaylists((prev) => [playlist, ...prev]);
onChange(playlist.id);
setShowCreate(false);
setTitle("");
setDescription("");
setPrivacy("private");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to create playlist");
} finally {
setCreating(false);
}
}
if (!enabled) {
return (
<p className="text-sm text-gray-400">
Add uploaded videos to a YouTube playlist with{" "}
<UpgradeProLink className="text-accent hover:underline" />.
</p>
);
}
return (
<div className="space-y-3">
<div className="space-y-2">
<label className="block text-sm text-gray-400">Add to YouTube playlist (optional)</label>
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={loading || creating}
className="input-field w-full"
>
<option value="">None (dont add to a playlist)</option>
{playlists.map((playlist) => (
<option key={playlist.id} value={playlist.id}>
{playlist.title} ({playlist.itemCount})
</option>
))}
</select>
</div>
<div className="flex flex-wrap gap-3">
<button
type="button"
onClick={() => setShowCreate((prev) => !prev)}
className="text-sm text-accent hover:underline"
>
{showCreate ? "Cancel" : "Create new playlist"}
</button>
<button
type="button"
onClick={() => void loadPlaylists()}
disabled={loading}
className="text-sm text-gray-400 hover:text-white"
>
Refresh list
</button>
</div>
{showCreate && (
<div className="space-y-3 rounded border border-gray-700 bg-surface-dark/60 p-4">
<div>
<label className="mb-1 block text-sm text-gray-400">Playlist title</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void handleCreate();
}
}}
className="input-field w-full"
placeholder="My Songs2YT uploads"
/>
</div>
<div>
<label className="mb-1 block text-sm text-gray-400">Description (optional)</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
void handleCreate();
}
}}
className="input-field w-full"
/>
</div>
<div>
<label className="mb-1 block text-sm text-gray-400">Privacy</label>
<select
value={privacy}
onChange={(e) => setPrivacy(e.target.value as typeof privacy)}
className="input-field w-full"
>
<option value="private">Private</option>
<option value="unlisted">Unlisted</option>
<option value="public">Public</option>
</select>
</div>
<button
type="button"
onClick={() => void handleCreate()}
disabled={creating || !title.trim()}
className="rounded bg-accent px-4 py-2 text-sm font-medium text-white hover:bg-accent-hover disabled:opacity-50"
>
{creating ? "Creating…" : "Create & select"}
</button>
</div>
)}
{loading && <p className="text-xs text-gray-500">Loading playlists</p>}
{error && <p className="text-xs text-red-400">{error}</p>}
{!loading && !error && playlists.length === 0 && !showCreate && (
<p className="text-xs text-gray-500">
No playlists yet. Create one above. If playlist access was just added, sign out and sign
in again to grant the new Google permission.
</p>
)}
</div>
);
}