feat: hackathon

This commit is contained in:
Maulana Sodiqin
2025-11-25 01:47:56 +07:00
parent 5a99f216cb
commit 9e9bfc2793
53 changed files with 6892 additions and 927 deletions
+62 -73
View File
@@ -1,83 +1,72 @@
import { useMutation, UseMutationResult } from '@tanstack/react-query';
import { postLogin, postRegister, postSendOtp, postVerifyEmail, getGoogleAuthUrl, postGoogleCallback } from '../../api/auth';
import {
TLoginRequest,
TLoginResponse,
TRegisterRequest,
TSendOTPRequest,
TVerifyEmailRequest,
TGoogleCallbackResponse,
} from '../../types/auth';
import { supabase } from '../../supabase';
import { TResponseError, TResponseMessage } from '../../types/common';
// Supabase GitHub OAuth hook
export const useGitHubAuth = () => {
const signInWithGitHub = async () => {
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${globalThis.location.origin}/auth/callback`,
},
});
export const usePostLogin = (): UseMutationResult<
TLoginResponse,
TResponseError,
TLoginRequest,
unknown
> => {
return useMutation({
mutationKey: ['post-login'],
mutationFn: async (payload) => await postLogin(payload),
});
};
if (error) {
throw error;
}
export const usePostRegister = (): UseMutationResult<
TResponseMessage,
TResponseError,
TRegisterRequest,
unknown
> => {
return useMutation({
mutationKey: ['post-register'],
mutationFn: async (payload) => await postRegister(payload),
});
};
export const usePostVerifyEmail = (): UseMutationResult<
TResponseMessage,
TResponseError,
TVerifyEmailRequest,
unknown
> => {
return useMutation({
mutationKey: ['post-verify-email'],
mutationFn: async (payload) => await postVerifyEmail(payload),
});
};
export const usePostSendOTP = (): UseMutationResult<
TResponseMessage,
TResponseError,
TSendOTPRequest,
unknown
> => {
return useMutation({
mutationKey: ['post-send-otp'],
mutationFn: async (payload) => await postSendOtp(payload),
});
};
export const useGoogleAuth = () => {
const redirectToGoogle = async (): Promise<string> => {
return await getGoogleAuthUrl();
// Return the OAuth URL for debugging
return data;
};
return {
redirectToGoogle,
signInWithGitHub,
};
};
export const useGoogleCallback = (): UseMutationResult<
TGoogleCallbackResponse,
TResponseError,
{ code: string; state: string },
unknown
> => {
return useMutation({
mutationKey: ['google-callback'],
mutationFn: async ({ code, state }) => await postGoogleCallback(code, state),
});
};
// Supabase Email/Password authentication hook
export const useEmailAuth = () => {
const signInWithEmail = async (email: string, password: string) => {
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) {
throw error;
}
return data;
};
const signUpWithEmail = async (email: string, password: string, fullname: string) => {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: {
full_name: fullname,
},
},
});
if (error) {
throw error;
}
return data;
};
const signOut = async () => {
const { error } = await supabase.auth.signOut();
if (error) {
throw error;
}
};
return {
signInWithEmail,
signUpWithEmail,
signOut,
};
};
+2
View File
@@ -3,3 +3,5 @@ export * from './gacha';
export * from './users';
export * from './mentors';
export * from './upload';
export * from './teams';
export * from './messages';
+181
View File
@@ -0,0 +1,181 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '../../supabase';
import { useAuthStore } from '@imphnen-frontend-service/utils';
import { useEffect } from 'react';
export type Message = {
id: string;
team_id: string;
user_id: string;
message: string;
created_at: string;
updated_at: string;
user?: {
id: string;
fullname: string;
avatar: string;
email: string;
};
};
// Query keys
export const messageKeys = {
all: ['messages'] as const,
team: (teamId: string) => [...messageKeys.all, 'team', teamId] as const,
};
// Fetch messages for a team
export const useTeamMessages = (teamId: string) => {
const queryClient = useQueryClient();
const query = useQuery({
queryKey: messageKeys.team(teamId),
queryFn: async () => {
const { data: messages, error } = await supabase
.from('team_messages')
.select(`
id,
team_id,
user_id,
message,
created_at,
updated_at,
user:users(id, fullname, avatar, email)
`)
.eq('team_id', teamId)
.order('created_at', { ascending: true });
if (error) {
console.error('Failed to fetch messages:', error);
throw new Error(error.message || 'Failed to fetch messages');
}
return messages as Message[];
},
enabled: !!teamId,
});
// Subscribe to realtime updates
useEffect(() => {
if (!teamId) return;
const channel = supabase
.channel(`team_messages:${teamId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'team_messages',
filter: `team_id=eq.${teamId}`,
},
async (payload) => {
console.log('[Realtime] New message:', payload);
// Fetch the full message with user data
const { data: newMessage } = await supabase
.from('team_messages')
.select(`
id,
team_id,
user_id,
message,
created_at,
updated_at,
user:users(id, fullname, avatar, email)
`)
.eq('id', payload.new.id)
.single();
if (newMessage) {
queryClient.setQueryData<Message[]>(
messageKeys.team(teamId),
(old) => [...(old || []), newMessage as Message]
);
}
}
)
.on(
'postgres_changes',
{
event: 'DELETE',
schema: 'public',
table: 'team_messages',
filter: `team_id=eq.${teamId}`,
},
(payload) => {
console.log('[Realtime] Message deleted:', payload);
queryClient.setQueryData<Message[]>(
messageKeys.team(teamId),
(old) => old?.filter((msg) => msg.id !== payload.old.id) || []
);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, [teamId, queryClient]);
return query;
};
// Send a message
export const useSendMessage = (teamId: string) => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
return useMutation({
mutationFn: async (message: string) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to send messages');
}
const { data, error } = await supabase
.from('team_messages')
.insert({
team_id: teamId,
user_id: session.user.id,
message,
})
.select()
.single();
if (error) {
console.error('Failed to send message:', error);
throw new Error(error.message || 'Failed to send message');
}
return data;
},
onSuccess: () => {
// Realtime will handle adding the message to the list
// But we can invalidate to ensure consistency
queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) });
},
});
};
// Delete a message
export const useDeleteMessage = (teamId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (messageId: string) => {
const { error } = await supabase
.from('team_messages')
.delete()
.eq('id', messageId);
if (error) {
console.error('Failed to delete message:', error);
throw new Error(error.message || 'Failed to delete message');
}
},
onSuccess: () => {
// Realtime will handle removing the message from the list
queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) });
},
});
};
+692
View File
@@ -0,0 +1,692 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import * as teamsApi from '../../api/teams';
import { supabase, getAuthenticatedClient } from '../../supabase';
import { useAuthStore } from '@imphnen-frontend-service/utils';
import type {
TCreateTeamRequest,
TUpdateTeamRequest,
TInviteMemberRequest,
TJoinTeamRequest,
TSubmitProjectRequest,
} from '../../types/teams';
// Query keys
export const teamKeys = {
all: ['teams'] as const,
lists: () => [...teamKeys.all, 'list'] as const,
list: (filters?: Record<string, unknown>) => [...teamKeys.lists(), filters] as const,
details: () => [...teamKeys.all, 'detail'] as const,
detail: (id: string) => [...teamKeys.details(), id] as const,
members: (id: string) => [...teamKeys.detail(id), 'members'] as const,
joinRequests: (id: string) => [...teamKeys.detail(id), 'join-requests'] as const,
submission: (id: string) => [...teamKeys.detail(id), 'submission'] as const,
myTeams: () => [...teamKeys.all, 'my-teams'] as const,
myInvitations: () => [...teamKeys.all, 'my-invitations'] as const,
};
// Team CRUD Hooks
export const useTeams = (params?: {
page?: number;
limit?: number;
city?: string;
visibility?: string;
search?: string;
}) => {
return useQuery({
queryKey: teamKeys.list(params),
queryFn: async () => {
try {
// Supabase client now has auth context from setSession()
let query = supabase.from('teams').select('*');
// Filter by visibility
if (params?.visibility) {
query = query.eq('visibility', params.visibility);
}
// Filter by city
if (params?.city) {
query = query.eq('city', params.city);
}
// Search by name
if (params?.search) {
query = query.ilike('name', `%${params.search}%`);
}
// Pagination
if (params?.page && params?.limit) {
const from = (params.page - 1) * params.limit;
const to = from + params.limit - 1;
query = query.range(from, to);
}
const { data, error } = await query;
if (error) {
// If error is 401/403, it means RLS policies need to be set up
// Return empty array for now
console.warn('Teams query error (RLS policies may need to be configured):', error);
return { data: [] };
}
return { data: data || [] };
} catch (err) {
console.error('Failed to fetch teams:', err);
return { data: [] };
}
},
});
};
export const useTeamById = (teamId: string, enabled = true) => {
return useQuery({
queryKey: teamKeys.detail(teamId),
queryFn: async () => {
// Supabase client now has auth context from setSession()
const { data: team, error } = await supabase
.from('teams')
.select(`
*,
leader:users!leader_id(id, email, fullname, avatar),
members:team_members(
id,
role,
status,
joined_at,
user:users(id, email, fullname, avatar)
)
`)
.eq('id', teamId)
.single();
if (error) {
console.error('Failed to fetch team:', error);
throw new Error(error.message || 'Failed to fetch team');
}
// Count active members
const activeMemberCount = team?.members?.filter((m: any) => m.status === 'active').length || 0;
// Check if team has a submission
const { data: submission } = await supabase
.from('project_submissions')
.select('id')
.eq('team_id', teamId)
.maybeSingle();
return {
data: {
...team,
member_count: activeMemberCount,
has_submission: !!submission,
},
};
},
enabled: enabled && !!teamId,
});
};
export const useCreateTeam = () => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
return useMutation({
mutationFn: async (data: TCreateTeamRequest) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to create a team');
}
// Supabase client now has auth context from setSession()
const { data: team, error: teamError } = await supabase
.from('teams')
.insert({
name: data.name,
logo: data.logo,
banner: data.banner,
description: data.description,
city: data.city,
visibility: data.visibility,
leader_id: session.user.id,
})
.select()
.single();
if (teamError) {
console.error('Failed to create team:', teamError);
throw new Error(teamError.message || 'Failed to create team');
}
// Insert team creator as leader in team_members table
const { error: memberError } = await supabase
.from('team_members')
.insert({
team_id: team.id,
user_id: session.user.id,
role: 'leader',
status: 'active',
});
if (memberError) {
// If duplicate key error (23505), it means leader is already a member (possibly by trigger)
// This is acceptable, so we can ignore it
if (memberError.code === '23505') {
console.log('Team leader already exists in team_members (likely added by trigger)');
} else {
// For other errors, clean up and throw
console.error('Failed to add team leader as member:', memberError);
await supabase.from('teams').delete().eq('id', team.id);
throw new Error('Failed to set up team membership');
}
}
return { data: team };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
queryClient.invalidateQueries({ queryKey: teamKeys.myTeams() });
},
});
};
export const useUpdateTeam = (teamId: string) => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
return useMutation({
mutationFn: async (data: TUpdateTeamRequest) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to update a team');
}
// Supabase client now has auth context from setSession()
const { data: team, error } = await supabase
.from('teams')
.update({
name: data.name,
logo: data.logo,
banner: data.banner,
description: data.description,
city: data.city,
visibility: data.visibility,
})
.eq('id', teamId)
.select()
.single();
if (error) {
console.error('Failed to update team:', error);
throw new Error(error.message || 'Failed to update team');
}
return { data: team };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) });
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
},
});
};
// Team Members Hooks
export const useTeamMembers = (teamId: string, enabled = true) => {
return useQuery({
queryKey: teamKeys.members(teamId),
queryFn: async () => {
// Supabase client now has auth context from setSession()
const { data: members, error } = await supabase
.from('team_members')
.select(`
id,
team_id,
user_id,
role,
status,
joined_at,
user:users(id, email, fullname, avatar)
`)
.eq('team_id', teamId)
.order('joined_at', { ascending: true });
if (error) {
console.error('Failed to fetch team members:', error);
throw new Error(error.message || 'Failed to fetch team members');
}
return { data: members || [] };
},
enabled: enabled && !!teamId,
});
};
export const useInviteMember = (teamId: string) => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
return useMutation({
mutationFn: async (data: TInviteMemberRequest) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to invite a member');
}
// Insert invitation into team_invitations table
const { data: invitation, error } = await supabase
.from('team_invitations')
.insert({
team_id: teamId,
inviter_id: session.user.id,
invitee_email: data.email,
status: 'pending',
})
.select()
.single();
if (error) {
console.error('Failed to create invitation:', error);
throw new Error(error.message || 'Failed to send invitation');
}
return { data: invitation };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
},
});
};
export const useManageMember = (teamId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ userId, data }: { userId: string; data: any }) =>
teamsApi.manageMember(teamId, userId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) });
},
});
};
export const useRemoveMember = (teamId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (userId: string) => teamsApi.removeMember(teamId, userId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) });
},
});
};
// Join Requests Hooks
export const useJoinTeam = () => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
return useMutation({
mutationFn: async ({ teamId, data }: { teamId: string; data: TJoinTeamRequest }) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to join a team');
}
// Insert join request into team_join_requests table
const { data: joinRequest, error } = await supabase
.from('team_join_requests')
.insert({
team_id: teamId,
user_id: session.user.id,
message: data.message,
status: 'pending',
})
.select()
.single();
if (error) {
console.error('Failed to create join request:', error);
throw new Error(error.message || 'Failed to send join request');
}
return { data: joinRequest };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
},
});
};
export const useTeamJoinRequests = (teamId: string, enabled = true) => {
return useQuery({
queryKey: teamKeys.joinRequests(teamId),
queryFn: async () => {
// Fetch join requests with user information
const { data: requests, error } = await supabase
.from('team_join_requests')
.select(`
id,
team_id,
user_id,
message,
status,
created_at,
user:users(id, email, fullname, avatar)
`)
.eq('team_id', teamId)
.order('created_at', { ascending: false });
if (error) {
console.error('Failed to fetch join requests:', error);
throw new Error(error.message || 'Failed to fetch join requests');
}
return { data: requests || [] };
},
enabled: enabled && !!teamId,
});
};
export const useRespondToJoinRequest = (teamId: string) => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
return useMutation({
mutationFn: async ({ requestId, action }: { requestId: string; action: 'approve' | 'reject' }) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to respond to join requests');
}
// First, get the join request details
const { data: joinRequest, error: fetchError } = await supabase
.from('team_join_requests')
.select('id, team_id, user_id, status')
.eq('id', requestId)
.single();
if (fetchError || !joinRequest) {
throw new Error('Join request not found');
}
if (joinRequest.status !== 'pending') {
throw new Error('Join request has already been processed');
}
if (action === 'approve') {
// Update join request status
const { error: updateError } = await supabase
.from('team_join_requests')
.update({ status: 'accepted' })
.eq('id', requestId);
if (updateError) {
throw new Error('Failed to update join request: ' + updateError.message);
}
// Add user as team member
const { error: memberError } = await supabase
.from('team_members')
.insert({
team_id: joinRequest.team_id,
user_id: joinRequest.user_id,
role: 'member',
status: 'active',
});
if (memberError) {
// If member creation fails, rollback join request update
await supabase
.from('team_join_requests')
.update({ status: 'pending' })
.eq('id', requestId);
throw new Error('Failed to add member to team: ' + memberError.message);
}
return { success: true, action: 'accepted' };
} else {
// Reject join request
const { error: updateError } = await supabase
.from('team_join_requests')
.update({ status: 'rejected' })
.eq('id', requestId);
if (updateError) {
throw new Error('Failed to update join request: ' + updateError.message);
}
return { success: true, action: 'rejected' };
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.joinRequests(teamId) });
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) });
},
});
};
// Invitations Hooks
export const useMyInvitations = () => {
const { session } = useAuthStore();
return useQuery({
queryKey: teamKeys.myInvitations(),
queryFn: async () => {
if (!session?.user?.email) {
return { data: [] };
}
// Query team_invitations where invitee_email matches current user's email
const { data: invitations, error } = await supabase
.from('team_invitations')
.select(`
id,
team_id,
inviter_id,
invitee_email,
invitee_id,
status,
created_at,
team:teams(
id,
name,
logo,
banner,
description,
city,
visibility,
leader_id
),
inviter:users!team_invitations_inviter_id_fkey(
id,
fullname,
email,
avatar
)
`)
.eq('invitee_email', session.user.email)
.eq('status', 'pending');
if (error) {
console.error('Failed to fetch invitations:', error);
return { data: [] };
}
return { data: invitations || [] };
},
enabled: !!session?.user?.email,
});
};
export const useRespondToInvitation = () => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
return useMutation({
mutationFn: async ({ invitationId, action }: { invitationId: string; action: 'accept' | 'reject' }) => {
if (!session?.user?.id) {
throw new Error('User not authenticated');
}
// First, get the invitation details
const { data: invitation, error: fetchError } = await supabase
.from('team_invitations')
.select('id, team_id, invitee_email, status')
.eq('id', invitationId)
.single();
if (fetchError || !invitation) {
throw new Error('Invitation not found');
}
if (invitation.status !== 'pending') {
throw new Error('Invitation has already been responded to');
}
if (action === 'accept') {
// Update invitation status and set invitee_id
const { error: updateError } = await supabase
.from('team_invitations')
.update({
status: 'accepted',
invitee_id: session.user.id,
})
.eq('id', invitationId);
if (updateError) {
throw new Error('Failed to update invitation: ' + updateError.message);
}
// Create team_members record
const { error: memberError } = await supabase
.from('team_members')
.insert({
team_id: invitation.team_id,
user_id: session.user.id,
role: 'member',
status: 'active',
});
if (memberError) {
// If member creation fails, rollback invitation update
await supabase
.from('team_invitations')
.update({
status: 'pending',
invitee_id: null,
})
.eq('id', invitationId);
throw new Error('Failed to add member to team: ' + memberError.message);
}
return { success: true, action: 'accepted' };
} else {
// Reject invitation
const { error: updateError } = await supabase
.from('team_invitations')
.update({
status: 'rejected',
invitee_id: session.user.id,
})
.eq('id', invitationId);
if (updateError) {
throw new Error('Failed to update invitation: ' + updateError.message);
}
return { success: true, action: 'rejected' };
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.myInvitations() });
queryClient.invalidateQueries({ queryKey: teamKeys.myTeams() });
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
},
});
};
// User's Teams
export const useMyTeams = () => {
const { session } = useAuthStore();
return useQuery({
queryKey: teamKeys.myTeams(),
queryFn: async () => {
if (!session?.user?.id) {
return { data: [] };
}
// Query team_members to find teams where user is a member
const { data: memberships, error: membershipsError } = await supabase
.from('team_members')
.select(`
team_id,
team:teams(
id,
name,
logo,
banner,
description,
city,
visibility,
leader_id,
created_at
)
`)
.eq('user_id', session.user.id)
.eq('status', 'active');
if (membershipsError) {
console.error('Failed to fetch user teams:', membershipsError);
return { data: [] };
}
// Extract teams from memberships
const teams = memberships?.map((m: any) => m.team).filter(Boolean) || [];
return { data: teams };
},
enabled: !!session?.user?.id,
});
};
// Project Submission Hooks
export const useSubmitProject = (teamId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: TSubmitProjectRequest) => {
return await teamsApi.submitProject(teamId, data);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.submission(teamId) });
queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) });
},
});
};
export const useTeamSubmission = (teamId: string, enabled = true) => {
return useQuery({
queryKey: teamKeys.submission(teamId),
queryFn: async () => {
const submission = await teamsApi.getTeamSubmission(teamId);
return { data: submission };
},
enabled: enabled && !!teamId,
});
};
// Leave Team Hook
export const useLeaveTeam = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (teamId: string) => {
return await teamsApi.leaveTeam(teamId);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.myTeams() });
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
},
});
};
+104 -24
View File
@@ -1,39 +1,119 @@
import { useMutation, UseMutationResult } from '@tanstack/react-query';
import { uploadService, UploadResponse } from '../../api/upload';
import { TResponseError } from '../../types/common';
import { useMutation } from '@tanstack/react-query';
import { supabase, getAuthenticatedClient } from '../../supabase';
import { useAuthStore } from '@imphnen-frontend-service/utils';
// Supabase Storage-based upload hooks
export const useUploadFile = () => {
const { session } = useAuthStore();
export const useUploadFile = (): UseMutationResult<
UploadResponse,
TResponseError,
File,
unknown
> => {
return useMutation({
mutationKey: ['upload-file'],
mutationFn: (file) => uploadService.uploadFile(file),
mutationFn: async (file: File) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to upload files');
}
// Generate a unique file name
const fileExt = file.name.split('.').pop();
const fileName = `${session.user.id}-${Date.now()}.${fileExt}`;
const filePath = `teams/${fileName}`;
// Supabase client now has auth context from setSession()
const { error } = await supabase.storage
.from('hackathon-uploads')
.upload(filePath, file, {
cacheControl: '3600',
upsert: true,
});
if (error) {
console.error('Failed to upload file:', error);
throw new Error(error.message || 'Failed to upload file');
}
// Get public URL
const { data: publicUrlData } = supabase.storage
.from('hackathon-uploads')
.getPublicUrl(filePath);
return { data: { url: publicUrlData.publicUrl } };
},
});
};
export const useUploadAvatar = (): UseMutationResult<
UploadResponse,
TResponseError,
File,
unknown
> => {
export const useUploadAvatar = () => {
const { session } = useAuthStore();
return useMutation({
mutationKey: ['upload-avatar'],
mutationFn: (file) => uploadService.uploadAvatar(file),
mutationFn: async (file: File) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to upload avatar');
}
// Generate a unique file name
const fileExt = file.name.split('.').pop();
const fileName = `${session.user.id}-${Date.now()}.${fileExt}`;
const filePath = `avatars/${fileName}`;
// Supabase client now has auth context from setSession()
const { error } = await supabase.storage
.from('hackathon-uploads')
.upload(filePath, file, {
cacheControl: '3600',
upsert: true,
});
if (error) {
console.error('Failed to upload avatar:', error);
throw new Error(error.message || 'Failed to upload avatar');
}
// Get public URL
const { data: publicUrlData } = supabase.storage
.from('hackathon-uploads')
.getPublicUrl(filePath);
return { data: { url: publicUrlData.publicUrl } };
},
});
};
export const useUploadCV = (): UseMutationResult<
UploadResponse,
TResponseError,
File,
unknown
> => {
export const useUploadCV = () => {
const { session } = useAuthStore();
return useMutation({
mutationKey: ['upload-cv'],
mutationFn: (file) => uploadService.uploadCV(file),
mutationFn: async (file: File) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to upload CV');
}
// Generate a unique file name
const fileExt = file.name.split('.').pop();
const fileName = `${session.user.id}-${Date.now()}.${fileExt}`;
const filePath = `cvs/${fileName}`;
// Supabase client now has auth context from setSession()
const { error } = await supabase.storage
.from('hackathon-uploads')
.upload(filePath, file, {
cacheControl: '3600',
upsert: true,
});
if (error) {
console.error('Failed to upload CV:', error);
throw new Error(error.message || 'Failed to upload CV');
}
// Get public URL
const { data: publicUrlData } = supabase.storage
.from('hackathon-uploads')
.getPublicUrl(filePath);
return { data: { url: publicUrlData.publicUrl } };
},
});
};
+73 -23
View File
@@ -1,44 +1,94 @@
import { useQuery, useMutation, UseQueryResult, UseMutationResult, UseQueryOptions } from '@tanstack/react-query';
import { userService, UserDetailResponseDto, UserUpdateRequestDto } from '../../api/users';
import { TResponseError } from '../../types/common';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { userService } from '../../api/users';
import { supabase, getAuthenticatedClient } from '../../supabase';
import { useAuthStore } from '@imphnen-frontend-service/utils';
export const useUserMe = (options?: UseQueryOptions<UserDetailResponseDto, TResponseError>): UseQueryResult<UserDetailResponseDto, TResponseError> => {
// Supabase-based user hooks
export const useUserMe = () => {
return useQuery({
queryKey: ['user-me'],
queryFn: () => userService.getUserMe(),
...options,
queryFn: async () => {
const user = await userService.getUserMe();
return { data: user };
},
});
};
export const useUserById = (id: string, options?: UseQueryOptions<UserDetailResponseDto, TResponseError>): UseQueryResult<UserDetailResponseDto, TResponseError> => {
export const useUserById = (id: string) => {
return useQuery({
queryKey: ['user-by-id', id],
queryFn: () => userService.getUserById(id),
queryFn: async () => {
const user = await userService.getUserById(id);
return { data: user };
},
enabled: !!id,
...options,
});
};
export const useUpdateUserMe = (): UseMutationResult<
UserDetailResponseDto,
TResponseError,
UserUpdateRequestDto,
unknown
> => {
export const useUpdateUserMe = () => {
const queryClient = useQueryClient();
const { session, setSession } = useAuthStore();
return useMutation({
mutationKey: ['update-user-me'],
mutationFn: (data) => userService.updateUserMe(data),
mutationFn: async (data: any) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to update profile');
}
// Supabase client now has auth context from setSession()
const { data: updatedUser, error } = await supabase
.from('users')
.update({
fullname: data.fullname,
bio: data.bio,
location: data.location,
avatar: data.avatar,
skills: data.skills,
updated_at: new Date().toISOString(),
})
.eq('id', session.user.id)
.select()
.single();
if (error) {
console.error('Failed to update user profile:', error);
throw new Error(error.message || 'Failed to update profile');
}
return { data: updatedUser };
},
onSuccess: (result) => {
// Update Zustand session store with new user data
if (session && result.data) {
setSession({
token: session.token,
user: {
...session.user,
fullname: result.data.fullname || session.user.fullname,
bio: result.data.bio || '',
location: result.data.location || '',
avatar: result.data.avatar || session.user.avatar,
skills: result.data.skills || [],
},
});
}
queryClient.invalidateQueries({ queryKey: ['user-me'] });
},
});
};
export const useUpdateUserById = (): UseMutationResult<
UserDetailResponseDto,
TResponseError,
{ id: string; data: UserUpdateRequestDto },
unknown
> => {
export const useUpdateUserById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['update-user-by-id'],
mutationFn: ({ id, data }) => userService.updateUserById(id, data),
mutationFn: async ({ id, data }: { id: string; data: any }) => {
const updated = await userService.updateUserById(id, data);
return { data: updated };
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['user-by-id', variables.id] });
},
});
};