1/** @jsxImportSource https://esm.sh/react */
2// Set to true to disable all editing functionality
3const READ_ONLY = true; // Keep this as is or change as needed
4
5import { nanoid } from "https://esm.sh/nanoid";
6import React from "https://esm.sh/react";
7import { createRoot } from "https://esm.sh/react-dom/client";
8
9interface Song {
16}
17
18// --- React Components (AddSongForm, SongCard, App) ---
19// --- These remain the same as the PREVIOUS enhanced version ---
20// --- (Includes improved AddSongForm, SongCard with confirmation, App with ViewMode, etc.) ---
21
22function AddSongForm({ onAdd }: { onAdd: (song: Omit<Song, "id" | "created_at">) => void }) {
23 const [url, setUrl] = React.useState("");
24 const [title, setTitle] = React.useState("");
25 const [tag, setTag] = React.useState("");
26 const [isLoading, setIsLoading] = React.useState(false);
27 const [error, setError] = React.useState("");
28
29 const handleSubmit = async (e: React.FormEvent) => {
30 e.preventDefault();
31 setIsLoading(true);
120 },
121) {
122 const [isEditing, setIsEditing] = React.useState(false);
123 const [editUrl, setEditUrl] = React.useState(song.url);
124 const [editTitle, setEditTitle] = React.useState(song.title);
125 const [editTag, setEditTag] = React.useState(song.tag || "");
126 const [isDeleting, setIsDeleting] = React.useState(false); // For loading state during delete API call
127 const [isConfirmingDelete, setIsConfirmingDelete] = React.useState(false); // For the confirmation step
128 const [isLoading, setIsLoading] = React.useState(false); // For loading state during edit save
129 const confirmationTimer = React.useRef<number | null>(null); // Ref to hold timer ID
130
131 // Clear confirmation timer on unmount or when confirmation changes
132 React.useEffect(() => {
133 return () => {
134 if (confirmationTimer.current) {
164 };
165
166 const handleEditSubmit = async (e: React.FormEvent) => {
167 e.preventDefault();
168 setIsLoading(true); // Indicate loading for edit save
313
314function App() {
315 const [songs, setSongs] = React.useState<Song[]>([]);
316 const [sortBy, setSortBy] = React.useState<"date" | "title">("date"); // Default sort should match initial fetch if possible
317 const [selectedTag, setSelectedTag] = React.useState<string>("");
318 const [isLoading, setIsLoading] = React.useState(true);
319 const [error, setError] = React.useState("");
320 const [viewMode, setViewMode] = React.useState<"grid" | "list">("grid"); // State for view mode
321
322 // Using useCallback to stabilize the function reference
323 const fetchSongs = React.useCallback(async () => {
324 setError(""); // Clear previous errors before fetching
325 // Keep isLoading true if it was already true (initial load)
348 }, []); // Empty dependency array: function created once
349
350 React.useEffect(() => {
351 setIsLoading(true); // Set loading true when the component mounts
352 fetchSongs();
444
445 // Memoize tags derivation
446 const tags = React.useMemo(() => {
447 const tagSet = new Set<string>();
448 songs.forEach(s => {
455
456 // Memoize filtered and sorted songs
457 const filteredAndSortedSongs = React.useMemo(() => {
458 let filtered = songs;
459
621 </main>
622 <footer className="app-footer">
623 <p>© {new Date().getFullYear()} Audio Archive. Built with React & CSS.</p>
624 </footer>
625 </div>
732
733 // HTML response (SSR Shell) - Kept from enhanced version
734 // Renders the page structure, CSS, and JS includes. React hydrates on the client.
735 return new Response(
736 `