Tag Enhancement + bugfixes

This commit is contained in:
Stefan Hardegger
2025-08-17 17:16:40 +02:00
parent 6b83783381
commit 1a99d9830d
34 changed files with 2996 additions and 97 deletions

View File

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

View File

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

View File

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