mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 08:31:12 -05:00
feat: Implement persistent sort order
This commit is contained in:
56
package-lock.json
generated
56
package-lock.json
generated
@@ -8,6 +8,9 @@
|
|||||||
"name": "vite_react_shadcn_ts",
|
"name": "vite_react_shadcn_ts",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@marsidev/react-turnstile": "^1.3.1",
|
"@marsidev/react-turnstile": "^1.3.1",
|
||||||
"@novu/react": "^3.10.1",
|
"@novu/react": "^3.10.1",
|
||||||
@@ -173,6 +176,59 @@
|
|||||||
"solid-js": "^1.8"
|
"solid-js": "^1.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@dnd-kit/accessibility": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/core": {
|
||||||
|
"version": "6.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
|
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/accessibility": "^3.1.1",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/sortable": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.0",
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@dnd-kit/utilities": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.25.10",
|
"version": "0.25.10",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@marsidev/react-turnstile": "^1.3.1",
|
"@marsidev/react-turnstile": "^1.3.1",
|
||||||
"@novu/react": "^3.10.1",
|
"@novu/react": "^3.10.1",
|
||||||
|
|||||||
@@ -23,12 +23,13 @@ import {
|
|||||||
|
|
||||||
interface RideCreditCardProps {
|
interface RideCreditCardProps {
|
||||||
credit: UserRideCredit;
|
credit: UserRideCredit;
|
||||||
|
position: number;
|
||||||
viewMode: 'grid' | 'list';
|
viewMode: 'grid' | 'list';
|
||||||
onUpdate: () => void;
|
onUpdate: () => void;
|
||||||
onDelete: () => 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 [isEditing, setIsEditing] = useState(false);
|
||||||
const [editCount, setEditCount] = useState(credit.ride_count);
|
const [editCount, setEditCount] = useState(credit.ride_count);
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
@@ -116,6 +117,9 @@ export function RideCreditCard({ credit, viewMode, onUpdate, onDelete }: RideCre
|
|||||||
>
|
>
|
||||||
{rideName}
|
{rideName}
|
||||||
</Link>
|
</Link>
|
||||||
|
<Badge variant="secondary" className="text-xs font-semibold">
|
||||||
|
#{position}
|
||||||
|
</Badge>
|
||||||
{getCategoryBadge(category)}
|
{getCategoryBadge(category)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -240,8 +244,13 @@ export function RideCreditCard({ credit, viewMode, onUpdate, onDelete }: RideCre
|
|||||||
>
|
>
|
||||||
{rideName}
|
{rideName}
|
||||||
</Link>
|
</Link>
|
||||||
|
<div className="flex gap-1 flex-shrink-0">
|
||||||
|
<Badge variant="secondary" className="text-xs font-semibold">
|
||||||
|
#{position}
|
||||||
|
</Badge>
|
||||||
{getCategoryBadge(category)}
|
{getCategoryBadge(category)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to={`/parks/${parkSlug}`}
|
to={`/parks/${parkSlug}`}
|
||||||
|
|||||||
@@ -2,13 +2,27 @@ import { useState, useEffect } from 'react';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
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 { supabase } from '@/integrations/supabase/client';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { getErrorMessage } from '@/lib/errorHandler';
|
import { getErrorMessage } from '@/lib/errorHandler';
|
||||||
import { AddRideCreditDialog } from './AddRideCreditDialog';
|
import { AddRideCreditDialog } from './AddRideCreditDialog';
|
||||||
import { RideCreditCard } from './RideCreditCard';
|
import { RideCreditCard } from './RideCreditCard';
|
||||||
|
import { SortableRideCreditCard } from './SortableRideCreditCard';
|
||||||
import { UserRideCredit } from '@/types/database';
|
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 {
|
interface RideCreditsManagerProps {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -18,9 +32,18 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
const [credits, setCredits] = useState<UserRideCredit[]>([]);
|
const [credits, setCredits] = useState<UserRideCredit[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
|
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 [filterCategory, setFilterCategory] = useState<string>('all');
|
||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, {
|
||||||
|
activationConstraint: {
|
||||||
|
distance: 8,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCredits();
|
fetchCredits();
|
||||||
@@ -72,7 +95,7 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply sorting
|
// Apply sorting - default to sort_order when available
|
||||||
if (sortBy === 'date') {
|
if (sortBy === 'date') {
|
||||||
query = query.order('first_ride_date', { ascending: false, nullsFirst: false });
|
query = query.order('first_ride_date', { ascending: false, nullsFirst: false });
|
||||||
} else if (sortBy === 'count') {
|
} else if (sortBy === 'count') {
|
||||||
@@ -80,12 +103,15 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
} else if (sortBy === 'name') {
|
} else if (sortBy === 'name') {
|
||||||
// Note: Can't order by joined table - we'll sort client-side
|
// Note: Can't order by joined table - we'll sort client-side
|
||||||
query = query.order('created_at', { ascending: false });
|
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;
|
const { data, error } = await query;
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
let processedData = (data || []) as UserRideCredit[];
|
let processedData = (data || []) as any[];
|
||||||
|
|
||||||
// Sort by name client-side if needed
|
// Sort by name client-side if needed
|
||||||
if (sortBy === 'name') {
|
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 = {
|
const stats = {
|
||||||
totalRides: credits.reduce((sum, c) => sum + c.ride_count, 0),
|
totalRides: credits.reduce((sum, c) => sum + c.ride_count, 0),
|
||||||
uniqueCredits: credits.length,
|
uniqueCredits: credits.length,
|
||||||
@@ -190,6 +250,14 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Add Credit
|
Add Credit
|
||||||
</Button>
|
</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>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -211,6 +279,7 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
<SelectValue placeholder="Sort by" />
|
<SelectValue placeholder="Sort by" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
<SelectItem value="custom">Custom Order</SelectItem>
|
||||||
<SelectItem value="date">Most Recent</SelectItem>
|
<SelectItem value="date">Most Recent</SelectItem>
|
||||||
<SelectItem value="count">Most Ridden</SelectItem>
|
<SelectItem value="count">Most Ridden</SelectItem>
|
||||||
<SelectItem value="name">Ride Name</SelectItem>
|
<SelectItem value="name">Ride Name</SelectItem>
|
||||||
@@ -252,15 +321,43 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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'
|
<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'
|
||||||
}>
|
}>
|
||||||
{credits.map((credit) => (
|
{credits.map((credit, index) => (
|
||||||
<RideCreditCard
|
<RideCreditCard
|
||||||
key={credit.id}
|
key={credit.id}
|
||||||
credit={credit}
|
credit={credit}
|
||||||
|
position={index + 1}
|
||||||
viewMode={viewMode}
|
viewMode={viewMode}
|
||||||
onUpdate={handleCreditUpdated}
|
onUpdate={handleCreditUpdated}
|
||||||
onDelete={() => handleCreditDeleted(credit.id)}
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3184,6 +3184,7 @@ export type Database = {
|
|||||||
id: string
|
id: string
|
||||||
ride_count: number | null
|
ride_count: number | null
|
||||||
ride_id: string
|
ride_id: string
|
||||||
|
sort_order: number | null
|
||||||
updated_at: string
|
updated_at: string
|
||||||
user_id: string
|
user_id: string
|
||||||
}
|
}
|
||||||
@@ -3193,6 +3194,7 @@ export type Database = {
|
|||||||
id?: string
|
id?: string
|
||||||
ride_count?: number | null
|
ride_count?: number | null
|
||||||
ride_id: string
|
ride_id: string
|
||||||
|
sort_order?: number | null
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
user_id: string
|
user_id: string
|
||||||
}
|
}
|
||||||
@@ -3202,6 +3204,7 @@ export type Database = {
|
|||||||
id?: string
|
id?: string
|
||||||
ride_count?: number | null
|
ride_count?: number | null
|
||||||
ride_id?: string
|
ride_id?: string
|
||||||
|
sort_order?: number | null
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
user_id?: string
|
user_id?: string
|
||||||
}
|
}
|
||||||
@@ -3421,6 +3424,10 @@ export type Database = {
|
|||||||
Args: { target_user_id: string }
|
Args: { target_user_id: string }
|
||||||
Returns: undefined
|
Returns: undefined
|
||||||
}
|
}
|
||||||
|
backfill_sort_orders: {
|
||||||
|
Args: Record<PropertyKey, never>
|
||||||
|
Returns: undefined
|
||||||
|
}
|
||||||
can_approve_submission_item: {
|
can_approve_submission_item: {
|
||||||
Args: { item_id: string }
|
Args: { item_id: string }
|
||||||
Returns: boolean
|
Returns: boolean
|
||||||
@@ -3602,6 +3609,10 @@ export type Database = {
|
|||||||
Args: { moderator_id: string; submission_id: string }
|
Args: { moderator_id: string; submission_id: string }
|
||||||
Returns: boolean
|
Returns: boolean
|
||||||
}
|
}
|
||||||
|
reorder_ride_credit: {
|
||||||
|
Args: { p_credit_id: string; p_new_position: number }
|
||||||
|
Returns: undefined
|
||||||
|
}
|
||||||
revoke_my_session: {
|
revoke_my_session: {
|
||||||
Args: { session_id: string }
|
Args: { session_id: string }
|
||||||
Returns: undefined
|
Returns: undefined
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
-- Add sort_order column to user_ride_credits
|
||||||
|
ALTER TABLE user_ride_credits
|
||||||
|
ADD COLUMN sort_order INTEGER;
|
||||||
|
|
||||||
|
-- Create unique index to prevent duplicate positions per user
|
||||||
|
CREATE UNIQUE INDEX idx_user_ride_credits_sort_order
|
||||||
|
ON user_ride_credits(user_id, sort_order)
|
||||||
|
WHERE sort_order IS NOT NULL;
|
||||||
|
|
||||||
|
-- Function to reorder a ride credit to a new position
|
||||||
|
CREATE OR REPLACE FUNCTION reorder_ride_credit(
|
||||||
|
p_credit_id UUID,
|
||||||
|
p_new_position INTEGER
|
||||||
|
) RETURNS VOID AS $$
|
||||||
|
DECLARE
|
||||||
|
v_user_id UUID;
|
||||||
|
v_old_position INTEGER;
|
||||||
|
v_max_position INTEGER;
|
||||||
|
BEGIN
|
||||||
|
-- Get credit details
|
||||||
|
SELECT user_id, sort_order INTO v_user_id, v_old_position
|
||||||
|
FROM user_ride_credits
|
||||||
|
WHERE id = p_credit_id;
|
||||||
|
|
||||||
|
IF v_user_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Credit not found';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Lock user's credits to prevent race conditions
|
||||||
|
PERFORM 1 FROM user_ride_credits WHERE user_id = v_user_id FOR UPDATE;
|
||||||
|
|
||||||
|
-- Get max position for validation
|
||||||
|
SELECT COALESCE(MAX(sort_order), 0) INTO v_max_position
|
||||||
|
FROM user_ride_credits
|
||||||
|
WHERE user_id = v_user_id;
|
||||||
|
|
||||||
|
-- Validate new position
|
||||||
|
IF p_new_position < 1 OR p_new_position > v_max_position THEN
|
||||||
|
RAISE EXCEPTION 'Invalid position. Must be between 1 and %', v_max_position;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- If position unchanged, do nothing
|
||||||
|
IF v_old_position = p_new_position THEN
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Temporarily set to NULL to avoid unique constraint violation
|
||||||
|
UPDATE user_ride_credits SET sort_order = NULL WHERE id = p_credit_id;
|
||||||
|
|
||||||
|
IF p_new_position < v_old_position THEN
|
||||||
|
-- Moving up: shift down items between new and old position
|
||||||
|
UPDATE user_ride_credits
|
||||||
|
SET sort_order = sort_order + 1
|
||||||
|
WHERE user_id = v_user_id
|
||||||
|
AND sort_order >= p_new_position
|
||||||
|
AND sort_order < v_old_position;
|
||||||
|
ELSE
|
||||||
|
-- Moving down: shift up items between old and new position
|
||||||
|
UPDATE user_ride_credits
|
||||||
|
SET sort_order = sort_order - 1
|
||||||
|
WHERE user_id = v_user_id
|
||||||
|
AND sort_order > v_old_position
|
||||||
|
AND sort_order <= p_new_position;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Set new position
|
||||||
|
UPDATE user_ride_credits SET sort_order = p_new_position WHERE id = p_credit_id;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
|
||||||
|
|
||||||
|
-- Function to backfill sort_order for existing credits
|
||||||
|
CREATE OR REPLACE FUNCTION backfill_sort_orders()
|
||||||
|
RETURNS void AS $$
|
||||||
|
DECLARE
|
||||||
|
user_record RECORD;
|
||||||
|
credit_record RECORD;
|
||||||
|
position_counter INTEGER;
|
||||||
|
BEGIN
|
||||||
|
FOR user_record IN SELECT DISTINCT user_id FROM user_ride_credits LOOP
|
||||||
|
position_counter := 0;
|
||||||
|
|
||||||
|
FOR credit_record IN
|
||||||
|
SELECT id
|
||||||
|
FROM user_ride_credits
|
||||||
|
WHERE user_id = user_record.user_id
|
||||||
|
ORDER BY first_ride_date ASC NULLS LAST, created_at ASC
|
||||||
|
LOOP
|
||||||
|
position_counter := position_counter + 1;
|
||||||
|
UPDATE user_ride_credits
|
||||||
|
SET sort_order = position_counter
|
||||||
|
WHERE id = credit_record.id;
|
||||||
|
END LOOP;
|
||||||
|
END LOOP;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
|
||||||
|
|
||||||
|
-- Run backfill to populate existing data
|
||||||
|
SELECT backfill_sort_orders();
|
||||||
|
|
||||||
|
-- Update auto-review trigger to assign position at end
|
||||||
|
CREATE OR REPLACE FUNCTION auto_add_ride_credit_on_review()
|
||||||
|
RETURNS trigger AS $$
|
||||||
|
DECLARE
|
||||||
|
v_next_position INTEGER;
|
||||||
|
BEGIN
|
||||||
|
IF NEW.ride_id IS NOT NULL THEN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM public.user_ride_credits
|
||||||
|
WHERE user_id = NEW.user_id AND ride_id = NEW.ride_id
|
||||||
|
) THEN
|
||||||
|
-- Get next position
|
||||||
|
SELECT COALESCE(MAX(sort_order), 0) + 1 INTO v_next_position
|
||||||
|
FROM public.user_ride_credits
|
||||||
|
WHERE user_id = NEW.user_id;
|
||||||
|
|
||||||
|
-- Insert with position at end
|
||||||
|
INSERT INTO public.user_ride_credits (
|
||||||
|
user_id, ride_id, first_ride_date, ride_count, sort_order
|
||||||
|
) VALUES (
|
||||||
|
NEW.user_id, NEW.ride_id, NEW.visit_date, 1, v_next_position
|
||||||
|
);
|
||||||
|
|
||||||
|
RAISE NOTICE 'Auto-added ride credit for user % on ride %', NEW.user_id, NEW.ride_id;
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
|
||||||
Reference in New Issue
Block a user