feat(hackathon): improve auth flow and UX

- Add email activation requirement for signup (no auto-login)
- Add form validation with React Hook Form and Zod on signup page
- Update callback page to handle email confirmation and password reset redirects
- Fix token format in API interceptor (use access_token)
- Fix middleware to use SessionToken for auth check
- Add infinite scroll with IntersectionObserver on browse teams page
- Replace all internal <a href> with <Link> components
- Add useInfiniteTeams hook for paginated team browsing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Maulana Sodiqin
2025-11-28 17:40:34 +07:00
co-authored by Claude
parent f8d592d811
commit fa30849d03
10 changed files with 363 additions and 181 deletions
+2 -2
View File
@@ -16,8 +16,8 @@ export const hackathonApi = axios.create({
hackathonApi.interceptors.request.use(
(config) => {
const { session } = useAuthStore.getState();
if (session?.token) {
config.headers.Authorization = `Bearer ${session.token}`;
if (session?.token?.access_token) {
config.headers.Authorization = `Bearer ${session.token.access_token}`;
}
return config;
},
+2 -29
View File
@@ -111,43 +111,16 @@ export const useLogin = () => {
});
};
// Email/Password Signup
// Email/Password Signup - returns message only (user needs to activate via email)
export const useSignup = () => {
const { setSession } = useAuthStore();
return useMutation({
mutationFn: async (data: SignupRequest) => {
const response = await hackathonApi.post<HackathonApiResponse<AuthResponse>>(
const response = await hackathonApi.post<HackathonApiResponse<MessageResponse>>(
'/auth/signup',
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: '',
},
},
});
},
});
};
+34 -1
View File
@@ -1,4 +1,4 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQuery, useQueryClient, useInfiniteQuery } from '@tanstack/react-query';
import { hackathonApi, HackathonApiResponse } from '../../api/hackathon';
import { useAuthStore } from '../auth';
import type {
@@ -131,6 +131,39 @@ export const useTeams = (params?: {
});
};
// Infinite scroll teams hook
const TEAMS_PAGE_SIZE = 12;
export const useInfiniteTeams = (params?: {
city?: string;
visibility?: string;
search?: string;
}) => {
return useInfiniteQuery({
queryKey: [...teamKeys.lists(), 'infinite', params],
queryFn: async ({ pageParam = 1 }) => {
const queryParams = new URLSearchParams();
queryParams.append('page', String(pageParam));
queryParams.append('limit', String(TEAMS_PAGE_SIZE));
if (params?.search) queryParams.append('search', params.search);
if (params?.city) queryParams.append('city', params.city);
if (params?.visibility) queryParams.append('visibility', params.visibility);
const response = await hackathonApi.get<HackathonApiResponse<Team[]>>(
`/teams/browse?${queryParams.toString()}`
);
const teams = response.data.data || [];
return {
data: teams,
nextPage: teams.length === TEAMS_PAGE_SIZE ? pageParam + 1 : undefined,
};
},
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
});
};
export const useTeamById = (teamId: string, enabled = true) => {
return useQuery({
queryKey: teamKeys.detail(teamId),