Tag Enhancement + bugfixes
This commit is contained in:
@@ -3,11 +3,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Input } from '../ui/Input';
|
||||
import Button from '../ui/Button';
|
||||
import TagDisplay from '../tags/TagDisplay';
|
||||
import { Story, Tag } from '../../types/api';
|
||||
|
||||
interface MinimalLayoutProps {
|
||||
stories: Story[];
|
||||
tags: Tag[];
|
||||
totalElements: number;
|
||||
searchQuery: string;
|
||||
selectedTags: string[];
|
||||
viewMode: 'grid' | 'list';
|
||||
@@ -26,6 +28,7 @@ interface MinimalLayoutProps {
|
||||
export default function MinimalLayout({
|
||||
stories,
|
||||
tags,
|
||||
totalElements,
|
||||
searchQuery,
|
||||
selectedTags,
|
||||
viewMode,
|
||||
@@ -41,8 +44,14 @@ export default function MinimalLayout({
|
||||
children
|
||||
}: MinimalLayoutProps) {
|
||||
const [tagBrowserOpen, setTagBrowserOpen] = useState(false);
|
||||
const [tagSearch, setTagSearch] = useState('');
|
||||
|
||||
const popularTags = tags.slice(0, 5);
|
||||
|
||||
// Filter tags based on search query
|
||||
const filteredTags = tagSearch
|
||||
? tags.filter(tag => tag.name.toLowerCase().includes(tagSearch.toLowerCase()))
|
||||
: tags;
|
||||
|
||||
const getSortDisplayText = () => {
|
||||
const sortLabels: Record<string, string> = {
|
||||
@@ -62,7 +71,7 @@ export default function MinimalLayout({
|
||||
<div className="text-center mb-10">
|
||||
<h1 className="text-4xl font-light theme-header mb-2">Story Library</h1>
|
||||
<p className="theme-text text-lg mb-8">
|
||||
Your personal collection of {stories.length} stories
|
||||
Your personal collection of {totalElements} stories
|
||||
</p>
|
||||
<div>
|
||||
<Button variant="primary" onClick={onRandomStory}>
|
||||
@@ -139,17 +148,20 @@ export default function MinimalLayout({
|
||||
All
|
||||
</button>
|
||||
{popularTags.map((tag) => (
|
||||
<button
|
||||
<div
|
||||
key={tag.id}
|
||||
onClick={() => onTagToggle(tag.name)}
|
||||
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors border ${
|
||||
selectedTags.includes(tag.name)
|
||||
? 'bg-blue-500 text-white border-blue-500'
|
||||
: 'bg-white dark:bg-gray-800 theme-text border-gray-300 dark:border-gray-600 hover:border-blue-500 hover:text-blue-500'
|
||||
className={`cursor-pointer transition-all hover:scale-105 ${
|
||||
selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-2' : ''
|
||||
}`}
|
||||
>
|
||||
{tag.name}
|
||||
</button>
|
||||
<TagDisplay
|
||||
tag={tag}
|
||||
size="md"
|
||||
clickable={true}
|
||||
className={`${selectedTags.includes(tag.name) ? 'bg-blue-500 text-white border-blue-500' : 'border-gray-300 dark:border-gray-600 hover:border-blue-500'}`}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div>
|
||||
@@ -173,7 +185,10 @@ export default function MinimalLayout({
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<h3 className="text-xl font-semibold theme-header">Browse All Tags</h3>
|
||||
<button
|
||||
onClick={() => setTagBrowserOpen(false)}
|
||||
onClick={() => {
|
||||
setTagBrowserOpen(false);
|
||||
setTagSearch('');
|
||||
}}
|
||||
className="text-2xl theme-text hover:theme-accent transition-colors"
|
||||
>
|
||||
✕
|
||||
@@ -184,31 +199,48 @@ export default function MinimalLayout({
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search tags..."
|
||||
value={tagSearch}
|
||||
onChange={(e) => setTagSearch(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-2 max-md:grid-cols-2">
|
||||
{tags.map((tag) => (
|
||||
<button
|
||||
{filteredTags.length === 0 && tagSearch ? (
|
||||
<div className="col-span-4 text-center text-sm text-gray-500 py-4">
|
||||
No tags match "{tagSearch}"
|
||||
</div>
|
||||
) : (
|
||||
filteredTags.map((tag) => (
|
||||
<div
|
||||
key={tag.id}
|
||||
onClick={() => onTagToggle(tag.name)}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors border text-left ${
|
||||
selectedTags.includes(tag.name)
|
||||
? 'bg-blue-500 text-white border-blue-500'
|
||||
: 'bg-white dark:bg-gray-700 theme-text border-gray-300 dark:border-gray-600 hover:border-blue-500 hover:text-blue-500'
|
||||
className={`cursor-pointer transition-all hover:scale-105 ${
|
||||
selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-1' : ''
|
||||
}`}
|
||||
>
|
||||
{tag.name} ({tag.storyCount})
|
||||
</button>
|
||||
))}
|
||||
<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'}`}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<Button variant="ghost" onClick={() => setTagSearch('')}>
|
||||
Clear Search
|
||||
</Button>
|
||||
<Button variant="ghost" onClick={onClearFilters}>
|
||||
Clear All
|
||||
</Button>
|
||||
<Button variant="primary" onClick={() => setTagBrowserOpen(false)}>
|
||||
<Button variant="primary" onClick={() => {
|
||||
setTagBrowserOpen(false);
|
||||
setTagSearch('');
|
||||
}}>
|
||||
Apply Filters
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Input } from '../ui/Input';
|
||||
import Button from '../ui/Button';
|
||||
import TagDisplay from '../tags/TagDisplay';
|
||||
import { Story, Tag } from '../../types/api';
|
||||
|
||||
interface SidebarLayoutProps {
|
||||
stories: Story[];
|
||||
tags: Tag[];
|
||||
totalElements: number;
|
||||
searchQuery: string;
|
||||
selectedTags: string[];
|
||||
viewMode: 'grid' | 'list';
|
||||
@@ -26,6 +28,7 @@ interface SidebarLayoutProps {
|
||||
export default function SidebarLayout({
|
||||
stories,
|
||||
tags,
|
||||
totalElements,
|
||||
searchQuery,
|
||||
selectedTags,
|
||||
viewMode,
|
||||
@@ -40,6 +43,13 @@ export default function SidebarLayout({
|
||||
onClearFilters,
|
||||
children
|
||||
}: SidebarLayoutProps) {
|
||||
const [tagSearch, setTagSearch] = useState('');
|
||||
|
||||
// Filter tags based on search query
|
||||
const filteredTags = tags.filter(tag =>
|
||||
tag.name.toLowerCase().includes(tagSearch.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
{/* Left Sidebar */}
|
||||
@@ -58,7 +68,7 @@ export default function SidebarLayout({
|
||||
{/* 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>
|
||||
<p className="theme-text mt-1">{totalElements} stories total</p>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
@@ -125,6 +135,8 @@ export default function SidebarLayout({
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search tags..."
|
||||
value={tagSearch}
|
||||
onChange={(e) => setTagSearch(e.target.value)}
|
||||
className="w-full px-2 py-1 text-xs border rounded theme-card border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
@@ -136,9 +148,9 @@ export default function SidebarLayout({
|
||||
checked={selectedTags.length === 0}
|
||||
onChange={() => onClearFilters()}
|
||||
/>
|
||||
<span className="text-xs">All Stories ({stories.length})</span>
|
||||
<span className="text-xs">All Stories ({totalElements})</span>
|
||||
</label>
|
||||
{tags.map((tag) => (
|
||||
{filteredTags.map((tag) => (
|
||||
<label
|
||||
key={tag.id}
|
||||
className="flex items-center gap-2 py-1 cursor-pointer"
|
||||
@@ -148,14 +160,27 @@ export default function SidebarLayout({
|
||||
checked={selectedTags.includes(tag.name)}
|
||||
onChange={() => onTagToggle(tag.name)}
|
||||
/>
|
||||
<span className="text-xs">
|
||||
{tag.name} ({tag.storyCount})
|
||||
</span>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<TagDisplay
|
||||
tag={tag}
|
||||
size="sm"
|
||||
clickable={false}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400 flex-shrink-0">
|
||||
({tag.storyCount})
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
{tags.length > 10 && (
|
||||
{filteredTags.length === 0 && tagSearch && (
|
||||
<div className="text-center text-xs text-gray-500 py-2">
|
||||
... and {tags.length - 10} more tags
|
||||
No tags match "{tagSearch}"
|
||||
</div>
|
||||
)}
|
||||
{filteredTags.length > 10 && !tagSearch && (
|
||||
<div className="text-center text-xs text-gray-500 py-2">
|
||||
... and {filteredTags.length - 10} more tags
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Input } from '../ui/Input';
|
||||
import Button from '../ui/Button';
|
||||
import TagDisplay from '../tags/TagDisplay';
|
||||
import { Story, Tag } from '../../types/api';
|
||||
|
||||
interface ToolbarLayoutProps {
|
||||
stories: Story[];
|
||||
tags: Tag[];
|
||||
totalElements: number;
|
||||
searchQuery: string;
|
||||
selectedTags: string[];
|
||||
viewMode: 'grid' | 'list';
|
||||
@@ -26,6 +28,7 @@ interface ToolbarLayoutProps {
|
||||
export default function ToolbarLayout({
|
||||
stories,
|
||||
tags,
|
||||
totalElements,
|
||||
searchQuery,
|
||||
selectedTags,
|
||||
viewMode,
|
||||
@@ -41,9 +44,17 @@ export default function ToolbarLayout({
|
||||
children
|
||||
}: ToolbarLayoutProps) {
|
||||
const [tagSearchExpanded, setTagSearchExpanded] = useState(false);
|
||||
const [tagSearch, setTagSearch] = useState('');
|
||||
|
||||
const popularTags = tags.slice(0, 6);
|
||||
const remainingTagsCount = Math.max(0, tags.length - 6);
|
||||
|
||||
// Filter remaining tags based on search query
|
||||
const remainingTags = tags.slice(6);
|
||||
const filteredRemainingTags = tagSearch
|
||||
? remainingTags.filter(tag => tag.name.toLowerCase().includes(tagSearch.toLowerCase()))
|
||||
: remainingTags;
|
||||
|
||||
const remainingTagsCount = Math.max(0, remainingTags.length);
|
||||
|
||||
return (
|
||||
<div className="max-w-7xl mx-auto p-6 max-md:p-4">
|
||||
@@ -53,7 +64,7 @@ export default function ToolbarLayout({
|
||||
<div className="flex justify-between items-start mb-6 max-md:flex-col max-md:gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold theme-header">Your Story Library</h1>
|
||||
<p className="theme-text mt-1">{stories.length} stories in your collection</p>
|
||||
<p className="theme-text mt-1">{totalElements} stories in your collection</p>
|
||||
</div>
|
||||
<div className="max-md:self-end">
|
||||
<Button variant="secondary" onClick={onRandomStory}>
|
||||
@@ -142,17 +153,20 @@ export default function ToolbarLayout({
|
||||
All Stories
|
||||
</button>
|
||||
{popularTags.map((tag) => (
|
||||
<button
|
||||
<div
|
||||
key={tag.id}
|
||||
onClick={() => onTagToggle(tag.name)}
|
||||
className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${
|
||||
selectedTags.includes(tag.name)
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-100 dark:bg-gray-700 theme-text hover:bg-blue-100 dark:hover:bg-blue-900'
|
||||
className={`cursor-pointer transition-all hover:scale-105 ${
|
||||
selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-1' : ''
|
||||
}`}
|
||||
>
|
||||
{tag.name} ({tag.storyCount})
|
||||
</button>
|
||||
<TagDisplay
|
||||
tag={{...tag, name: `${tag.name} (${tag.storyCount})`}}
|
||||
size="sm"
|
||||
clickable={true}
|
||||
className={selectedTags.includes(tag.name) ? 'bg-blue-500 text-white border-blue-500' : ''}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{remainingTagsCount > 0 && (
|
||||
<button
|
||||
@@ -163,7 +177,7 @@ export default function ToolbarLayout({
|
||||
</button>
|
||||
)}
|
||||
<div className="ml-auto text-sm theme-text">
|
||||
Showing {stories.length} stories
|
||||
Showing {stories.length} of {totalElements} stories
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -174,9 +188,15 @@ export default function ToolbarLayout({
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search from all available tags..."
|
||||
value={tagSearch}
|
||||
onChange={(e) => setTagSearch(e.target.value)}
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button variant="secondary">Search</Button>
|
||||
{tagSearch && (
|
||||
<Button variant="ghost" onClick={() => setTagSearch('')}>
|
||||
Clear
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => setTagSearchExpanded(false)}
|
||||
@@ -185,19 +205,28 @@ 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">
|
||||
{tags.slice(6).map((tag) => (
|
||||
<button
|
||||
{filteredRemainingTags.length === 0 && tagSearch ? (
|
||||
<div className="col-span-4 text-center text-sm text-gray-500 py-4">
|
||||
No tags match "{tagSearch}"
|
||||
</div>
|
||||
) : (
|
||||
filteredRemainingTags.map((tag) => (
|
||||
<div
|
||||
key={tag.id}
|
||||
onClick={() => onTagToggle(tag.name)}
|
||||
className={`px-2 py-1 rounded text-xs font-medium transition-colors ${
|
||||
selectedTags.includes(tag.name)
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-white dark:bg-gray-700 theme-text hover:bg-blue-100 dark:hover:bg-blue-900'
|
||||
className={`cursor-pointer transition-all hover:scale-105 ${
|
||||
selectedTags.includes(tag.name) ? 'ring-2 ring-blue-500 ring-offset-1' : ''
|
||||
}`}
|
||||
>
|
||||
{tag.name} ({tag.storyCount})
|
||||
</button>
|
||||
))}
|
||||
<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' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user