feat: integrate all backend APIs into frontend apps

- Fix API service paths to use /v1/iam/, /v1/dimentorin/, /v1/gacha/, /v1/hackathon/ prefixes
- Add roles, permissions, events, testimonials, sessions API services and hooks
- Rewrite gacha service with full CRUD + roll/claim endpoints
- Replace all mock data in backoffice pages with real API calls (accounts, roles, permissions, gacha-roll, dashboard, sessions, users-dimentorin, feedback-review, settings)
- Wire dimentorin mentoring list to useMentorList with search + pagination
- Wire dimentorin mentor detail page to useMentorById, pass real data to all sections
- Wire appointment modal to useBookSession with controlled schedule inputs
- Wire gacha app Spin Now to useExecuteGachaRoll, show credits and real items

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-05 17:08:58 +07:00
co-authored by Claude Sonnet 4.6
parent 8d2aaf5188
commit 53ad40f3fd
74 changed files with 3028 additions and 1942 deletions
+130 -292
View File
@@ -1,242 +1,140 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
import { backofficeApi, BackofficeApiResponse } from '../../api/backoffice';
import { useAuthStore } from './use-auth-store';
import {
postLogin,
postRegister,
postVerifyEmail,
postSendOtp,
postForgotPassword,
postNewPassword,
} from '../../api/auth';
import { getUserMe } from '../../api/users';
export * from './use-auth-store';
interface TokenInfo {
access_token: string;
refresh_token: string;
}
interface User {
id: string;
email: string;
fullname: string;
phone_number?: string;
avatar?: string;
birthdate?: string;
gender?: string;
is_active: boolean;
location?: string;
bio?: string;
skills?: string[];
role_id?: string;
created_at: string;
updated_at?: string;
}
interface AuthResponse {
token: TokenInfo;
user: User;
}
interface SessionResponse {
user: User;
}
interface MessageResponse {
message: string;
}
interface LoginRequest {
email: string;
password: string;
}
interface SignupRequest {
email: string;
password: string;
fullname: string;
}
interface GitHubAuthRequest {
code: string;
}
interface ForgotPasswordRequest {
email: string;
}
interface ResetPasswordRequest {
access_token: string;
new_password: string;
}
export const useLogin = () => {
const { setSession } = useAuthStore();
return useMutation({
mutationFn: async (data: LoginRequest) => {
const response = await hackathonApi.post<
HackathonApiResponse<AuthResponse>
>('/auth/login', data);
return response.data.data;
},
onSuccess: (data) => {
mutationFn: postLogin,
onSuccess: (res) => {
const u = res.data?.user;
const t = res.data?.token;
if (!u || !t) return;
setSession({
token: data.token,
token: t,
user: {
id: data.user.id,
email: data.user.email,
fullname: data.user.fullname,
phone_number: data.user.phone_number || '',
avatar: data.user.avatar || '',
birthdate: data.user.birthdate || '',
gender: data.user.gender || '',
is_active: data.user.is_active,
location: data.user.location,
bio: data.user.bio,
skills: data.user.skills,
role: {
id: '',
name: 'user',
permissions: [],
created_at: '',
updated_at: '',
},
id: u.id,
email: u.email,
fullname: u.fullname,
phone_number: u.phone_number || '',
avatar: u.avatar || '',
birthdate: u.birthdate || '',
gender: u.gender || '',
is_active: u.is_active,
location: u.location,
bio: u.bio,
skills: u.skills,
role: u.role ?? { id: '', name: 'user', permissions: [], created_at: '', updated_at: '' },
},
});
},
});
};
export const useSignup = () => {
return useMutation({
mutationFn: async (data: SignupRequest) => {
const response = await hackathonApi.post<
HackathonApiResponse<MessageResponse>
>('/auth/signup', data);
return response.data.data;
},
});
};
export const useGitHubCallback = () => {
const { setSession } = useAuthStore();
return useMutation({
mutationFn: async (data: GitHubAuthRequest) => {
const response = await hackathonApi.post<
HackathonApiResponse<AuthResponse>
>('/auth/github', data);
return response.data.data;
},
onSuccess: (data) => {
setSession({
token: data.token,
user: {
id: data.user.id,
email: data.user.email,
fullname: data.user.fullname,
phone_number: data.user.phone_number || '',
avatar: data.user.avatar || '',
birthdate: data.user.birthdate || '',
gender: data.user.gender || '',
is_active: data.user.is_active,
location: data.user.location,
bio: data.user.bio,
skills: data.user.skills,
role: {
id: '',
name: 'user',
permissions: [],
created_at: '',
updated_at: '',
},
},
});
},
});
};
export const useSessionQuery = () => {
const { session } = useAuthStore();
return useQuery({
queryKey: ['auth-session'],
queryFn: async () => {
const response = await hackathonApi.get<
HackathonApiResponse<SessionResponse>
>('/auth/session');
return response.data.data;
},
enabled: !!session?.token,
});
};
export const useForgotPassword = () => {
return useMutation({
mutationFn: async (data: ForgotPasswordRequest) => {
const response = await hackathonApi.post<
HackathonApiResponse<MessageResponse>
>('/auth/forgot-password', data);
return response.data.data;
},
});
};
export const useResetPassword = () => {
return useMutation({
mutationFn: async (data: ResetPasswordRequest) => {
const response = await hackathonApi.post<
HackathonApiResponse<MessageResponse>
>('/auth/reset-password', data);
return response.data.data;
},
});
};
export const useSignOut = () => {
const { clearSession } = useAuthStore();
return useMutation({
mutationFn: async () => {
clearSession();
return { success: true };
},
});
};
export const useBackofficeLogin = () => {
const { setSession } = useAuthStore();
return useMutation({
mutationFn: async (data: LoginRequest) => {
const response = await backofficeApi.post<
BackofficeApiResponse<AuthResponse>
>('/auth/login', data);
return response.data.data;
},
onSuccess: (data) => {
mutationFn: postLogin,
onSuccess: (res) => {
const u = res.data?.user;
const t = res.data?.token;
if (!u || !t) return;
setSession({
token: data.token,
token: t,
user: {
id: data.user.id,
email: data.user.email,
fullname: data.user.fullname,
phone_number: data.user.phone_number || '',
avatar: data.user.avatar || '',
birthdate: data.user.birthdate || '',
gender: data.user.gender || '',
is_active: data.user.is_active,
location: data.user.location,
bio: data.user.bio,
skills: data.user.skills,
role: {
id: data.user.role_id || '',
name: 'admin',
permissions: [],
created_at: '',
updated_at: '',
},
id: u.id,
email: u.email,
fullname: u.fullname,
phone_number: u.phone_number || '',
avatar: u.avatar || '',
birthdate: u.birthdate || '',
gender: u.gender || '',
is_active: u.is_active,
location: u.location,
bio: u.bio,
skills: u.skills,
role: u.role ?? { id: '', name: 'admin', permissions: [], created_at: '', updated_at: '' },
},
});
},
});
};
export const useSignup = () => {
return useMutation({ mutationFn: postRegister });
};
export const usePostLogin = () => {
return useMutation({
mutationFn: postLogin,
onSuccess: (res) => res,
});
};
export const usePostRegister = () => {
return useMutation({ mutationFn: postRegister });
};
export const usePostVerifyEmail = () => {
return useMutation({ mutationFn: postVerifyEmail });
};
export const usePostSendOtp = () => {
return useMutation({ mutationFn: postSendOtp });
};
export const useForgotPassword = () => {
return useMutation({ mutationFn: postForgotPassword });
};
export const useResetPassword = () => {
return useMutation({
mutationFn: (data: { token: string; password: string }) => postNewPassword(data),
});
};
export const useSignOut = () => {
const { clearSession } = useAuthStore();
return useMutation({
mutationFn: async () => {
clearSession();
return { success: true };
},
});
};
export const useSessionQuery = () => {
const { session } = useAuthStore();
return useQuery({
queryKey: ['auth-session'],
queryFn: async () => {
const user = await getUserMe();
return { user };
},
enabled: !!session?.token,
});
};
export const useGoogleCallback = () => {
return useMutation({
mutationFn: async () => {
throw new Error('Google OAuth not supported. Use GitHub OAuth instead.');
},
});
};
export const getGitHubOAuthUrl = (clientId: string, redirectUri: string) => {
const params = new URLSearchParams({
client_id: clientId,
@@ -257,21 +155,23 @@ export const useGitHubAuth = () => {
if (meta.env?.VITE_GITHUB_CLIENT_ID) clientId = meta.env.VITE_GITHUB_CLIENT_ID;
} catch { /* not in Vite context */ }
}
if (!clientId) {
throw new Error(
'GitHub Client ID not configured.'
);
}
if (!clientId) throw new Error('GitHub Client ID not configured.');
const redirectUri = `${globalThis.location.origin}/auth/callback`;
const url = getGitHubOAuthUrl(clientId, redirectUri);
return { url };
return { url: getGitHubOAuthUrl(clientId, redirectUri) };
};
return { signInWithGitHub };
};
return {
signInWithGitHub,
};
export const useGitHubCallback = () => {
const { setSession } = useAuthStore();
return useMutation({
mutationFn: async (data: { code: string }) => {
// GitHub OAuth callback is handled by the backend redirect.
// This hook exists for compatibility; in practice the backend
// redirects to the frontend with a token in the URL params.
throw new Error(`GitHub callback must be handled via backend redirect. Code: ${data.code}`);
},
});
};
export const useEmailAuth = () => {
@@ -282,82 +182,20 @@ export const useEmailAuth = () => {
const signInWithEmail = async (email: string, password: string) => {
const result = await loginMutation.mutateAsync({ email, password });
return {
user: result.user,
user: result.data?.user,
session: {
access_token: result.token.access_token,
refresh_token: result.token.refresh_token,
access_token: result.data?.token?.access_token,
refresh_token: result.data?.token?.refresh_token,
},
};
};
const signUpWithEmail = async (
email: string,
password: string,
fullname: string
) => {
const result = await signupMutation.mutateAsync({
email,
password,
fullname,
});
return {
message: result.message,
};
const signUpWithEmail = async (email: string, password: string, fullname: string) => {
const result = await signupMutation.mutateAsync({ email, password, fullname });
return { message: result.message };
};
const signOut = async () => {
clearSession();
};
const signOut = async () => { clearSession(); };
return {
signInWithEmail,
signUpWithEmail,
signOut,
};
};
export const usePostLogin = () => {
return useMutation({
mutationFn: async (data: LoginRequest) => {
const response = await hackathonApi.post<
HackathonApiResponse<AuthResponse>
>('/auth/login', data);
return { data: response.data.data };
},
});
};
export const usePostRegister = () => {
return useMutation({
mutationFn: async (data: SignupRequest) => {
const response = await hackathonApi.post<
HackathonApiResponse<AuthResponse>
>('/auth/signup', data);
return { data: response.data.data };
},
});
};
export const usePostVerifyEmail = () => {
return useMutation({
mutationFn: async () => {
throw new Error('Email verification not required with new backend');
},
});
};
export const usePostSendOtp = () => {
return useMutation({
mutationFn: async () => {
throw new Error('OTP not required with new backend');
},
});
};
export const useGoogleCallback = () => {
return useMutation({
mutationFn: async () => {
throw new Error('Google OAuth not supported. Use GitHub OAuth instead.');
},
});
return { signInWithEmail, signUpWithEmail, signOut };
};
+59
View File
@@ -0,0 +1,59 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getEventList,
getEventById,
createEvent,
updateEvent,
deleteEvent,
} from '../../api/events';
import type { TEventCreateRequest, TEventUpdateRequest } from '../../types/events';
import type { TPaginationParams } from '../../types/common';
export const eventKeys = {
all: ['events'] as const,
lists: () => [...eventKeys.all, 'list'] as const,
list: (params?: TPaginationParams) => [...eventKeys.lists(), params] as const,
detail: (id: string) => [...eventKeys.all, 'detail', id] as const,
};
export const useEventList = (params?: TPaginationParams) => {
return useQuery({
queryKey: eventKeys.list(params),
queryFn: () => getEventList(params),
});
};
export const useEventById = (id: string) => {
return useQuery({
queryKey: eventKeys.detail(id),
queryFn: () => getEventById(id),
enabled: !!id,
});
};
export const useCreateEvent = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: TEventCreateRequest) => createEvent(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: eventKeys.lists() }),
});
};
export const useUpdateEvent = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: TEventUpdateRequest }) => updateEvent(id, data),
onSuccess: (_, vars) => {
queryClient.invalidateQueries({ queryKey: eventKeys.lists() });
queryClient.invalidateQueries({ queryKey: eventKeys.detail(vars.id) });
},
});
};
export const useDeleteEvent = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteEvent(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: eventKeys.lists() }),
});
};
+109 -1
View File
@@ -1 +1,109 @@
export {};
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getUserCredits,
addCredits,
consumeCredit,
getGachaItemList,
getGachaItemById,
createGachaItem,
updateGachaItem,
deleteGachaItem,
executeGachaRoll,
createGachaClaim,
} from '../../api/gacha';
import type {
TGachaCreditAddRequest,
TGachaItemCreateRequest,
TGachaItemUpdateRequest,
TGachaClaimCreateRequest,
} from '../../types/gacha';
import type { TPaginationParams } from '../../types/common';
export const gachaKeys = {
credits: ['gacha-credits'] as const,
items: (params?: TPaginationParams) => ['gacha-items', params] as const,
item: (id: string) => ['gacha-item', id] as const,
};
// ----- Credits -----
export const useUserCredits = () => {
return useQuery({
queryKey: gachaKeys.credits,
queryFn: getUserCredits,
});
};
export const useAddCredits = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: TGachaCreditAddRequest) => addCredits(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: gachaKeys.credits }),
});
};
export const useConsumeCredit = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: consumeCredit,
onSuccess: () => queryClient.invalidateQueries({ queryKey: gachaKeys.credits }),
});
};
// ----- Items -----
export const useGachaItemList = (params?: TPaginationParams) => {
return useQuery({
queryKey: gachaKeys.items(params),
queryFn: () => getGachaItemList(params),
});
};
export const useGachaItemById = (id: string) => {
return useQuery({
queryKey: gachaKeys.item(id),
queryFn: () => getGachaItemById(id),
enabled: !!id,
});
};
export const useCreateGachaItem = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: TGachaItemCreateRequest) => createGachaItem(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['gacha-items'] }),
});
};
export const useUpdateGachaItem = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: TGachaItemUpdateRequest }) => updateGachaItem(id, data),
onSuccess: (_, vars) => {
queryClient.invalidateQueries({ queryKey: ['gacha-items'] });
queryClient.invalidateQueries({ queryKey: gachaKeys.item(vars.id) });
},
});
};
export const useDeleteGachaItem = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteGachaItem(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['gacha-items'] }),
});
};
// ----- Roll -----
export const useExecuteGachaRoll = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: executeGachaRoll,
onSuccess: () => queryClient.invalidateQueries({ queryKey: gachaKeys.credits }),
});
};
// ----- Claims -----
export const useCreateGachaClaim = () => {
return useMutation({
mutationFn: (data: TGachaClaimCreateRequest) => createGachaClaim(data),
});
};
+5
View File
@@ -7,3 +7,8 @@ export * from './teams';
export * from './messages';
export * from './winners';
export * from './use-session';
export * from './roles';
export * from './permissions';
export * from './events';
export * from './testimonials';
export * from './sessions';
+51 -24
View File
@@ -1,45 +1,72 @@
import { useQuery, useMutation, UseQueryResult, UseMutationResult, UseQueryOptions } from '@tanstack/react-query';
import { mentorService } from '../../api/mentors';
import { MentorDetailResponseDto, MentorUpdateRequestDto } from '../../types/mentors';
import { TResponseError } from '../../types/common';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getMentorMe,
getMentorById,
getMentorList,
updateMentorMe,
updateMentorById,
verifyMentor,
deleteMentor,
} from '../../api/mentors';
import type { MentorDetailResponseDto, MentorUpdateRequestDto } from '../../types/mentors';
import type { TPaginationParams } from '../../types/common';
export const useMentorMe = (options?: UseQueryOptions<MentorDetailResponseDto, TResponseError>): UseQueryResult<MentorDetailResponseDto, TResponseError> => {
export const useMentorMe = () => {
return useQuery({
queryKey: ['mentor-me'],
queryFn: () => mentorService.getMentorMe(),
...options,
queryFn: getMentorMe,
});
};
export const useMentorById = (id: string, options?: UseQueryOptions<MentorDetailResponseDto, TResponseError>): UseQueryResult<MentorDetailResponseDto, TResponseError> => {
export const useMentorById = (id: string) => {
return useQuery({
queryKey: ['mentor-by-id', id],
queryFn: () => mentorService.getMentorById(id),
queryFn: () => getMentorById(id),
enabled: !!id,
...options,
});
};
export const useUpdateMentorMe = (): UseMutationResult<
MentorDetailResponseDto,
TResponseError,
MentorUpdateRequestDto,
unknown
> => {
export const useMentorList = (params?: TPaginationParams) => {
return useQuery({
queryKey: ['mentor-list', params],
queryFn: () => getMentorList(params),
});
};
export const useUpdateMentorMe = () => {
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['update-mentor-me'],
mutationFn: (data) => mentorService.updateMentorMe(data),
mutationFn: (data: MentorUpdateRequestDto) => updateMentorMe(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['mentor-me'] }),
});
};
export const useUpdateMentorById = (): UseMutationResult<
MentorDetailResponseDto,
TResponseError,
{ id: string; data: MentorUpdateRequestDto },
unknown
> => {
export const useUpdateMentorById = () => {
const queryClient = useQueryClient();
return useMutation({
mutationKey: ['update-mentor-by-id'],
mutationFn: ({ id, data }) => mentorService.updateMentorById(id, data),
mutationFn: ({ id, data }: { id: string; data: MentorUpdateRequestDto }) =>
updateMentorById(id, data),
onSuccess: (_, vars) => {
queryClient.invalidateQueries({ queryKey: ['mentor-by-id', vars.id] });
queryClient.invalidateQueries({ queryKey: ['mentor-list'] });
},
});
};
export const useVerifyMentor = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => verifyMentor(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['mentor-list'] }),
});
};
export const useDeleteMentor = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteMentor(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['mentor-list'] }),
});
};
+10 -26
View File
@@ -1,5 +1,5 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
import { api } from '../../api/index';
import { useAuthStore } from '../auth';
export type Message = {
@@ -9,12 +9,7 @@ export type Message = {
message: string;
created_at: string;
updated_at: string;
user?: {
id: string;
fullname: string;
avatar: string;
email: string;
};
user?: { id: string; fullname: string; avatar: string; email: string };
};
export const messageKeys = {
@@ -22,13 +17,13 @@ export const messageKeys = {
team: (teamId: string) => [...messageKeys.all, 'team', teamId] as const,
};
interface ApiResp<T> { data: T; message?: string; }
export const useTeamMessages = (teamId: string) => {
return useQuery({
queryKey: messageKeys.team(teamId),
queryFn: async () => {
const response = await hackathonApi.get<HackathonApiResponse<Message[]>>(
`/chat/teams/${teamId}`
);
const response = await api.get<ApiResp<Message[]>>(`/v1/hackathon/chat/teams/${teamId}`);
return response.data.data || [];
},
enabled: !!teamId,
@@ -43,20 +38,11 @@ export const useSendMessage = (teamId: string) => {
return useMutation({
mutationFn: async (message: string) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to send messages');
}
const response = await hackathonApi.post<HackathonApiResponse<Message>>(
`/chat/teams/${teamId}`,
{ message }
);
if (!session?.user?.id) throw new Error('You must be logged in to send messages');
const response = await api.post<ApiResp<Message>>(`/v1/hackathon/chat/teams/${teamId}`, { message });
return response.data.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) });
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) }),
});
};
@@ -65,10 +51,8 @@ export const useDeleteMessage = (teamId: string) => {
return useMutation({
mutationFn: async (messageId: string) => {
await hackathonApi.delete(`/chat/messages/${messageId}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) });
await api.delete(`/v1/hackathon/chat/messages/${messageId}`);
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) }),
});
};
@@ -0,0 +1,59 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getPermissionList,
getPermissionById,
createPermission,
updatePermission,
deletePermission,
} from '../../api/permissions';
import type { TPermissionCreateRequest, TPermissionUpdateRequest } from '../../types/permissions';
import type { TPaginationParams } from '../../types/common';
export const permissionKeys = {
all: ['permissions'] as const,
lists: () => [...permissionKeys.all, 'list'] as const,
list: (params?: TPaginationParams) => [...permissionKeys.lists(), params] as const,
detail: (id: string) => [...permissionKeys.all, 'detail', id] as const,
};
export const usePermissionList = (params?: TPaginationParams) => {
return useQuery({
queryKey: permissionKeys.list(params),
queryFn: () => getPermissionList(params),
});
};
export const usePermissionById = (id: string) => {
return useQuery({
queryKey: permissionKeys.detail(id),
queryFn: () => getPermissionById(id),
enabled: !!id,
});
};
export const useCreatePermission = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: TPermissionCreateRequest) => createPermission(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: permissionKeys.lists() }),
});
};
export const useUpdatePermission = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: TPermissionUpdateRequest }) => updatePermission(id, data),
onSuccess: (_, vars) => {
queryClient.invalidateQueries({ queryKey: permissionKeys.lists() });
queryClient.invalidateQueries({ queryKey: permissionKeys.detail(vars.id) });
},
});
};
export const useDeletePermission = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deletePermission(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: permissionKeys.lists() }),
});
};
+53
View File
@@ -0,0 +1,53 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getRoleList, getRoleById, createRole, updateRole, deleteRole } from '../../api/roles';
import type { TRoleCreateRequest, TRoleUpdateRequest } from '../../types/roles';
import type { TPaginationParams } from '../../types/common';
export const roleKeys = {
all: ['roles'] as const,
lists: () => [...roleKeys.all, 'list'] as const,
list: (params?: TPaginationParams) => [...roleKeys.lists(), params] as const,
detail: (id: string) => [...roleKeys.all, 'detail', id] as const,
};
export const useRoleList = (params?: TPaginationParams) => {
return useQuery({
queryKey: roleKeys.list(params),
queryFn: () => getRoleList(params),
});
};
export const useRoleById = (id: string) => {
return useQuery({
queryKey: roleKeys.detail(id),
queryFn: () => getRoleById(id),
enabled: !!id,
});
};
export const useCreateRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: TRoleCreateRequest) => createRole(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: roleKeys.lists() }),
});
};
export const useUpdateRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: TRoleUpdateRequest }) => updateRole(id, data),
onSuccess: (_, vars) => {
queryClient.invalidateQueries({ queryKey: roleKeys.lists() });
queryClient.invalidateQueries({ queryKey: roleKeys.detail(vars.id) });
},
});
};
export const useDeleteRole = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteRole(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: roleKeys.lists() }),
});
};
+73
View File
@@ -0,0 +1,73 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getMentorAvailability,
bookSession,
getMentorSessions,
getMySessions,
updateSessionStatus,
submitFeedback,
} from '../../api/sessions';
import type {
TBookSessionRequest,
TUpdateSessionStatusRequest,
TSessionFeedbackRequest,
} from '../../types/sessions';
export const sessionKeys = {
all: ['sessions'] as const,
mine: (params?: Record<string, unknown>) => [...sessionKeys.all, 'mine', params] as const,
mentor: (id: string) => [...sessionKeys.all, 'mentor', id] as const,
availability: (id: string) => [...sessionKeys.all, 'availability', id] as const,
};
export const useMentorAvailability = (mentorId: string) => {
return useQuery({
queryKey: sessionKeys.availability(mentorId),
queryFn: () => getMentorAvailability(mentorId),
enabled: !!mentorId,
});
};
export const useBookSession = (mentorId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: TBookSessionRequest) => bookSession(mentorId, data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: sessionKeys.mentor(mentorId) });
queryClient.invalidateQueries({ queryKey: sessionKeys.all });
},
});
};
export const useMentorSessions = (mentorId: string, params?: { status?: string }) => {
return useQuery({
queryKey: sessionKeys.mentor(mentorId),
queryFn: () => getMentorSessions(mentorId, params),
enabled: !!mentorId,
});
};
export const useMySessions = (params?: { status?: string }) => {
return useQuery({
queryKey: sessionKeys.mine(params as Record<string, unknown>),
queryFn: () => getMySessions(params),
});
};
export const useUpdateSessionStatus = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: TUpdateSessionStatusRequest }) =>
updateSessionStatus(id, data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: sessionKeys.all }),
});
};
export const useSubmitFeedback = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: TSessionFeedbackRequest }) =>
submitFeedback(id, data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: sessionKeys.all }),
});
};
+74 -259
View File
@@ -1,5 +1,5 @@
import { useMutation, useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
import { api } from '../../api/index';
import { useAuthStore } from '../auth';
import type {
TCreateTeamRequest,
@@ -23,110 +23,41 @@ export const teamKeys = {
};
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;
};
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;
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;
};
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;
};
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;
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;
}
interface ListMeta { page: number; per_page: number; total_page: number; total_data: number; }
interface ListResponse<T> { message: string; data: T[]; meta: ListMeta; }
interface ApiResp<T> { data: T; message?: string; }
interface ListResponseWithMeta<T> {
message: string;
data: T[];
meta: {
page: number;
per_page: number;
total_page: number;
total_data: number;
};
}
const TEAMS_PAGE_SIZE = 12;
export const useTeams = (params?: {
page?: number;
limit?: number;
city?: string;
visibility?: string;
search?: string;
minMembers?: number;
maxMembers?: number;
hasSubmission?: boolean;
page?: number; limit?: number; city?: string; visibility?: string; search?: string;
minMembers?: number; maxMembers?: number; hasSubmission?: boolean;
}) => {
return useQuery({
queryKey: teamKeys.list(params),
queryKey: teamKeys.list(params as Record<string, unknown>),
queryFn: async () => {
const queryParams = new URLSearchParams();
if (params?.page) queryParams.append('page', String(params.page));
@@ -137,11 +68,10 @@ export const useTeams = (params?: {
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 qs = queryParams.toString();
const response = await api.get<ListResponse<Team>>(
`/v1/hackathon/teams/browse${qs ? `?${qs}` : ''}`
);
const { data, meta } = response.data;
return {
teams: data || [],
@@ -154,13 +84,7 @@ export const useTeams = (params?: {
});
};
const TEAMS_PAGE_SIZE = 12;
export const useInfiniteTeams = (params?: {
city?: string;
visibility?: string;
search?: string;
}) => {
export const useInfiniteTeams = (params?: { city?: string; visibility?: string; search?: string }) => {
return useInfiniteQuery({
queryKey: [...teamKeys.lists(), 'infinite', params],
queryFn: async ({ pageParam = 1 }) => {
@@ -170,19 +94,12 @@ export const useInfiniteTeams = (params?: {
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 response = await api.get<ApiResp<Team[]>>(`/v1/hackathon/teams/browse?${queryParams}`);
const teams = response.data.data || [];
return {
data: teams,
nextPage: teams.length === TEAMS_PAGE_SIZE ? pageParam + 1 : undefined,
};
return { data: teams, nextPage: teams.length === TEAMS_PAGE_SIZE ? pageParam + 1 : undefined };
},
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
getNextPageParam: (lastPage: { nextPage?: number }) => lastPage.nextPage,
});
};
@@ -190,7 +107,7 @@ export const useTeamById = (teamId: string, enabled = true) => {
return useQuery({
queryKey: teamKeys.detail(teamId),
queryFn: async () => {
const response = await hackathonApi.get<HackathonApiResponse<Team>>(`/teams/${teamId}`);
const response = await api.get<ApiResp<Team>>(`/v1/hackathon/teams/${teamId}`);
return { data: response.data.data };
},
enabled: enabled && !!teamId,
@@ -200,22 +117,10 @@ export const useTeamById = (teamId: string, enabled = true) => {
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,
});
if (!session?.user?.id) throw new Error('You must be logged in to create a team');
const response = await api.post<ApiResp<Team>>('/v1/hackathon/teams', data);
return { data: response.data.data };
},
onSuccess: () => {
@@ -228,22 +133,10 @@ export const useCreateTeam = () => {
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,
});
if (!session?.user?.id) throw new Error('You must be logged in to update a team');
const response = await api.put<ApiResp<Team>>(`/v1/hackathon/teams/${teamId}`, data);
return { data: response.data.data };
},
onSuccess: () => {
@@ -257,7 +150,7 @@ export const useTeamMembers = (teamId: string, enabled = true) => {
return useQuery({
queryKey: teamKeys.members(teamId),
queryFn: async () => {
const response = await hackathonApi.get<HackathonApiResponse<Team>>(`/teams/${teamId}`);
const response = await api.get<ApiResp<Team>>(`/v1/hackathon/teams/${teamId}`);
return { data: response.data.data?.members || [] };
},
enabled: enabled && !!teamId,
@@ -267,32 +160,26 @@ export const useTeamMembers = (teamId: string, enabled = true) => {
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`,
if (!session?.user?.id) throw new Error('You must be logged in to invite a member');
const response = await api.post<ApiResp<Invitation>>(
`/v1/hackathon/invitations/teams/${teamId}/invite`,
{ invitee_email: data.email }
);
return { data: response.data.data };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
},
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 } }) => {
throw new Error('Manage member functionality not yet implemented in backend');
// Remove member is the available operation; role management not exposed by backend
await api.delete(`/v1/hackathon/teams/${teamId}/members/${userId}`);
return { success: true };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.members(teamId) });
@@ -304,14 +191,10 @@ export const useManageMember = (teamId: string) => {
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}`);
if (!session?.user?.id) throw new Error('You must be logged in to remove a member');
await api.delete(`/v1/hackathon/teams/${teamId}/members/${userId}`);
return { success: true };
},
onSuccess: () => {
@@ -324,23 +207,16 @@ export const useRemoveMember = (teamId: string) => {
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}`,
if (!session?.user?.id) throw new Error('You must be logged in to join a team');
const response = await api.post<ApiResp<JoinRequest>>(
`/v1/hackathon/join-requests/teams/${teamId}`,
{ message: data.message }
);
return { data: response.data.data };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.lists() });
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: teamKeys.lists() }),
});
};
@@ -348,8 +224,8 @@ 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`
const response = await api.get<ApiResp<JoinRequest[]>>(
`/v1/hackathon/join-requests/teams/${teamId}/pending`
);
return { data: response.data.data || [] };
},
@@ -360,19 +236,11 @@ export const useTeamJoinRequests = (teamId: string, enabled = true) => {
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');
}
if (!session?.user?.id) throw new Error('You must be logged in to respond to join requests');
const backendAction = action === 'approve' ? 'accept' : 'reject';
await hackathonApi.post(`/join-requests/${requestId}/respond`, {
action: backendAction,
});
await api.post(`/v1/hackathon/join-requests/${requestId}/respond`, { action: backendAction });
return { success: true, action };
},
onSuccess: () => {
@@ -385,11 +253,10 @@ export const useRespondToJoinRequest = (teamId: string) => {
export const useMyInvitations = () => {
const { session } = useAuthStore();
return useQuery({
queryKey: teamKeys.myInvitations(),
queryFn: async () => {
const response = await hackathonApi.get<HackathonApiResponse<Invitation[]>>('/invitations/my');
const response = await api.get<ApiResp<Invitation[]>>('/v1/hackathon/invitations/my');
return { data: response.data.data || [] };
},
enabled: !!session?.user?.id,
@@ -399,15 +266,10 @@ export const useMyInvitations = () => {
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 });
if (!session?.user?.id) throw new Error('User not authenticated');
await api.post(`/v1/hackathon/invitations/${invitationId}/respond`, { action });
return { success: true, action };
},
onSuccess: () => {
@@ -420,13 +282,12 @@ export const useRespondToInvitation = () => {
export const useMyTeams = () => {
const { session } = useAuthStore();
return useQuery({
queryKey: teamKeys.myTeams(),
queryFn: async () => {
const response = await hackathonApi.get<HackathonApiResponse<any[]>>('/teams/my');
const response = await api.get<ApiResp<unknown[]>>('/v1/hackathon/teams/my');
const rawData = response.data.data || [];
const teams = rawData.map((item: any) => item.team || item);
const teams = rawData.map((item: unknown) => (item as Record<string, unknown>).team ?? item);
return { data: teams };
},
enabled: !!session?.user?.id,
@@ -435,58 +296,22 @@ export const useMyTeams = () => {
export const useSubmitProject = (teamId: string) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: TSubmitProjectRequest) => {
let submissionId: string;
try {
const existingResponse = await hackathonApi.get<HackathonApiResponse<Submission | null>>(
`/submissions/teams/${teamId}`
);
if (existingResponse.data.data?.id) {
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');
}
const existing = await api.get<ApiResp<Submission | null>>(`/v1/hackathon/submissions/teams/${teamId}`);
if (existing.data.data?.id) {
const res = await api.put<ApiResp<Submission>>(`/v1/hackathon/submissions/${existing.data.data.id}`, data);
submissionId = res.data.data.id;
} else throw new Error('No existing submission');
} catch {
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;
const res = await api.post<ApiResp<Submission>>(`/v1/hackathon/submissions/teams/${teamId}`, data);
submissionId = res.data.data.id;
}
await hackathonApi.post<HackathonApiResponse<Submission>>(
`/submissions/${submissionId}/submit`
);
const finalResponse = await hackathonApi.post<HackathonApiResponse<Submission>>(
`/submissions/${submissionId}/confirm`
);
return { data: finalResponse.data.data };
await api.post<ApiResp<Submission>>(`/v1/hackathon/submissions/${submissionId}/submit`);
const final = await api.post<ApiResp<Submission>>(`/v1/hackathon/submissions/${submissionId}/confirm`);
return { data: final.data.data };
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: teamKeys.submission(teamId) });
@@ -499,9 +324,7 @@ 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}`
);
const response = await api.get<ApiResp<Submission | null>>(`/v1/hackathon/submissions/teams/${teamId}`);
return { data: response.data.data };
},
enabled: enabled && !!teamId,
@@ -511,14 +334,10 @@ export const useTeamSubmission = (teamId: string, enabled = true) => {
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');
}
await hackathonApi.post(`/teams/${teamId}/leave`);
if (!session?.user?.id) throw new Error('You must be logged in to leave a team');
await api.post(`/v1/hackathon/teams/${teamId}/leave`);
return { success: true };
},
onSuccess: () => {
@@ -531,14 +350,10 @@ export const useLeaveTeam = () => {
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}`);
if (!session?.user?.id) throw new Error('You must be logged in to delete a team');
await api.delete(`/v1/hackathon/teams/${teamId}`);
return { success: true };
},
onSuccess: () => {
@@ -552,9 +367,9 @@ 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 response = await api.get<ApiResp<unknown[]>>(`/v1/hackathon/users/${userId}/teams`);
const rawData = response.data.data || [];
const teams = rawData.map((item: any) => item.team || item);
const teams = rawData.map((item: unknown) => (item as Record<string, unknown>).team ?? item);
return { data: teams };
},
enabled: !!userId,
@@ -0,0 +1,59 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getTestimonialList,
getTestimonialById,
createTestimonial,
updateTestimonial,
deleteTestimonial,
} from '../../api/testimonials';
import type { TTestimonialCreateRequest, TTestimonialUpdateRequest } from '../../types/testimonials';
import type { TPaginationParams } from '../../types/common';
export const testimonialKeys = {
all: ['testimonials'] as const,
lists: () => [...testimonialKeys.all, 'list'] as const,
list: (params?: TPaginationParams) => [...testimonialKeys.lists(), params] as const,
detail: (id: string) => [...testimonialKeys.all, 'detail', id] as const,
};
export const useTestimonialList = (params?: TPaginationParams) => {
return useQuery({
queryKey: testimonialKeys.list(params),
queryFn: () => getTestimonialList(params),
});
};
export const useTestimonialById = (id: string) => {
return useQuery({
queryKey: testimonialKeys.detail(id),
queryFn: () => getTestimonialById(id),
enabled: !!id,
});
};
export const useCreateTestimonial = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: TTestimonialCreateRequest) => createTestimonial(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: testimonialKeys.lists() }),
});
};
export const useUpdateTestimonial = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: TTestimonialUpdateRequest }) => updateTestimonial(id, data),
onSuccess: (_, vars) => {
queryClient.invalidateQueries({ queryKey: testimonialKeys.lists() });
queryClient.invalidateQueries({ queryKey: testimonialKeys.detail(vars.id) });
},
});
};
export const useDeleteTestimonial = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteTestimonial(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: testimonialKeys.lists() }),
});
};
+34 -158
View File
@@ -1,208 +1,84 @@
import { useMutation } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
import { useAuthStore } from '../auth';
interface UploadResponse {
url: string;
}
const fileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
const base64 = (reader.result as string).split(',')[1];
resolve(base64);
};
reader.onerror = (error) => reject(error);
});
};
import {
uploadHackathonAvatar,
uploadHackathonTeamFile,
uploadHackathonSubmission,
uploadHackathonFile,
uploadUserFile,
} from '../../api/upload';
export const useUploadFile = () => {
const { session } = useAuthStore();
return useMutation({
mutationKey: ['upload-file'],
mutationFn: async (file: File) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to upload files');
}
const base64Data = await fileToBase64(file);
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
'/upload/team',
{
filename: file.name,
content_type: file.type,
data: base64Data,
}
);
return { data: { url: response.data.data.url } };
if (!session?.user?.id) throw new Error('You must be logged in to upload files');
const data = await uploadHackathonTeamFile(file);
return { data };
},
});
};
export const useUploadAvatar = () => {
const { session } = useAuthStore();
return useMutation({
mutationKey: ['upload-avatar'],
mutationFn: async (file: File) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to upload avatar');
}
if (!session?.user?.id) throw new Error('You must be logged in to upload avatar');
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type. Allowed types: JPEG, PNG, WebP, GIF');
}
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('File too large. Maximum size: 5MB');
}
const base64Data = await fileToBase64(file);
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
'/upload/avatar',
{
filename: file.name,
content_type: file.type,
data: base64Data,
}
);
return { data: { url: response.data.data.url } };
if (!allowedTypes.includes(file.type)) throw new Error('Invalid file type. Allowed: JPEG, PNG, WebP, GIF');
if (file.size > 5 * 1024 * 1024) throw new Error('File too large. Maximum size: 5MB');
const data = await uploadHackathonAvatar(file);
return { data };
},
});
};
export const useUploadTeamFile = () => {
const { session } = useAuthStore();
return useMutation({
mutationKey: ['upload-team-file'],
mutationFn: async (file: File) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to upload files');
}
const allowedTypes = [
'image/jpeg',
'image/jpg',
'image/png',
'image/webp',
'image/gif',
'application/pdf',
];
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type. Allowed types: JPEG, PNG, WebP, GIF, PDF');
}
const maxSize = 20 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('File too large. Maximum size: 20MB');
}
const base64Data = await fileToBase64(file);
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
'/upload/team',
{
filename: file.name,
content_type: file.type,
data: base64Data,
}
);
return { data: { url: response.data.data.url } };
if (!session?.user?.id) throw new Error('You must be logged in to upload files');
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif', 'application/pdf'];
if (!allowedTypes.includes(file.type)) throw new Error('Invalid file type. Allowed: JPEG, PNG, WebP, GIF, PDF');
if (file.size > 20 * 1024 * 1024) throw new Error('File too large. Maximum size: 20MB');
const data = await uploadHackathonTeamFile(file);
return { data };
},
});
};
export const useUploadSubmission = () => {
const { session } = useAuthStore();
return useMutation({
mutationKey: ['upload-submission'],
mutationFn: async (file: File) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to upload submissions');
}
if (!session?.user?.id) throw new Error('You must be logged in to upload submissions');
const allowedTypes = [
'image/jpeg',
'image/jpg',
'image/png',
'image/webp',
'image/gif',
'application/pdf',
'application/zip',
'application/x-zip-compressed',
'video/mp4',
'video/webm',
'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif',
'application/pdf', 'application/zip', 'application/x-zip-compressed',
'video/mp4', 'video/webm',
];
if (!allowedTypes.includes(file.type)) {
throw new Error(
'Invalid file type. Allowed types: Images, PDF, ZIP, MP4, WebM'
);
}
const maxSize = 50 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('File too large. Maximum size: 50MB');
}
const base64Data = await fileToBase64(file);
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
'/upload/submission',
{
filename: file.name,
content_type: file.type,
data: base64Data,
}
);
return { data: { url: response.data.data.url } };
if (!allowedTypes.includes(file.type)) throw new Error('Invalid file type. Allowed: Images, PDF, ZIP, MP4, WebM');
if (file.size > 50 * 1024 * 1024) throw new Error('File too large. Maximum size: 50MB');
const data = await uploadHackathonSubmission(file);
return { data };
},
});
};
export const useUploadCV = () => {
const { session } = useAuthStore();
return useMutation({
mutationKey: ['upload-cv'],
mutationFn: async (file: File) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to upload CV');
}
if (file.type !== 'application/pdf') {
throw new Error('CV must be a PDF file');
}
const maxSize = 20 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('File too large. Maximum size: 20MB');
}
const base64Data = await fileToBase64(file);
const response = await hackathonApi.post<HackathonApiResponse<UploadResponse>>(
'/upload/team',
{
filename: file.name,
content_type: file.type,
data: base64Data,
}
);
return { data: { url: response.data.data.url } };
if (!session?.user?.id) throw new Error('You must be logged in to upload CV');
if (file.type !== 'application/pdf') throw new Error('CV must be a PDF file');
if (file.size > 20 * 1024 * 1024) throw new Error('File too large. Maximum size: 20MB');
const data = await uploadUserFile(file);
return { data };
},
});
};
+49 -94
View File
@@ -1,69 +1,30 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
import { useAuthStore } from '../auth';
interface User {
id: string;
email: string;
fullname: string;
bio?: string;
location?: string;
avatar?: string;
skills?: string[];
created_at: string;
updated_at?: string;
}
interface CertificateUserData {
id: string;
fullname: string;
email: string;
avatar?: string;
}
interface CertificateTeamData {
id: string;
name: string;
logo?: string;
is_leader: boolean;
}
interface CertificateSubmissionData {
id: string;
title: string;
description: string;
repository_url?: string;
demo_url?: string;
}
interface CertificateWinnerData {
rank: number;
prize?: string;
}
import {
getUserMe,
getUserById,
getUserList,
updateUserMe,
updateUserById,
} from '../../api/users';
import { api } from '../../api/index';
import type { TApiPaginated, TPaginationParams } from '../../types/common';
import type { TUsersDetailItem, TUserUpdateRequest } from '../../types/users';
export interface CertificatePublicData {
user: CertificateUserData;
team?: CertificateTeamData;
submission?: CertificateSubmissionData;
winner?: CertificateWinnerData;
}
interface UpdateUserRequest {
fullname?: string;
bio?: string;
location?: string;
avatar?: string;
skills?: string[];
user: { id: string; fullname: string; email: string; avatar?: string };
team?: { id: string; name: string; logo?: string; is_leader: boolean };
submission?: { id: string; title: string; description: string; repository_url?: string; demo_url?: string };
winner?: { rank: number; prize?: string };
}
export const useUserMe = () => {
const { session } = useAuthStore();
return useQuery({
queryKey: ['user-me'],
queryFn: async () => {
const response = await hackathonApi.get<HackathonApiResponse<User>>('/users/me');
return { data: response.data.data };
const data = await getUserMe();
return { data };
},
enabled: !!session?.user?.id,
});
@@ -73,45 +34,52 @@ export const useUserById = (id: string) => {
return useQuery({
queryKey: ['user-by-id', id],
queryFn: async () => {
const response = await hackathonApi.get<HackathonApiResponse<User>>(`/users/${id}`);
return { data: response.data.data };
const data = await getUserById(id);
return { data };
},
enabled: !!id,
});
};
export const useUserDetailsById = (userId: string) => {
return useQuery({
queryKey: ['user-details', userId],
queryFn: async () => {
const data = await getUserById(userId);
return { data };
},
enabled: !!userId,
});
};
export const useUserList = (params?: TPaginationParams) => {
return useQuery({
queryKey: ['user-list', params],
queryFn: () => getUserList(params),
});
};
export const useUpdateUserMe = () => {
const queryClient = useQueryClient();
const { session, setSession } = useAuthStore();
return useMutation({
mutationKey: ['update-user-me'],
mutationFn: async (data: UpdateUserRequest) => {
if (!session?.user?.id) {
throw new Error('You must be logged in to update profile');
}
const response = await hackathonApi.put<HackathonApiResponse<User>>('/users/me', {
fullname: data.fullname,
bio: data.bio,
location: data.location,
avatar: data.avatar,
skills: data.skills,
});
return { data: response.data.data };
mutationFn: (data: TUserUpdateRequest) => {
if (!session?.user?.id) throw new Error('You must be logged in to update profile');
return updateUserMe(data);
},
onSuccess: (result) => {
if (session?.user && result.data) {
if (session?.user && result) {
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 || [],
fullname: result.fullname || session.user.fullname,
bio: result.profile_extension?.bio || '',
location: result.profile_extension?.location || '',
avatar: result.avatar || session.user.avatar,
skills: result.profile_extension?.skills || [],
},
});
}
@@ -125,33 +93,20 @@ export const useUpdateUserById = () => {
return useMutation({
mutationKey: ['update-user-by-id'],
mutationFn: async ({ id, data }: { id: string; data: UpdateUserRequest }) => {
const response = await hackathonApi.put<HackathonApiResponse<User>>(`/users/${id}`, data);
return { data: response.data.data };
},
mutationFn: ({ id, data }: { id: string; data: TUserUpdateRequest }) => updateUserById(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['user-by-id', variables.id] });
queryClient.invalidateQueries({ queryKey: ['user-list'] });
},
});
};
export const useUserDetailsById = (userId: string) => {
return useQuery({
queryKey: ['user-details', userId],
queryFn: async () => {
const response = await hackathonApi.get<HackathonApiResponse<User>>(`/users/${userId}`);
return { data: response.data.data };
},
enabled: !!userId,
});
};
export const useCertificatePublicData = (userId: string, enabled = true) => {
return useQuery({
queryKey: ['certificate-public-data', userId],
queryFn: async () => {
const response = await hackathonApi.get<HackathonApiResponse<CertificatePublicData>>(
`/certificates/${userId}`
const response = await api.get<{ data: CertificatePublicData }>(
`/v1/hackathon/certificates/${userId}`
);
return { data: response.data.data };
},
+7 -22
View File
@@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
import { api } from '../../api/index';
export const winnerKeys = {
all: ['winners'] as const,
@@ -7,34 +7,19 @@ export const winnerKeys = {
};
interface Team {
id: string;
name: string;
description: string;
city: string;
visibility: string;
logo: string;
banner: string;
leader_id: string;
created_at: string;
updated_at: string;
id: string; name: string; description: string; city: string; visibility: string;
logo: string; banner: string; leader_id: string; created_at: string; updated_at: string;
}
interface Winner {
id: string;
team_id: string;
team: Team;
rank: number;
prize: string;
announced_at: string;
created_at: string;
updated_at: string;
id: string; team_id: string; team: Team; rank: number; prize: string;
announced_at: string; created_at: string; updated_at: string;
}
export const useWinners = () => {
return useQuery<HackathonApiResponse<Winner[]>>({
return useQuery({
queryKey: winnerKeys.lists(),
queryFn: async () => {
const response = await hackathonApi.get('/winners');
const response = await api.get<{ data: Winner[] }>('/v1/hackathon/winners');
return response.data;
},
});