Library Switching functionality

This commit is contained in:
Stefan Hardegger
2025-08-20 15:10:40 +02:00
parent 5e347f2e2e
commit 6128d61349
24 changed files with 2934 additions and 94 deletions

View File

@@ -0,0 +1,602 @@
'use client';
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Button from '../ui/Button';
import { Input } from '../ui/Input';
import LibrarySwitchLoader from '../ui/LibrarySwitchLoader';
import { useLibrarySwitch } from '../../hooks/useLibrarySwitch';
interface Library {
id: string;
name: string;
description: string;
isActive: boolean;
isInitialized: boolean;
}
export default function LibrarySettings() {
const router = useRouter();
const { state: switchState, switchLibrary, clearError, reset } = useLibrarySwitch();
const [libraries, setLibraries] = useState<Library[]>([]);
const [currentLibrary, setCurrentLibrary] = useState<Library | null>(null);
const [loading, setLoading] = useState(true);
const [switchPassword, setSwitchPassword] = useState('');
const [showSwitchForm, setShowSwitchForm] = useState(false);
const [passwordChangeForm, setPasswordChangeForm] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
const [showPasswordChangeForm, setShowPasswordChangeForm] = useState(false);
const [passwordChangeLoading, setPasswordChangeLoading] = useState(false);
const [passwordChangeMessage, setPasswordChangeMessage] = useState<{type: 'success' | 'error', text: string} | null>(null);
const [createLibraryForm, setCreateLibraryForm] = useState({
name: '',
description: '',
password: '',
confirmPassword: ''
});
const [showCreateLibraryForm, setShowCreateLibraryForm] = useState(false);
const [createLibraryLoading, setCreateLibraryLoading] = useState(false);
const [createLibraryMessage, setCreateLibraryMessage] = useState<{type: 'success' | 'error', text: string} | null>(null);
useEffect(() => {
loadLibraries();
loadCurrentLibrary();
}, []);
const loadLibraries = async () => {
try {
const response = await fetch('/api/libraries');
if (response.ok) {
const data = await response.json();
setLibraries(data);
}
} catch (error) {
console.error('Failed to load libraries:', error);
}
};
const loadCurrentLibrary = async () => {
try {
const response = await fetch('/api/libraries/current');
if (response.ok) {
const data = await response.json();
setCurrentLibrary(data);
}
} catch (error) {
console.error('Failed to load current library:', error);
} finally {
setLoading(false);
}
};
const handleSwitchLibrary = async (e: React.FormEvent) => {
e.preventDefault();
if (!switchPassword.trim()) {
return;
}
const success = await switchLibrary(switchPassword);
if (success) {
// The LibrarySwitchLoader will handle the rest
}
};
const handleSwitchComplete = () => {
// Refresh the page to reload with new library context
router.refresh();
window.location.reload();
};
const handleSwitchError = (error: string) => {
console.error('Library switch error:', error);
reset();
};
const handlePasswordChange = async (e: React.FormEvent) => {
e.preventDefault();
if (passwordChangeForm.newPassword !== passwordChangeForm.confirmPassword) {
setPasswordChangeMessage({type: 'error', text: 'New passwords do not match'});
return;
}
if (passwordChangeForm.newPassword.length < 8) {
setPasswordChangeMessage({type: 'error', text: 'Password must be at least 8 characters long'});
return;
}
setPasswordChangeLoading(true);
setPasswordChangeMessage(null);
try {
const response = await fetch('/api/libraries/password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
currentPassword: passwordChangeForm.currentPassword,
newPassword: passwordChangeForm.newPassword,
}),
});
const data = await response.json();
if (response.ok && data.success) {
setPasswordChangeMessage({type: 'success', text: 'Password changed successfully'});
setPasswordChangeForm({
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
setShowPasswordChangeForm(false);
} else {
setPasswordChangeMessage({type: 'error', text: data.error || 'Failed to change password'});
}
} catch (error) {
setPasswordChangeMessage({type: 'error', text: 'Network error occurred'});
} finally {
setPasswordChangeLoading(false);
}
};
const handleCreateLibrary = async (e: React.FormEvent) => {
e.preventDefault();
if (createLibraryForm.password !== createLibraryForm.confirmPassword) {
setCreateLibraryMessage({type: 'error', text: 'Passwords do not match'});
return;
}
if (createLibraryForm.password.length < 8) {
setCreateLibraryMessage({type: 'error', text: 'Password must be at least 8 characters long'});
return;
}
if (createLibraryForm.name.trim().length < 2) {
setCreateLibraryMessage({type: 'error', text: 'Library name must be at least 2 characters long'});
return;
}
setCreateLibraryLoading(true);
setCreateLibraryMessage(null);
try {
const response = await fetch('/api/libraries/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: createLibraryForm.name.trim(),
description: createLibraryForm.description.trim(),
password: createLibraryForm.password,
}),
});
const data = await response.json();
if (response.ok && data.success) {
setCreateLibraryMessage({
type: 'success',
text: `Library "${data.library.name}" created successfully! You can now log out and log in with the new password to access it.`
});
setCreateLibraryForm({
name: '',
description: '',
password: '',
confirmPassword: ''
});
setShowCreateLibraryForm(false);
loadLibraries(); // Refresh the library list
} else {
setCreateLibraryMessage({type: 'error', text: data.error || 'Failed to create library'});
}
} catch (error) {
setCreateLibraryMessage({type: 'error', text: 'Network error occurred'});
} finally {
setCreateLibraryLoading(false);
}
};
if (loading) {
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
Library Settings
</h2>
<div className="animate-pulse">
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-1/4 mb-2"></div>
<div className="h-4 bg-gray-300 dark:bg-gray-600 rounded w-1/2"></div>
</div>
</div>
);
}
return (
<>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4 text-gray-900 dark:text-white">
Library Settings
</h2>
{/* Current Library Info */}
{currentLibrary && (
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<h3 className="font-medium text-blue-900 dark:text-blue-100 mb-1">
Active Library
</h3>
<p className="text-blue-700 dark:text-blue-300 text-sm">
<strong>{currentLibrary.name}</strong>
</p>
<p className="text-blue-600 dark:text-blue-400 text-xs mt-1">
{currentLibrary.description}
</p>
</div>
)}
{/* Change Password Section */}
<div className="mb-6 border-t pt-4">
<h3 className="font-medium text-gray-900 dark:text-white mb-3">
Change Library Password
</h3>
{passwordChangeMessage && (
<div className={`p-3 rounded-lg mb-4 ${
passwordChangeMessage.type === 'success'
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800'
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
}`}>
<p className={`text-sm ${
passwordChangeMessage.type === 'success'
? 'text-green-700 dark:text-green-300'
: 'text-red-700 dark:text-red-300'
}`}>
{passwordChangeMessage.text}
</p>
</div>
)}
{!showPasswordChangeForm ? (
<div>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
Change the password for the current library ({currentLibrary?.name}).
</p>
<Button
onClick={() => setShowPasswordChangeForm(true)}
variant="secondary"
>
Change Password
</Button>
</div>
) : (
<form onSubmit={handlePasswordChange} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Current Password
</label>
<Input
type="password"
value={passwordChangeForm.currentPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPasswordChangeForm(prev => ({ ...prev, currentPassword: e.target.value }))
}
placeholder="Enter current password"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
New Password
</label>
<Input
type="password"
value={passwordChangeForm.newPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPasswordChangeForm(prev => ({ ...prev, newPassword: e.target.value }))
}
placeholder="Enter new password (min 8 characters)"
required
minLength={8}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirm New Password
</label>
<Input
type="password"
value={passwordChangeForm.confirmPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPasswordChangeForm(prev => ({ ...prev, confirmPassword: e.target.value }))
}
placeholder="Confirm new password"
required
minLength={8}
/>
</div>
<div className="flex space-x-3">
<Button
type="submit"
disabled={passwordChangeLoading}
loading={passwordChangeLoading}
>
{passwordChangeLoading ? 'Changing...' : 'Change Password'}
</Button>
<Button
type="button"
variant="secondary"
onClick={() => {
setShowPasswordChangeForm(false);
setPasswordChangeForm({
currentPassword: '',
newPassword: '',
confirmPassword: ''
});
setPasswordChangeMessage(null);
}}
>
Cancel
</Button>
</div>
</form>
)}
</div>
{/* Available Libraries */}
<div className="mb-6">
<h3 className="font-medium text-gray-900 dark:text-white mb-3">
Available Libraries
</h3>
<div className="space-y-2">
{libraries.map((library) => (
<div
key={library.id}
className={`p-3 rounded-lg border ${
library.isActive
? 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20'
: 'border-gray-200 bg-gray-50 dark:border-gray-700 dark:bg-gray-900/50'
}`}
>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900 dark:text-white">
{library.name}
{library.isActive && (
<span className="ml-2 text-xs px-2 py-1 bg-blue-100 dark:bg-blue-800 text-blue-800 dark:text-blue-200 rounded-full">
Active
</span>
)}
</p>
<p className="text-sm text-gray-600 dark:text-gray-300">
{library.description}
</p>
</div>
{!library.isActive && (
<div className="text-xs text-gray-500 dark:text-gray-400">
ID: {library.id}
</div>
)}
</div>
</div>
))}
</div>
</div>
{/* Switch Library Section */}
<div className="border-t pt-4">
<h3 className="font-medium text-gray-900 dark:text-white mb-3">
Switch Library
</h3>
{!showSwitchForm ? (
<div>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
Enter the password for a different library to switch to it.
</p>
<Button
onClick={() => setShowSwitchForm(true)}
variant="secondary"
>
Switch to Different Library
</Button>
</div>
) : (
<form onSubmit={handleSwitchLibrary} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Library Password
</label>
<Input
type="password"
value={switchPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSwitchPassword(e.target.value)}
placeholder="Enter password for the library you want to access"
required
/>
</div>
{switchState.error && (
<div className="p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p className="text-sm text-red-700 dark:text-red-300">
{switchState.error}
</p>
</div>
)}
<div className="flex space-x-3">
<Button type="submit" disabled={switchState.isLoading}>
{switchState.isLoading ? 'Switching...' : 'Switch Library'}
</Button>
<Button
type="button"
variant="secondary"
onClick={() => {
setShowSwitchForm(false);
setSwitchPassword('');
clearError();
}}
>
Cancel
</Button>
</div>
</form>
)}
</div>
{/* Create New Library Section */}
<div className="border-t pt-4 mb-6">
<h3 className="font-medium text-gray-900 dark:text-white mb-3">
Create New Library
</h3>
{createLibraryMessage && (
<div className={`p-3 rounded-lg mb-4 ${
createLibraryMessage.type === 'success'
? 'bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800'
: 'bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800'
}`}>
<p className={`text-sm ${
createLibraryMessage.type === 'success'
? 'text-green-700 dark:text-green-300'
: 'text-red-700 dark:text-red-300'
}`}>
{createLibraryMessage.text}
</p>
</div>
)}
{!showCreateLibraryForm ? (
<div>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-3">
Create a completely separate library with its own stories, authors, and password.
</p>
<Button
onClick={() => setShowCreateLibraryForm(true)}
variant="secondary"
>
Create New Library
</Button>
</div>
) : (
<form onSubmit={handleCreateLibrary} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Library Name *
</label>
<Input
type="text"
value={createLibraryForm.name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCreateLibraryForm(prev => ({ ...prev, name: e.target.value }))
}
placeholder="e.g., Private Stories, Work Collection"
required
minLength={2}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<Input
type="text"
value={createLibraryForm.description}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCreateLibraryForm(prev => ({ ...prev, description: e.target.value }))
}
placeholder="Optional description for this library"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Password *
</label>
<Input
type="password"
value={createLibraryForm.password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCreateLibraryForm(prev => ({ ...prev, password: e.target.value }))
}
placeholder="Enter password (min 8 characters)"
required
minLength={8}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirm Password *
</label>
<Input
type="password"
value={createLibraryForm.confirmPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setCreateLibraryForm(prev => ({ ...prev, confirmPassword: e.target.value }))
}
placeholder="Confirm password"
required
minLength={8}
/>
</div>
<div className="flex space-x-3">
<Button
type="submit"
disabled={createLibraryLoading}
loading={createLibraryLoading}
>
{createLibraryLoading ? 'Creating...' : 'Create Library'}
</Button>
<Button
type="button"
variant="secondary"
onClick={() => {
setShowCreateLibraryForm(false);
setCreateLibraryForm({
name: '',
description: '',
password: '',
confirmPassword: ''
});
setCreateLibraryMessage(null);
}}
>
Cancel
</Button>
</div>
</form>
)}
</div>
{/* Info Box */}
<div className="mt-6 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg">
<p className="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Note:</strong> Libraries are completely separate datasets. Switching libraries
will reload the application with a different set of stories, authors, and settings.
Each library has its own password for security.
</p>
</div>
</div>
{/* Library Switch Loader */}
<LibrarySwitchLoader
isVisible={switchState.isLoading}
targetLibraryName={switchState.targetLibraryName || undefined}
onComplete={handleSwitchComplete}
onError={handleSwitchError}
/>
</>
);
}