chore: upgrade dependencies, restructure shared libs, and fix UI

- Upgrade Nx 22.1.1 → 22.6.3 and all patch/minor dependencies
- Restructure shared libs: move business logic from utils to service
- Consolidate shadcn-ui into ui lib with atomic design pattern
- Fix container centering for landing app (Tailwind v4 compatibility)
- Fix button styling by updating @source directive in globals.css
- Fix SiCss3 → SiCss rename in react-icons 5.6
- Fix duplicate useSession export conflict
- Remove dead code, comments, and unused files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-03-31 01:44:17 +07:00
co-authored by Claude Opus 4.6
parent f68d97188c
commit 3f4461c65c
231 changed files with 6062 additions and 39166 deletions
-3
View File
@@ -7,7 +7,6 @@ import type {
const ADMIN_BASE_URL = '/admin';
// Admin Users
export const getAdminUsers = async (params?: {
page?: number;
per_page?: number;
@@ -26,7 +25,6 @@ export const getAdminUsers = async (params?: {
return response.data;
};
// Admin Teams
export const getAdminTeams = async (params?: {
page?: number;
per_page?: number;
@@ -39,7 +37,6 @@ export const getAdminTeams = async (params?: {
return response.data;
};
// Admin Submissions
export const getAdminSubmissions = async (params?: {
page?: number;
per_page?: number;
+3 -3
View File
@@ -1,4 +1,4 @@
import { api } from '../';
import { api, getBaseURL } from '../';
import {
TLoginRequest,
TLoginResponse,
@@ -54,7 +54,7 @@ export const postSendOtp = async (
};
export const getGoogleAuthUrl = async (): Promise<string> => {
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
const baseUrl = getBaseURL() || 'http://localhost:8080';
return `${baseUrl}/auth/google/login`;
};
@@ -67,7 +67,7 @@ export const postGoogleCallback = async (code: string, state: string): Promise<T
};
export const getGitHubAuthUrl = async (): Promise<string> => {
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
const baseUrl = getBaseURL() || 'http://localhost:8080';
return `${baseUrl}/auth/github/login`;
};
-9
View File
@@ -1,11 +1,8 @@
import axios from 'axios';
import { useAuthStore } from '../hooks/auth';
// Backoffice Backend API Base URL
// In development, use proxy; in production, use full URL
const BACKOFFICE_API_URL = 'https://api.hackathon.imphnen.dev/api/v1';
// Create axios instance for backoffice backend
export const backofficeApi = axios.create({
baseURL: BACKOFFICE_API_URL,
headers: {
@@ -13,7 +10,6 @@ export const backofficeApi = axios.create({
},
});
// Add auth token interceptor
backofficeApi.interceptors.request.use(
(config) => {
const { session } = useAuthStore.getState();
@@ -27,11 +23,9 @@ backofficeApi.interceptors.request.use(
}
);
// Error handling interceptor
backofficeApi.interceptors.response.use(
(response) => response,
(error) => {
// Handle 401 - clear session and redirect to login
if (error.response?.status === 401) {
const isAuthPage =
globalThis.window !== undefined &&
@@ -45,18 +39,15 @@ backofficeApi.interceptors.response.use(
}
}
// If backend sends a message, use it
const backendMsg = error?.response?.data?.message;
if (backendMsg && typeof backendMsg === 'string') {
return Promise.reject(new Error(backendMsg));
}
// Fallback error message
return Promise.reject(new Error(error.message || 'An error occurred'));
}
);
// Response type
export interface BackofficeApiResponse<T> {
data: T;
message: string;
-9
View File
@@ -1,10 +1,8 @@
import axios from 'axios';
import { useAuthStore } from '../hooks/auth';
// Hackathon Backend API Base URL
const HACKATHON_API_URL = 'https://api.hackathon.imphnen.dev/api/v1';
// Create axios instance for hackathon backend
export const hackathonApi = axios.create({
baseURL: HACKATHON_API_URL,
headers: {
@@ -12,7 +10,6 @@ export const hackathonApi = axios.create({
},
});
// Add auth token interceptor
hackathonApi.interceptors.request.use(
(config) => {
const { session } = useAuthStore.getState();
@@ -26,17 +23,13 @@ hackathonApi.interceptors.request.use(
}
);
// Error handling interceptor
hackathonApi.interceptors.response.use(
(response) => response,
(error) => {
// Handle 401 - clear session and redirect to login
// But skip redirect if already on auth pages or certificate pages (to avoid reload on login failure)
if (error.response?.status === 401) {
const isAuthPage = globalThis.window !== undefined && globalThis.location.pathname.startsWith('/auth');
const isCertificatePage = globalThis.window !== undefined && globalThis.location.pathname.startsWith('/certificate/');
// Only clear session and redirect if not on auth page or certificate page
if (!isAuthPage && !isCertificatePage) {
useAuthStore.getState().clearSession();
if (globalThis.window !== undefined) {
@@ -45,7 +38,6 @@ hackathonApi.interceptors.response.use(
}
}
// If backend sends a message, use it
const backendMsg = error?.response?.data?.message;
if (backendMsg && typeof backendMsg === 'string') {
return Promise.reject(new Error(backendMsg));
@@ -55,7 +47,6 @@ hackathonApi.interceptors.response.use(
}
);
// API Response wrapper type
export interface HackathonApiResponse<T> {
data: T;
message?: string;
+15 -12
View File
@@ -8,7 +8,6 @@ export * from './upload';
export * from './hackathon';
export * from './admin';
// Common API response wrapper interface
export interface ApiResponse<T> {
data: T;
version: string;
@@ -16,7 +15,6 @@ export interface ApiResponse<T> {
const TOKEN_KEY = 'token';
// Helper functions for session management (avoiding circular dependency)
const getSessionTokenFromCookies = () => {
if (typeof document === 'undefined') return null;
@@ -54,13 +52,25 @@ const removeSessionTokenFromCookies = () => {
document.cookie = `${TOKEN_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
};
export const getBaseURL = () => {
if (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_API_URL) {
return process.env.NEXT_PUBLIC_API_URL;
}
try {
const meta = import.meta as unknown as Record<string, Record<string, string>>;
if (meta.env?.VITE_API_URL) return meta.env.VITE_API_URL;
} catch {
// not in Vite context
}
return '';
};
const config: AxiosRequestConfig = {
baseURL: import.meta.env.VITE_API_URL,
baseURL: getBaseURL(),
};
export const api = axios.create(config);
// Add request interceptor to include authentication token
api.interceptors.request.use(
(config) => {
const sessionData = getSessionTokenFromCookies();
@@ -77,7 +87,6 @@ api.interceptors.request.use(
}
);
// Add response interceptor to handle token refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
@@ -88,7 +97,6 @@ api.interceptors.response.use(
return handleTokenRefresh(originalRequest);
}
// If backend sends a message, use it
const backendMsg = error?.response?.data?.message;
if (backendMsg && typeof backendMsg === 'string') {
return Promise.reject(new Error(backendMsg));
@@ -98,7 +106,6 @@ api.interceptors.response.use(
}
);
// Helper function to handle token refresh
async function handleTokenRefresh(originalRequest: AxiosRequestConfig) {
const sessionData = getSessionTokenFromCookies();
const refreshToken = sessionData?.token?.refresh_token;
@@ -112,7 +119,6 @@ async function handleTokenRefresh(originalRequest: AxiosRequestConfig) {
const response = await refreshAccessToken(refreshToken);
if (response.data?.access_token) {
// Update session in cookies
setSessionTokenToCookies({
token: {
access_token: response.data.access_token,
@@ -120,7 +126,6 @@ async function handleTokenRefresh(originalRequest: AxiosRequestConfig) {
},
});
// Retry original request with new token
originalRequest.headers ??= {};
originalRequest.headers.Authorization = `Bearer ${response.data.access_token}`;
return api(originalRequest);
@@ -134,14 +139,12 @@ async function handleTokenRefresh(originalRequest: AxiosRequestConfig) {
}
}
// Helper function to refresh access token
async function refreshAccessToken(refreshToken: string) {
return axios.post(`${import.meta.env.VITE_API_URL}/auth/refresh`, {
return axios.post(`${getBaseURL()}/auth/refresh`, {
refresh_token: refreshToken,
});
}
// Helper function to clear session and redirect
function clearSessionAndRedirect() {
removeSessionTokenFromCookies();
if (typeof window !== 'undefined') {
-6
View File
@@ -16,7 +16,6 @@ import type {
const TEAMS_BASE_URL = '/teams';
// Team CRUD
export const getTeams = async (params?: {
page?: number;
limit?: number;
@@ -43,7 +42,6 @@ export const updateTeam = async (teamId: string, data: TUpdateTeamRequest) => {
return response.data;
};
// Team Members
export const getTeamMembers = async (teamId: string) => {
const response = await api.get<TTeamMembersResponse>(`${TEAMS_BASE_URL}/${teamId}/members`);
return response.data;
@@ -64,7 +62,6 @@ export const removeMember = async (teamId: string, userId: string) => {
return response.data;
};
// Join Requests
export const joinTeam = async (teamId: string, data: TJoinTeamRequest) => {
const response = await api.post(`${TEAMS_BASE_URL}/${teamId}/join-request`, data);
return response.data;
@@ -80,7 +77,6 @@ export const respondToJoinRequest = async (teamId: string, requestId: string, ac
return response.data;
};
// Invitations
export const getMyInvitations = async () => {
const response = await api.get<TTeamInvitationsResponse>(`${TEAMS_BASE_URL}/invitations/me`);
return response.data;
@@ -91,13 +87,11 @@ export const respondToInvitation = async (invitationId: string, action: 'accept'
return response.data;
};
// User's Teams
export const getMyTeams = async () => {
const response = await api.get<TTeamListResponse>(`${TEAMS_BASE_URL}/me`);
return response.data;
};
// Project Submission
export const submitProject = async (teamId: string, data: TSubmitProjectRequest) => {
const response = await api.post<TProjectSubmissionResponse>(`${TEAMS_BASE_URL}/${teamId}/submission`, data);
return response.data;
+2 -6
View File
@@ -32,13 +32,11 @@ export const uploadService: UploadService = {
},
async uploadAvatar(file: File) {
// Validate file type
if (!file.type.startsWith('image/')) {
throw new Error('File harus berupa gambar');
}
// Validate file size (max 5MB for images)
const maxSize = 5 * 1024 * 1024; // 5MB
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('Ukuran file maksimal 5MB');
}
@@ -47,13 +45,11 @@ export const uploadService: UploadService = {
},
async uploadCV(file: File) {
// Validate file type
if (file.type !== 'application/pdf') {
throw new Error('CV harus berupa file PDF');
}
// Validate file size (max 10MB for PDFs)
const maxSize = 10 * 1024 * 1024; // 10MB
const maxSize = 10 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('Ukuran file maksimal 10MB');
}
+2
View File
@@ -0,0 +1,2 @@
export * from './cities';
export * from './permissions';
+40
View File
@@ -0,0 +1,40 @@
export const PERMISSIONS = {
USERS: {
READ_LIST: 'Read List Users',
READ_DETAIL: 'Read Detail Users',
CREATE: 'Create Users',
UPDATE: 'Update Users',
DELETE: 'Delete Users',
ACTIVATE: 'Activate Users',
},
ROLES: {
READ_LIST: 'Read List Roles',
READ_DETAIL: 'Read Detail Roles',
CREATE: 'Create Roles',
UPDATE: 'Update Roles',
DELETE: 'Delete Roles',
},
PERMISSIONS: {
READ_LIST: 'Read List Permissions',
READ_DETAIL: 'Read Detail Permissions',
CREATE: 'Create Permissions',
UPDATE: 'Update Permissions',
DELETE: 'Delete Permissions',
},
GACHA_CLAIMS: {
CREATE: 'Create Gacha Claims',
READ_DETAIL: 'Read Detail Gacha Claims',
},
GACHA_ITEMS: {
READ_LIST: 'Read List Gacha Items',
READ_DETAIL: 'Read Detail Gacha Items',
CREATE: 'Create Gacha Items',
UPDATE: 'Update Gacha Items',
DELETE: 'Delete Gacha Items',
},
GACHA_ROLLS: {
READ_DETAIL: 'Read Detail Gacha Rolls',
CREATE: 'Create Gacha Rolls',
EXECUTE: 'Execute Gacha Rolls',
},
};
+11 -36
View File
@@ -5,7 +5,6 @@ import { useAuthStore } from './use-auth-store';
export * from './use-auth-store';
// Types matching backend response
interface TokenInfo {
access_token: string;
refresh_token: string;
@@ -41,38 +40,30 @@ interface MessageResponse {
message: string;
}
// Login request type
interface LoginRequest {
email: string;
password: string;
}
// Signup request type
interface SignupRequest {
email: string;
password: string;
fullname: string;
}
// GitHub auth request type
interface GitHubAuthRequest {
code: string;
}
// Forgot password request type
interface ForgotPasswordRequest {
email: string;
}
// Reset password request type
interface ResetPasswordRequest {
access_token: string;
new_password: string;
}
// Backend API-based auth hooks
// Email/Password Login
export const useLogin = () => {
const { setSession } = useAuthStore();
@@ -111,7 +102,6 @@ export const useLogin = () => {
});
};
// Email/Password Signup - returns message only (user needs to activate via email)
export const useSignup = () => {
return useMutation({
mutationFn: async (data: SignupRequest) => {
@@ -123,7 +113,6 @@ export const useSignup = () => {
});
};
// GitHub OAuth - exchange code for token
export const useGitHubCallback = () => {
const { setSession } = useAuthStore();
@@ -162,8 +151,7 @@ export const useGitHubCallback = () => {
});
};
// Get current session (protected)
export const useSession = () => {
export const useSessionQuery = () => {
const { session } = useAuthStore();
return useQuery({
@@ -178,7 +166,6 @@ export const useSession = () => {
});
};
// Forgot password
export const useForgotPassword = () => {
return useMutation({
mutationFn: async (data: ForgotPasswordRequest) => {
@@ -190,7 +177,6 @@ export const useForgotPassword = () => {
});
};
// Reset password
export const useResetPassword = () => {
return useMutation({
mutationFn: async (data: ResetPasswordRequest) => {
@@ -202,20 +188,17 @@ export const useResetPassword = () => {
});
};
// Sign out (clears local session)
export const useSignOut = () => {
const { clearSession } = useAuthStore();
return useMutation({
mutationFn: async () => {
// No backend call needed - just clear local session
clearSession();
return { success: true };
},
});
};
// Backoffice Login
export const useBackofficeLogin = () => {
const { setSession } = useAuthStore();
@@ -254,9 +237,6 @@ export const useBackofficeLogin = () => {
});
};
// GitHub OAuth URL helper
// The frontend needs to redirect to GitHub with the client_id
// After GitHub redirects back with a code, use useGitHubCallback
export const getGitHubOAuthUrl = (clientId: string, redirectUri: string) => {
const params = new URLSearchParams({
client_id: clientId,
@@ -266,16 +246,20 @@ export const getGitHubOAuthUrl = (clientId: string, redirectUri: string) => {
return `https://github.com/login/oauth/authorize?${params.toString()}`;
};
// Backward compatibility hooks - these wrap the new backend API
// GitHub OAuth hook (backward compatible)
export const useGitHubAuth = () => {
const signInWithGitHub = async () => {
// Get GitHub client ID from environment
const clientId = import.meta.env.VITE_GITHUB_CLIENT_ID || '';
let clientId = '';
if (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID) {
clientId = process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID;
} else {
try {
const meta = import.meta as unknown as Record<string, Record<string, string>>;
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. Set VITE_GITHUB_CLIENT_ID environment variable.'
'GitHub Client ID not configured.'
);
}
@@ -290,7 +274,6 @@ export const useGitHubAuth = () => {
};
};
// Email/Password auth hook (backward compatible)
export const useEmailAuth = () => {
const loginMutation = useLogin();
const signupMutation = useSignup();
@@ -317,7 +300,6 @@ export const useEmailAuth = () => {
password,
fullname,
});
// Signup only returns a message (user needs to verify email first)
return {
message: result.message,
};
@@ -334,9 +316,6 @@ export const useEmailAuth = () => {
};
};
// Legacy hooks for old API compatibility (deprecated)
/** @deprecated Use useLogin instead */
export const usePostLogin = () => {
return useMutation({
mutationFn: async (data: LoginRequest) => {
@@ -348,7 +327,6 @@ export const usePostLogin = () => {
});
};
/** @deprecated Use useSignup instead */
export const usePostRegister = () => {
return useMutation({
mutationFn: async (data: SignupRequest) => {
@@ -360,7 +338,6 @@ export const usePostRegister = () => {
});
};
/** @deprecated Not needed with new backend */
export const usePostVerifyEmail = () => {
return useMutation({
mutationFn: async () => {
@@ -369,7 +346,6 @@ export const usePostVerifyEmail = () => {
});
};
/** @deprecated Not needed with new backend */
export const usePostSendOtp = () => {
return useMutation({
mutationFn: async () => {
@@ -378,7 +354,6 @@ export const usePostSendOtp = () => {
});
};
/** @deprecated Use useGitHubCallback instead */
export const useGoogleCallback = () => {
return useMutation({
mutationFn: async () => {
+1
View File
@@ -6,3 +6,4 @@ export * from './upload';
export * from './teams';
export * from './messages';
export * from './winners';
export * from './use-session';
-7
View File
@@ -17,13 +17,11 @@ export type Message = {
};
};
// Query keys
export const messageKeys = {
all: ['messages'] as const,
team: (teamId: string) => [...messageKeys.all, 'team', teamId] as const,
};
// Fetch messages for a team with polling
export const useTeamMessages = (teamId: string) => {
return useQuery({
queryKey: messageKeys.team(teamId),
@@ -34,14 +32,11 @@ export const useTeamMessages = (teamId: string) => {
return response.data.data || [];
},
enabled: !!teamId,
// Poll every 3 seconds for new messages
refetchInterval: 3000,
// Keep refetching even when window loses focus
refetchIntervalInBackground: true,
});
};
// Send a message
export const useSendMessage = (teamId: string) => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
@@ -60,13 +55,11 @@ export const useSendMessage = (teamId: string) => {
return response.data.data;
},
onSuccess: () => {
// Invalidate to trigger immediate refetch
queryClient.invalidateQueries({ queryKey: messageKeys.team(teamId) });
},
});
};
// Delete a message
export const useDeleteMessage = (teamId: string) => {
const queryClient = useQueryClient();
-24
View File
@@ -9,7 +9,6 @@ import type {
TSubmitProjectRequest,
} from '../../types/teams';
// Query keys
export const teamKeys = {
all: ['teams'] as const,
lists: () => [...teamKeys.all, 'list'] as const,
@@ -23,7 +22,6 @@ export const teamKeys = {
myInvitations: () => [...teamKeys.all, 'my-invitations'] as const,
};
// API response types
interface TeamMember {
id: string;
team_id: string;
@@ -106,7 +104,6 @@ interface Submission {
created_at: string;
}
// List response type with pagination
interface ListResponseWithMeta<T> {
message: string;
data: T[];
@@ -118,7 +115,6 @@ interface ListResponseWithMeta<T> {
};
}
// Team CRUD Hooks
export const useTeams = (params?: {
page?: number;
limit?: number;
@@ -158,7 +154,6 @@ export const useTeams = (params?: {
});
};
// Infinite scroll teams hook
const TEAMS_PAGE_SIZE = 12;
export const useInfiniteTeams = (params?: {
@@ -258,7 +253,6 @@ export const useUpdateTeam = (teamId: string) => {
});
};
// Team Members Hooks - using team detail endpoint which includes members
export const useTeamMembers = (teamId: string, enabled = true) => {
return useQuery({
queryKey: teamKeys.members(teamId),
@@ -298,8 +292,6 @@ export const useManageMember = (teamId: string) => {
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: () => {
@@ -329,7 +321,6 @@ export const useRemoveMember = (teamId: string) => {
});
};
// Join Requests Hooks
export const useJoinTeam = () => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
@@ -376,7 +367,6 @@ export const useRespondToJoinRequest = (teamId: string) => {
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`, {
@@ -393,7 +383,6 @@ export const useRespondToJoinRequest = (teamId: string) => {
});
};
// Invitations Hooks
export const useMyInvitations = () => {
const { session } = useAuthStore();
@@ -429,7 +418,6 @@ export const useRespondToInvitation = () => {
});
};
// User's Teams
export const useMyTeams = () => {
const { session } = useAuthStore();
@@ -438,7 +426,6 @@ export const useMyTeams = () => {
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 };
},
@@ -446,7 +433,6 @@ export const useMyTeams = () => {
});
};
// Project Submission Hooks
export const useSubmitProject = (teamId: string) => {
const queryClient = useQueryClient();
@@ -454,14 +440,12 @@ export const useSubmitProject = (teamId: string) => {
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}`,
{
@@ -479,7 +463,6 @@ export const useSubmitProject = (teamId: string) => {
throw new Error('No existing submission');
}
} catch {
// No existing submission, create new one
const response = await hackathonApi.post<HackathonApiResponse<Submission>>(
`/submissions/teams/${teamId}`,
{
@@ -495,12 +478,10 @@ export const useSubmitProject = (teamId: string) => {
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`
);
@@ -527,7 +508,6 @@ export const useTeamSubmission = (teamId: string, enabled = true) => {
});
};
// Leave Team Hook
export const useLeaveTeam = () => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
@@ -538,7 +518,6 @@ export const useLeaveTeam = () => {
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 };
},
@@ -549,7 +528,6 @@ export const useLeaveTeam = () => {
});
};
// Delete Team Hook (Leader only)
export const useDeleteTeam = () => {
const queryClient = useQueryClient();
const { session } = useAuthStore();
@@ -570,14 +548,12 @@ export const useDeleteTeam = () => {
});
};
// 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 };
},
-14
View File
@@ -2,18 +2,15 @@ import { useMutation } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
import { useAuthStore } from '../auth';
// Upload response type from backend
interface UploadResponse {
url: string;
}
// Helper function to convert File to base64
const fileToBase64 = (file: File): Promise<string> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => {
// Remove the data:image/xxx;base64, prefix
const base64 = (reader.result as string).split(',')[1];
resolve(base64);
};
@@ -21,8 +18,6 @@ const fileToBase64 = (file: File): Promise<string> => {
});
};
// Backend API-based upload hooks
export const useUploadFile = () => {
const { session } = useAuthStore();
@@ -59,13 +54,11 @@ export const useUploadAvatar = () => {
throw new Error('You must be logged in to upload avatar');
}
// Validate file type
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');
}
// Validate file size (max 5MB)
const maxSize = 5 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('File too large. Maximum size: 5MB');
@@ -97,7 +90,6 @@ export const useUploadTeamFile = () => {
throw new Error('You must be logged in to upload files');
}
// Validate file type
const allowedTypes = [
'image/jpeg',
'image/jpg',
@@ -110,7 +102,6 @@ export const useUploadTeamFile = () => {
throw new Error('Invalid file type. Allowed types: JPEG, PNG, WebP, GIF, PDF');
}
// Validate file size (max 20MB)
const maxSize = 20 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('File too large. Maximum size: 20MB');
@@ -142,7 +133,6 @@ export const useUploadSubmission = () => {
throw new Error('You must be logged in to upload submissions');
}
// Validate file type
const allowedTypes = [
'image/jpeg',
'image/jpg',
@@ -161,7 +151,6 @@ export const useUploadSubmission = () => {
);
}
// Validate file size (max 50MB)
const maxSize = 50 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('File too large. Maximum size: 50MB');
@@ -183,7 +172,6 @@ export const useUploadSubmission = () => {
});
};
// Keep useUploadCV for compatibility, using team upload endpoint
export const useUploadCV = () => {
const { session } = useAuthStore();
@@ -194,12 +182,10 @@ export const useUploadCV = () => {
throw new Error('You must be logged in to upload CV');
}
// Validate file type
if (file.type !== 'application/pdf') {
throw new Error('CV must be a PDF file');
}
// Validate file size (max 20MB)
const maxSize = 20 * 1024 * 1024;
if (file.size > maxSize) {
throw new Error('File too large. Maximum size: 20MB');
+20
View File
@@ -0,0 +1,20 @@
'use client';
import { useAuthStore } from './auth';
export const useSession = () => {
const { clearSession, session, status } = useAuthStore();
const isAuthenticated = status === 'authenticated';
const signOut = () => {
clearSession();
localStorage.clear();
window.location.href = '/auth/login';
};
return {
session,
signOut,
isAuthenticated,
};
};
-9
View File
@@ -2,7 +2,6 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
import { useAuthStore } from '../auth';
// User type
interface User {
id: string;
email: string;
@@ -15,7 +14,6 @@ interface User {
updated_at?: string;
}
// Certificate public data types
interface CertificateUserData {
id: string;
fullname: string;
@@ -50,7 +48,6 @@ export interface CertificatePublicData {
winner?: CertificateWinnerData;
}
// Update user request type
interface UpdateUserRequest {
fullname?: string;
bio?: string;
@@ -59,8 +56,6 @@ interface UpdateUserRequest {
skills?: string[];
}
// Backend API-based user hooks
export const useUserMe = () => {
const { session } = useAuthStore();
@@ -107,7 +102,6 @@ export const useUpdateUserMe = () => {
return { data: response.data.data };
},
onSuccess: (result) => {
// Update Zustand session store with new user data
if (session?.user && result.data) {
setSession({
token: session.token,
@@ -132,8 +126,6 @@ export const useUpdateUserById = () => {
return useMutation({
mutationKey: ['update-user-by-id'],
mutationFn: async ({ id, data }: { id: string; data: UpdateUserRequest }) => {
// Note: This might not be supported by backend (only /users/me for updates)
// Keeping for API compatibility but it will likely fail
const response = await hackathonApi.put<HackathonApiResponse<User>>(`/users/${id}`, data);
return { data: response.data.data };
},
@@ -154,7 +146,6 @@ export const useUserDetailsById = (userId: string) => {
});
};
// Public certificate data hook (no authentication required)
export const useCertificatePublicData = (userId: string, enabled = true) => {
return useQuery({
queryKey: ['certificate-public-data', userId],
-2
View File
@@ -1,13 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
// Query keys
export const winnerKeys = {
all: ['winners'] as const,
lists: () => [...winnerKeys.all, 'list'] as const,
};
// API response types
interface Team {
id: string;
name: string;
+1 -1
View File
@@ -3,4 +3,4 @@ export * from './hooks';
export * from './types';
export * from './schemas';
export * from './storage';
// Note: Supabase export removed - using backend API instead
export * from './constants';
-3
View File
@@ -10,7 +10,6 @@ export type TAdminListResponse<T = unknown> = {
meta: TAdminMetaResponse;
};
// Admin Users
export type TAdminUserItem = {
id: string;
email: string;
@@ -27,7 +26,6 @@ export type TAdminUserItem = {
export type TAdminUsersResponse = TAdminListResponse<TAdminUserItem>;
// Admin Teams
export type TAdminTeamItem = {
id: string;
name: string;
@@ -43,7 +41,6 @@ export type TAdminTeamItem = {
export type TAdminTeamsResponse = TAdminListResponse<TAdminTeamItem>;
// Admin Submissions
export type TAdminSubmissionItem = {
id: string;
team_id: string;
-1
View File
@@ -96,7 +96,6 @@ export type TProjectSubmissionItem = {
updated_at: string;
};
// Request/Response DTOs
export type TCreateTeamRequest = {
name: string;
logo: string | null;
-1
View File
@@ -15,5 +15,4 @@ export type TUserItem = {
skills?: string[];
};
// Re-export types from API for convenience
export type { UserDetailResponseDto, UserUpdateRequestDto } from '../../api/users';
-10
View File
@@ -18,12 +18,6 @@ export default defineConfig(() => ({
tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
}),
],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: '../../dist/libs/service',
emptyOutDir: true,
@@ -32,16 +26,12 @@ export default defineConfig(() => ({
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: 'service',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const],
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react', 'react-dom', 'react/jsx-runtime'],
},
},