layout enhancement. Reading position reset
This commit is contained in:
@@ -237,9 +237,9 @@ export default function MinimalLayout({
|
||||
/>
|
||||
</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 ? (
|
||||
<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}"
|
||||
</div>
|
||||
) : (
|
||||
@@ -251,9 +251,9 @@ export default function MinimalLayout({
|
||||
selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-1' : ''
|
||||
}`}
|
||||
>
|
||||
<TagDisplay
|
||||
tag={{...tag, name: `${tag.name} (${tag.storyCount})`}}
|
||||
size="sm"
|
||||
<TagDisplay
|
||||
tag={{...tag, name: `${tag.name} (${tag.storyCount})`}}
|
||||
size="sm"
|
||||
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'}`}
|
||||
/>
|
||||
|
||||
@@ -62,9 +62,113 @@ export default function SidebarLayout({
|
||||
).length;
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Left Sidebar */}
|
||||
<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="flex min-h-screen max-md:flex-col">
|
||||
{/* Mobile Header - Only shown on mobile */}
|
||||
<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 */}
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
@@ -172,9 +276,9 @@ export default function SidebarLayout({
|
||||
onChange={() => onTagToggle(tag.name)}
|
||||
/>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<TagDisplay
|
||||
tag={tag}
|
||||
size="sm"
|
||||
<TagDisplay
|
||||
tag={tag}
|
||||
size="sm"
|
||||
clickable={false}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
@@ -204,7 +308,7 @@ export default function SidebarLayout({
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
|
||||
|
||||
{/* Advanced Filters Toggle */}
|
||||
{onAdvancedFiltersChange && (
|
||||
<Button
|
||||
@@ -221,7 +325,7 @@ export default function SidebarLayout({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Advanced Filters Section */}
|
||||
{showAdvancedFilters && onAdvancedFiltersChange && (
|
||||
<div className="mt-4 pt-4 border-t theme-border">
|
||||
@@ -236,8 +340,95 @@ export default function SidebarLayout({
|
||||
</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 */}
|
||||
<div className="flex-1 p-4 max-md:p-4">
|
||||
<div className="flex-1 p-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,7 +98,7 @@ export default function ToolbarLayout({
|
||||
</div>
|
||||
|
||||
{/* Sort */}
|
||||
<div>
|
||||
<div className="max-md:order-3">
|
||||
<select
|
||||
value={`${sortOption}_${sortDirection}`}
|
||||
onChange={(e) => {
|
||||
@@ -108,7 +108,7 @@ export default function ToolbarLayout({
|
||||
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_asc">Sort: Last Read ↑</option>
|
||||
@@ -123,27 +123,58 @@ export default function ToolbarLayout({
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* View Toggle & Clear */}
|
||||
<div className="flex gap-2">
|
||||
{/* View Toggle, Advanced Filters & Clear */}
|
||||
<div className="flex gap-2 max-md:order-2">
|
||||
<div className="flex border theme-border rounded-lg overflow-hidden">
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'primary' : 'ghost'}
|
||||
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
|
||||
variant={viewMode === 'list' ? 'primary' : 'ghost'}
|
||||
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>
|
||||
</div>
|
||||
{(searchQuery || selectedTags.length > 0) && (
|
||||
<Button variant="ghost" onClick={onClearFilters}>
|
||||
Clear
|
||||
|
||||
{/* Advanced Filters Button */}
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
@@ -181,20 +212,31 @@ export default function ToolbarLayout({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Filter expand button with counts */}
|
||||
<button
|
||||
onClick={() => setFilterExpanded(!filterExpanded)}
|
||||
className={`px-3 py-1 rounded-full text-xs font-medium border-2 border-dashed transition-colors ${
|
||||
filterExpanded || activeAdvancedFiltersCount > 0 || remainingTagsCount > 0
|
||||
? 'bg-blue-50 dark:bg-blue-900/20 border-blue-500 text-blue-700 dark:text-blue-300'
|
||||
: 'bg-gray-50 dark:bg-gray-800 theme-text border-gray-300 dark:border-gray-600 hover:border-blue-500'
|
||||
}`}
|
||||
>
|
||||
{remainingTagsCount > 0 && `+${remainingTagsCount} tags`}
|
||||
{remainingTagsCount > 0 && activeAdvancedFiltersCount > 0 && ' • '}
|
||||
{activeAdvancedFiltersCount > 0 && `${activeAdvancedFiltersCount} filters`}
|
||||
{remainingTagsCount === 0 && activeAdvancedFiltersCount === 0 && 'More Filters'}
|
||||
</button>
|
||||
{/* More Tags Button */}
|
||||
{remainingTagsCount > 0 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!filterExpanded) {
|
||||
// Panel closed → open and switch to tags tab
|
||||
setFilterExpanded(true);
|
||||
setActiveTab('tags');
|
||||
} else if (activeTab !== 'tags') {
|
||||
// Panel open but wrong tab → just switch to tags tab
|
||||
setActiveTab('tags');
|
||||
} else {
|
||||
// Panel open and on tags tab → close panel
|
||||
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">
|
||||
Showing {stories.length} of {totalElements} stories
|
||||
@@ -207,32 +249,36 @@ export default function ToolbarLayout({
|
||||
{/* Tab Navigation */}
|
||||
<div className="flex gap-1 mb-4">
|
||||
<button
|
||||
onClick={() => setActiveTab('tags')}
|
||||
onClick={() => setActiveTab('advanced')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === 'tags'
|
||||
? 'bg-white dark:bg-gray-700 theme-text shadow-sm'
|
||||
: 'theme-text hover:bg-white/50 dark:hover:bg-gray-700/50'
|
||||
activeTab === 'advanced'
|
||||
? 'bg-blue-500 text-white shadow-sm'
|
||||
: 'theme-text hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
|
||||
}`}
|
||||
>
|
||||
📋 Tags
|
||||
{remainingTagsCount > 0 && (
|
||||
<span className="ml-1 text-xs bg-gray-200 dark:bg-gray-600 px-1 rounded">
|
||||
{remainingTagsCount}
|
||||
⚙️ Advanced Filters
|
||||
{activeAdvancedFiltersCount > 0 && (
|
||||
<span className="ml-1 text-xs bg-white text-blue-500 px-1.5 py-0.5 rounded font-bold">
|
||||
{activeAdvancedFiltersCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('advanced')}
|
||||
onClick={() => setActiveTab('tags')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === 'advanced'
|
||||
? 'bg-white dark:bg-gray-700 theme-text shadow-sm'
|
||||
: 'theme-text hover:bg-white/50 dark:hover:bg-gray-700/50'
|
||||
activeTab === 'tags'
|
||||
? 'bg-blue-500 text-white shadow-sm'
|
||||
: 'theme-text hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-blue-200 dark:border-blue-800'
|
||||
}`}
|
||||
>
|
||||
⚙️ Advanced
|
||||
{activeAdvancedFiltersCount > 0 && (
|
||||
<span className="ml-1 text-xs bg-blue-500 text-white px-1 rounded">
|
||||
{activeAdvancedFiltersCount}
|
||||
📋 More Tags
|
||||
{remainingTagsCount > 0 && (
|
||||
<span className={`ml-1 text-xs px-1.5 py-0.5 rounded font-bold ${
|
||||
activeTab === 'tags'
|
||||
? 'bg-white text-blue-500'
|
||||
: 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}>
|
||||
{remainingTagsCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@@ -255,9 +301,9 @@ export default function ToolbarLayout({
|
||||
</Button>
|
||||
)}
|
||||
</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 ? (
|
||||
<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}"
|
||||
</div>
|
||||
) : (
|
||||
@@ -269,9 +315,9 @@ export default function ToolbarLayout({
|
||||
selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-1' : ''
|
||||
}`}
|
||||
>
|
||||
<TagDisplay
|
||||
tag={{...tag, name: `${tag.name} (${tag.storyCount})`}}
|
||||
size="sm"
|
||||
<TagDisplay
|
||||
tag={{...tag, name: `${tag.name} (${tag.storyCount})`}}
|
||||
size="sm"
|
||||
clickable={true}
|
||||
className={`w-full ${selectedTags.includes(tag.name) ? 'bg-blue-500 text-white border-blue-500' : ''}`}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user