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

@@ -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>