feat(auth): add Google authentication endpoints and hooks (#51)
- Implemented `getGoogleAuthUrl` and `postGoogleCallback` in auth API. - Added corresponding hooks `useGoogleAuth` and `useGoogleCallback` for handling Google authentication. feat(api): enhance API structure with mentors and upload services - Created `mentors` and `upload` API modules with respective services for managing mentor details and file uploads. - Added session management utilities to handle authentication tokens in cookies. feat(users): expand user service with detailed user management - Enhanced user service to include detailed user information and update capabilities. - Added hooks for fetching and updating user data. feat(hooks): introduce hooks for mentors and upload functionalities - Added hooks for fetching mentor details and updating mentor information. - Implemented hooks for file upload, avatar upload, and CV upload with validation. refactor(types): organize and extend type definitions - Introduced new types for mentors and enhanced existing user types. - Re-exported user-related types for better accessibility. chore: update index files to include new services and hooks - Updated index files to export new services and hooks for mentors and uploads.
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
TRegisterRequest,
|
||||
TSendOTPRequest,
|
||||
TVerifyEmailRequest,
|
||||
TGoogleCallbackResponse,
|
||||
} from '../../types/auth';
|
||||
import { TResponseMessage } from '../../types/common';
|
||||
|
||||
@@ -51,3 +52,16 @@ export const postSendOtp = async (
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getGoogleAuthUrl = async (): Promise<string> => {
|
||||
const baseUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
return `${baseUrl}/auth/google/login`;
|
||||
};
|
||||
|
||||
export const postGoogleCallback = async (code: string, state: string): Promise<TGoogleCallbackResponse> => {
|
||||
const { data } = await api({
|
||||
method: 'GET',
|
||||
url: `/auth/google/callback?code=${code}&state=${state}`,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -3,9 +3,140 @@ import axios, { AxiosRequestConfig } from 'axios';
|
||||
export * from './auth';
|
||||
export * from './gacha';
|
||||
export * from './users';
|
||||
export * from './mentors';
|
||||
export * from './upload';
|
||||
|
||||
// Common API response wrapper interface
|
||||
export interface ApiResponse<T> {
|
||||
data: T;
|
||||
version: string;
|
||||
}
|
||||
|
||||
const TOKEN_KEY = 'token';
|
||||
|
||||
// Helper functions for session management (avoiding circular dependency)
|
||||
const getSessionTokenFromCookies = () => {
|
||||
if (typeof document === 'undefined') return null;
|
||||
|
||||
const cookies = document.cookie.split(';');
|
||||
const tokenCookie = cookies.find(cookie => cookie.trim().startsWith(`${TOKEN_KEY}=`));
|
||||
|
||||
if (!tokenCookie) return null;
|
||||
|
||||
try {
|
||||
const tokenValue = tokenCookie.split('=')[1];
|
||||
return JSON.parse(decodeURIComponent(tokenValue));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const setSessionTokenToCookies = (tokenData: { token: { access_token: string; refresh_token: string } }) => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const expires = new Date();
|
||||
expires.setDate(expires.getDate() + 7);
|
||||
|
||||
document.cookie = `${TOKEN_KEY}=${encodeURIComponent(JSON.stringify(tokenData))}; expires=${expires.toUTCString()}; path=/; secure; samesite=strict`;
|
||||
};
|
||||
|
||||
const removeSessionTokenFromCookies = () => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
document.cookie = `${TOKEN_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
};
|
||||
|
||||
const config: AxiosRequestConfig = {
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
};
|
||||
|
||||
export const api = axios.create(config);
|
||||
|
||||
// Add request interceptor to include authentication token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const sessionData = getSessionTokenFromCookies();
|
||||
const token = sessionData?.token?.access_token;
|
||||
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(new Error(error.message || 'Request failed'));
|
||||
}
|
||||
);
|
||||
|
||||
// Add response interceptor to handle token refresh
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
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));
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(error.message || 'Request failed'));
|
||||
}
|
||||
);
|
||||
|
||||
// Helper function to handle token refresh
|
||||
async function handleTokenRefresh(originalRequest: AxiosRequestConfig) {
|
||||
const sessionData = getSessionTokenFromCookies();
|
||||
const refreshToken = sessionData?.token?.refresh_token;
|
||||
|
||||
if (!refreshToken) {
|
||||
clearSessionAndRedirect();
|
||||
throw new Error('No refresh token available');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await refreshAccessToken(refreshToken);
|
||||
|
||||
if (response.data?.access_token) {
|
||||
// Update session in cookies
|
||||
setSessionTokenToCookies({
|
||||
token: {
|
||||
access_token: response.data.access_token,
|
||||
refresh_token: response.data.refresh_token || refreshToken,
|
||||
},
|
||||
});
|
||||
|
||||
// Retry original request with new token
|
||||
originalRequest.headers ??= {};
|
||||
originalRequest.headers.Authorization = `Bearer ${response.data.access_token}`;
|
||||
return api(originalRequest);
|
||||
}
|
||||
|
||||
throw new Error('Invalid refresh response');
|
||||
} catch (refreshError) {
|
||||
console.error('Token refresh failed:', refreshError);
|
||||
clearSessionAndRedirect();
|
||||
throw new Error('Token refresh failed');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to refresh access token
|
||||
async function refreshAccessToken(refreshToken: string) {
|
||||
return axios.post(`${import.meta.env.VITE_API_URL}/auth/refresh`, {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to clear session and redirect
|
||||
function clearSessionAndRedirect() {
|
||||
removeSessionTokenFromCookies();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
import type {
|
||||
MentorDetailResponseDto,
|
||||
MentorUpdateRequestDto
|
||||
} from '../../types/mentors';
|
||||
|
||||
export interface MentorService {
|
||||
getMentorMe(): Promise<MentorDetailResponseDto>;
|
||||
getMentorById(id: string): Promise<MentorDetailResponseDto>;
|
||||
updateMentorMe(data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
|
||||
updateMentorById(id: string, data: MentorUpdateRequestDto): Promise<MentorDetailResponseDto>;
|
||||
}
|
||||
|
||||
export const mentorService: MentorService = {
|
||||
async getMentorMe() {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>('/mentors/me');
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async getMentorById(id: string) {
|
||||
const response = await api.get<ApiResponse<MentorDetailResponseDto>>(`/mentors/detail/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateMentorMe(data: MentorUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>('/mentors/update/me', data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateMentorById(id: string, data: MentorUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<MentorDetailResponseDto>>(`/mentors/update/${id}`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
import { api, ApiResponse } from '../index';
|
||||
|
||||
export interface UploadResponse {
|
||||
filename: string;
|
||||
original_filename: string;
|
||||
uploaded_path: string;
|
||||
url: string;
|
||||
size: number;
|
||||
content_type: string;
|
||||
file_type: string;
|
||||
user_id: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface UploadService {
|
||||
uploadFile(file: File): Promise<UploadResponse>;
|
||||
uploadAvatar(file: File): Promise<UploadResponse>;
|
||||
uploadCV(file: File): Promise<UploadResponse>;
|
||||
}
|
||||
|
||||
export const uploadService: UploadService = {
|
||||
async uploadFile(file: File) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await api.post<ApiResponse<UploadResponse>>('/users/upload', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
});
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
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
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('Ukuran file maksimal 5MB');
|
||||
}
|
||||
|
||||
return this.uploadFile(file);
|
||||
},
|
||||
|
||||
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
|
||||
if (file.size > maxSize) {
|
||||
throw new Error('Ukuran file maksimal 10MB');
|
||||
}
|
||||
|
||||
return this.uploadFile(file);
|
||||
},
|
||||
};
|
||||
@@ -1 +1,89 @@
|
||||
export {};
|
||||
import { api, ApiResponse } from '../index';
|
||||
import { TUserItem } from '../../types/users';
|
||||
|
||||
export interface UserDetailResponseDto extends TUserItem {
|
||||
bio?: string;
|
||||
location?: string;
|
||||
website_url?: string;
|
||||
linkedin_url?: string;
|
||||
github_url?: string;
|
||||
twitter_url?: string;
|
||||
skills?: string[];
|
||||
career_status?: string;
|
||||
experience?: Array<{
|
||||
id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
duration: string;
|
||||
period: string;
|
||||
}>;
|
||||
education?: Array<{
|
||||
id: string;
|
||||
institution: string;
|
||||
degree: string;
|
||||
field: string;
|
||||
period: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface UserUpdateRequestDto {
|
||||
fullname?: string;
|
||||
bio?: string;
|
||||
location?: string;
|
||||
website_url?: string;
|
||||
linkedin_url?: string;
|
||||
github_url?: string;
|
||||
twitter_url?: string;
|
||||
skills?: string[];
|
||||
phone_number?: string;
|
||||
birthdate?: string;
|
||||
gender?: string;
|
||||
career_status?: string;
|
||||
avatar?: string;
|
||||
cv_url?: string;
|
||||
phone_for_verification?: string;
|
||||
domicile?: string;
|
||||
experience?: Array<{
|
||||
id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
duration: string;
|
||||
period: string;
|
||||
}>;
|
||||
education?: Array<{
|
||||
id: string;
|
||||
institution: string;
|
||||
degree: string;
|
||||
field: string;
|
||||
period: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface UserService {
|
||||
getUserMe(): Promise<UserDetailResponseDto>;
|
||||
getUserById(id: string): Promise<UserDetailResponseDto>;
|
||||
updateUserMe(data: UserUpdateRequestDto): Promise<UserDetailResponseDto>;
|
||||
updateUserById(id: string, data: UserUpdateRequestDto): Promise<UserDetailResponseDto>;
|
||||
}
|
||||
|
||||
export const userService: UserService = {
|
||||
async getUserMe() {
|
||||
const response = await api.get<ApiResponse<UserDetailResponseDto>>('/users/me');
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async getUserById(id: string) {
|
||||
const response = await api.get<ApiResponse<UserDetailResponseDto>>(`/users/detail/${id}`);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateUserMe(data: UserUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<UserDetailResponseDto>>('/users/update/me', data);
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
async updateUserById(id: string, data: UserUpdateRequestDto) {
|
||||
const response = await api.put<ApiResponse<UserDetailResponseDto>>(`/users/${id}`, data);
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useMutation, UseMutationResult } from '@tanstack/react-query';
|
||||
import { postLogin, postRegister, postSendOtp, postVerifyEmail } from '../../api/auth';
|
||||
import { postLogin, postRegister, postSendOtp, postVerifyEmail, getGoogleAuthUrl, postGoogleCallback } from '../../api/auth';
|
||||
import {
|
||||
TLoginRequest,
|
||||
TLoginResponse,
|
||||
TRegisterRequest,
|
||||
TSendOTPRequest,
|
||||
TVerifyEmailRequest,
|
||||
TGoogleCallbackResponse,
|
||||
} from '../../types/auth';
|
||||
|
||||
import { TResponseError, TResponseMessage } from '../../types/common';
|
||||
@@ -21,7 +22,7 @@ export const usePostLogin = (): UseMutationResult<
|
||||
mutationFn: async (payload) => await postLogin(payload),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const usePostRegister = (): UseMutationResult<
|
||||
TResponseMessage,
|
||||
TResponseError,
|
||||
@@ -58,3 +59,25 @@ export const usePostSendOTP = (): UseMutationResult<
|
||||
});
|
||||
};
|
||||
|
||||
export const useGoogleAuth = () => {
|
||||
const redirectToGoogle = async (): Promise<string> => {
|
||||
return await getGoogleAuthUrl();
|
||||
};
|
||||
|
||||
return {
|
||||
redirectToGoogle,
|
||||
};
|
||||
};
|
||||
|
||||
export const useGoogleCallback = (): UseMutationResult<
|
||||
TGoogleCallbackResponse,
|
||||
TResponseError,
|
||||
{ code: string; state: string },
|
||||
unknown
|
||||
> => {
|
||||
return useMutation({
|
||||
mutationKey: ['google-callback'],
|
||||
mutationFn: async ({ code, state }) => await postGoogleCallback(code, state),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './auth';
|
||||
export * from './gacha';
|
||||
export * from './users';
|
||||
export * from './mentors';
|
||||
export * from './upload';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
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';
|
||||
|
||||
export const useMentorMe = (options?: UseQueryOptions<MentorDetailResponseDto, TResponseError>): UseQueryResult<MentorDetailResponseDto, TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['mentor-me'],
|
||||
queryFn: () => mentorService.getMentorMe(),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useMentorById = (id: string, options?: UseQueryOptions<MentorDetailResponseDto, TResponseError>): UseQueryResult<MentorDetailResponseDto, TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['mentor-by-id', id],
|
||||
queryFn: () => mentorService.getMentorById(id),
|
||||
enabled: !!id,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateMentorMe = (): UseMutationResult<
|
||||
MentorDetailResponseDto,
|
||||
TResponseError,
|
||||
MentorUpdateRequestDto,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation({
|
||||
mutationKey: ['update-mentor-me'],
|
||||
mutationFn: (data) => mentorService.updateMentorMe(data),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateMentorById = (): UseMutationResult<
|
||||
MentorDetailResponseDto,
|
||||
TResponseError,
|
||||
{ id: string; data: MentorUpdateRequestDto },
|
||||
unknown
|
||||
> => {
|
||||
return useMutation({
|
||||
mutationKey: ['update-mentor-by-id'],
|
||||
mutationFn: ({ id, data }) => mentorService.updateMentorById(id, data),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useMutation, UseMutationResult } from '@tanstack/react-query';
|
||||
import { uploadService, UploadResponse } from '../../api/upload';
|
||||
import { TResponseError } from '../../types/common';
|
||||
|
||||
export const useUploadFile = (): UseMutationResult<
|
||||
UploadResponse,
|
||||
TResponseError,
|
||||
File,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation({
|
||||
mutationKey: ['upload-file'],
|
||||
mutationFn: (file) => uploadService.uploadFile(file),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUploadAvatar = (): UseMutationResult<
|
||||
UploadResponse,
|
||||
TResponseError,
|
||||
File,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation({
|
||||
mutationKey: ['upload-avatar'],
|
||||
mutationFn: (file) => uploadService.uploadAvatar(file),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUploadCV = (): UseMutationResult<
|
||||
UploadResponse,
|
||||
TResponseError,
|
||||
File,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation({
|
||||
mutationKey: ['upload-cv'],
|
||||
mutationFn: (file) => uploadService.uploadCV(file),
|
||||
});
|
||||
};
|
||||
@@ -1 +1,44 @@
|
||||
export {};
|
||||
import { useQuery, useMutation, UseQueryResult, UseMutationResult, UseQueryOptions } from '@tanstack/react-query';
|
||||
import { userService, UserDetailResponseDto, UserUpdateRequestDto } from '../../api/users';
|
||||
import { TResponseError } from '../../types/common';
|
||||
|
||||
export const useUserMe = (options?: UseQueryOptions<UserDetailResponseDto, TResponseError>): UseQueryResult<UserDetailResponseDto, TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['user-me'],
|
||||
queryFn: () => userService.getUserMe(),
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUserById = (id: string, options?: UseQueryOptions<UserDetailResponseDto, TResponseError>): UseQueryResult<UserDetailResponseDto, TResponseError> => {
|
||||
return useQuery({
|
||||
queryKey: ['user-by-id', id],
|
||||
queryFn: () => userService.getUserById(id),
|
||||
enabled: !!id,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateUserMe = (): UseMutationResult<
|
||||
UserDetailResponseDto,
|
||||
TResponseError,
|
||||
UserUpdateRequestDto,
|
||||
unknown
|
||||
> => {
|
||||
return useMutation({
|
||||
mutationKey: ['update-user-me'],
|
||||
mutationFn: (data) => userService.updateUserMe(data),
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateUserById = (): UseMutationResult<
|
||||
UserDetailResponseDto,
|
||||
TResponseError,
|
||||
{ id: string; data: UserUpdateRequestDto },
|
||||
unknown
|
||||
> => {
|
||||
return useMutation({
|
||||
mutationKey: ['update-user-by-id'],
|
||||
mutationFn: ({ id, data }) => userService.updateUserById(id, data),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,4 +30,9 @@ export type TVerifyEmailRequest = {
|
||||
|
||||
export type TSendOTPRequest = {
|
||||
email: string
|
||||
};
|
||||
};
|
||||
|
||||
export type TGoogleCallbackResponse = {
|
||||
token: TTokenItem;
|
||||
user: TUserItem;
|
||||
};
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './gacha';
|
||||
export * from './users';
|
||||
export * from './roles';
|
||||
export * from './permissions';
|
||||
export * from './mentors';
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
export interface MentoringRate {
|
||||
amount: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface MentorDetailResponseDto {
|
||||
availability_commitment: string;
|
||||
bio?: string | null;
|
||||
created_at: string;
|
||||
current_company: string;
|
||||
current_role: string;
|
||||
cv_url?: string | null;
|
||||
domicile?: string | null;
|
||||
email?: string | null;
|
||||
expertise: string[];
|
||||
fullname?: string | null;
|
||||
gender?: string | null;
|
||||
github_url?: string | null;
|
||||
id: string;
|
||||
industries: string[];
|
||||
languages: string[];
|
||||
last_education?: string | null;
|
||||
legal_name?: string | null;
|
||||
linkedin_url?: string | null;
|
||||
mentoring_rate: MentoringRate;
|
||||
phone_for_verification?: string | null;
|
||||
portfolio_url?: string | null;
|
||||
preferred_mentee_level: string[];
|
||||
preferred_mentoring_formats: string[];
|
||||
status: string;
|
||||
topics_of_interest: string[];
|
||||
updated_at: string;
|
||||
user_id: string;
|
||||
years_of_experience: number;
|
||||
mentoring_sessions?: number;
|
||||
rating?: number;
|
||||
experience?: Array<{
|
||||
id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
duration: string;
|
||||
period: string;
|
||||
}>;
|
||||
education?: Array<{
|
||||
id: string;
|
||||
institution: string;
|
||||
degree: string;
|
||||
field: string;
|
||||
period: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface MentorUpdateRequestDto {
|
||||
availability_commitment?: string | null;
|
||||
bio?: string | null;
|
||||
current_company?: string | null;
|
||||
current_role?: string | null;
|
||||
cv_url?: string | null;
|
||||
domicile?: string | null;
|
||||
expertise?: string[] | null;
|
||||
gender?: string | null;
|
||||
github_url?: string | null;
|
||||
industries?: string[] | null;
|
||||
languages?: string[] | null;
|
||||
last_education?: string | null;
|
||||
legal_name?: string | null;
|
||||
linkedin_url?: string | null;
|
||||
mentoring_rate_amount?: number | null;
|
||||
phone_for_verification?: string | null;
|
||||
portfolio_url?: string | null;
|
||||
preferred_mentee_level?: string[] | null;
|
||||
preferred_mentoring_formats?: string[] | null;
|
||||
topics_of_interest?: string[] | null;
|
||||
years_of_experience?: number | null;
|
||||
experience?: Array<{
|
||||
id: string;
|
||||
company: string;
|
||||
position: string;
|
||||
duration: string;
|
||||
period: string;
|
||||
}>;
|
||||
education?: Array<{
|
||||
id: string;
|
||||
institution: string;
|
||||
degree: string;
|
||||
field: string;
|
||||
period: string;
|
||||
}>;
|
||||
}
|
||||
@@ -11,3 +11,6 @@ export type TUserItem = {
|
||||
phone_number: string;
|
||||
role: TRoleDetailItem;
|
||||
};
|
||||
|
||||
// Re-export types from API for convenience
|
||||
export type { UserDetailResponseDto, UserUpdateRequestDto } from '../../api/users';
|
||||
|
||||
Reference in New Issue
Block a user