Favorites improvements
This commit is contained in:
@@ -8,6 +8,7 @@ import FavoritesMenu from './components/FavoritesMenu';
|
||||
import SearchComponent from './components/SearchComponent';
|
||||
import VersionSelector from './components/VersionSelector';
|
||||
import { getBooks } from './services/api';
|
||||
import { Section } from './types/favorites';
|
||||
|
||||
interface BookData {
|
||||
books: string[];
|
||||
@@ -29,6 +30,7 @@ function App() {
|
||||
const [versionSelected, setVersionSelected] = useState(false); // Track if version has been chosen
|
||||
const [availableVersions, setAvailableVersions] = useState<any[]>([]);
|
||||
const [favorites, setFavorites] = useState<any[]>([]); // Centralized favorites state
|
||||
const [sections, setSections] = useState<Section[]>([]); // Sections for organizing favorites
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -72,8 +74,10 @@ function App() {
|
||||
if (user) {
|
||||
loadUserPreferences();
|
||||
loadFavorites(); // Load favorites once when user logs in
|
||||
loadSections(); // Load sections when user logs in
|
||||
} else {
|
||||
setFavorites([]); // Clear favorites when user logs out
|
||||
setSections([]); // Clear sections when user logs out
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
@@ -278,6 +282,34 @@ function App() {
|
||||
}
|
||||
};
|
||||
|
||||
// Load sections for the user
|
||||
const loadSections = async () => {
|
||||
if (!user) {
|
||||
setSections([]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sections', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSections(data.sections);
|
||||
console.log('Loaded sections:', data.sections.length);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load sections:', error);
|
||||
setSections([]);
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for section changes (create, update, delete)
|
||||
const handleSectionChange = async () => {
|
||||
await loadSections();
|
||||
};
|
||||
|
||||
// Helper functions to filter favorites by type
|
||||
const getBookFavorites = (version: string) => {
|
||||
return new Set(
|
||||
@@ -361,6 +393,8 @@ function App() {
|
||||
onBack={handleBack}
|
||||
onSearchClick={() => setShowSearch(true)}
|
||||
favorites={versionId ? getBookFavorites(versionId) : new Set()}
|
||||
sections={sections}
|
||||
onSectionChange={handleSectionChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -393,6 +427,8 @@ function App() {
|
||||
version={versionId}
|
||||
onSearchClick={() => setShowSearch(true)}
|
||||
favorites={versionId ? getChapterFavorites(actualBookName, versionId) : new Set()}
|
||||
sections={sections}
|
||||
onSectionChange={handleSectionChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -421,6 +457,8 @@ function App() {
|
||||
version={selectedVersion}
|
||||
onSearchClick={() => setShowSearch(true)}
|
||||
favorites={getVerseFavorites(actualBookName, chapterNumber, selectedVersion)}
|
||||
sections={sections}
|
||||
onSectionChange={handleSectionChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -547,6 +585,8 @@ function App() {
|
||||
getBookUrlName={getBookUrlName}
|
||||
setSelectedVersion={setSelectedVersion}
|
||||
onFavoriteChange={handleFavoriteChange}
|
||||
sections={sections}
|
||||
onSectionChange={handleSectionChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { ArrowLeft, BookOpen, ChevronLeft, ChevronRight, Star, Search } from 'lucide-react';
|
||||
import { getChapter, getBook } from '../services/api';
|
||||
import { Section } from '../types/favorites';
|
||||
import SectionPicker from './SectionPicker';
|
||||
|
||||
interface BibleReaderProps {
|
||||
book: string;
|
||||
@@ -12,6 +14,8 @@ interface BibleReaderProps {
|
||||
version?: string;
|
||||
onSearchClick?: () => void;
|
||||
favorites?: Set<string>; // Favorites passed from parent (centralized state)
|
||||
sections?: Section[];
|
||||
onSectionChange?: () => void;
|
||||
}
|
||||
|
||||
const BibleReader: React.FC<BibleReaderProps> = memo(({
|
||||
@@ -23,7 +27,9 @@ const BibleReader: React.FC<BibleReaderProps> = memo(({
|
||||
onFavoriteChange,
|
||||
version = 'esv',
|
||||
onSearchClick,
|
||||
favorites = new Set() // Default to empty set if not provided
|
||||
favorites = new Set(), // Default to empty set if not provided
|
||||
sections = [],
|
||||
onSectionChange
|
||||
}) => {
|
||||
const [content, setContent] = useState<string>('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -33,6 +39,8 @@ const BibleReader: React.FC<BibleReaderProps> = memo(({
|
||||
const saved = localStorage.getItem('fontSize');
|
||||
return (saved as 'small' | 'medium' | 'large') || 'medium';
|
||||
});
|
||||
const [showSectionPicker, setShowSectionPicker] = useState(false);
|
||||
const [pendingVerse, setPendingVerse] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadChapter();
|
||||
@@ -59,18 +67,18 @@ const BibleReader: React.FC<BibleReaderProps> = memo(({
|
||||
}
|
||||
}, [loading, content]);
|
||||
|
||||
const toggleFavorite = async (verseNumber: string) => {
|
||||
const toggleFavorite = async (verseNumber: string, sectionId?: number | null) => {
|
||||
if (!user) return;
|
||||
|
||||
const isFavorited = favorites.has(verseNumber);
|
||||
|
||||
|
||||
try {
|
||||
if (isFavorited) {
|
||||
// Remove favorite
|
||||
const response = await fetch('/api/favorites', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const verseFavorite = data.favorites.find((fav: any) =>
|
||||
@@ -79,7 +87,7 @@ const BibleReader: React.FC<BibleReaderProps> = memo(({
|
||||
fav.verse_start === parseInt(verseNumber) &&
|
||||
fav.version === version
|
||||
);
|
||||
|
||||
|
||||
if (verseFavorite) {
|
||||
const deleteResponse = await fetch(`/api/favorites/${verseFavorite.id}`, {
|
||||
method: 'DELETE',
|
||||
@@ -88,33 +96,26 @@ const BibleReader: React.FC<BibleReaderProps> = memo(({
|
||||
|
||||
if (deleteResponse.ok) {
|
||||
console.log('Removed verse favorite:', verseNumber);
|
||||
onFavoriteChange?.(); // Notify parent to refresh favorites
|
||||
onFavoriteChange?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add favorite
|
||||
const response = await fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
book: book,
|
||||
chapter: chapter,
|
||||
verse_start: parseInt(verseNumber),
|
||||
version: version
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Added verse favorite:', verseNumber);
|
||||
onFavoriteChange?.(); // Notify parent to refresh favorites
|
||||
} else if (response.status === 409) {
|
||||
// 409 means it already exists, which is fine
|
||||
console.log('Verse favorite already exists:', verseNumber);
|
||||
onFavoriteChange?.(); // Notify parent to refresh favorites
|
||||
// Add favorite - check if we need section picker
|
||||
if (sections.length > 0 && sectionId === undefined) {
|
||||
// User has sections, check for default
|
||||
const defaultSection = sections.find(s => s.is_default);
|
||||
if (defaultSection) {
|
||||
// Use default section automatically
|
||||
await addFavoriteWithSection(verseNumber, defaultSection.id);
|
||||
} else {
|
||||
// Show section picker
|
||||
setPendingVerse(verseNumber);
|
||||
setShowSectionPicker(true);
|
||||
}
|
||||
} else {
|
||||
// No sections or section already specified
|
||||
await addFavoriteWithSection(verseNumber, sectionId ?? null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -122,6 +123,43 @@ const BibleReader: React.FC<BibleReaderProps> = memo(({
|
||||
}
|
||||
};
|
||||
|
||||
const addFavoriteWithSection = async (verseNumber: string, sectionId: number | null) => {
|
||||
try {
|
||||
const response = await fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
book: book,
|
||||
chapter: chapter,
|
||||
verse_start: parseInt(verseNumber),
|
||||
version: version,
|
||||
section_id: sectionId
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Added verse favorite:', verseNumber, 'to section:', sectionId);
|
||||
onFavoriteChange?.();
|
||||
} else if (response.status === 409) {
|
||||
console.log('Verse favorite already exists:', verseNumber);
|
||||
onFavoriteChange?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add favorite:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSectionSelect = async (sectionId: number | null) => {
|
||||
if (pendingVerse) {
|
||||
await addFavoriteWithSection(pendingVerse, sectionId);
|
||||
setPendingVerse(null);
|
||||
}
|
||||
setShowSectionPicker(false);
|
||||
};
|
||||
|
||||
const loadChapters = async () => {
|
||||
try {
|
||||
const response = await fetch(`/books/${book}?version=${version}`);
|
||||
@@ -400,6 +438,19 @@ const BibleReader: React.FC<BibleReaderProps> = memo(({
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section Picker Modal */}
|
||||
{showSectionPicker && (
|
||||
<SectionPicker
|
||||
sections={sections}
|
||||
onSelect={handleSectionSelect}
|
||||
onCancel={() => {
|
||||
setShowSectionPicker(false);
|
||||
setPendingVerse(null);
|
||||
}}
|
||||
onSectionCreated={onSectionChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { BookOpen, Star, ChevronLeft, Search } from 'lucide-react';
|
||||
import React, { useState, memo } from 'react';
|
||||
import { BookOpen, Star, Search } from 'lucide-react';
|
||||
import { Section } from '../types/favorites';
|
||||
import SectionPicker from './SectionPicker';
|
||||
|
||||
interface BookSelectorProps {
|
||||
books: string[];
|
||||
@@ -10,7 +12,9 @@ interface BookSelectorProps {
|
||||
version?: string;
|
||||
onBack?: () => void;
|
||||
onSearchClick?: () => void;
|
||||
favorites?: Set<string>; // Favorites passed from parent (centralized state)
|
||||
favorites?: Set<string>;
|
||||
sections?: Section[];
|
||||
onSectionChange?: () => void;
|
||||
}
|
||||
|
||||
const BookSelector: React.FC<BookSelectorProps> = memo(({
|
||||
@@ -22,27 +26,31 @@ const BookSelector: React.FC<BookSelectorProps> = memo(({
|
||||
version = 'esv',
|
||||
onBack,
|
||||
onSearchClick,
|
||||
favorites = new Set() // Default to empty set if not provided
|
||||
favorites = new Set(),
|
||||
sections = [],
|
||||
onSectionChange
|
||||
}) => {
|
||||
const [showSectionPicker, setShowSectionPicker] = useState(false);
|
||||
const [pendingBook, setPendingBook] = useState<string | null>(null);
|
||||
|
||||
const toggleFavorite = async (book: string, event: React.MouseEvent, sectionId?: number | null) => {
|
||||
event.stopPropagation();
|
||||
|
||||
const toggleFavorite = async (book: string, event: React.MouseEvent) => {
|
||||
event.stopPropagation(); // Prevent book selection when clicking star
|
||||
|
||||
if (!user) return;
|
||||
|
||||
const isFavorited = favorites.has(book);
|
||||
|
||||
|
||||
try {
|
||||
if (isFavorited) {
|
||||
// Remove favorite
|
||||
const response = await fetch('/api/favorites', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const bookFavorite = data.favorites.find((fav: any) => fav.book === book && !fav.chapter);
|
||||
|
||||
|
||||
if (bookFavorite) {
|
||||
const deleteResponse = await fetch(`/api/favorites/${bookFavorite.id}`, {
|
||||
method: 'DELETE',
|
||||
@@ -51,29 +59,22 @@ const BookSelector: React.FC<BookSelectorProps> = memo(({
|
||||
|
||||
if (deleteResponse.ok) {
|
||||
console.log('Removed book favorite:', book);
|
||||
onFavoriteChange?.(); // Notify parent to refresh favorites
|
||||
onFavoriteChange?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add favorite - simplified like ChapterSelector
|
||||
const response = await fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
book: book,
|
||||
version: version
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Added book favorite:', book);
|
||||
onFavoriteChange?.(); // Notify parent to refresh favorites
|
||||
// Add favorite - check if we need section picker
|
||||
if (sections.length > 0 && sectionId === undefined) {
|
||||
const defaultSection = sections.find(s => s.is_default);
|
||||
if (defaultSection) {
|
||||
await addFavoriteWithSection(book, defaultSection.id);
|
||||
} else {
|
||||
setPendingBook(book);
|
||||
setShowSectionPicker(true);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to add favorite:', response.status, response.statusText);
|
||||
await addFavoriteWithSection(book, sectionId ?? null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -81,9 +82,43 @@ const BookSelector: React.FC<BookSelectorProps> = memo(({
|
||||
}
|
||||
};
|
||||
|
||||
const addFavoriteWithSection = async (book: string, sectionId: number | null) => {
|
||||
try {
|
||||
const response = await fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
book: book,
|
||||
version: version,
|
||||
section_id: sectionId
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Added book favorite:', book, 'to section:', sectionId);
|
||||
onFavoriteChange?.();
|
||||
} else {
|
||||
console.error('Failed to add favorite:', response.status, response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add favorite:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSectionSelect = async (sectionId: number | null) => {
|
||||
if (pendingBook) {
|
||||
await addFavoriteWithSection(pendingBook, sectionId);
|
||||
setPendingBook(null);
|
||||
}
|
||||
setShowSectionPicker(false);
|
||||
};
|
||||
|
||||
// Group books by testament
|
||||
const oldTestament = books.slice(0, 39); // First 39 books
|
||||
const newTestament = books.slice(39); // Remaining books
|
||||
const oldTestament = books.slice(0, 39);
|
||||
const newTestament = books.slice(39);
|
||||
|
||||
const BookGroup: React.FC<{ title: string; books: string[] }> = ({ title, books }) => (
|
||||
<div className="mb-8">
|
||||
@@ -102,18 +137,17 @@ const BookSelector: React.FC<BookSelectorProps> = memo(({
|
||||
{formatBookName(book)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Star button - only show for authenticated users */}
|
||||
|
||||
{user && (
|
||||
<button
|
||||
onClick={(e) => toggleFavorite(book, e)}
|
||||
className="absolute top-2 right-2 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title={favorites.has(book) ? 'Remove from favorites' : 'Add to favorites'}
|
||||
>
|
||||
<Star
|
||||
<Star
|
||||
className={`h-4 w-4 ${
|
||||
favorites.has(book)
|
||||
? 'text-yellow-500 fill-yellow-500'
|
||||
favorites.has(book)
|
||||
? 'text-yellow-500 fill-yellow-500'
|
||||
: 'text-gray-400 hover:text-yellow-500'
|
||||
} transition-colors`}
|
||||
/>
|
||||
@@ -163,13 +197,26 @@ const BookSelector: React.FC<BookSelectorProps> = memo(({
|
||||
</p>
|
||||
{user && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
Click the ★ to add books to your favorites
|
||||
Click the star to add books to your favorites
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BookGroup title="Old Testament" books={oldTestament} />
|
||||
<BookGroup title="New Testament" books={newTestament} />
|
||||
|
||||
{/* Section Picker Modal */}
|
||||
{showSectionPicker && (
|
||||
<SectionPicker
|
||||
sections={sections}
|
||||
onSelect={handleSectionSelect}
|
||||
onCancel={() => {
|
||||
setShowSectionPicker(false);
|
||||
setPendingBook(null);
|
||||
}}
|
||||
onSectionCreated={onSectionChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect, memo } from 'react';
|
||||
import { ArrowLeft, FileText, Star, ChevronRight, Search } from 'lucide-react';
|
||||
import { getBook } from '../services/api';
|
||||
import { Section } from '../types/favorites';
|
||||
import SectionPicker from './SectionPicker';
|
||||
|
||||
interface ChapterSelectorProps {
|
||||
book: string;
|
||||
@@ -11,7 +13,9 @@ interface ChapterSelectorProps {
|
||||
onFavoriteChange?: () => void;
|
||||
version?: string;
|
||||
onSearchClick?: () => void;
|
||||
favorites?: Set<string>; // Favorites passed from parent (centralized state)
|
||||
favorites?: Set<string>;
|
||||
sections?: Section[];
|
||||
onSectionChange?: () => void;
|
||||
}
|
||||
|
||||
const ChapterSelector: React.FC<ChapterSelectorProps> = memo(({
|
||||
@@ -23,35 +27,39 @@ const ChapterSelector: React.FC<ChapterSelectorProps> = memo(({
|
||||
onFavoriteChange,
|
||||
version = 'esv',
|
||||
onSearchClick,
|
||||
favorites = new Set() // Default to empty set if not provided
|
||||
favorites = new Set(),
|
||||
sections = [],
|
||||
onSectionChange
|
||||
}) => {
|
||||
const [chapters, setChapters] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showSectionPicker, setShowSectionPicker] = useState(false);
|
||||
const [pendingChapter, setPendingChapter] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadChapters();
|
||||
}, [book]);
|
||||
|
||||
const toggleFavorite = async (chapter: string, event: React.MouseEvent) => {
|
||||
event.stopPropagation(); // Prevent chapter selection when clicking star
|
||||
|
||||
const toggleFavorite = async (chapter: string, event: React.MouseEvent, sectionId?: number | null) => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!user) return;
|
||||
|
||||
const isFavorited = favorites.has(chapter);
|
||||
|
||||
|
||||
try {
|
||||
if (isFavorited) {
|
||||
// Remove favorite
|
||||
const response = await fetch('/api/favorites', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const chapterFavorite = data.favorites.find((fav: any) =>
|
||||
const chapterFavorite = data.favorites.find((fav: any) =>
|
||||
fav.book === book && fav.chapter === chapter && !fav.verse_start
|
||||
);
|
||||
|
||||
|
||||
if (chapterFavorite) {
|
||||
await fetch(`/api/favorites/${chapterFavorite.id}`, {
|
||||
method: 'DELETE',
|
||||
@@ -59,27 +67,21 @@ const ChapterSelector: React.FC<ChapterSelectorProps> = memo(({
|
||||
});
|
||||
|
||||
console.log('Removed chapter favorite:', chapter);
|
||||
onFavoriteChange?.(); // Notify parent to refresh favorites
|
||||
onFavoriteChange?.();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Add favorite
|
||||
const response = await fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
book: book,
|
||||
chapter: chapter,
|
||||
version: version
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Added chapter favorite:', chapter);
|
||||
onFavoriteChange?.(); // Notify parent to refresh favorites
|
||||
// Add favorite - check if we need section picker
|
||||
if (sections.length > 0 && sectionId === undefined) {
|
||||
const defaultSection = sections.find(s => s.is_default);
|
||||
if (defaultSection) {
|
||||
await addFavoriteWithSection(chapter, defaultSection.id);
|
||||
} else {
|
||||
setPendingChapter(chapter);
|
||||
setShowSectionPicker(true);
|
||||
}
|
||||
} else {
|
||||
await addFavoriteWithSection(chapter, sectionId ?? null);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -87,14 +89,45 @@ const ChapterSelector: React.FC<ChapterSelectorProps> = memo(({
|
||||
}
|
||||
};
|
||||
|
||||
const addFavoriteWithSection = async (chapter: string, sectionId: number | null) => {
|
||||
try {
|
||||
const response = await fetch('/api/favorites', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({
|
||||
book: book,
|
||||
chapter: chapter,
|
||||
version: version,
|
||||
section_id: sectionId
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Added chapter favorite:', chapter, 'to section:', sectionId);
|
||||
onFavoriteChange?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add favorite:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSectionSelect = async (sectionId: number | null) => {
|
||||
if (pendingChapter) {
|
||||
await addFavoriteWithSection(pendingChapter, sectionId);
|
||||
setPendingChapter(null);
|
||||
}
|
||||
setShowSectionPicker(false);
|
||||
};
|
||||
|
||||
const loadChapters = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await getBook(book, version);
|
||||
|
||||
// The API now returns { chapters: ["1", "2", "3", ...] }
|
||||
if (response.chapters) {
|
||||
// Sort chapters numerically to ensure proper order
|
||||
const sortedChapters = response.chapters.sort((a, b) => parseInt(a, 10) - parseInt(b, 10));
|
||||
setChapters(sortedChapters);
|
||||
} else {
|
||||
@@ -103,7 +136,6 @@ const ChapterSelector: React.FC<ChapterSelectorProps> = memo(({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load chapters:', error);
|
||||
// Don't show fallback chapters - just show an empty list
|
||||
setChapters([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -160,7 +192,7 @@ const ChapterSelector: React.FC<ChapterSelectorProps> = memo(({
|
||||
</p>
|
||||
{user && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
|
||||
Click the ★ to add chapters to your favorites
|
||||
Click the star to add chapters to your favorites
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -178,18 +210,17 @@ const ChapterSelector: React.FC<ChapterSelectorProps> = memo(({
|
||||
{chapter}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Star button - only show for authenticated users */}
|
||||
|
||||
{user && (
|
||||
<button
|
||||
onClick={(e) => toggleFavorite(chapter, e)}
|
||||
className="absolute top-1 right-1 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
||||
title={favorites.has(chapter) ? 'Remove from favorites' : 'Add to favorites'}
|
||||
>
|
||||
<Star
|
||||
<Star
|
||||
className={`h-3 w-3 ${
|
||||
favorites.has(chapter)
|
||||
? 'text-yellow-500 fill-yellow-500'
|
||||
favorites.has(chapter)
|
||||
? 'text-yellow-500 fill-yellow-500'
|
||||
: 'text-gray-400 hover:text-yellow-500'
|
||||
} transition-colors`}
|
||||
/>
|
||||
@@ -205,6 +236,19 @@ const ChapterSelector: React.FC<ChapterSelectorProps> = memo(({
|
||||
{chapters.length} chapters available
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Section Picker Modal */}
|
||||
{showSectionPicker && (
|
||||
<SectionPicker
|
||||
sections={sections}
|
||||
onSelect={handleSectionSelect}
|
||||
onCancel={() => {
|
||||
setShowSectionPicker(false);
|
||||
setPendingChapter(null);
|
||||
}}
|
||||
onSectionCreated={onSectionChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Star, ChevronDown, ChevronUp, X, Book, FileText, Quote } from 'lucide-react';
|
||||
import { Star, ChevronDown, ChevronUp, X, Settings, FolderOpen, ChevronRight } from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
interface Favorite {
|
||||
id: number;
|
||||
book: string;
|
||||
chapter?: string;
|
||||
verse_start?: number;
|
||||
verse_end?: number;
|
||||
version: string;
|
||||
note?: string;
|
||||
created_at: string;
|
||||
}
|
||||
import { Section, Favorite } from '../types/favorites';
|
||||
import SectionsManager from './SectionsManager';
|
||||
|
||||
interface FavoritesMenuProps {
|
||||
user: any;
|
||||
@@ -19,12 +10,25 @@ interface FavoritesMenuProps {
|
||||
getBookUrlName: (bookName: string) => string;
|
||||
onFavoriteChange?: () => void;
|
||||
setSelectedVersion?: (version: string) => void;
|
||||
sections: Section[];
|
||||
onSectionChange: () => void;
|
||||
}
|
||||
|
||||
const FavoritesMenu: React.FC<FavoritesMenuProps> = ({ user, formatBookName, getBookUrlName, onFavoriteChange, setSelectedVersion }) => {
|
||||
const FavoritesMenu: React.FC<FavoritesMenuProps> = ({
|
||||
user,
|
||||
formatBookName,
|
||||
getBookUrlName,
|
||||
onFavoriteChange,
|
||||
setSelectedVersion,
|
||||
sections,
|
||||
onSectionChange
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [favorites, setFavorites] = useState<Favorite[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [showSectionsManager, setShowSectionsManager] = useState(false);
|
||||
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(new Set());
|
||||
const [movingFavorite, setMovingFavorite] = useState<number | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Load favorites when user is available
|
||||
@@ -36,13 +40,13 @@ const FavoritesMenu: React.FC<FavoritesMenuProps> = ({ user, formatBookName, get
|
||||
|
||||
const loadFavorites = async () => {
|
||||
if (!user) return;
|
||||
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await fetch('/api/favorites', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setFavorites(data.favorites || []);
|
||||
@@ -60,29 +64,47 @@ const FavoritesMenu: React.FC<FavoritesMenuProps> = ({ user, formatBookName, get
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
setFavorites(favorites.filter(f => f.id !== favoriteId));
|
||||
onFavoriteChange?.(); // Notify parent to refresh other components
|
||||
onFavoriteChange?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to remove favorite:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const moveFavoriteToSection = async (favoriteId: number, sectionId: number | null) => {
|
||||
try {
|
||||
const response = await fetch(`/api/favorites/${favoriteId}/section`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ section_id: sectionId })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await loadFavorites();
|
||||
onFavoriteChange?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to move favorite:', error);
|
||||
} finally {
|
||||
setMovingFavorite(null);
|
||||
}
|
||||
};
|
||||
|
||||
const navigateToFavorite = (favorite: Favorite) => {
|
||||
const urlBookName = getBookUrlName(favorite.book);
|
||||
const versionPath = favorite.version || 'esv'; // Default to ESV if no version
|
||||
const versionPath = favorite.version || 'esv';
|
||||
|
||||
if (favorite.chapter) {
|
||||
// Navigate to chapter, with verse hash if it's a verse favorite
|
||||
let navUrl = `/version/${versionPath}/book/${urlBookName}/chapter/${favorite.chapter}`;
|
||||
if (favorite.verse_start) {
|
||||
navUrl += `#verse-${favorite.verse_start}`;
|
||||
}
|
||||
navigate(navUrl);
|
||||
} else {
|
||||
// Navigate to book
|
||||
navigate(`/version/${versionPath}/book/${urlBookName}`);
|
||||
}
|
||||
setIsOpen(false);
|
||||
@@ -90,7 +112,7 @@ const FavoritesMenu: React.FC<FavoritesMenuProps> = ({ user, formatBookName, get
|
||||
|
||||
const getFavoriteDisplayText = (favorite: Favorite) => {
|
||||
const bookName = formatBookName(favorite.book);
|
||||
const versionAbbrev = favorite.version.toUpperCase(); // ESV, NKJV, etc.
|
||||
const versionAbbrev = favorite.version.toUpperCase();
|
||||
|
||||
let reference = '';
|
||||
if (favorite.verse_start && favorite.verse_end) {
|
||||
@@ -98,7 +120,7 @@ const FavoritesMenu: React.FC<FavoritesMenuProps> = ({ user, formatBookName, get
|
||||
} else if (favorite.verse_start) {
|
||||
reference = `${bookName} ${favorite.chapter}:${favorite.verse_start}`;
|
||||
} else if (favorite.chapter) {
|
||||
reference = `${bookName} Chapter ${favorite.chapter}`;
|
||||
reference = `${bookName} ${favorite.chapter}`;
|
||||
} else {
|
||||
reference = bookName;
|
||||
}
|
||||
@@ -106,123 +128,262 @@ const FavoritesMenu: React.FC<FavoritesMenuProps> = ({ user, formatBookName, get
|
||||
return `${reference} (${versionAbbrev})`;
|
||||
};
|
||||
|
||||
// Organize favorites by type
|
||||
const organizedFavorites = {
|
||||
books: favorites.filter(f => !f.chapter),
|
||||
chapters: favorites.filter(f => f.chapter && !f.verse_start),
|
||||
verses: favorites.filter(f => f.verse_start)
|
||||
const toggleSectionCollapse = (sectionKey: string) => {
|
||||
setCollapsedSections(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(sectionKey)) {
|
||||
next.delete(sectionKey);
|
||||
} else {
|
||||
next.add(sectionKey);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const renderFavoriteSection = (title: string, items: Favorite[], icon: React.ReactNode) => {
|
||||
if (items.length === 0) return null;
|
||||
// Group favorites by section
|
||||
const groupedFavorites = () => {
|
||||
const groups: { [key: string]: { section: Section | null; favorites: Favorite[] } } = {};
|
||||
|
||||
// Initialize groups for each section
|
||||
sections.forEach(section => {
|
||||
groups[`section-${section.id}`] = { section, favorites: [] };
|
||||
});
|
||||
|
||||
// Add uncategorized group
|
||||
groups['uncategorized'] = { section: null, favorites: [] };
|
||||
|
||||
// Sort favorites into groups
|
||||
favorites.forEach(fav => {
|
||||
if (fav.section_id) {
|
||||
const key = `section-${fav.section_id}`;
|
||||
if (groups[key]) {
|
||||
groups[key].favorites.push(fav);
|
||||
} else {
|
||||
// Section was deleted, put in uncategorized
|
||||
groups['uncategorized'].favorites.push(fav);
|
||||
}
|
||||
} else {
|
||||
groups['uncategorized'].favorites.push(fav);
|
||||
}
|
||||
});
|
||||
|
||||
// Return as array, sorted by section sort_order
|
||||
return Object.entries(groups)
|
||||
.map(([key, value]) => ({ key, ...value }))
|
||||
.sort((a, b) => {
|
||||
if (a.section === null) return 1; // Uncategorized at end
|
||||
if (b.section === null) return -1;
|
||||
return a.section.sort_order - b.section.sort_order;
|
||||
})
|
||||
.filter(group => group.favorites.length > 0 || group.section !== null);
|
||||
};
|
||||
|
||||
const renderFavorite = (favorite: Favorite) => (
|
||||
<div
|
||||
key={favorite.id}
|
||||
className="flex items-center justify-between px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 group"
|
||||
>
|
||||
<button
|
||||
onClick={() => navigateToFavorite(favorite)}
|
||||
className="flex-1 text-left"
|
||||
>
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100 text-sm">
|
||||
{getFavoriteDisplayText(favorite)}
|
||||
</div>
|
||||
{favorite.note && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">
|
||||
{favorite.note}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{sections.length > 0 && (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setMovingFavorite(movingFavorite === favorite.id ? null : favorite.id)}
|
||||
className="p-1 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors"
|
||||
title="Move to section"
|
||||
>
|
||||
<FolderOpen className="h-4 w-4 text-gray-400 hover:text-blue-500" />
|
||||
</button>
|
||||
{movingFavorite === favorite.id && (
|
||||
<div className="absolute right-0 top-full mt-1 z-10 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg min-w-[150px]">
|
||||
<div className="py-1">
|
||||
<button
|
||||
onClick={() => moveFavoriteToSection(favorite.id, null)}
|
||||
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 ${
|
||||
!favorite.section_id ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" />
|
||||
Uncategorized
|
||||
</button>
|
||||
{sections.map(section => (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => moveFavoriteToSection(favorite.id, section.id)}
|
||||
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 ${
|
||||
favorite.section_id === section.id ? 'bg-blue-50 dark:bg-blue-900/20' : ''
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: section.color }}
|
||||
/>
|
||||
{section.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => removeFavorite(favorite.id)}
|
||||
className="p-1 hover:bg-red-100 dark:hover:bg-red-900 rounded transition-colors"
|
||||
title="Remove favorite"
|
||||
>
|
||||
<X className="h-4 w-4 text-red-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSectionGroup = (group: { key: string; section: Section | null; favorites: Favorite[] }) => {
|
||||
const isCollapsed = collapsedSections.has(group.key);
|
||||
const sectionName = group.section?.name || 'Uncategorized';
|
||||
const sectionColor = group.section?.color || '#6B7280';
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
<div className="flex items-center space-x-2 px-3 py-2 bg-gray-50 dark:bg-gray-700 border-b border-gray-200 dark:border-gray-600">
|
||||
{icon}
|
||||
<span className="text-xs font-medium text-gray-600 dark:text-gray-300 uppercase tracking-wide">
|
||||
{title} ({items.length})
|
||||
</span>
|
||||
</div>
|
||||
{items.map((favorite) => (
|
||||
<div key={group.key} className="mb-1">
|
||||
<button
|
||||
onClick={() => toggleSectionCollapse(group.key)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 bg-gray-50 dark:bg-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors"
|
||||
>
|
||||
<ChevronRight
|
||||
className={`h-4 w-4 text-gray-400 transition-transform ${
|
||||
!isCollapsed ? 'rotate-90' : ''
|
||||
}`}
|
||||
/>
|
||||
<div
|
||||
key={favorite.id}
|
||||
className="flex items-center justify-between px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700 group"
|
||||
>
|
||||
<button
|
||||
onClick={() => navigateToFavorite(favorite)}
|
||||
className="flex-1 text-left"
|
||||
>
|
||||
<div className="font-medium text-gray-900 dark:text-gray-100 text-sm">
|
||||
{getFavoriteDisplayText(favorite)}
|
||||
</div>
|
||||
{favorite.note && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">
|
||||
{favorite.note}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => removeFavorite(favorite.id)}
|
||||
className="p-2 hover:bg-red-100 dark:hover:bg-red-900 rounded transition-colors"
|
||||
title="Remove favorite"
|
||||
>
|
||||
<X className="h-4 w-4 text-red-500" />
|
||||
</button>
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: sectionColor }}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 flex-1 text-left">
|
||||
{sectionName}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{group.favorites.length}
|
||||
</span>
|
||||
</button>
|
||||
{!isCollapsed && group.favorites.length > 0 && (
|
||||
<div className="border-l-2 ml-4" style={{ borderColor: sectionColor }}>
|
||||
{group.favorites.map(renderFavorite)}
|
||||
</div>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (!user) {
|
||||
return null; // Don't show favorites menu for non-authenticated users
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Favorites Toggle Button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center space-x-1 px-3 py-1 text-sm bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 rounded hover:bg-yellow-200 dark:hover:bg-yellow-800 transition-colors"
|
||||
>
|
||||
<Star className="h-4 w-4" />
|
||||
<span>Favorites</span>
|
||||
{favorites.length > 0 && (
|
||||
<span className="bg-yellow-600 text-white text-xs rounded-full px-1.5 py-0.5 min-w-[1.25rem] text-center">
|
||||
{favorites.length}
|
||||
</span>
|
||||
)}
|
||||
{isOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||
</button>
|
||||
<>
|
||||
<div className="relative">
|
||||
{/* Favorites Toggle Button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex items-center space-x-1 px-3 py-1 text-sm bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 rounded hover:bg-yellow-200 dark:hover:bg-yellow-800 transition-colors"
|
||||
>
|
||||
<Star className="h-4 w-4" />
|
||||
<span>Favorites</span>
|
||||
{favorites.length > 0 && (
|
||||
<span className="bg-yellow-600 text-white text-xs rounded-full px-1.5 py-0.5 min-w-[1.25rem] text-center">
|
||||
{favorites.length}
|
||||
</span>
|
||||
)}
|
||||
{isOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
||||
</button>
|
||||
|
||||
{/* Favorites Dropdown */}
|
||||
{isOpen && (
|
||||
<div className="absolute top-full right-0 mt-2 w-80 max-w-[calc(100vw-1rem)] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 max-h-96 overflow-hidden">
|
||||
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium text-gray-900 dark:text-gray-100">My Favorites</h3>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
<X className="h-4 w-4 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="p-4 text-center text-gray-500">Loading favorites...</div>
|
||||
) : favorites.length === 0 ? (
|
||||
<div className="p-4 text-center text-gray-500">
|
||||
<Star className="h-8 w-8 mx-auto mb-2 text-gray-300" />
|
||||
<p>No favorites yet</p>
|
||||
<p className="text-sm">Click the ★ next to books, chapters, or verses to add them here</p>
|
||||
{/* Favorites Dropdown */}
|
||||
{isOpen && (
|
||||
<div className="absolute top-full right-0 mt-2 w-80 max-w-[calc(100vw-1rem)] bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 max-h-[70vh] overflow-hidden">
|
||||
<div className="p-3 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-medium text-gray-900 dark:text-gray-100">My Favorites</h3>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setShowSectionsManager(true)}
|
||||
className="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
title="Manage sections"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
<X className="h-4 w-4 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1">
|
||||
{renderFavoriteSection(
|
||||
"Books",
|
||||
organizedFavorites.books,
|
||||
<Book className="h-3 w-3 text-blue-600 dark:text-blue-400" />
|
||||
)}
|
||||
{renderFavoriteSection(
|
||||
"Chapters",
|
||||
organizedFavorites.chapters,
|
||||
<FileText className="h-3 w-3 text-green-600 dark:text-green-400" />
|
||||
)}
|
||||
{renderFavoriteSection(
|
||||
"Verses",
|
||||
organizedFavorites.verses,
|
||||
<Quote className="h-3 w-3 text-purple-600 dark:text-purple-400" />
|
||||
)}
|
||||
{sections.length > 0 && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{sections.length} section{sections.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-h-[50vh] overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="p-4 text-center text-gray-500">Loading favorites...</div>
|
||||
) : favorites.length === 0 && sections.length === 0 ? (
|
||||
<div className="p-4 text-center text-gray-500">
|
||||
<Star className="h-8 w-8 mx-auto mb-2 text-gray-300" />
|
||||
<p>No favorites yet</p>
|
||||
<p className="text-sm mt-1">Click the star next to books, chapters, or verses to add them here</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1">
|
||||
{groupedFavorites().map(renderSectionGroup)}
|
||||
{favorites.length === 0 && sections.length > 0 && (
|
||||
<div className="p-4 text-center text-gray-500">
|
||||
<p className="text-sm">No favorites yet. Add some by clicking the star icons!</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick actions footer */}
|
||||
{sections.length === 0 && favorites.length > 0 && (
|
||||
<div className="p-3 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={() => setShowSectionsManager(true)}
|
||||
className="w-full text-sm text-blue-600 dark:text-blue-400 hover:underline"
|
||||
>
|
||||
Create sections to organize your favorites
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sections Manager Modal */}
|
||||
{showSectionsManager && (
|
||||
<SectionsManager
|
||||
sections={sections}
|
||||
onClose={() => setShowSectionsManager(false)}
|
||||
onSectionChange={() => {
|
||||
onSectionChange();
|
||||
loadFavorites();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
209
frontend/src/components/SectionPicker.tsx
Normal file
209
frontend/src/components/SectionPicker.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, Plus, FolderOpen } from 'lucide-react';
|
||||
import { Section, SECTION_COLORS } from '../types/favorites';
|
||||
|
||||
interface SectionPickerProps {
|
||||
sections: Section[];
|
||||
onSelect: (sectionId: number | null) => void;
|
||||
onCancel: () => void;
|
||||
onSectionCreated?: () => void;
|
||||
}
|
||||
|
||||
const SectionPicker: React.FC<SectionPickerProps> = ({
|
||||
sections,
|
||||
onSelect,
|
||||
onCancel,
|
||||
onSectionCreated
|
||||
}) => {
|
||||
const [selectedSection, setSelectedSection] = useState<number | null>(() => {
|
||||
// Default to the user's default section if one exists
|
||||
const defaultSection = sections.find(s => s.is_default);
|
||||
return defaultSection ? defaultSection.id : null;
|
||||
});
|
||||
const [showNewForm, setShowNewForm] = useState(false);
|
||||
const [newSectionName, setNewSectionName] = useState('');
|
||||
const [newSectionColor, setNewSectionColor] = useState('#3B82F6');
|
||||
const [error, setError] = useState('');
|
||||
const [creating, setCreating] = useState(false);
|
||||
|
||||
const createAndSelect = async () => {
|
||||
if (!newSectionName.trim()) {
|
||||
setError('Section name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
setCreating(true);
|
||||
try {
|
||||
const response = await fetch('/api/sections', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ name: newSectionName.trim(), color: newSectionColor })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
onSectionCreated?.();
|
||||
// Select the newly created section
|
||||
onSelect(data.section.id);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(data.error || 'Failed to create section');
|
||||
setCreating(false);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to create section');
|
||||
setCreating(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-sm w-full">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<FolderOpen className="h-5 w-5 text-blue-500" />
|
||||
<h3 className="font-medium text-gray-900 dark:text-gray-100">Add to Section</h3>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
<X className="h-5 w-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Section options */}
|
||||
<div className="p-4 space-y-2 max-h-64 overflow-y-auto">
|
||||
{/* Uncategorized option */}
|
||||
<label className="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
|
||||
<input
|
||||
type="radio"
|
||||
name="section"
|
||||
checked={selectedSection === null}
|
||||
onChange={() => setSelectedSection(null)}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div className="w-4 h-4 rounded-full bg-gray-300 dark:bg-gray-600" />
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Uncategorized</span>
|
||||
</label>
|
||||
|
||||
{/* User sections */}
|
||||
{sections.map(section => (
|
||||
<label
|
||||
key={section.id}
|
||||
className="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="section"
|
||||
checked={selectedSection === section.id}
|
||||
onChange={() => setSelectedSection(section.id)}
|
||||
className="w-4 h-4 text-blue-600"
|
||||
/>
|
||||
<div
|
||||
className="w-4 h-4 rounded-full"
|
||||
style={{ backgroundColor: section.color }}
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300 flex-1">
|
||||
{section.name}
|
||||
</span>
|
||||
{section.is_default && (
|
||||
<span className="text-xs bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300 px-1.5 py-0.5 rounded">
|
||||
default
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Create new section */}
|
||||
<div className="px-4 pb-4">
|
||||
{showNewForm ? (
|
||||
<div className="space-y-2">
|
||||
{error && (
|
||||
<p className="text-xs text-red-600 dark:text-red-400">{error}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
<select
|
||||
value={newSectionColor}
|
||||
onChange={(e) => setNewSectionColor(e.target.value)}
|
||||
className="w-10 h-8 p-0 border border-gray-300 dark:border-gray-600 rounded cursor-pointer"
|
||||
style={{ backgroundColor: newSectionColor }}
|
||||
>
|
||||
{SECTION_COLORS.map(color => (
|
||||
<option key={color.value} value={color.value} style={{ backgroundColor: color.value }}>
|
||||
{color.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={newSectionName}
|
||||
onChange={(e) => setNewSectionName(e.target.value)}
|
||||
placeholder="New section name..."
|
||||
className="flex-1 px-2 py-1 text-sm bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !creating) createAndSelect();
|
||||
if (e.key === 'Escape') {
|
||||
setShowNewForm(false);
|
||||
setNewSectionName('');
|
||||
setError('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowNewForm(false);
|
||||
setNewSectionName('');
|
||||
setError('');
|
||||
}}
|
||||
className="px-3 py-1 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={createAndSelect}
|
||||
disabled={creating}
|
||||
className="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
{creating ? 'Creating...' : 'Create & Add'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowNewForm(true)}
|
||||
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Create new section</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex justify-end gap-2 p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<button
|
||||
onClick={onCancel}
|
||||
className="px-4 py-2 text-sm text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSelect(selectedSection)}
|
||||
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Add Favorite
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionPicker;
|
||||
379
frontend/src/components/SectionsManager.tsx
Normal file
379
frontend/src/components/SectionsManager.tsx
Normal file
@@ -0,0 +1,379 @@
|
||||
import React, { useState } from 'react';
|
||||
import { X, Plus, Pencil, Trash2, Check, ChevronUp, ChevronDown, Star } from 'lucide-react';
|
||||
import { Section, SECTION_COLORS } from '../types/favorites';
|
||||
|
||||
interface SectionsManagerProps {
|
||||
sections: Section[];
|
||||
onClose: () => void;
|
||||
onSectionChange: () => void;
|
||||
}
|
||||
|
||||
const SectionsManager: React.FC<SectionsManagerProps> = ({
|
||||
sections,
|
||||
onClose,
|
||||
onSectionChange
|
||||
}) => {
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [editName, setEditName] = useState('');
|
||||
const [editColor, setEditColor] = useState('');
|
||||
const [newSectionName, setNewSectionName] = useState('');
|
||||
const [newSectionColor, setNewSectionColor] = useState('#3B82F6');
|
||||
const [showNewForm, setShowNewForm] = useState(false);
|
||||
const [showColorPicker, setShowColorPicker] = useState<number | 'new' | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const createSection = async () => {
|
||||
if (!newSectionName.trim()) {
|
||||
setError('Section name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sections', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ name: newSectionName.trim(), color: newSectionColor })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setNewSectionName('');
|
||||
setNewSectionColor('#3B82F6');
|
||||
setShowNewForm(false);
|
||||
setError('');
|
||||
onSectionChange();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(data.error || 'Failed to create section');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to create section');
|
||||
}
|
||||
};
|
||||
|
||||
const updateSection = async (id: number) => {
|
||||
if (!editName.trim()) {
|
||||
setError('Section name is required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sections/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ name: editName.trim(), color: editColor })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setEditingId(null);
|
||||
setError('');
|
||||
onSectionChange();
|
||||
} else {
|
||||
const data = await response.json();
|
||||
setError(data.error || 'Failed to update section');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to update section');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSection = async (id: number) => {
|
||||
if (!confirm('Delete this section? Favorites in this section will be moved to Uncategorized.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/sections/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
onSectionChange();
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to delete section');
|
||||
}
|
||||
};
|
||||
|
||||
const setDefaultSection = async (id: number) => {
|
||||
try {
|
||||
const response = await fetch(`/api/sections/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ is_default: true })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
onSectionChange();
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to set default section');
|
||||
}
|
||||
};
|
||||
|
||||
const moveSection = async (id: number, direction: 'up' | 'down') => {
|
||||
const currentIndex = sections.findIndex(s => s.id === id);
|
||||
if (
|
||||
(direction === 'up' && currentIndex === 0) ||
|
||||
(direction === 'down' && currentIndex === sections.length - 1)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newSections = [...sections];
|
||||
const swapIndex = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
|
||||
[newSections[currentIndex], newSections[swapIndex]] = [newSections[swapIndex], newSections[currentIndex]];
|
||||
|
||||
const sectionIds = newSections.map(s => s.id);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sections/reorder', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ sectionIds })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
onSectionChange();
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to reorder sections');
|
||||
}
|
||||
};
|
||||
|
||||
const startEditing = (section: Section) => {
|
||||
setEditingId(section.id);
|
||||
setEditName(section.name);
|
||||
setEditColor(section.color);
|
||||
setShowColorPicker(null);
|
||||
};
|
||||
|
||||
const ColorPicker = ({ selectedColor, onSelect, target }: { selectedColor: string; onSelect: (color: string) => void; target: number | 'new' }) => (
|
||||
<div className="absolute z-10 mt-1 p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg">
|
||||
<div className="grid grid-cols-6 gap-1">
|
||||
{SECTION_COLORS.map(color => (
|
||||
<button
|
||||
key={color.value}
|
||||
onClick={() => {
|
||||
onSelect(color.value);
|
||||
setShowColorPicker(null);
|
||||
}}
|
||||
className={`w-6 h-6 rounded-full border-2 ${
|
||||
selectedColor === color.value ? 'border-gray-900 dark:border-white' : 'border-transparent'
|
||||
}`}
|
||||
style={{ backgroundColor: color.value }}
|
||||
title={color.name}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl max-w-md w-full max-h-[80vh] overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Manage Sections</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
<X className="h-5 w-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="mx-4 mt-4 p-2 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-200 text-sm rounded">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sections list */}
|
||||
<div className="p-4 overflow-y-auto max-h-[50vh]">
|
||||
{sections.length === 0 ? (
|
||||
<p className="text-center text-gray-500 dark:text-gray-400 py-4">
|
||||
No sections yet. Create one to organize your favorites!
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{sections.map((section, index) => (
|
||||
<div
|
||||
key={section.id}
|
||||
className="flex items-center gap-2 p-2 bg-gray-50 dark:bg-gray-700 rounded-lg"
|
||||
>
|
||||
{editingId === section.id ? (
|
||||
// Edit mode
|
||||
<>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowColorPicker(showColorPicker === section.id ? null : section.id)}
|
||||
className="w-6 h-6 rounded-full border-2 border-gray-300"
|
||||
style={{ backgroundColor: editColor }}
|
||||
/>
|
||||
{showColorPicker === section.id && (
|
||||
<ColorPicker
|
||||
selectedColor={editColor}
|
||||
onSelect={setEditColor}
|
||||
target={section.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
className="flex-1 px-2 py-1 text-sm bg-white dark:bg-gray-600 border border-gray-300 dark:border-gray-500 rounded"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') updateSection(section.id);
|
||||
if (e.key === 'Escape') setEditingId(null);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={() => updateSection(section.id)}
|
||||
className="p-1 text-green-600 hover:bg-green-100 dark:hover:bg-green-900 rounded"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setEditingId(null)}
|
||||
className="p-1 text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-600 rounded"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
// Display mode
|
||||
<>
|
||||
<div
|
||||
className="w-4 h-4 rounded-full flex-shrink-0"
|
||||
style={{ backgroundColor: section.color }}
|
||||
/>
|
||||
<span className="flex-1 text-sm text-gray-900 dark:text-gray-100">
|
||||
{section.name}
|
||||
</span>
|
||||
{section.is_default && (
|
||||
<span className="text-xs bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300 px-2 py-0.5 rounded">
|
||||
default
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => moveSection(section.id, 'up')}
|
||||
disabled={index === 0}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30"
|
||||
title="Move up"
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => moveSection(section.id, 'down')}
|
||||
disabled={index === sections.length - 1}
|
||||
className="p-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 disabled:opacity-30"
|
||||
title="Move down"
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</button>
|
||||
{!section.is_default && (
|
||||
<button
|
||||
onClick={() => setDefaultSection(section.id)}
|
||||
className="p-1 text-gray-400 hover:text-yellow-500"
|
||||
title="Set as default"
|
||||
>
|
||||
<Star className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => startEditing(section)}
|
||||
className="p-1 text-gray-400 hover:text-blue-500"
|
||||
title="Edit"
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => deleteSection(section.id)}
|
||||
className="p-1 text-gray-400 hover:text-red-500"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add new section */}
|
||||
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||
{showNewForm ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowColorPicker(showColorPicker === 'new' ? null : 'new')}
|
||||
className="w-6 h-6 rounded-full border-2 border-gray-300"
|
||||
style={{ backgroundColor: newSectionColor }}
|
||||
/>
|
||||
{showColorPicker === 'new' && (
|
||||
<ColorPicker
|
||||
selectedColor={newSectionColor}
|
||||
onSelect={setNewSectionColor}
|
||||
target="new"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={newSectionName}
|
||||
onChange={(e) => setNewSectionName(e.target.value)}
|
||||
placeholder="Section name..."
|
||||
className="flex-1 px-2 py-1 text-sm bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded text-gray-900 dark:text-gray-100"
|
||||
autoFocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') createSection();
|
||||
if (e.key === 'Escape') {
|
||||
setShowNewForm(false);
|
||||
setNewSectionName('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={createSection}
|
||||
className="p-1 text-green-600 hover:bg-green-100 dark:hover:bg-green-900 rounded"
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowNewForm(false);
|
||||
setNewSectionName('');
|
||||
}}
|
||||
className="p-1 text-gray-500 hover:bg-gray-200 dark:hover:bg-gray-600 rounded"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowNewForm(true)}
|
||||
className="flex items-center gap-2 w-full px-3 py-2 text-sm text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded-lg transition-colors"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span>Add Section</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SectionsManager;
|
||||
44
frontend/src/types/favorites.ts
Normal file
44
frontend/src/types/favorites.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export interface Section {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
sort_order: number;
|
||||
is_default: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface Favorite {
|
||||
id: number;
|
||||
book: string;
|
||||
chapter?: string;
|
||||
verse_start?: number;
|
||||
verse_end?: number;
|
||||
version: string;
|
||||
note?: string;
|
||||
section_id?: number;
|
||||
section_name?: string;
|
||||
section_color?: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Color palette for sections
|
||||
export const SECTION_COLORS = [
|
||||
{ name: 'Gray', value: '#6B7280' },
|
||||
{ name: 'Red', value: '#EF4444' },
|
||||
{ name: 'Orange', value: '#F97316' },
|
||||
{ name: 'Amber', value: '#F59E0B' },
|
||||
{ name: 'Yellow', value: '#EAB308' },
|
||||
{ name: 'Lime', value: '#84CC16' },
|
||||
{ name: 'Green', value: '#22C55E' },
|
||||
{ name: 'Emerald', value: '#10B981' },
|
||||
{ name: 'Teal', value: '#14B8A6' },
|
||||
{ name: 'Cyan', value: '#06B6D4' },
|
||||
{ name: 'Sky', value: '#0EA5E9' },
|
||||
{ name: 'Blue', value: '#3B82F6' },
|
||||
{ name: 'Indigo', value: '#6366F1' },
|
||||
{ name: 'Violet', value: '#8B5CF6' },
|
||||
{ name: 'Purple', value: '#A855F7' },
|
||||
{ name: 'Fuchsia', value: '#D946EF' },
|
||||
{ name: 'Pink', value: '#EC4899' },
|
||||
{ name: 'Rose', value: '#F43F5E' },
|
||||
];
|
||||
Reference in New Issue
Block a user