- Add has_submission filter option to useTeams hook - Add Submission Status dropdown filter (All/Submitted/Not Submitted) - Filter persists in URL query params 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
587 lines
17 KiB
TypeScript
587 lines
17 KiB
TypeScript
import { useMutation, useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
|
|
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
|
|
import { useAuthStore } from '../auth';
|
|
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,
|
|
};
|
|
|
|
// API response types
|
|
interface TeamMember {
|
|
id: string;
|
|
team_id: string;
|
|
user_id: string;
|
|
role: string;
|
|
status: string;
|
|
joined_at: string;
|
|
user?: {
|
|
id: string;
|
|
email: string;
|
|
fullname: string;
|
|
avatar: string;
|
|
};
|
|
}
|
|
|
|
interface Team {
|
|
id: string;
|
|
name: string;
|
|
logo?: string;
|
|
banner?: string;
|
|
description?: string;
|
|
city?: string;
|
|
visibility: string;
|
|
leader_id: string;
|
|
created_at: string;
|
|
leader?: {
|
|
id: string;
|
|
email: string;
|
|
fullname: string;
|
|
avatar: string;
|
|
};
|
|
members?: TeamMember[];
|
|
member_count?: number;
|
|
has_submission?: boolean;
|
|
}
|
|
|
|
interface JoinRequest {
|
|
id: string;
|
|
team_id: string;
|
|
user_id: string;
|
|
message?: string;
|
|
status: string;
|
|
created_at: string;
|
|
user?: {
|
|
id: string;
|
|
email: string;
|
|
fullname: string;
|
|
avatar: string;
|
|
};
|
|
}
|
|
|
|
interface Invitation {
|
|
id: string;
|
|
team_id: string;
|
|
inviter_id: string;
|
|
invitee_email: string;
|
|
invitee_id?: string;
|
|
status: string;
|
|
created_at: string;
|
|
team?: Team;
|
|
inviter?: {
|
|
id: string;
|
|
fullname: string;
|
|
email: string;
|
|
avatar: string;
|
|
};
|
|
}
|
|
|
|
interface Submission {
|
|
id: string;
|
|
team_id: string;
|
|
project_name: string;
|
|
description?: string;
|
|
repository_url?: string;
|
|
demo_url?: string;
|
|
video_url?: string;
|
|
presentation_url?: string;
|
|
status: string;
|
|
submitted_at?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
// List response type with pagination
|
|
interface ListResponseWithMeta<T> {
|
|
message: string;
|
|
data: T[];
|
|
meta: {
|
|
page: number;
|
|
per_page: number;
|
|
total_page: number;
|
|
total_data: number;
|
|
};
|
|
}
|
|
|
|
// Team CRUD Hooks
|
|
export const useTeams = (params?: {
|
|
page?: number;
|
|
limit?: number;
|
|
city?: string;
|
|
visibility?: string;
|
|
search?: string;
|
|
minMembers?: number;
|
|
maxMembers?: number;
|
|
hasSubmission?: boolean;
|
|
}) => {
|
|
return useQuery({
|
|
queryKey: teamKeys.list(params),
|
|
queryFn: async () => {
|
|
const queryParams = new URLSearchParams();
|
|
if (params?.page) queryParams.append('page', String(params.page));
|
|
if (params?.limit) queryParams.append('per_page', String(params.limit));
|
|
if (params?.search) queryParams.append('search', params.search);
|
|
if (params?.city) queryParams.append('city', params.city);
|
|
if (params?.visibility) queryParams.append('visibility', params.visibility);
|
|
if (params?.minMembers) queryParams.append('min_members', String(params.minMembers));
|
|
if (params?.maxMembers) queryParams.append('max_members', String(params.maxMembers));
|
|
if (params?.hasSubmission !== undefined) queryParams.append('has_submission', String(params.hasSubmission));
|
|
|
|
const response = await hackathonApi.get<ListResponseWithMeta<Team>>(
|
|
`/teams/browse${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
|
|
);
|
|
|
|
const { data, meta } = response.data;
|
|
return {
|
|
teams: data || [],
|
|
total: meta?.total_data || 0,
|
|
page: meta?.page || 1,
|
|
perPage: meta?.per_page || 12,
|
|
totalPages: meta?.total_page || 1,
|
|
};
|
|
},
|
|
});
|
|
};
|
|
|
|
// Infinite scroll teams hook
|
|
const TEAMS_PAGE_SIZE = 12;
|
|
|
|
export const useInfiniteTeams = (params?: {
|
|
city?: string;
|
|
visibility?: string;
|
|
search?: string;
|
|
}) => {
|
|
return useInfiniteQuery({
|
|
queryKey: [...teamKeys.lists(), 'infinite', params],
|
|
queryFn: async ({ pageParam = 1 }) => {
|
|
const queryParams = new URLSearchParams();
|
|
queryParams.append('page', String(pageParam));
|
|
queryParams.append('limit', String(TEAMS_PAGE_SIZE));
|
|
if (params?.search) queryParams.append('search', params.search);
|
|
if (params?.city) queryParams.append('city', params.city);
|
|
if (params?.visibility) queryParams.append('visibility', params.visibility);
|
|
|
|
const response = await hackathonApi.get<HackathonApiResponse<Team[]>>(
|
|
`/teams/browse?${queryParams.toString()}`
|
|
);
|
|
|
|
const teams = response.data.data || [];
|
|
return {
|
|
data: teams,
|
|
nextPage: teams.length === TEAMS_PAGE_SIZE ? pageParam + 1 : undefined,
|
|
};
|
|
},
|
|
initialPageParam: 1,
|
|
getNextPageParam: (lastPage) => lastPage.nextPage,
|
|
});
|
|
};
|
|
|
|
export const useTeamById = (teamId: string, enabled = true) => {
|
|
return useQuery({
|
|
queryKey: teamKeys.detail(teamId),
|
|
queryFn: async () => {
|
|
const response = await hackathonApi.get<HackathonApiResponse<Team>>(`/teams/${teamId}`);
|
|
return { data: response.data.data };
|
|
},
|
|
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');
|
|
}
|
|
|
|
const response = await hackathonApi.post<HackathonApiResponse<Team>>('/teams', {
|
|
name: data.name,
|
|
logo: data.logo,
|
|
banner: data.banner,
|
|
description: data.description,
|
|
city: data.city,
|
|
visibility: data.visibility,
|
|
});
|
|
|
|
return { data: response.data.data };
|
|
},
|
|
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');
|
|
}
|
|
|
|
const response = await hackathonApi.put<HackathonApiResponse<Team>>(`/teams/${teamId}`, {
|
|
name: data.name,
|
|
logo: data.logo,
|
|
banner: data.banner,
|
|
description: data.description,
|
|
city: data.city,
|
|
visibility: data.visibility,
|
|
});
|
|
|
|
return { data: response.data.data };
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) });
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
|
|
},
|
|
});
|
|
};
|
|
|
|
// Team Members Hooks - using team detail endpoint which includes members
|
|
export const useTeamMembers = (teamId: string, enabled = true) => {
|
|
return useQuery({
|
|
queryKey: teamKeys.members(teamId),
|
|
queryFn: async () => {
|
|
const response = await hackathonApi.get<HackathonApiResponse<Team>>(`/teams/${teamId}`);
|
|
return { data: response.data.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');
|
|
}
|
|
|
|
const response = await hackathonApi.post<HackathonApiResponse<Invitation>>(
|
|
`/teams/${teamId}/invite`,
|
|
{ invitee_email: data.email }
|
|
);
|
|
|
|
return { data: response.data.data };
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useManageMember = (teamId: string) => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async ({ userId, data }: { userId: string; data: { role?: string; status?: string } }) => {
|
|
// This endpoint may not exist in the backend yet
|
|
// For now, we'll throw an error indicating it's not implemented
|
|
throw new Error('Manage member functionality not yet implemented in backend');
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.detail(teamId) });
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useRemoveMember = (teamId: string) => {
|
|
const queryClient = useQueryClient();
|
|
const { session } = useAuthStore();
|
|
|
|
return useMutation({
|
|
mutationFn: async (userId: string) => {
|
|
if (!session?.user?.id) {
|
|
throw new Error('You must be logged in to remove a member');
|
|
}
|
|
|
|
await hackathonApi.delete(`/teams/${teamId}/members/${userId}`);
|
|
return { success: true };
|
|
},
|
|
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');
|
|
}
|
|
|
|
const response = await hackathonApi.post<HackathonApiResponse<JoinRequest>>(
|
|
`/join-requests/teams/${teamId}`,
|
|
{ message: data.message }
|
|
);
|
|
|
|
return { data: response.data.data };
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
|
|
},
|
|
});
|
|
};
|
|
|
|
export const useTeamJoinRequests = (teamId: string, enabled = true) => {
|
|
return useQuery({
|
|
queryKey: teamKeys.joinRequests(teamId),
|
|
queryFn: async () => {
|
|
const response = await hackathonApi.get<HackathonApiResponse<JoinRequest[]>>(
|
|
`/join-requests/teams/${teamId}/pending`
|
|
);
|
|
return { data: response.data.data || [] };
|
|
},
|
|
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');
|
|
}
|
|
|
|
// Backend uses 'accept' instead of 'approve'
|
|
const backendAction = action === 'approve' ? 'accept' : 'reject';
|
|
|
|
await hackathonApi.post(`/join-requests/${requestId}/respond`, {
|
|
action: backendAction,
|
|
});
|
|
|
|
return { success: true, action };
|
|
},
|
|
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 () => {
|
|
const response = await hackathonApi.get<HackathonApiResponse<Invitation[]>>('/invitations/my');
|
|
return { data: response.data.data || [] };
|
|
},
|
|
enabled: !!session?.user?.id,
|
|
});
|
|
};
|
|
|
|
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');
|
|
}
|
|
|
|
await hackathonApi.post(`/invitations/${invitationId}/respond`, { action });
|
|
|
|
return { success: true, action };
|
|
},
|
|
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 () => {
|
|
const response = await hackathonApi.get<HackathonApiResponse<any[]>>('/teams/my');
|
|
const rawData = response.data.data || [];
|
|
// Unwrap nested team data if present (API returns [{team: {...}}] or [{id, name, ...}])
|
|
const teams = rawData.map((item: any) => item.team || item);
|
|
return { data: teams };
|
|
},
|
|
enabled: !!session?.user?.id,
|
|
});
|
|
};
|
|
|
|
// Project Submission Hooks
|
|
export const useSubmitProject = (teamId: string) => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (data: TSubmitProjectRequest) => {
|
|
let submissionId: string;
|
|
|
|
// First, check if submission exists
|
|
try {
|
|
const existingResponse = await hackathonApi.get<HackathonApiResponse<Submission | null>>(
|
|
`/submissions/teams/${teamId}`
|
|
);
|
|
|
|
if (existingResponse.data.data?.id) {
|
|
// Update existing submission
|
|
const response = await hackathonApi.put<HackathonApiResponse<Submission>>(
|
|
`/submissions/${existingResponse.data.data.id}`,
|
|
{
|
|
project_name: data.project_name,
|
|
description: data.description,
|
|
repository_url: data.repository_url,
|
|
demo_url: data.demo_url,
|
|
video_url: data.video_url,
|
|
presentation_url: data.presentation_url,
|
|
screenshots: data.screenshots,
|
|
}
|
|
);
|
|
submissionId = response.data.data.id;
|
|
} else {
|
|
throw new Error('No existing submission');
|
|
}
|
|
} catch {
|
|
// No existing submission, create new one
|
|
const response = await hackathonApi.post<HackathonApiResponse<Submission>>(
|
|
`/submissions/teams/${teamId}`,
|
|
{
|
|
project_name: data.project_name,
|
|
description: data.description,
|
|
repository_url: data.repository_url,
|
|
demo_url: data.demo_url,
|
|
video_url: data.video_url,
|
|
presentation_url: data.presentation_url,
|
|
screenshots: data.screenshots,
|
|
}
|
|
);
|
|
submissionId = response.data.data.id;
|
|
}
|
|
|
|
// Step 2: Submit the project (draft -> pending_verification)
|
|
await hackathonApi.post<HackathonApiResponse<Submission>>(
|
|
`/submissions/${submissionId}/submit`
|
|
);
|
|
|
|
// Step 3: Confirm the submission (pending_verification -> submitted)
|
|
const finalResponse = await hackathonApi.post<HackathonApiResponse<Submission>>(
|
|
`/submissions/${submissionId}/confirm`
|
|
);
|
|
|
|
return { data: finalResponse.data.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 response = await hackathonApi.get<HackathonApiResponse<Submission | null>>(
|
|
`/submissions/teams/${teamId}`
|
|
);
|
|
return { data: response.data.data };
|
|
},
|
|
enabled: enabled && !!teamId,
|
|
});
|
|
};
|
|
|
|
// Leave Team Hook
|
|
export const useLeaveTeam = () => {
|
|
const queryClient = useQueryClient();
|
|
const { session } = useAuthStore();
|
|
|
|
return useMutation({
|
|
mutationFn: async (teamId: string) => {
|
|
if (!session?.user?.id) {
|
|
throw new Error('You must be logged in to leave a team');
|
|
}
|
|
|
|
// Use the dedicated leave team endpoint
|
|
await hackathonApi.post(`/teams/${teamId}/leave`);
|
|
return { success: true };
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.myTeams() });
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
|
|
},
|
|
});
|
|
};
|
|
|
|
// Delete Team Hook (Leader only)
|
|
export const useDeleteTeam = () => {
|
|
const queryClient = useQueryClient();
|
|
const { session } = useAuthStore();
|
|
|
|
return useMutation({
|
|
mutationFn: async (teamId: string) => {
|
|
if (!session?.user?.id) {
|
|
throw new Error('You must be logged in to delete a team');
|
|
}
|
|
|
|
await hackathonApi.delete(`/teams/${teamId}`);
|
|
return { success: true };
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.myTeams() });
|
|
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
|
|
},
|
|
});
|
|
};
|
|
|
|
// Get Teams by User ID - uses /users/{user_id}/teams
|
|
export const useTeamsByUserId = (userId: string) => {
|
|
return useQuery({
|
|
queryKey: ['teams-by-user', userId],
|
|
queryFn: async () => {
|
|
const response = await hackathonApi.get<HackathonApiResponse<any[]>>(`/users/${userId}/teams`);
|
|
const rawData = response.data.data || [];
|
|
// Unwrap nested team data if present (API returns [{team: {...}}] or [{id, name, ...}])
|
|
const teams = rawData.map((item: any) => item.team || item);
|
|
return { data: teams };
|
|
},
|
|
enabled: !!userId,
|
|
});
|
|
};
|