layout enhancement. Reading position reset

This commit is contained in:
Stefan Hardegger
2025-09-16 09:34:27 +02:00
parent f92dcc5314
commit c92308c24a
5 changed files with 452 additions and 65 deletions

View File

@@ -23,6 +23,7 @@ export default function EditStoryPage() {
const [story, setStory] = useState<Story | null>(null); const [story, setStory] = useState<Story | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [resetingPosition, setResetingPosition] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({}); const [errors, setErrors] = useState<Record<string, string>>({});
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
@@ -201,6 +202,32 @@ export default function EditStoryPage() {
} }
}; };
const handleResetReadingPosition = async () => {
if (!story || !confirm('Are you sure you want to reset the reading position to the beginning? This will remove your current place in the story.')) {
return;
}
try {
setResetingPosition(true);
await storyApi.updateReadingProgress(storyId, 0);
setStory(prev => prev ? { ...prev, readingPosition: 0 } : null);
// Show success feedback
setErrors({ resetSuccess: 'Reading position reset! The story will start from the beginning next time you read it.' });
// Clear success message after 4 seconds
setTimeout(() => {
setErrors(prev => {
const { resetSuccess, ...rest } = prev;
return rest;
});
}, 4000);
} catch (error) {
console.error('Failed to reset reading position:', error);
setErrors({ submit: 'Failed to reset reading position' });
} finally {
setResetingPosition(false);
}
};
const handleDelete = async () => { const handleDelete = async () => {
if (!story || !confirm('Are you sure you want to delete this story? This action cannot be undone.')) { if (!story || !confirm('Are you sure you want to delete this story? This action cannot be undone.')) {
return; return;
@@ -374,6 +401,38 @@ export default function EditStoryPage() {
placeholder="https://example.com/original-story-url" placeholder="https://example.com/original-story-url"
/> />
{/* Reading Position Reset Section */}
<div className="theme-card p-4 rounded-lg border theme-border">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium theme-header">Reading Position</h3>
<p className="text-sm theme-text mt-1">
{story?.readingPosition && story.readingPosition > 0
? `Currently saved at position ${story.readingPosition.toLocaleString()}`
: 'No reading position saved (story will start from the beginning)'
}
</p>
</div>
<Button
type="button"
variant="ghost"
onClick={handleResetReadingPosition}
loading={resetingPosition}
disabled={saving || !story?.readingPosition || story.readingPosition === 0}
className="text-orange-600 hover:text-orange-700 dark:text-orange-400 dark:hover:text-orange-300"
>
Reset to Beginning
</Button>
</div>
</div>
{/* Success Message */}
{errors.resetSuccess && (
<div className="p-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<p className="text-green-800 dark:text-green-200">{errors.resetSuccess}</p>
</div>
)}
{/* Submit Error */} {/* Submit Error */}
{errors.submit && ( {errors.submit && (
<div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"> <div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">

View File

@@ -24,6 +24,9 @@ export default function StoryReadingPage() {
const [hasScrolledToPosition, setHasScrolledToPosition] = useState(false); const [hasScrolledToPosition, setHasScrolledToPosition] = useState(false);
const [showToc, setShowToc] = useState(false); const [showToc, setShowToc] = useState(false);
const [hasHeadings, setHasHeadings] = useState(false); const [hasHeadings, setHasHeadings] = useState(false);
const [showEndOfStoryPopup, setShowEndOfStoryPopup] = useState(false);
const [hasReachedEnd, setHasReachedEnd] = useState(false);
const [resettingPosition, setResettingPosition] = useState(false);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null); const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
@@ -194,13 +197,41 @@ export default function StoryReadingPage() {
const articleTop = article.offsetTop; const articleTop = article.offsetTop;
const articleHeight = article.scrollHeight; const articleHeight = article.scrollHeight;
const windowHeight = window.innerHeight; const windowHeight = window.innerHeight;
const progress = Math.min(100, Math.max(0, const progress = Math.min(100, Math.max(0,
((scrolled - articleTop + windowHeight) / articleHeight) * 100 ((scrolled - articleTop + windowHeight) / articleHeight) * 100
)); ));
setReadingProgress(progress); setReadingProgress(progress);
// Multi-method end-of-story detection
const documentHeight = document.documentElement.scrollHeight;
const windowBottom = scrolled + windowHeight;
const distanceFromBottom = documentHeight - windowBottom;
// Method 1: Distance from bottom (most reliable)
const nearBottom = distanceFromBottom <= 200;
// Method 2: High progress but only as secondary check
const highProgress = progress >= 98;
// Method 3: Check if story content itself is fully visible
const storyContentElement = contentRef.current;
let storyContentFullyVisible = false;
if (storyContentElement) {
const contentRect = storyContentElement.getBoundingClientRect();
const contentBottom = scrolled + contentRect.bottom;
const documentContentHeight = Math.max(documentHeight - 300, contentBottom); // Account for footer padding
storyContentFullyVisible = windowBottom >= documentContentHeight;
}
// Trigger end detection if user is near bottom AND (has high progress OR story content is fully visible)
if (nearBottom && (highProgress || storyContentFullyVisible) && !hasReachedEnd && hasScrolledToPosition) {
console.log('End of story detected:', { nearBottom, highProgress, storyContentFullyVisible, distanceFromBottom, progress });
setHasReachedEnd(true);
setShowEndOfStoryPopup(true);
}
// Save reading position (debounced) // Save reading position (debounced)
if (hasScrolledToPosition) { // Only save after initial auto-scroll if (hasScrolledToPosition) { // Only save after initial auto-scroll
const characterPosition = getCharacterPositionFromScroll(); const characterPosition = getCharacterPositionFromScroll();
@@ -220,11 +251,11 @@ export default function StoryReadingPage() {
clearTimeout(saveTimeoutRef.current); clearTimeout(saveTimeoutRef.current);
} }
}; };
}, [story, hasScrolledToPosition, getCharacterPositionFromScroll, debouncedSavePosition]); }, [story, hasScrolledToPosition, getCharacterPositionFromScroll, debouncedSavePosition, hasReachedEnd]);
const handleRatingUpdate = async (newRating: number) => { const handleRatingUpdate = async (newRating: number) => {
if (!story) return; if (!story) return;
try { try {
await storyApi.updateRating(story.id, newRating); await storyApi.updateRating(story.id, newRating);
setStory(prev => prev ? { ...prev, rating: newRating } : null); setStory(prev => prev ? { ...prev, rating: newRating } : null);
@@ -233,6 +264,25 @@ export default function StoryReadingPage() {
} }
}; };
const handleResetReadingPosition = async () => {
if (!story) return;
try {
setResettingPosition(true);
await storyApi.updateReadingProgress(story.id, 0);
setStory(prev => prev ? { ...prev, readingPosition: 0 } : null);
setShowEndOfStoryPopup(false);
setHasReachedEnd(false);
// DON'T scroll immediately - let user stay at current position
// The reset will take effect when they next open the story
} catch (error) {
console.error('Failed to reset reading position:', error);
} finally {
setResettingPosition(false);
}
};
const findNextStory = (): Story | null => { const findNextStory = (): Story | null => {
if (!story?.seriesId || seriesStories.length <= 1) return null; if (!story?.seriesId || seriesStories.length <= 1) return null;
@@ -350,6 +400,47 @@ export default function StoryReadingPage() {
</> </>
)} )}
{/* End of Story Popup */}
{showEndOfStoryPopup && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black bg-opacity-50 z-50"
onClick={() => setShowEndOfStoryPopup(false)}
/>
{/* Popup Modal */}
<div className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 max-w-md w-full mx-4">
<div className="theme-card theme-shadow rounded-lg p-6">
<div className="text-center">
<h3 className="text-lg font-semibold theme-header mb-3">
🎉 Story Complete!
</h3>
<p className="theme-text mb-6">
You've reached the end of "{story?.title}". Would you like to reset your reading position so the story starts from the beginning next time you open it?
</p>
<div className="flex gap-3 justify-center">
<Button
variant="ghost"
onClick={() => setShowEndOfStoryPopup(false)}
>
Keep Current Position
</Button>
<Button
variant="primary"
onClick={handleResetReadingPosition}
loading={resettingPosition}
>
Reset for Next Time
</Button>
</div>
</div>
</div>
</div>
</>
)}
{/* Story Content */} {/* Story Content */}
<main className="max-w-4xl mx-auto px-4 py-8"> <main className="max-w-4xl mx-auto px-4 py-8">
<article data-reading-content> <article data-reading-content>

View File

@@ -237,9 +237,9 @@ export default function MinimalLayout({
/> />
</div> </div>
<div className="grid grid-cols-4 gap-2 max-md:grid-cols-2"> <div className="grid grid-cols-4 gap-2 max-md:grid-cols-2 max-sm:grid-cols-1">
{filteredTags.length === 0 && tagSearch ? ( {filteredTags.length === 0 && tagSearch ? (
<div className="col-span-4 text-center text-sm text-gray-500 py-4"> <div className="col-span-4 max-md:col-span-2 max-sm:col-span-1 text-center text-sm text-gray-500 py-4">
No tags match "{tagSearch}" No tags match "{tagSearch}"
</div> </div>
) : ( ) : (
@@ -251,9 +251,9 @@ export default function MinimalLayout({
selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-1' : '' selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-1' : ''
}`} }`}
> >
<TagDisplay <TagDisplay
tag={{...tag, name: `${tag.name} (${tag.storyCount})`}} tag={{...tag, name: `${tag.name} (${tag.storyCount})`}}
size="sm" size="sm"
clickable={true} clickable={true}
className={`w-full text-left ${selectedTags.includes(tag.name) ? 'bg-blue-500 text-white border-blue-500' : 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 hover:border-blue-500'}`} className={`w-full text-left ${selectedTags.includes(tag.name) ? 'bg-blue-500 text-white border-blue-500' : 'bg-white dark:bg-gray-700 border-gray-300 dark:border-gray-600 hover:border-blue-500'}`}
/> />

View File

@@ -62,9 +62,113 @@ export default function SidebarLayout({
).length; ).length;
return ( return (
<div className="flex min-h-screen"> <div className="flex min-h-screen max-md:flex-col">
{/* Left Sidebar */} {/* Mobile Header - Only shown on mobile */}
<div className="w-80 min-w-80 max-w-80 bg-white dark:bg-gray-800 p-4 border-r theme-border sticky top-0 h-screen overflow-y-auto overflow-x-hidden max-md:w-full max-md:min-w-full max-md:max-w-full max-md:h-auto max-md:static max-md:border-r-0 max-md:border-b max-md:max-h-96"> <div className="hidden max-md:block bg-white dark:bg-gray-800 p-4 border-b theme-border">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-xl font-bold theme-header">Your Library</h1>
<p className="theme-text text-sm">{totalElements} stories total</p>
</div>
<Button
onClick={onRandomStory}
variant="primary"
size="sm"
>
🎲 Random
</Button>
</div>
{/* Mobile Search */}
<div className="mb-4">
<Input
type="search"
placeholder="Search stories..."
value={searchQuery}
onChange={onSearchChange}
className="w-full"
/>
</div>
{/* Mobile Controls Row */}
<div className="grid grid-cols-3 gap-2">
{/* View Toggle */}
<div className="flex border theme-border rounded-lg overflow-hidden">
<Button
variant={viewMode === 'grid' ? 'primary' : 'ghost'}
onClick={() => onViewModeChange('grid')}
className="rounded-none border-0 flex-1 px-2 py-1 text-xs"
>
</Button>
<Button
variant={viewMode === 'list' ? 'primary' : 'ghost'}
onClick={() => onViewModeChange('list')}
className="rounded-none border-0 flex-1 px-2 py-1 text-xs"
>
</Button>
</div>
{/* Sort */}
<select
value={`${sortOption}_${sortDirection}`}
onChange={(e) => {
const [option, direction] = e.target.value.split('_');
onSortChange(option);
if (sortDirection !== direction) {
onSortDirectionToggle();
}
}}
className="px-2 py-1 border rounded-lg theme-card border-gray-300 dark:border-gray-600 text-xs"
>
<option value="lastRead_desc">Last Read </option>
<option value="lastRead_asc">Last Read </option>
<option value="createdAt_desc">Date Added </option>
<option value="createdAt_asc">Date Added </option>
<option value="title_asc">Title </option>
<option value="title_desc">Title </option>
<option value="authorName_asc">Author </option>
<option value="authorName_desc">Author </option>
<option value="rating_desc">Rating </option>
<option value="rating_asc">Rating </option>
</select>
{/* Filter Toggle */}
<Button
variant={showAdvancedFilters || selectedTags.length > 0 || activeAdvancedFiltersCount > 0 ? "primary" : "ghost"}
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
className="text-xs px-2 py-1"
>
Filters
{(selectedTags.length + activeAdvancedFiltersCount) > 0 && (
<span className="ml-1 bg-white text-blue-500 px-1 rounded text-xs">
{selectedTags.length + activeAdvancedFiltersCount}
</span>
)}
</Button>
</div>
{/* Mobile Tag Pills - Show selected tags */}
{selectedTags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-3">
{selectedTags.slice(0, 3).map((tagName) => {
const tag = tags.find(t => t.name === tagName);
return tag ? (
<div key={tag.id} onClick={() => onTagToggle(tag.name)} className="cursor-pointer">
<TagDisplay tag={tag} size="sm" clickable={true} className="bg-blue-500 text-white" />
</div>
) : null;
})}
{selectedTags.length > 3 && (
<span className="text-xs text-gray-500 px-2 py-1">+{selectedTags.length - 3} more</span>
)}
</div>
)}
</div>
{/* Left Sidebar - Hidden on mobile by default */}
<div className="w-80 min-w-80 max-w-80 bg-white dark:bg-gray-800 p-4 border-r theme-border sticky top-0 h-screen overflow-y-auto overflow-x-hidden max-md:hidden">
{/* Random Story Button */} {/* Random Story Button */}
<div className="mb-6"> <div className="mb-6">
<Button <Button
@@ -172,9 +276,9 @@ export default function SidebarLayout({
onChange={() => onTagToggle(tag.name)} onChange={() => onTagToggle(tag.name)}
/> />
<div className="flex items-center gap-2 flex-1 min-w-0"> <div className="flex items-center gap-2 flex-1 min-w-0">
<TagDisplay <TagDisplay
tag={tag} tag={tag}
size="sm" size="sm"
clickable={false} clickable={false}
className="flex-shrink-0" className="flex-shrink-0"
/> />
@@ -204,7 +308,7 @@ export default function SidebarLayout({
> >
Clear All Clear All
</Button> </Button>
{/* Advanced Filters Toggle */} {/* Advanced Filters Toggle */}
{onAdvancedFiltersChange && ( {onAdvancedFiltersChange && (
<Button <Button
@@ -221,7 +325,7 @@ export default function SidebarLayout({
</Button> </Button>
)} )}
</div> </div>
{/* Advanced Filters Section */} {/* Advanced Filters Section */}
{showAdvancedFilters && onAdvancedFiltersChange && ( {showAdvancedFilters && onAdvancedFiltersChange && (
<div className="mt-4 pt-4 border-t theme-border"> <div className="mt-4 pt-4 border-t theme-border">
@@ -236,8 +340,95 @@ export default function SidebarLayout({
</div> </div>
</div> </div>
{/* Mobile Filter Panel - Shows when filters expanded */}
{showAdvancedFilters && (
<div className="hidden max-md:block bg-white dark:bg-gray-800 border-b theme-border">
<div className="p-4">
<div className="flex justify-between items-center mb-4">
<h3 className="font-medium theme-header">Filters</h3>
<Button
variant="ghost"
onClick={() => setShowAdvancedFilters(false)}
size="sm"
>
Close
</Button>
</div>
{/* Tag Grid */}
<div className="mb-4">
<h4 className="text-sm font-medium theme-header mb-2">Tags</h4>
<div className="mb-2">
<input
type="text"
placeholder="Search tags..."
value={tagSearch}
onChange={(e) => setTagSearch(e.target.value)}
className="w-full px-2 py-1 text-sm border rounded theme-card border-gray-300 dark:border-gray-600"
/>
</div>
<div className="max-h-32 overflow-y-auto">
<div className="grid grid-cols-2 gap-1">
<button
onClick={() => onClearFilters()}
className={`px-2 py-1 text-xs border rounded text-left ${
selectedTags.length === 0 ? 'bg-blue-500 text-white border-blue-500' : 'theme-card border-gray-300 dark:border-gray-600'
}`}
>
All ({totalElements})
</button>
{filteredTags.slice(0, 19).map((tag) => (
<button
key={tag.id}
onClick={() => onTagToggle(tag.name)}
className={`px-2 py-1 text-xs border rounded text-left truncate ${
selectedTags.includes(tag.name) ? 'bg-blue-500 text-white border-blue-500' : 'theme-card border-gray-300 dark:border-gray-600'
}`}
>
{tag.name} ({tag.storyCount})
</button>
))}
</div>
</div>
</div>
{/* Advanced Filters */}
{onAdvancedFiltersChange && (
<div>
<h4 className="text-sm font-medium theme-header mb-2">Advanced Filters</h4>
<AdvancedFilters
filters={advancedFilters}
onChange={onAdvancedFiltersChange}
onReset={() => onAdvancedFiltersChange({})}
className="space-y-3"
/>
</div>
)}
<div className="flex gap-2 mt-4">
<Button
variant="ghost"
onClick={onClearFilters}
size="sm"
className="flex-1"
>
Clear All
</Button>
<Button
variant="primary"
onClick={() => setShowAdvancedFilters(false)}
size="sm"
className="flex-1"
>
Apply
</Button>
</div>
</div>
</div>
)}
{/* Main Content */} {/* Main Content */}
<div className="flex-1 p-4 max-md:p-4"> <div className="flex-1 p-4">
{children} {children}
</div> </div>
</div> </div>

View File

@@ -98,7 +98,7 @@ export default function ToolbarLayout({
</div> </div>
{/* Sort */} {/* Sort */}
<div> <div className="max-md:order-3">
<select <select
value={`${sortOption}_${sortDirection}`} value={`${sortOption}_${sortDirection}`}
onChange={(e) => { onChange={(e) => {
@@ -108,7 +108,7 @@ export default function ToolbarLayout({
onSortDirectionToggle(); onSortDirectionToggle();
} }
}} }}
className="w-full px-3 py-2 border rounded-lg theme-card border-gray-300 dark:border-gray-600" className="w-full px-3 py-2 border rounded-lg theme-card border-gray-300 dark:border-gray-600 max-md:text-sm"
> >
<option value="lastRead_desc">Sort: Last Read </option> <option value="lastRead_desc">Sort: Last Read </option>
<option value="lastRead_asc">Sort: Last Read </option> <option value="lastRead_asc">Sort: Last Read </option>
@@ -123,27 +123,58 @@ export default function ToolbarLayout({
</select> </select>
</div> </div>
{/* View Toggle & Clear */} {/* View Toggle, Advanced Filters & Clear */}
<div className="flex gap-2"> <div className="flex gap-2 max-md:order-2">
<div className="flex border theme-border rounded-lg overflow-hidden"> <div className="flex border theme-border rounded-lg overflow-hidden">
<Button <Button
variant={viewMode === 'grid' ? 'primary' : 'ghost'} variant={viewMode === 'grid' ? 'primary' : 'ghost'}
onClick={() => onViewModeChange('grid')} onClick={() => onViewModeChange('grid')}
className="rounded-none border-0" className="rounded-none border-0 max-md:px-2 max-md:text-sm"
> >
Grid <span className="max-md:hidden"> Grid</span>
<span className="md:hidden"></span>
</Button> </Button>
<Button <Button
variant={viewMode === 'list' ? 'primary' : 'ghost'} variant={viewMode === 'list' ? 'primary' : 'ghost'}
onClick={() => onViewModeChange('list')} onClick={() => onViewModeChange('list')}
className="rounded-none border-0" className="rounded-none border-0 max-md:px-2 max-md:text-sm"
> >
List <span className="max-md:hidden"> List</span>
<span className="md:hidden"></span>
</Button> </Button>
</div> </div>
{(searchQuery || selectedTags.length > 0) && (
<Button variant="ghost" onClick={onClearFilters}> {/* Advanced Filters Button */}
Clear <Button
variant={filterExpanded && activeTab === 'advanced' ? 'primary' : 'ghost'}
onClick={() => {
if (!filterExpanded) {
// Panel closed → open and switch to advanced tab
setFilterExpanded(true);
setActiveTab('advanced');
} else if (activeTab !== 'advanced') {
// Panel open but wrong tab → just switch to advanced tab
setActiveTab('advanced');
} else {
// Panel open and on advanced tab → close panel
setFilterExpanded(false);
}
}}
className="max-md:text-sm max-md:px-2"
>
<span className="max-md:hidden"> Advanced</span>
<span className="md:hidden"></span>
{activeAdvancedFiltersCount > 0 && (
<span className="ml-1 text-xs bg-blue-500 text-white px-1.5 py-0.5 rounded font-bold max-md:ml-0.5 max-md:px-1">
{activeAdvancedFiltersCount}
</span>
)}
</Button>
{(searchQuery || selectedTags.length > 0 || activeAdvancedFiltersCount > 0) && (
<Button variant="ghost" onClick={onClearFilters} className="max-md:text-sm max-md:px-2">
<span className="max-md:hidden">Clear</span>
<span className="md:hidden"></span>
</Button> </Button>
)} )}
</div> </div>
@@ -181,20 +212,31 @@ export default function ToolbarLayout({
</div> </div>
))} ))}
{/* Filter expand button with counts */} {/* More Tags Button */}
<button {remainingTagsCount > 0 && (
onClick={() => setFilterExpanded(!filterExpanded)} <button
className={`px-3 py-1 rounded-full text-xs font-medium border-2 border-dashed transition-colors ${ onClick={() => {
filterExpanded || activeAdvancedFiltersCount > 0 || remainingTagsCount > 0 if (!filterExpanded) {
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 text-blue-700 dark:text-blue-300' // Panel closed → open and switch to tags tab
: 'bg-gray-50 dark:bg-gray-800 theme-text border-gray-300 dark:border-gray-600 hover:border-blue-500' setFilterExpanded(true);
}`} setActiveTab('tags');
> } else if (activeTab !== 'tags') {
{remainingTagsCount > 0 && `+${remainingTagsCount} tags`} // Panel open but wrong tab → just switch to tags tab
{remainingTagsCount > 0 && activeAdvancedFiltersCount > 0 && ' • '} setActiveTab('tags');
{activeAdvancedFiltersCount > 0 && `${activeAdvancedFiltersCount} filters`} } else {
{remainingTagsCount === 0 && activeAdvancedFiltersCount === 0 && 'More Filters'} // Panel open and on tags tab → close panel
</button> setFilterExpanded(false);
}
}}
className={`px-3 py-1 rounded-full text-xs font-medium border-2 transition-colors ${
filterExpanded && activeTab === 'tags'
? 'bg-blue-100 dark:bg-blue-900/30 border-blue-500 text-blue-700 dark:text-blue-300'
: 'border-dashed bg-gray-50 dark:bg-gray-800 theme-text border-gray-300 dark:border-gray-600 hover:border-blue-500'
}`}
>
+{remainingTagsCount} more tags
</button>
)}
<div className="ml-auto text-sm theme-text"> <div className="ml-auto text-sm theme-text">
Showing {stories.length} of {totalElements} stories Showing {stories.length} of {totalElements} stories
@@ -207,32 +249,36 @@ export default function ToolbarLayout({
{/* Tab Navigation */} {/* Tab Navigation */}
<div className="flex gap-1 mb-4"> <div className="flex gap-1 mb-4">
<button <button
onClick={() => setActiveTab('tags')} onClick={() => setActiveTab('advanced')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'tags' activeTab === 'advanced'
? 'bg-white dark:bg-gray-700 theme-text shadow-sm' ? 'bg-blue-500 text-white shadow-sm'
: 'theme-text hover:bg-white/50 dark:hover:bg-gray-700/50' : 'theme-text hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
}`} }`}
> >
📋 Tags Advanced Filters
{remainingTagsCount > 0 && ( {activeAdvancedFiltersCount > 0 && (
<span className="ml-1 text-xs bg-gray-200 dark:bg-gray-600 px-1 rounded"> <span className="ml-1 text-xs bg-white text-blue-500 px-1.5 py-0.5 rounded font-bold">
{remainingTagsCount} {activeAdvancedFiltersCount}
</span> </span>
)} )}
</button> </button>
<button <button
onClick={() => setActiveTab('advanced')} onClick={() => setActiveTab('tags')}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
activeTab === 'advanced' activeTab === 'tags'
? 'bg-white dark:bg-gray-700 theme-text shadow-sm' ? 'bg-blue-500 text-white shadow-sm'
: 'theme-text hover:bg-white/50 dark:hover:bg-gray-700/50' : 'theme-text hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
}`} }`}
> >
Advanced 📋 More Tags
{activeAdvancedFiltersCount > 0 && ( {remainingTagsCount > 0 && (
<span className="ml-1 text-xs bg-blue-500 text-white px-1 rounded"> <span className={`ml-1 text-xs px-1.5 py-0.5 rounded font-bold ${
{activeAdvancedFiltersCount} activeTab === 'tags'
? 'bg-white text-blue-500'
: 'bg-gray-200 dark:bg-gray-600'
}`}>
{remainingTagsCount}
</span> </span>
)} )}
</button> </button>
@@ -255,9 +301,9 @@ export default function ToolbarLayout({
</Button> </Button>
)} )}
</div> </div>
<div className="grid grid-cols-4 gap-2 max-h-40 overflow-y-auto max-md:grid-cols-2"> <div className="grid grid-cols-4 gap-2 max-h-40 overflow-y-auto max-md:grid-cols-2 max-sm:grid-cols-1">
{filteredRemainingTags.length === 0 && tagSearch ? ( {filteredRemainingTags.length === 0 && tagSearch ? (
<div className="col-span-4 text-center text-sm text-gray-500 py-4"> <div className="col-span-4 max-md:col-span-2 max-sm:col-span-1 text-center text-sm text-gray-500 py-4">
No tags match "{tagSearch}" No tags match "{tagSearch}"
</div> </div>
) : ( ) : (
@@ -269,9 +315,9 @@ export default function ToolbarLayout({
selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-1' : '' selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-1' : ''
}`} }`}
> >
<TagDisplay <TagDisplay
tag={{...tag, name: `${tag.name} (${tag.storyCount})`}} tag={{...tag, name: `${tag.name} (${tag.storyCount})`}}
size="sm" size="sm"
clickable={true} clickable={true}
className={`w-full ${selectedTags.includes(tag.name) ? 'bg-blue-500 text-white border-blue-500' : ''}`} className={`w-full ${selectedTags.includes(tag.name) ? 'bg-blue-500 text-white border-blue-500' : ''}`}
/> />