mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 18:31:13 -05:00
Implement API improvements Phases 1-4
This commit is contained in:
@@ -13,6 +13,7 @@ import { RideCreditFilters } from './RideCreditFilters';
|
|||||||
import { UserRideCredit } from '@/types/database';
|
import { UserRideCredit } from '@/types/database';
|
||||||
import { useRideCreditFilters } from '@/hooks/useRideCreditFilters';
|
import { useRideCreditFilters } from '@/hooks/useRideCreditFilters';
|
||||||
import { useIsMobile } from '@/hooks/use-mobile';
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
|
import { useRideCreditsMutation } from '@/hooks/rides/useRideCreditsMutation';
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -39,6 +40,7 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
||||||
const [isEditMode, setIsEditMode] = useState(false);
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const { reorderCredit, isReordering } = useRideCreditsMutation();
|
||||||
|
|
||||||
// Use the filter hook
|
// Use the filter hook
|
||||||
const {
|
const {
|
||||||
@@ -246,24 +248,16 @@ export function RideCreditsManager({ userId }: RideCreditsManagerProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReorder = async (creditId: string, newPosition: number) => {
|
const handleReorder = (creditId: string, newPosition: number) => {
|
||||||
try {
|
return new Promise<void>((resolve, reject) => {
|
||||||
const { error } = await supabase.rpc('reorder_ride_credit', {
|
reorderCredit.mutate(
|
||||||
p_credit_id: creditId,
|
{ creditId, newPosition },
|
||||||
p_new_position: newPosition
|
{
|
||||||
});
|
onSuccess: () => resolve(),
|
||||||
|
onError: (error) => reject(error)
|
||||||
if (error) throw error;
|
}
|
||||||
|
);
|
||||||
// No refetch - optimistic update is already applied
|
});
|
||||||
} catch (error: unknown) {
|
|
||||||
handleError(error, {
|
|
||||||
action: 'Reorder Ride Credit',
|
|
||||||
userId,
|
|
||||||
metadata: { creditId, newPosition }
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragEnd = async (event: DragEndEvent) => {
|
const handleDragEnd = async (event: DragEndEvent) => {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert';
|
import { Alert, AlertDescription } from '@/components/ui/alert';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@@ -8,6 +8,7 @@ import { Progress } from '@/components/ui/progress';
|
|||||||
import { Mail, Info, CheckCircle2, Circle, Loader2 } from 'lucide-react';
|
import { Mail, Info, CheckCircle2, Circle, Loader2 } from 'lucide-react';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
import { handleError, handleSuccess } from '@/lib/errorHandler';
|
||||||
|
import { useEmailChangeStatus } from '@/hooks/security/useEmailChangeStatus';
|
||||||
|
|
||||||
interface EmailChangeStatusProps {
|
interface EmailChangeStatusProps {
|
||||||
currentEmail: string;
|
currentEmail: string;
|
||||||
@@ -15,55 +16,19 @@ interface EmailChangeStatusProps {
|
|||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailChangeData = {
|
|
||||||
has_pending_change: boolean;
|
|
||||||
current_email?: string;
|
|
||||||
new_email?: string;
|
|
||||||
current_email_verified?: boolean;
|
|
||||||
new_email_verified?: boolean;
|
|
||||||
change_sent_at?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function EmailChangeStatus({
|
export function EmailChangeStatus({
|
||||||
currentEmail,
|
currentEmail,
|
||||||
pendingEmail,
|
pendingEmail,
|
||||||
onCancel
|
onCancel
|
||||||
}: EmailChangeStatusProps) {
|
}: EmailChangeStatusProps) {
|
||||||
const [verificationStatus, setVerificationStatus] = useState({
|
|
||||||
oldEmailVerified: false,
|
|
||||||
newEmailVerified: false
|
|
||||||
});
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [resending, setResending] = useState(false);
|
const [resending, setResending] = useState(false);
|
||||||
|
const { data: emailStatus, isLoading } = useEmailChangeStatus();
|
||||||
|
|
||||||
const checkVerificationStatus = async () => {
|
const verificationStatus = {
|
||||||
try {
|
oldEmailVerified: emailStatus?.current_email_verified || false,
|
||||||
const { data, error } = await supabase.rpc('get_email_change_status');
|
newEmailVerified: emailStatus?.new_email_verified || false
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
const emailData = data as EmailChangeData;
|
|
||||||
|
|
||||||
if (emailData.has_pending_change) {
|
|
||||||
setVerificationStatus({
|
|
||||||
oldEmailVerified: emailData.current_email_verified || false,
|
|
||||||
newEmailVerified: emailData.new_email_verified || false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
handleError(error, { action: 'Check verification status' });
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
checkVerificationStatus();
|
|
||||||
// Poll every 30 seconds
|
|
||||||
const interval = setInterval(checkVerificationStatus, 30000);
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleResendVerification = async () => {
|
const handleResendVerification = async () => {
|
||||||
setResending(true);
|
setResending(true);
|
||||||
try {
|
try {
|
||||||
@@ -88,7 +53,7 @@ export function EmailChangeStatus({
|
|||||||
(verificationStatus.oldEmailVerified ? 50 : 0) +
|
(verificationStatus.oldEmailVerified ? 50 : 0) +
|
||||||
(verificationStatus.newEmailVerified ? 50 : 0);
|
(verificationStatus.newEmailVerified ? 50 : 0);
|
||||||
|
|
||||||
if (loading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Card className="border-blue-500/30">
|
<Card className="border-blue-500/30">
|
||||||
<CardContent className="flex items-center justify-center py-8">
|
<CardContent className="flex items-center justify-center py-8">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { TOTPSetup } from '@/components/auth/TOTPSetup';
|
|||||||
import { GoogleIcon } from '@/components/icons/GoogleIcon';
|
import { GoogleIcon } from '@/components/icons/GoogleIcon';
|
||||||
import { DiscordIcon } from '@/components/icons/DiscordIcon';
|
import { DiscordIcon } from '@/components/icons/DiscordIcon';
|
||||||
import { PasswordUpdateDialog } from './PasswordUpdateDialog';
|
import { PasswordUpdateDialog } from './PasswordUpdateDialog';
|
||||||
|
import { useSessions } from '@/hooks/security/useSessions';
|
||||||
import {
|
import {
|
||||||
getUserIdentities,
|
getUserIdentities,
|
||||||
checkDisconnectSafety,
|
checkDisconnectSafety,
|
||||||
@@ -37,14 +38,14 @@ export function SecurityTab() {
|
|||||||
const [disconnectingProvider, setDisconnectingProvider] = useState<OAuthProvider | null>(null);
|
const [disconnectingProvider, setDisconnectingProvider] = useState<OAuthProvider | null>(null);
|
||||||
const [hasPassword, setHasPassword] = useState(false);
|
const [hasPassword, setHasPassword] = useState(false);
|
||||||
const [addingPassword, setAddingPassword] = useState(false);
|
const [addingPassword, setAddingPassword] = useState(false);
|
||||||
const [sessions, setSessions] = useState<AuthSession[]>([]);
|
|
||||||
const [loadingSessions, setLoadingSessions] = useState(true);
|
|
||||||
const [sessionToRevoke, setSessionToRevoke] = useState<{ id: string; isCurrent: boolean } | null>(null);
|
const [sessionToRevoke, setSessionToRevoke] = useState<{ id: string; isCurrent: boolean } | null>(null);
|
||||||
|
|
||||||
|
// Fetch sessions using hook
|
||||||
|
const { data: sessions = [], isLoading: loadingSessions, refetch: refetchSessions } = useSessions(user?.id);
|
||||||
|
|
||||||
// Load user identities on mount
|
// Load user identities on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadIdentities();
|
loadIdentities();
|
||||||
fetchSessions();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadIdentities = async () => {
|
const loadIdentities = async () => {
|
||||||
@@ -145,35 +146,6 @@ export function SecurityTab() {
|
|||||||
setAddingPassword(false);
|
setAddingPassword(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSessions = async () => {
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
setLoadingSessions(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data, error } = await supabase.rpc('get_my_sessions');
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSessions((data as AuthSession[]) || []);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
logger.error('Failed to fetch sessions', {
|
|
||||||
userId: user.id,
|
|
||||||
action: 'fetch_sessions',
|
|
||||||
error: error instanceof Error ? error.message : String(error)
|
|
||||||
});
|
|
||||||
handleError(error, {
|
|
||||||
action: 'Load active sessions',
|
|
||||||
userId: user.id
|
|
||||||
});
|
|
||||||
setSessions([]);
|
|
||||||
} finally {
|
|
||||||
setLoadingSessions(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initiateSessionRevoke = async (sessionId: string) => {
|
const initiateSessionRevoke = async (sessionId: string) => {
|
||||||
// Get current session to check if revoking self
|
// Get current session to check if revoking self
|
||||||
const { data: { session: currentSession } } = await supabase.auth.getSession();
|
const { data: { session: currentSession } } = await supabase.auth.getSession();
|
||||||
@@ -192,7 +164,7 @@ export function SecurityTab() {
|
|||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
if (!sessionToRevoke.isCurrent) {
|
if (!sessionToRevoke.isCurrent) {
|
||||||
fetchSessions();
|
refetchSessions();
|
||||||
}
|
}
|
||||||
setSessionToRevoke(null);
|
setSessionToRevoke(null);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -306,6 +306,41 @@ When migrating a component:
|
|||||||
- Features: Automatic caching, refetch on window focus, 5-minute stale time
|
- Features: Automatic caching, refetch on window focus, 5-minute stale time
|
||||||
- Returns: Array of blocked users with profile information
|
- Returns: Array of blocked users with profile information
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- **`useEmailChangeStatus`** - Query email change verification status
|
||||||
|
- Queries: `get_email_change_status` RPC function
|
||||||
|
- Features: Automatic polling every 30 seconds, 15-second stale time
|
||||||
|
- Returns: Email change status with verification flags
|
||||||
|
|
||||||
|
- **`useSessions`** - Fetch active user sessions
|
||||||
|
- Queries: `get_my_sessions` RPC function
|
||||||
|
- Features: Automatic caching, refetch on window focus, 5-minute stale time
|
||||||
|
- Returns: Array of active sessions with device info
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Type Safety Guidelines
|
||||||
|
|
||||||
|
Always use proper TypeScript types in hooks:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// ✅ CORRECT - Define proper interfaces
|
||||||
|
interface Profile {
|
||||||
|
display_name?: string;
|
||||||
|
bio?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.setQueryData<Profile>(['profile', userId], (old) =>
|
||||||
|
old ? { ...old, ...updates } : old
|
||||||
|
);
|
||||||
|
|
||||||
|
// ❌ WRONG - Using any type
|
||||||
|
queryClient.setQueryData(['profile', userId], (old: any) => ({
|
||||||
|
...old,
|
||||||
|
...updates
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Cache Invalidation Guidelines
|
## Cache Invalidation Guidelines
|
||||||
|
|||||||
@@ -57,8 +57,22 @@ export function useHomepageRecentChanges(
|
|||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
|
interface DatabaseRecentChange {
|
||||||
|
entity_id: string;
|
||||||
|
entity_name: string;
|
||||||
|
entity_type: string;
|
||||||
|
entity_slug: string;
|
||||||
|
park_slug?: string;
|
||||||
|
image_url?: string;
|
||||||
|
change_type: string;
|
||||||
|
changed_at: string;
|
||||||
|
changed_by_username?: string;
|
||||||
|
changed_by_avatar?: string;
|
||||||
|
change_reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Transform the database response to match our interface
|
// Transform the database response to match our interface
|
||||||
const result: RecentChange[] = (data || []).map((item: any) => ({
|
const result: RecentChange[] = (data as unknown as DatabaseRecentChange[] || []).map((item) => ({
|
||||||
id: item.entity_id,
|
id: item.entity_id,
|
||||||
name: item.entity_name,
|
name: item.entity_name,
|
||||||
type: item.entity_type as 'park' | 'ride' | 'company',
|
type: item.entity_type as 'park' | 'ride' | 'company',
|
||||||
|
|||||||
@@ -67,15 +67,22 @@ export function usePrivacyMutations() {
|
|||||||
await queryClient.cancelQueries({ queryKey: ['profile', user?.id] });
|
await queryClient.cancelQueries({ queryKey: ['profile', user?.id] });
|
||||||
|
|
||||||
// Snapshot current value
|
// Snapshot current value
|
||||||
const previousProfile = queryClient.getQueryData(['profile', user?.id]);
|
interface Profile {
|
||||||
|
privacy_level?: string;
|
||||||
|
show_pronouns?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousProfile = queryClient.getQueryData<Profile>(['profile', user?.id]);
|
||||||
|
|
||||||
// Optimistically update cache
|
// Optimistically update cache
|
||||||
if (previousProfile) {
|
if (previousProfile) {
|
||||||
queryClient.setQueryData(['profile', user?.id], (old: any) => ({
|
queryClient.setQueryData<Profile>(['profile', user?.id], (old) =>
|
||||||
...old,
|
old ? {
|
||||||
privacy_level: newData.privacy_level,
|
...old,
|
||||||
show_pronouns: newData.show_pronouns,
|
privacy_level: newData.privacy_level,
|
||||||
}));
|
show_pronouns: newData.show_pronouns,
|
||||||
|
} : old
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { previousProfile };
|
return { previousProfile };
|
||||||
|
|||||||
@@ -166,13 +166,28 @@ export function useProfileActivity(
|
|||||||
photoItemsMap.get(item.photo_submission_id)!.push(item);
|
photoItemsMap.get(item.photo_submission_id)!.push(item);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface DatabaseEntity {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
}
|
||||||
|
|
||||||
const entityMap = new Map<string, EntityData>([
|
const entityMap = new Map<string, EntityData>([
|
||||||
...parks.map((p: any): [string, EntityData] => [p.id, p]),
|
...parks.map((p: DatabaseEntity): [string, EntityData] => [p.id, p]),
|
||||||
...rides.map((r: any): [string, EntityData] => [r.id, r])
|
...rides.map((r: DatabaseEntity): [string, EntityData] => [r.id, r])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
interface PhotoSubmissionWithAllFields {
|
||||||
|
id: string;
|
||||||
|
photo_count?: number;
|
||||||
|
photo_preview?: string;
|
||||||
|
entity_type?: string;
|
||||||
|
entity_id?: string;
|
||||||
|
content?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
// Enrich submissions
|
// Enrich submissions
|
||||||
photoSubmissions.forEach((sub: any) => {
|
photoSubmissions.forEach((sub: PhotoSubmissionWithAllFields) => {
|
||||||
const photoSub = photoSubMap.get(sub.id);
|
const photoSub = photoSubMap.get(sub.id);
|
||||||
if (photoSub) {
|
if (photoSub) {
|
||||||
const items = photoItemsMap.get(photoSub.id) || [];
|
const items = photoItemsMap.get(photoSub.id) || [];
|
||||||
|
|||||||
@@ -64,18 +64,26 @@ export function useProfileLocationMutation() {
|
|||||||
await queryClient.cancelQueries({ queryKey: ['profile', user?.id] });
|
await queryClient.cancelQueries({ queryKey: ['profile', user?.id] });
|
||||||
|
|
||||||
// Snapshot current value
|
// Snapshot current value
|
||||||
const previousProfile = queryClient.getQueryData(['profile', user?.id]);
|
interface Profile {
|
||||||
|
personal_location?: string;
|
||||||
|
home_park_id?: string;
|
||||||
|
timezone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousProfile = queryClient.getQueryData<Profile>(['profile', user?.id]);
|
||||||
|
|
||||||
// Optimistically update cache
|
// Optimistically update cache
|
||||||
if (previousProfile) {
|
if (previousProfile) {
|
||||||
queryClient.setQueryData(['profile', user?.id], (old: any) => ({
|
queryClient.setQueryData<Profile>(['profile', user?.id], (old) =>
|
||||||
...old,
|
old ? {
|
||||||
personal_location: newData.personal_location,
|
...old,
|
||||||
home_park_id: newData.home_park_id,
|
personal_location: newData.personal_location,
|
||||||
timezone: newData.timezone,
|
home_park_id: newData.home_park_id,
|
||||||
preferred_language: newData.preferred_language,
|
timezone: newData.timezone,
|
||||||
preferred_pronouns: newData.preferred_pronouns,
|
preferred_language: newData.preferred_language,
|
||||||
}));
|
preferred_pronouns: newData.preferred_pronouns,
|
||||||
|
} : old
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { previousProfile };
|
return { previousProfile };
|
||||||
|
|||||||
@@ -37,14 +37,20 @@ export function useProfileUpdateMutation() {
|
|||||||
// Cancel outgoing refetches
|
// Cancel outgoing refetches
|
||||||
await queryClient.cancelQueries({ queryKey: ['profile', userId] });
|
await queryClient.cancelQueries({ queryKey: ['profile', userId] });
|
||||||
|
|
||||||
|
interface Profile {
|
||||||
|
display_name?: string;
|
||||||
|
bio?: string;
|
||||||
|
location_id?: string;
|
||||||
|
website?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Snapshot previous value
|
// Snapshot previous value
|
||||||
const previousProfile = queryClient.getQueryData(['profile', userId]);
|
const previousProfile = queryClient.getQueryData<Profile>(['profile', userId]);
|
||||||
|
|
||||||
// Optimistically update
|
// Optimistically update
|
||||||
queryClient.setQueryData(['profile', userId], (old: any) => ({
|
queryClient.setQueryData<Profile>(['profile', userId], (old) =>
|
||||||
...old,
|
old ? { ...old, ...updates } : old
|
||||||
...updates,
|
);
|
||||||
}));
|
|
||||||
|
|
||||||
return { previousProfile, userId };
|
return { previousProfile, userId };
|
||||||
},
|
},
|
||||||
|
|||||||
38
src/hooks/security/useEmailChangeStatus.ts
Normal file
38
src/hooks/security/useEmailChangeStatus.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
|
export interface EmailChangeStatus {
|
||||||
|
has_pending_change: boolean;
|
||||||
|
current_email?: string;
|
||||||
|
new_email?: string;
|
||||||
|
current_email_verified?: boolean;
|
||||||
|
new_email_verified?: boolean;
|
||||||
|
change_sent_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to query email change verification status
|
||||||
|
* Provides: automatic polling every 30 seconds, cache management, loading states
|
||||||
|
*/
|
||||||
|
export function useEmailChangeStatus() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['email-change-status'],
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data, error } = await supabase.rpc('get_email_change_status');
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.error('Failed to fetch email change status', {
|
||||||
|
action: 'fetch_email_change_status',
|
||||||
|
error: error.message,
|
||||||
|
errorCode: error.code
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data as unknown as EmailChangeStatus;
|
||||||
|
},
|
||||||
|
refetchInterval: 30000, // Poll every 30 seconds
|
||||||
|
staleTime: 15000, // 15 seconds
|
||||||
|
});
|
||||||
|
}
|
||||||
34
src/hooks/security/useSessions.ts
Normal file
34
src/hooks/security/useSessions.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { supabase } from '@/integrations/supabase/client';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
import type { AuthSession } from '@/types/auth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to fetch active user sessions
|
||||||
|
* Provides: automatic caching, refetch on window focus, loading states
|
||||||
|
*/
|
||||||
|
export function useSessions(userId?: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['sessions', userId],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!userId) throw new Error('User ID required');
|
||||||
|
|
||||||
|
const { data, error } = await supabase.rpc('get_my_sessions');
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
logger.error('Failed to fetch sessions', {
|
||||||
|
userId,
|
||||||
|
action: 'fetch_sessions',
|
||||||
|
error: error.message,
|
||||||
|
errorCode: error.code
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (data as AuthSession[]) || [];
|
||||||
|
},
|
||||||
|
enabled: !!userId,
|
||||||
|
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -93,7 +93,14 @@ export function useEntityVersions(entityType: EntityType, entityId: string) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const versionsWithProfiles = (data || []).map((v: any) => ({
|
interface DatabaseVersion {
|
||||||
|
profiles?: {
|
||||||
|
username?: string;
|
||||||
|
display_name?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const versionsWithProfiles = (data as DatabaseVersion[] || []).map((v) => ({
|
||||||
...v,
|
...v,
|
||||||
profiles: v.profiles || {
|
profiles: v.profiles || {
|
||||||
username: 'Unknown',
|
username: 'Unknown',
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export function useRideCreditFilters(credits: UserRideCredit[]) {
|
|||||||
const [filters, setFilters] = useState<RideCreditFilters>({});
|
const [filters, setFilters] = useState<RideCreditFilters>({});
|
||||||
const debouncedSearchQuery = useDebounce(filters.searchQuery || '', 300);
|
const debouncedSearchQuery = useDebounce(filters.searchQuery || '', 300);
|
||||||
|
|
||||||
const updateFilter = useCallback((key: keyof RideCreditFilters, value: any) => {
|
const updateFilter = useCallback((key: keyof RideCreditFilters, value: RideCreditFilters[typeof key]) => {
|
||||||
setFilters(prev => ({ ...prev, [key]: value }));
|
setFilters(prev => ({ ...prev, [key]: value }));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user