New Switchable Library Layout
This commit is contained in:
181
frontend/src/components/library/SidebarLayout.tsx
Normal file
181
frontend/src/components/library/SidebarLayout.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Input } from '../ui/Input';
|
||||
import Button from '../ui/Button';
|
||||
import { Story, Tag } from '../../types/api';
|
||||
|
||||
interface SidebarLayoutProps {
|
||||
stories: Story[];
|
||||
tags: Tag[];
|
||||
searchQuery: string;
|
||||
selectedTags: string[];
|
||||
viewMode: 'grid' | 'list';
|
||||
sortOption: string;
|
||||
sortDirection: 'asc' | 'desc';
|
||||
onSearchChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onTagToggle: (tagName: string) => void;
|
||||
onViewModeChange: (mode: 'grid' | 'list') => void;
|
||||
onSortChange: (option: string) => void;
|
||||
onSortDirectionToggle: () => void;
|
||||
onRandomStory: () => void;
|
||||
onClearFilters: () => void;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function SidebarLayout({
|
||||
stories,
|
||||
tags,
|
||||
searchQuery,
|
||||
selectedTags,
|
||||
viewMode,
|
||||
sortOption,
|
||||
sortDirection,
|
||||
onSearchChange,
|
||||
onTagToggle,
|
||||
onViewModeChange,
|
||||
onSortChange,
|
||||
onSortDirectionToggle,
|
||||
onRandomStory,
|
||||
onClearFilters,
|
||||
children
|
||||
}: SidebarLayoutProps) {
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Left Sidebar */}
|
||||
<div className="w-80 bg-white dark:bg-gray-800 p-4 border-r theme-border sticky top-0 h-screen overflow-y-auto max-md:w-full max-md:h-auto max-md:static max-md:border-r-0 max-md:border-b max-md:max-h-96">
|
||||
{/* Random Story Button */}
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
onClick={onRandomStory}
|
||||
variant="primary"
|
||||
className="w-full"
|
||||
>
|
||||
🎲 Random Story
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-6">
|
||||
<h1 className="text-2xl font-bold theme-header">Your Library</h1>
|
||||
<p className="theme-text mt-1">{stories.length} stories total</p>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="mb-6">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search stories..."
|
||||
value={searchQuery}
|
||||
onChange={onSearchChange}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* View Toggle */}
|
||||
<div className="mb-6">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'primary' : 'ghost'}
|
||||
onClick={() => onViewModeChange('grid')}
|
||||
className="flex-1"
|
||||
>
|
||||
⊞ Grid
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'primary' : 'ghost'}
|
||||
onClick={() => onViewModeChange('list')}
|
||||
className="flex-1"
|
||||
>
|
||||
☰ List
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sort Controls */}
|
||||
<div className="mb-6 theme-card p-4 rounded-lg">
|
||||
<h3 className="text-sm font-medium theme-header mb-3">Sort By</h3>
|
||||
<div className="flex gap-2 items-center">
|
||||
<select
|
||||
value={sortOption}
|
||||
onChange={(e) => onSortChange(e.target.value)}
|
||||
className="flex-1 px-3 py-2 border rounded-lg theme-card border-gray-300 dark:border-gray-600"
|
||||
>
|
||||
<option value="lastRead">Last Read</option>
|
||||
<option value="createdAt">Date Added</option>
|
||||
<option value="title">Title</option>
|
||||
<option value="authorName">Author</option>
|
||||
<option value="rating">Rating</option>
|
||||
</select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onSortDirectionToggle}
|
||||
className="px-3 py-2"
|
||||
title={`Toggle sort direction (currently ${sortDirection === 'asc' ? 'ascending' : 'descending'})`}
|
||||
>
|
||||
{sortDirection === 'asc' ? '↑' : '↓'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tag Filters */}
|
||||
<div className="theme-card p-4 rounded-lg">
|
||||
<h3 className="text-sm font-medium theme-header mb-3">Filter by Tags</h3>
|
||||
<div className="mb-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search tags..."
|
||||
className="w-full px-2 py-1 text-xs border rounded theme-card border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-48 overflow-y-auto border theme-border rounded p-2">
|
||||
<div className="space-y-1">
|
||||
<label className="flex items-center gap-2 py-1 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTags.length === 0}
|
||||
onChange={() => onClearFilters()}
|
||||
/>
|
||||
<span className="text-xs">All Stories ({stories.length})</span>
|
||||
</label>
|
||||
{tags.map((tag) => (
|
||||
<label
|
||||
key={tag.id}
|
||||
className="flex items-center gap-2 py-1 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedTags.includes(tag.name)}
|
||||
onChange={() => onTagToggle(tag.name)}
|
||||
/>
|
||||
<span className="text-xs">
|
||||
{tag.name} ({tag.storyCount})
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
{tags.length > 10 && (
|
||||
<div className="text-center text-xs text-gray-500 py-2">
|
||||
... and {tags.length - 10} more tags
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onClearFilters}
|
||||
className="w-full text-xs py-1"
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 p-4 max-md:p-4">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user