mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
feat: Implement sidebar layout for filters
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user