mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 19:11:12 -05:00
Refactor ride credit sorting UI
This commit is contained in:
@@ -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>
|
||||||
<Badge variant="secondary" className="text-xs font-semibold">
|
{isEditMode && maxPosition ? (
|
||||||
#{position}
|
<div className="flex items-center gap-1">
|
||||||
</Badge>
|
<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">
|
||||||
|
#{position}
|
||||||
|
</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">
|
||||||
<Badge variant="secondary" className="text-xs font-semibold">
|
{isEditMode && maxPosition ? (
|
||||||
#{position}
|
<div className="flex items-center gap-1">
|
||||||
</Badge>
|
<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">
|
||||||
|
#{position}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{getCategoryBadge(category)}
|
{getCategoryBadge(category)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user