diff --git a/web/src/library.ts b/web/src/library.ts new file mode 100644 index 0000000..f775155 --- /dev/null +++ b/web/src/library.ts @@ -0,0 +1,394 @@ +// Library management functionality for admin/library page +// Procedural/imperative style (no OOP) + +interface Library { + id: string; + name: string; + description: string | null; + library_type_id: string; + type_name: string; + created_at: string; + updated_at: string; +} + +interface LibraryFolder { + id: string; + library_id: string; + folder_path: string; + created_at: string; +} + +interface User { + id: string; + username: string; + email: string; + role: string; +} + +interface LibrariesResponse { + data: Library[]; +} + +interface VisibleLibrariesResponse { + data: Array<{ + id: string; + name: string; + type_name: string; + }>; +} + +// State (already SSR'd, used for updates) +let libraries: Library[] = []; +let users: User[] = []; + +// Reload libraries from API (called after create/delete/update) +async function reloadLibraries(): Promise { + try { + const response = await (window as any).api.get('/libraries'); + const result = await (window as any).api.handleResponse(response) as LibrariesResponse; + libraries = result.data; + renderLibraries(); + } catch (error) { + (window as any).api.handleError(error, 'Failed to load libraries'); + } +} + +// Render libraries list (replaces SSR content after updates) +function renderLibraries(): void { + const container = document.getElementById('libraries-list'); + if (!container) return; + + if (libraries.length === 0) { + container.innerHTML = '

No libraries yet. Create your first library to get started.

'; + return; + } + + container.innerHTML = libraries.map(library => + '
' + + '
' + + '
' + + `

${escapeHtmlLocal(library.name)}

` + + (library.description ? `

${escapeHtmlLocal(library.description)}

` : '') + + `${escapeHtmlLocal(library.type_name)}` + + '
' + + '
' + + `` + + `` + + `` + + '
' + + '
' + + `` + + '
' + ).join(''); +} + +// Load user's visible libraries for visibility management +async function loadUserVisibility(): Promise { + const select = document.getElementById('user-select') as HTMLSelectElement; + const userId = select?.value; + if (!userId) { + const container = document.getElementById('user-libraries'); + if (container) { + container.innerHTML = '

Please select a user

'; + } + return; + } + + try { + const response = await (window as any).api.get('/libraries/visible'); + const visibleLibraries = await (window as any).api.handleResponse(response) as Library[]; + const container = document.getElementById('user-libraries'); + if (!container) return; + + const visibleIds = new Set(visibleLibraries.map((lib: Library) => lib.id)); + + container.innerHTML = libraries.map(library => { + const isVisible = visibleIds.has(library.id); + return ''; + }).join(''); + } catch (error) { + (window as any).api.handleError(error, 'Failed to load user libraries'); + } +} + +// Set library visibility for a user +async function setLibraryVisibility(userId: string, libraryId: string, isVisible: boolean): Promise { + // userId is used in the HTML onchange handler but the API gets user from JWT context + console.debug('Setting visibility for user:', userId, 'library:', libraryId, 'visible:', isVisible); + + try { + const response = await (window as any).api.post('/libraries/visibility', { + library_id: libraryId, + is_visible: isVisible + }); + await (window as any).api.handleVoidResponse(response); + + if ((window as any).showToast?.success) { + (window as any).showToast.success('Library visibility updated'); + } + + // Refresh visibility controls + void loadUserVisibility(); + } catch (error) { + (window as any).api.handleError(error, 'Failed to update library visibility'); + } +} + +// Create library form handler +async function handleCreateLibrarySubmit(event: Event): Promise { + event.preventDefault(); + + const form = event.target as HTMLFormElement; + const formData = new FormData(form); + + const libraryData = { + name: formData.get('name') as string, + description: formData.get('description') as string, + type: formData.get('type') as string + }; + + try { + const response = await (window as any).api.post('/libraries', libraryData); + await (window as any).api.handleResponse(response) as { data: Library }; + + if ((window as any).showToast?.success) { + (window as any).showToast.success('Library created successfully'); + } + + hideCreateLibraryModal(); + form.reset(); + void reloadLibraries(); + } catch (error) { + (window as any).api.handleError(error, 'Failed to create library'); + } +} + +// Delete library +async function deleteLibrary(libraryId: string): Promise { + const library = libraries.find(l => l.id === libraryId); + if (!library) return; + + if (!confirm(`Delete library "${library.name}"? This will permanently remove all associated media and cannot be undone.`)) { + return; + } + + try { + const response = await (window as any).api.delete(`/libraries/${libraryId}`); + await (window as any).api.handleVoidResponse(response); + + if ((window as any).showToast?.success) { + (window as any).showToast.success('Library deleted successfully'); + } + + await reloadLibraries(); + } catch (error) { + (window as any).api.handleError(error, 'Failed to delete library'); + } +} + +// Show library folders +async function showLibraryFolders(libraryId: string): Promise { + const container = document.getElementById(`library-folders-${libraryId}`); + if (!container) return; + + if (container.classList.contains('hidden')) { + try { + const response = await (window as any).api.get(`/libraries/${libraryId}/folders`); + const folders = await (window as any).api.handleResponse(response) as LibraryFolder[]; + + container.innerHTML = folders.map((folder: LibraryFolder) => + '
' + + `${escapeHtmlLocal(folder.folder_path)}` + + `' + + '
' + ).join(''); + + // Add folder input + container.innerHTML += '
' + + `' + + `' + + '
'; + + container.classList.remove('hidden'); + } catch (error) { + (window as any).api.handleError(error, 'Failed to load folders'); + } + } else { + container.classList.add('hidden'); + } +} + +// Add library folder +async function addLibraryFolder(libraryId: string): Promise { + const input = document.getElementById(`folder-path-${libraryId}`) as HTMLInputElement; + const folderPath = input?.value.trim(); + + if (!folderPath) { + return; + } + + try { + const response = await (window as any).api.post(`/libraries/${libraryId}/folders`, { + folder_path: folderPath + }); + await (window as any).api.handleVoidResponse(response); + + if ((window as any).showToast?.success) { + (window as any).showToast.success('Folder added successfully'); + } + + if (input) { + input.value = ''; + } + void showLibraryFolders(libraryId); // Refresh + } catch (error) { + (window as any).api.handleError(error, 'Failed to add folder'); + } +} + +// Remove library folder +async function removeLibraryFolder(libraryId: string, folderPath: string): Promise { + if (!confirm(`Remove folder "${folderPath}" from the library?`)) { + return; + } + + try { + const response = await (window as any).api.delete(`/libraries/${libraryId}/folders`); + await (window as any).api.handleVoidResponse(response); + + if ((window as any).showToast?.success) { + (window as any).showToast.success('Folder removed successfully'); + } + + void showLibraryFolders(libraryId); // Refresh + } catch (error) { + (window as any).api.handleError(error, 'Failed to remove folder'); + } +} + +// Edit library (placeholder - opens modal or navigates to edit page) +function editLibrary(libraryId: string): void { + console.log('Edit library:', libraryId); + if ((window as any).showToast?.info) { + (window as any).showToast.info('Edit library functionality coming soon'); + } +} + +// Modal controls +function showCreateLibraryModal(): void { + const modal = document.getElementById('create-library-modal') as HTMLElement; + if (modal) { + modal.classList.remove('hidden'); + } +} + +function hideCreateLibraryModal(): void { + const modal = document.getElementById('create-library-modal') as HTMLElement; + if (modal) { + modal.classList.add('hidden'); + } +} + +// Local escape HTML helper +function escapeHtmlLocal(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + +// Event delegation for handling dynamic button clicks +function handleLibraryListClick(event: Event): void { + const target = event.target as HTMLElement; + const button = target.closest('button') as HTMLElement; + if (!button) return; + + const action = button.dataset.action; + const libraryId = button.dataset.libraryId; + + switch (action) { + case 'show-folders': + if (libraryId) showLibraryFolders(libraryId); + break; + case 'delete': + if (libraryId) deleteLibrary(libraryId); + break; + case 'edit': + if (libraryId) editLibrary(libraryId); + break; + case 'add-folder': + if (libraryId) addLibraryFolder(libraryId); + break; + case 'remove-folder': + if (libraryId && button.dataset.folderPath) { + removeLibraryFolder(libraryId, button.dataset.folderPath); + } + break; + } +} + +function handleGlobalClick(event: Event): void { + const target = event.target as HTMLElement; + const button = target.closest('button') as HTMLElement; + if (!button) return; + + const action = button.dataset.action; + + switch (action) { + case 'show-create-modal': + showCreateLibraryModal(); + break; + case 'hide-create-modal': + hideCreateLibraryModal(); + break; + } +} + +// Initialize page +function initializeLibraryAdmin(): void { + // Setup event listeners + const librariesList = document.getElementById('libraries-list'); + if (librariesList) { + librariesList.addEventListener('click', handleLibraryListClick); + } + + document.addEventListener('click', handleGlobalClick); + + // Setup form submission + const createLibraryForm = document.getElementById('create-library-form'); + if (createLibraryForm) { + createLibraryForm.addEventListener('submit', handleCreateLibrarySubmit); + } + + // Initial data is already SSR'd, no need to fetch + // Just store it for updates + const ssrLibraries = document.querySelectorAll('#libraries-list [data-library-id]'); + if (ssrLibraries.length === 0) { + // No libraries SSR'd, which is fine + } +} + +// Export functions for global access +(window as any).deleteLibrary = deleteLibrary; +(window as any).showLibraryFolders = showLibraryFolders; +(window as any).addLibraryFolder = addLibraryFolder; +(window as any).removeLibraryFolder = removeLibraryFolder; +(window as any).setLibraryVisibility = setLibraryVisibility; +(window as any).loadUserVisibility = loadUserVisibility; +(window as any).editLibrary = editLibrary; +(window as any).handleCreateLibrarySubmit = handleCreateLibrarySubmit; + +// Initialize on DOM ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeLibraryAdmin); +} else { + initializeLibraryAdmin(); +}