feat: Implement sidebar layout for filters

This commit is contained in:
gpt-engineer-app[bot]
2025-10-16 15:48:18 +00:00
parent b95b06a7f2
commit e881778659
2 changed files with 174 additions and 136 deletions

View File

@@ -18,6 +18,7 @@ interface RideCreditFiltersProps {
activeFilterCount: number; activeFilterCount: number;
resultCount: number; resultCount: number;
totalCount: number; totalCount: number;
compact?: boolean;
} }
export function RideCreditFilters({ export function RideCreditFilters({
@@ -28,7 +29,11 @@ export function RideCreditFilters({
activeFilterCount, activeFilterCount,
resultCount, resultCount,
totalCount, totalCount,
compact = false,
}: RideCreditFiltersProps) { }: RideCreditFiltersProps) {
const spacingClass = compact ? 'space-y-3' : 'space-y-4';
const sectionSpacing = compact ? 'space-y-2' : 'space-y-3';
const paddingClass = compact ? 'p-4' : 'pt-6';
// Extract unique values from credits for filter options // Extract unique values from credits for filter options
const filterOptions = useMemo(() => { const filterOptions = useMemo(() => {
const countries = new Set<string>(); const countries = new Set<string>();
@@ -99,9 +104,10 @@ export function RideCreditFilters({
}; };
return ( return (
<div className="space-y-4"> <div className={compact ? 'p-4' : ''}>
{/* Search Bar - Always Visible */} <div className={spacingClass}>
<div className="relative"> {/* Search Bar - Always Visible */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" /> <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input <Input
placeholder="Search rides, parks, manufacturers..." placeholder="Search rides, parks, manufacturers..."
@@ -133,10 +139,10 @@ export function RideCreditFilters({
{cat.label} {cat.label}
</Badge> </Badge>
))} ))}
</div> </div>
{/* Collapsible Filter Sections */} {/* Collapsible Filter Sections */}
<div className="space-y-2"> <div className={sectionSpacing}>
{/* Geographic Filters */} {/* Geographic Filters */}
<Collapsible> <Collapsible>
<CollapsibleTrigger className="flex items-center justify-between w-full p-3 rounded-lg bg-muted/50 hover:bg-muted transition-colors"> <CollapsibleTrigger className="flex items-center justify-between w-full p-3 rounded-lg bg-muted/50 hover:bg-muted transition-colors">
@@ -356,11 +362,11 @@ export function RideCreditFilters({
</div> </div>
</CollapsibleContent> </CollapsibleContent>
</Collapsible> </Collapsible>
</div> </div>
{/* Active Filters Display */} {/* Active Filters Display */}
{activeFilterCount > 0 && ( {activeFilterCount > 0 && (
<div className="space-y-2"> <div className={sectionSpacing}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">
Showing <strong>{resultCount}</strong> of <strong>{totalCount}</strong> credits Showing <strong>{resultCount}</strong> of <strong>{totalCount}</strong> credits
@@ -410,10 +416,11 @@ export function RideCreditFilters({
Has inversions Has inversions
<X className="w-3 h-3 cursor-pointer" onClick={() => removeFilter('hasInversions')} /> <X className="w-3 h-3 cursor-pointer" onClick={() => removeFilter('hasInversions')} />
</Badge> </Badge>
)} )}
</div>
</div> </div>
</div> )}
)} </div>
</div> </div>
); );
} }

View File

@@ -12,6 +12,7 @@ import { SortableRideCreditCard } from './SortableRideCreditCard';
import { RideCreditFilters } from './RideCreditFilters'; import { RideCreditFilters } from './RideCreditFilters';
import { UserRideCredit } from '@/types/database'; import { UserRideCredit } from '@/types/database';
import { useRideCreditFilters } from '@/hooks/useRideCreditFilters'; import { useRideCreditFilters } from '@/hooks/useRideCreditFilters';
import { useIsMobile } from '@/hooks/use-mobile';
import { import {
DndContext, DndContext,
DragEndEvent, DragEndEvent,
@@ -37,6 +38,7 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
const [sortBy, setSortBy] = useState<'date' | 'count' | 'name' | 'custom'>('custom'); const [sortBy, setSortBy] = useState<'date' | 'count' | 'name' | 'custom'>('custom');
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
const [isEditMode, setIsEditMode] = useState(false); const [isEditMode, setIsEditMode] = useState(false);
const isMobile = useIsMobile();
// Use the filter hook // Use the filter hook
const { const {
@@ -328,146 +330,175 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
</Card> </Card>
</div> </div>
{/* Filters */} {/* Main Content Area with Sidebar */}
<Card> <div className="flex gap-6 items-start">
<CardContent className="pt-6"> {/* Left Sidebar - Desktop Only */}
<RideCreditFilters {!isMobile && (
filters={filters} <aside className="w-80 flex-shrink-0">
onFilterChange={updateFilter} <div className="sticky top-4">
onClearFilters={clearFilters} <div className="rounded-lg border bg-card">
credits={credits} <RideCreditFilters
activeFilterCount={activeFilterCount} filters={filters}
resultCount={displayCredits.length} onFilterChange={updateFilter}
totalCount={credits.length} onClearFilters={clearFilters}
/> credits={credits}
</CardContent> activeFilterCount={activeFilterCount}
</Card> resultCount={displayCredits.length}
totalCount={credits.length}
{/* Controls */} compact={true}
<div className="flex flex-wrap gap-4 items-center justify-between"> />
<div className="flex gap-2"> </div>
<Button onClick={() => setIsAddDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Credit
</Button>
<Button
variant={isEditMode ? 'default' : 'outline'}
onClick={() => setIsEditMode(!isEditMode)}
>
<GripVertical className="w-4 h-4 mr-2" />
{isEditMode ? 'Done Editing' : 'Edit Order'}
</Button>
</div>
<div className="flex gap-2">
<Select value={sortBy} onValueChange={(value) => setSortBy(value as typeof sortBy)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="custom">Custom Order</SelectItem>
<SelectItem value="date">Most Recent</SelectItem>
<SelectItem value="count">Most Ridden</SelectItem>
<SelectItem value="name">Ride Name</SelectItem>
</SelectContent>
</Select>
<div className="flex border rounded-md">
<Button
variant={viewMode === 'grid' ? 'default' : 'ghost'}
size="icon"
onClick={() => setViewMode('grid')}
>
<LayoutGrid className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'ghost'}
size="icon"
onClick={() => setViewMode('list')}
>
<List className="w-4 h-4" />
</Button>
</div>
</div>
</div>
{/* Credits Display */}
{displayCredits.length === 0 ? (
<Card>
<CardContent className="py-12">
<div className="text-center">
{credits.length === 0 ? (
<>
<h3 className="text-xl font-semibold mb-2">No Ride Credits Yet</h3>
<p className="text-muted-foreground mb-4">
Start tracking your ride experiences
</p>
<Button onClick={() => setIsAddDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Your First Credit
</Button>
</>
) : (
<>
<h3 className="text-xl font-semibold mb-2">No Results Found</h3>
<p className="text-muted-foreground mb-4">
Try adjusting your filters
</p>
<Button onClick={clearFilters} variant="outline">
Clear Filters
</Button>
</>
)}
</div> </div>
</CardContent> </aside>
</Card> )}
) : isEditMode ? (
<DndContext {/* Right Content Area */}
sensors={sensors} <div className="flex-1 min-w-0 space-y-4">
collisionDetection={closestCenter} {/* Mobile Filters - Collapsible Card */}
onDragEnd={handleDragEnd} {isMobile && (
> <Card>
<SortableContext <CardContent className="pt-6">
items={displayCredits.map(c => c.id)} <RideCreditFilters
strategy={verticalListSortingStrategy} filters={filters}
onFilterChange={updateFilter}
onClearFilters={clearFilters}
credits={credits}
activeFilterCount={activeFilterCount}
resultCount={displayCredits.length}
totalCount={credits.length}
compact={false}
/>
</CardContent>
</Card>
)}
{/* Controls */}
<div className="flex flex-wrap gap-4 items-center justify-between">
<div className="flex gap-2">
<Button onClick={() => setIsAddDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Credit
</Button>
<Button
variant={isEditMode ? 'default' : 'outline'}
onClick={() => setIsEditMode(!isEditMode)}
>
<GripVertical className="w-4 h-4 mr-2" />
{isEditMode ? 'Done Editing' : 'Edit Order'}
</Button>
</div>
<div className="flex gap-2">
<Select value={sortBy} onValueChange={(value) => setSortBy(value as typeof sortBy)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Sort by" />
</SelectTrigger>
<SelectContent>
<SelectItem value="custom">Custom Order</SelectItem>
<SelectItem value="date">Most Recent</SelectItem>
<SelectItem value="count">Most Ridden</SelectItem>
<SelectItem value="name">Ride Name</SelectItem>
</SelectContent>
</Select>
<div className="flex border rounded-md">
<Button
variant={viewMode === 'grid' ? 'default' : 'ghost'}
size="icon"
onClick={() => setViewMode('grid')}
>
<LayoutGrid className="w-4 h-4" />
</Button>
<Button
variant={viewMode === 'list' ? 'default' : 'ghost'}
size="icon"
onClick={() => setViewMode('list')}
>
<List className="w-4 h-4" />
</Button>
</div>
</div>
</div>
{/* Credits Display */}
{displayCredits.length === 0 ? (
<Card>
<CardContent className="py-12">
<div className="text-center">
{credits.length === 0 ? (
<>
<h3 className="text-xl font-semibold mb-2">No Ride Credits Yet</h3>
<p className="text-muted-foreground mb-4">
Start tracking your ride experiences
</p>
<Button onClick={() => setIsAddDialogOpen(true)}>
<Plus className="w-4 h-4 mr-2" />
Add Your First Credit
</Button>
</>
) : (
<>
<h3 className="text-xl font-semibold mb-2">No Results Found</h3>
<p className="text-muted-foreground mb-4">
Try adjusting your filters
</p>
<Button onClick={clearFilters} variant="outline">
Clear Filters
</Button>
</>
)}
</div>
</CardContent>
</Card>
) : isEditMode ? (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
> >
<SortableContext
items={displayCredits.map(c => c.id)}
strategy={verticalListSortingStrategy}
>
<div className={viewMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
: 'space-y-4'
}>
{displayCredits.map((credit, index) => (
<SortableRideCreditCard
key={credit.id}
credit={credit}
position={index + 1}
maxPosition={displayCredits.length}
viewMode={viewMode}
onUpdate={handleCreditUpdated}
onDelete={() => handleCreditDeleted(credit.id)}
onReorder={handleReorder}
/>
))}
</div>
</SortableContext>
</DndContext>
) : (
<div className={viewMode === 'grid' <div className={viewMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4' ? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
: 'space-y-4' : 'space-y-4'
}> }>
{displayCredits.map((credit, index) => ( {displayCredits.map((credit, index) => (
<SortableRideCreditCard <RideCreditCard
key={credit.id} key={credit.id}
credit={credit} credit={credit}
position={index + 1} position={index + 1}
maxPosition={displayCredits.length}
viewMode={viewMode} viewMode={viewMode}
onUpdate={handleCreditUpdated} onUpdate={handleCreditUpdated}
onDelete={() => handleCreditDeleted(credit.id)} onDelete={() => handleCreditDeleted(credit.id)}
onReorder={handleReorder}
/> />
))} ))}
</div> </div>
</SortableContext> )}
</DndContext>
) : (
<div className={viewMode === 'grid'
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
: 'space-y-4'
}>
{displayCredits.map((credit, index) => (
<RideCreditCard
key={credit.id}
credit={credit}
position={index + 1}
viewMode={viewMode}
onUpdate={handleCreditUpdated}
onDelete={() => handleCreditDeleted(credit.id)}
/>
))}
</div> </div>
)} </div>
{/* Add Credit Dialog */} {/* Add Credit Dialog */}
<AddRideCreditDialog <AddRideCreditDialog