feat: Implement persistent sort order

This commit is contained in:
gpt-engineer-app[bot]
2025-10-16 15:18:45 +00:00
parent 3283e47b25
commit 7b0faf9bb2
7 changed files with 368 additions and 7 deletions

View File

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