Library Switching functionality
This commit is contained in:
602
frontend/src/components/library/LibrarySettings.tsx
Normal file
602
frontend/src/components/library/LibrarySettings.tsx
Normal 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user