Refactor ride credit sorting UI

This commit is contained in:
gpt-engineer-app[bot]
2025-10-16 15:23:09 +00:00
parent 7b0faf9bb2
commit bd44597f9a
3 changed files with 94 additions and 19 deletions

View File

@@ -24,14 +24,18 @@ import {
interface RideCreditCardProps { interface RideCreditCardProps {
credit: UserRideCredit; credit: UserRideCredit;
position: number; position: number;
maxPosition?: number;
viewMode: 'grid' | 'list'; viewMode: 'grid' | 'list';
isEditMode?: boolean;
onUpdate: () => void; onUpdate: () => void;
onDelete: () => void; onDelete: () => void;
onReorder?: (creditId: string, newPosition: number) => Promise<void>;
} }
export function RideCreditCard({ credit, position, viewMode, onUpdate, onDelete }: RideCreditCardProps) { export function RideCreditCard({ credit, position, maxPosition, viewMode, isEditMode, onUpdate, onDelete, onReorder }: RideCreditCardProps) {
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
const [editCount, setEditCount] = useState(credit.ride_count); const [editCount, setEditCount] = useState(credit.ride_count);
const [editPosition, setEditPosition] = useState(position);
const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [updating, setUpdating] = useState(false); const [updating, setUpdating] = useState(false);
@@ -81,6 +85,25 @@ export function RideCreditCard({ credit, position, viewMode, onUpdate, onDelete
} }
}; };
const handlePositionChange = async () => {
if (editPosition === position || !onReorder || !maxPosition) return;
if (editPosition < 1 || editPosition > maxPosition) {
toast.error(`Position must be between 1 and ${maxPosition}`);
setEditPosition(position);
return;
}
try {
await onReorder(credit.id, editPosition);
toast.success('Position updated');
} catch (error) {
console.error('Error changing position:', error);
toast.error(getErrorMessage(error));
setEditPosition(position);
}
};
const getCategoryBadge = (category: string) => { const getCategoryBadge = (category: string) => {
const categoryMap: Record<string, { label: string; variant: 'default' | 'secondary' | 'outline' }> = { const categoryMap: Record<string, { label: string; variant: 'default' | 'secondary' | 'outline' }> = {
roller_coaster: { label: 'Coaster', variant: 'default' }, roller_coaster: { label: 'Coaster', variant: 'default' },
@@ -117,9 +140,25 @@ export function RideCreditCard({ credit, position, viewMode, onUpdate, onDelete
> >
{rideName} {rideName}
</Link> </Link>
{isEditMode && maxPosition ? (
<div className="flex items-center gap-1">
<span className="text-xs text-muted-foreground">#</span>
<Input
type="number"
value={editPosition}
onChange={(e) => setEditPosition(parseInt(e.target.value) || 1)}
onBlur={handlePositionChange}
onKeyDown={(e) => e.key === 'Enter' && handlePositionChange()}
className="w-14 h-6 text-xs p-1"
min="1"
max={maxPosition}
/>
</div>
) : (
<Badge variant="secondary" className="text-xs font-semibold"> <Badge variant="secondary" className="text-xs font-semibold">
#{position} #{position}
</Badge> </Badge>
)}
{getCategoryBadge(category)} {getCategoryBadge(category)}
</div> </div>
@@ -245,9 +284,25 @@ export function RideCreditCard({ credit, position, viewMode, onUpdate, onDelete
{rideName} {rideName}
</Link> </Link>
<div className="flex gap-1 flex-shrink-0"> <div className="flex gap-1 flex-shrink-0">
{isEditMode && maxPosition ? (
<div className="flex items-center gap-1">
<span className="text-xs text-muted-foreground">#</span>
<Input
type="number"
value={editPosition}
onChange={(e) => setEditPosition(parseInt(e.target.value) || 1)}
onBlur={handlePositionChange}
onKeyDown={(e) => e.key === 'Enter' && handlePositionChange()}
className="w-14 h-6 text-xs p-1"
min="1"
max={maxPosition}
/>
</div>
) : (
<Badge variant="secondary" className="text-xs font-semibold"> <Badge variant="secondary" className="text-xs font-semibold">
#{position} #{position}
</Badge> </Badge>
)}
{getCategoryBadge(category)} {getCategoryBadge(category)}
</div> </div>
</div> </div>

View File

@@ -158,6 +158,23 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
} }
}; };
const handleReorder = async (creditId: string, newPosition: number) => {
try {
const { error } = await supabase.rpc('reorder_ride_credit', {
p_credit_id: creditId,
p_new_position: newPosition
});
if (error) throw error;
// Refetch to get accurate sort_order values
await fetchCredits();
} catch (error) {
console.error('Error reordering credit:', error);
throw error;
}
};
const handleDragEnd = async (event: DragEndEvent) => { const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event; const { active, over } = event;
@@ -173,19 +190,9 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
setCredits(newCredits); setCredits(newCredits);
try { try {
// Call RPC to persist the change await handleReorder(String(active.id), newIndex + 1);
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'); toast.success('Order updated');
} catch (error) { } catch (error) {
console.error('Error reordering credit:', error);
toast.error(getErrorMessage(error)); toast.error(getErrorMessage(error));
// Revert on error // Revert on error
fetchCredits(); fetchCredits();
@@ -340,9 +347,11 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
key={credit.id} key={credit.id}
credit={credit} credit={credit}
position={index + 1} position={index + 1}
maxPosition={credits.length}
viewMode={viewMode} viewMode={viewMode}
onUpdate={handleCreditUpdated} onUpdate={handleCreditUpdated}
onDelete={() => handleCreditDeleted(credit.id)} onDelete={() => handleCreditDeleted(credit.id)}
onReorder={handleReorder}
/> />
))} ))}
</div> </div>

View File

@@ -7,17 +7,21 @@ import { UserRideCredit } from '@/types/database';
interface SortableRideCreditCardProps { interface SortableRideCreditCardProps {
credit: UserRideCredit; credit: UserRideCredit;
position: number; position: number;
maxPosition: number;
viewMode: 'grid' | 'list'; viewMode: 'grid' | 'list';
onUpdate: () => void; onUpdate: () => void;
onDelete: () => void; onDelete: () => void;
onReorder: (creditId: string, newPosition: number) => Promise<void>;
} }
export function SortableRideCreditCard({ export function SortableRideCreditCard({
credit, credit,
position, position,
maxPosition,
viewMode, viewMode,
onUpdate, onUpdate,
onDelete, onDelete,
onReorder,
}: SortableRideCreditCardProps) { }: SortableRideCreditCardProps) {
const { const {
attributes, attributes,
@@ -39,7 +43,11 @@ export function SortableRideCreditCard({
<div <div
{...attributes} {...attributes}
{...listeners} {...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" className={
viewMode === 'grid'
? "absolute top-2 right-2 z-10 cursor-grab active:cursor-grabbing p-2 rounded-full bg-background/90 backdrop-blur-sm hover:bg-accent transition-colors shadow-md"
: "absolute left-0 top-1/2 -translate-y-1/2 z-10 cursor-grab active:cursor-grabbing p-2 rounded-r-md bg-background/90 backdrop-blur-sm hover:bg-accent transition-colors"
}
> >
<GripVertical className="w-4 h-4 text-muted-foreground" /> <GripVertical className="w-4 h-4 text-muted-foreground" />
</div> </div>
@@ -47,9 +55,12 @@ export function SortableRideCreditCard({
<RideCreditCard <RideCreditCard
credit={credit} credit={credit}
position={position} position={position}
maxPosition={maxPosition}
viewMode={viewMode} viewMode={viewMode}
isEditMode={true}
onUpdate={onUpdate} onUpdate={onUpdate}
onDelete={onDelete} onDelete={onDelete}
onReorder={onReorder}
/> />
</div> </div>
); );