mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 18:31:12 -05:00
feat: Implement persistent sort order
This commit is contained in:
@@ -23,12 +23,13 @@ import {
|
||||
|
||||
interface RideCreditCardProps {
|
||||
credit: UserRideCredit;
|
||||
position: number;
|
||||
viewMode: 'grid' | 'list';
|
||||
onUpdate: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export function RideCreditCard({ credit, viewMode, onUpdate, onDelete }: RideCreditCardProps) {
|
||||
export function RideCreditCard({ credit, position, viewMode, onUpdate, onDelete }: RideCreditCardProps) {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editCount, setEditCount] = useState(credit.ride_count);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
@@ -116,6 +117,9 @@ export function RideCreditCard({ credit, viewMode, onUpdate, onDelete }: RideCre
|
||||
>
|
||||
{rideName}
|
||||
</Link>
|
||||
<Badge variant="secondary" className="text-xs font-semibold">
|
||||
#{position}
|
||||
</Badge>
|
||||
{getCategoryBadge(category)}
|
||||
</div>
|
||||
|
||||
@@ -240,7 +244,12 @@ export function RideCreditCard({ credit, viewMode, onUpdate, onDelete }: RideCre
|
||||
>
|
||||
{rideName}
|
||||
</Link>
|
||||
{getCategoryBadge(category)}
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
<Badge variant="secondary" className="text-xs font-semibold">
|
||||
#{position}
|
||||
</Badge>
|
||||
{getCategoryBadge(category)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
|
||||
@@ -2,13 +2,27 @@ import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Plus, LayoutGrid, List } from 'lucide-react';
|
||||
import { Plus, LayoutGrid, List, GripVertical } from 'lucide-react';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { toast } from 'sonner';
|
||||
import { getErrorMessage } from '@/lib/errorHandler';
|
||||
import { AddRideCreditDialog } from './AddRideCreditDialog';
|
||||
import { RideCreditCard } from './RideCreditCard';
|
||||
import { SortableRideCreditCard } from './SortableRideCreditCard';
|
||||
import { UserRideCredit } from '@/types/database';
|
||||
import {
|
||||
DndContext,
|
||||
DragEndEvent,
|
||||
closestCenter,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import {
|
||||
SortableContext,
|
||||
verticalListSortingStrategy,
|
||||
arrayMove,
|
||||
} from '@dnd-kit/sortable';
|
||||
|
||||
interface RideCreditsManagerProps {
|
||||
userId: string;
|
||||
@@ -18,9 +32,18 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
||||
const [credits, setCredits] = useState<UserRideCredit[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
||||
const [sortBy, setSortBy] = useState<'date' | 'count' | 'name'>('date');
|
||||
const [sortBy, setSortBy] = useState<'date' | 'count' | 'name' | 'custom'>('custom');
|
||||
const [filterCategory, setFilterCategory] = useState<string>('all');
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCredits();
|
||||
@@ -72,7 +95,7 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply sorting
|
||||
// Apply sorting - default to sort_order when available
|
||||
if (sortBy === 'date') {
|
||||
query = query.order('first_ride_date', { ascending: false, nullsFirst: false });
|
||||
} else if (sortBy === 'count') {
|
||||
@@ -80,12 +103,15 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
||||
} else if (sortBy === 'name') {
|
||||
// Note: Can't order by joined table - we'll sort client-side
|
||||
query = query.order('created_at', { ascending: false });
|
||||
} else {
|
||||
// Default to custom sort order
|
||||
query = query.order('sort_order', { ascending: true, nullsFirst: false });
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) throw error;
|
||||
|
||||
let processedData = (data || []) as UserRideCredit[];
|
||||
let processedData = (data || []) as any[];
|
||||
|
||||
// Sort by name client-side if needed
|
||||
if (sortBy === 'name') {
|
||||
@@ -132,6 +158,40 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = async (event: DragEndEvent) => {
|
||||
const { active, over } = event;
|
||||
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
const oldIndex = credits.findIndex(c => c.id === String(active.id));
|
||||
const newIndex = credits.findIndex(c => c.id === String(over.id));
|
||||
|
||||
if (oldIndex === -1 || newIndex === -1) return;
|
||||
|
||||
// Optimistic update
|
||||
const newCredits = arrayMove(credits, oldIndex, newIndex);
|
||||
setCredits(newCredits);
|
||||
|
||||
try {
|
||||
// Call RPC to persist the change
|
||||
const { error } = await supabase.rpc('reorder_ride_credit', {
|
||||
p_credit_id: String(active.id),
|
||||
p_new_position: newIndex + 1
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
// Refetch to get accurate sort_order values
|
||||
fetchCredits();
|
||||
toast.success('Order updated');
|
||||
} catch (error) {
|
||||
console.error('Error reordering credit:', error);
|
||||
toast.error(getErrorMessage(error));
|
||||
// Revert on error
|
||||
fetchCredits();
|
||||
}
|
||||
};
|
||||
|
||||
const stats = {
|
||||
totalRides: credits.reduce((sum, c) => sum + c.ride_count, 0),
|
||||
uniqueCredits: credits.length,
|
||||
@@ -190,6 +250,14 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
||||
<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">
|
||||
@@ -211,6 +279,7 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
||||
<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>
|
||||
@@ -252,15 +321,43 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : isEditMode ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={credits.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'
|
||||
}>
|
||||
{credits.map((credit, index) => (
|
||||
<SortableRideCreditCard
|
||||
key={credit.id}
|
||||
credit={credit}
|
||||
position={index + 1}
|
||||
viewMode={viewMode}
|
||||
onUpdate={handleCreditUpdated}
|
||||
onDelete={() => handleCreditDeleted(credit.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
) : (
|
||||
<div className={viewMode === 'grid'
|
||||
? 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4'
|
||||
: 'space-y-4'
|
||||
}>
|
||||
{credits.map((credit) => (
|
||||
{credits.map((credit, index) => (
|
||||
<RideCreditCard
|
||||
key={credit.id}
|
||||
credit={credit}
|
||||
position={index + 1}
|
||||
viewMode={viewMode}
|
||||
onUpdate={handleCreditUpdated}
|
||||
onDelete={() => handleCreditDeleted(credit.id)}
|
||||
|
||||
56
src/components/profile/SortableRideCreditCard.tsx
Normal file
56
src/components/profile/SortableRideCreditCard.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { GripVertical } from 'lucide-react';
|
||||
import { RideCreditCard } from './RideCreditCard';
|
||||
import { UserRideCredit } from '@/types/database';
|
||||
|
||||
interface SortableRideCreditCardProps {
|
||||
credit: UserRideCredit;
|
||||
position: number;
|
||||
viewMode: 'grid' | 'list';
|
||||
onUpdate: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
export function SortableRideCreditCard({
|
||||
credit,
|
||||
position,
|
||||
viewMode,
|
||||
onUpdate,
|
||||
onDelete,
|
||||
}: SortableRideCreditCardProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: credit.id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style} className="relative">
|
||||
<div
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className="absolute top-2 left-2 z-10 cursor-grab active:cursor-grabbing p-2 rounded bg-background/80 backdrop-blur-sm hover:bg-accent transition-colors"
|
||||
>
|
||||
<GripVertical className="w-4 h-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<RideCreditCard
|
||||
credit={credit}
|
||||
position={position}
|
||||
viewMode={viewMode}
|
||||
onUpdate={onUpdate}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user