196 lines
6.1 KiB
TypeScript
196 lines
6.1 KiB
TypeScript
"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 (don’t 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>
|
||
);
|
||
}
|