Files
thrilltrack-explorer/src/hooks/users/useRoleMutations.ts
2025-10-31 00:25:32 +00:00

145 lines
4.1 KiB
TypeScript

import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { queryKeys } from '@/lib/queryKeys';
import { toast } from 'sonner';
type ValidRole = 'admin' | 'moderator' | 'user';
interface GrantRoleParams {
userId: string;
role: ValidRole;
}
interface RevokeRoleParams {
roleId: string;
}
interface UserWithRoles {
id: string;
user_id: string;
username: string;
email: string;
display_name: string | null;
avatar_url: string | null;
banned: boolean;
created_at: string;
}
/**
* useRoleMutations Hook
*
* Provides TanStack Query mutations for granting and revoking user roles
* with optimistic updates for instant UI feedback.
*
* Features:
* - Optimistic updates for immediate UI response
* - Automatic cache invalidation on success
* - Error handling with rollback
* - Toast notifications
*
* @example
* ```tsx
* const { grantRole, revokeRole } = useRoleMutations();
*
* grantRole.mutate({ userId: 'user-id', role: 'moderator' });
* revokeRole.mutate({ roleId: 'role-id' });
* ```
*/
export function useRoleMutations() {
const queryClient = useQueryClient();
const grantRole = useMutation({
mutationFn: async ({ userId, role }: GrantRoleParams) => {
const { data, error } = await supabase
.from('user_roles')
.insert([{ user_id: userId, role }])
.select()
.single();
if (error) throw error;
return data;
},
onMutate: async ({ userId, role }) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: queryKeys.users.roles() });
// Snapshot previous value
const previousUsers = queryClient.getQueryData<UserWithRoles[]>(queryKeys.users.roles());
// Optimistically update cache - add role to user
queryClient.setQueryData<UserWithRoles[]>(queryKeys.users.roles(), (old) => {
if (!old) return old;
return old.map((user) =>
user.user_id === userId
? { ...user, role } // Optimistically assign role
: user
);
});
return { previousUsers };
},
onError: (error, variables, context) => {
// Rollback on error
if (context?.previousUsers) {
queryClient.setQueryData(queryKeys.users.roles(), context.previousUsers);
}
toast.error(`Failed to grant role: ${error.message}`);
},
onSuccess: (data, { role }) => {
toast.success(`Role ${role} granted successfully`);
},
onSettled: () => {
// Refetch to ensure consistency
queryClient.invalidateQueries({ queryKey: queryKeys.users.roles() });
queryClient.invalidateQueries({ queryKey: ['user-roles'] });
},
});
const revokeRole = useMutation({
mutationFn: async ({ roleId }: RevokeRoleParams) => {
const { error } = await supabase
.from('user_roles')
.delete()
.eq('id', roleId);
if (error) throw error;
},
onMutate: async ({ roleId }) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: queryKeys.users.roles() });
// Snapshot previous value
const previousUsers = queryClient.getQueryData<UserWithRoles[]>(queryKeys.users.roles());
// Optimistically remove role from cache
queryClient.setQueryData<UserWithRoles[]>(queryKeys.users.roles(), (old) => {
if (!old) return old;
// Remove the user from the list since they no longer have a role
return old.filter((user) => user.id !== roleId);
});
return { previousUsers };
},
onError: (error, variables, context) => {
// Rollback on error
if (context?.previousUsers) {
queryClient.setQueryData(queryKeys.users.roles(), context.previousUsers);
}
toast.error(`Failed to revoke role: ${error.message}`);
},
onSuccess: () => {
toast.success('Role revoked successfully');
},
onSettled: () => {
// Refetch to ensure consistency
queryClient.invalidateQueries({ queryKey: queryKeys.users.roles() });
queryClient.invalidateQueries({ queryKey: ['user-roles'] });
},
});
return {
grantRole,
revokeRole,
};
}