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:
co-authored by
Claude Opus 4.6
parent
f68d97188c
commit
3f4461c65c
@@ -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;
|
||||
|
||||
@@ -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`;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './cities';
|
||||
export * from './permissions';
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './upload';
|
||||
export * from './teams';
|
||||
export * from './messages';
|
||||
export * from './winners';
|
||||
export * from './use-session';
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 };
|
||||
},
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { useAuthStore } from '@imphnen-frontend-service/service';
|
||||
import { useNavigate } from 'react-router';
|
||||
'use client';
|
||||
|
||||
import { useAuthStore } from './auth';
|
||||
|
||||
export const useSession = () => {
|
||||
const navigate = useNavigate();
|
||||
const { clearSession, session, status } = useAuthStore();
|
||||
const isAuthenticated = status === 'authenticated';
|
||||
|
||||
const signOut = () => {
|
||||
clearSession();
|
||||
localStorage.clear();
|
||||
navigate('/auth/login');
|
||||
window.location.href = '/auth/login';
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -96,7 +96,6 @@ export type TProjectSubmissionItem = {
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
// Request/Response DTOs
|
||||
export type TCreateTeamRequest = {
|
||||
name: string;
|
||||
logo: string | null;
|
||||
|
||||
@@ -15,5 +15,4 @@ export type TUserItem = {
|
||||
skills?: string[];
|
||||
};
|
||||
|
||||
// Re-export types from API for convenience
|
||||
export type { UserDetailResponseDto, UserUpdateRequestDto } from '../../api/users';
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@nx/react/babel",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
]
|
||||
],
|
||||
"plugins": []
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
# shadcn-ui
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test shadcn-ui` to execute the unit tests via [Vitest](https://vitest.dev/).
|
||||
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@imphnen-frontend-service/service/components",
|
||||
"utils": "@imphnen-frontend-service/service/lib/utils",
|
||||
"ui": "@imphnen-frontend-service/service/components/ui",
|
||||
"lib": "@imphnen-frontend-service/service/lib",
|
||||
"hooks": "@imphnen-frontend-service/service/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
Generated
-2201
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"name": "@imphnen-frontend-service/shadcn-ui",
|
||||
"version": "0.0.1",
|
||||
"main": "./index.js",
|
||||
"types": "./index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./index.mjs",
|
||||
"require": "./index.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.503.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-icons": "^5.5.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss": "^4.1.4",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.25.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tw-animate-css": "^1.2.8"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"name": "shadcn-ui",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/shadcn-ui/src",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"// targets": "to see all targets run: nx show project shadcn-ui --web",
|
||||
"targets": {}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { Slot } from '@radix-ui/react-slot';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import * as React from 'react';
|
||||
import { cn } from '../lib';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center font-[600] rounded-md px-[16px] py-[10px] transition-colors duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'bg-primary-500 hover:bg-primary-600 text-white shadow-md',
|
||||
secondary:
|
||||
'bg-white hover:text-primary-600 hover:bg-gray-50 text-primary-500 shadow-md',
|
||||
text: 'bg-transparent hover:text-primary-600 hover:bg-gray-50 text-primary-500',
|
||||
bordered:
|
||||
'border border-primary-500 hover:border-primary-600 bg-transparent hover:text-primary-600 hover:bg-gray-50 text-primary-500',
|
||||
success: 'bg-success-500 hover:bg-success-600 text-white shadow-md',
|
||||
danger: 'bg-danger-100 hover:bg-danger-200 text-danger-500 shadow-md',
|
||||
},
|
||||
size: {
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : 'button';
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -1,8 +0,0 @@
|
||||
export * from './button';
|
||||
export * from './card';
|
||||
export * from './dialog';
|
||||
export * from './drawer';
|
||||
export * from './form';
|
||||
export * from './input';
|
||||
export * from './label';
|
||||
export * from './textarea';
|
||||
@@ -1,23 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import { cn } from '../lib';
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = 'Input';
|
||||
|
||||
export { Input };
|
||||
@@ -1,18 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '../lib';
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea };
|
||||
@@ -1,165 +0,0 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--color-primary-50: #f0f8ff;
|
||||
--color-primary-100: #e1f0fd;
|
||||
--color-primary-200: #bce1fb;
|
||||
--color-primary-300: #81cbf8;
|
||||
--color-primary-400: #3eb0f2;
|
||||
--color-primary-500: #23a1eb;
|
||||
--color-primary-600: #0877c1;
|
||||
--color-primary-700: #085f9c;
|
||||
--color-primary-800: #0b5181;
|
||||
--color-primary-900: #0f446b;
|
||||
--color-primary-950: #0a2b47;
|
||||
|
||||
--color-neutral-50: #f6f6f6;
|
||||
--color-neutral-100: #e7e7e7;
|
||||
--color-neutral-200: #d1d1d1;
|
||||
--color-neutral-300: #b0b0b0;
|
||||
--color-neutral-400: #888888;
|
||||
--color-neutral-500: #6d6d6d;
|
||||
--color-neutral-600: #5d5d5d;
|
||||
--color-neutral-700: #4f4f4f;
|
||||
--color-neutral-800: #454545;
|
||||
--color-neutral-900: #3d3d3d;
|
||||
--color-neutral-950: #2b2b2b;
|
||||
|
||||
--color-success-100: #e0fbd8;
|
||||
--color-success-200: #bcf8b0;
|
||||
--color-success-300: #8eea85;
|
||||
--color-success-400: #63d564;
|
||||
--color-success-500: #35ba43;
|
||||
--color-success-600: #269f3e;
|
||||
--color-success-700: #1a8439;
|
||||
--color-success-800: #106b32;
|
||||
--color-success-900: #0b592f;
|
||||
|
||||
--color-info-100: #ccfcfe;
|
||||
--color-info-200: #9bf3fd;
|
||||
--color-info-300: #67e3fb;
|
||||
--color-info-400: #42cdf8;
|
||||
--color-info-500: #04acf3;
|
||||
--color-info-600: #0185d0;
|
||||
--color-info-700: #0264af;
|
||||
--color-info-800: #01478d;
|
||||
--color-info-900: #003375;
|
||||
|
||||
--color-warning-100: #fffcd3;
|
||||
--color-warning-200: #fffaa9;
|
||||
--color-warning-300: #fff67d;
|
||||
--color-warning-400: #fff25d;
|
||||
--color-warning-500: #ffed27;
|
||||
--color-warning-600: #dbc91d;
|
||||
--color-warning-700: #b7a714;
|
||||
--color-warning-800: #93850b;
|
||||
--color-warning-900: #7a6d07;
|
||||
|
||||
--color-danger-100: #ffe8da;
|
||||
--color-danger-200: #ffcbb3;
|
||||
--color-danger-300: #ffaa8d;
|
||||
--color-danger-400: #ff8870;
|
||||
--color-danger-500: #ff5242;
|
||||
--color-danger-600: #da3030;
|
||||
--color-danger-700: #b7212d;
|
||||
--color-danger-800: #93152a;
|
||||
--color-danger-900: #7a0c27;
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
}
|
||||
|
||||
:root {
|
||||
--radius: 1rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.623 0.214 259.815);
|
||||
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.623 0.214 259.815);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.623 0.214 259.815);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* https://github.com/tailwindlabs/tailwindcss/issues/13129 */
|
||||
.container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './atoms/index';
|
||||
@@ -1,6 +0,0 @@
|
||||
import { ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => {
|
||||
return twMerge(clsx(inputs));
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './cn';
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"types": ["vite/client"]
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../tsconfig.base.json"
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"node",
|
||||
"@nx/react/typings/cssmodule.d.ts",
|
||||
"@nx/react/typings/image.d.ts",
|
||||
"vite/client",
|
||||
"next",
|
||||
"@nx/next/typings/image.d.ts"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.tsx",
|
||||
"**/*.test.tsx",
|
||||
"**/*.spec.js",
|
||||
"**/*.test.js",
|
||||
"**/*.spec.jsx",
|
||||
"**/*.test.jsx"
|
||||
],
|
||||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import * as path from 'path';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/libs/shadcn-ui',
|
||||
plugins: [
|
||||
react(),
|
||||
nxViteTsPaths(),
|
||||
nxCopyAssetsPlugin(['*.md']),
|
||||
dts({
|
||||
entryRoot: 'src',
|
||||
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/shadcn-ui',
|
||||
emptyOutDir: true,
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: 'src/index.ts',
|
||||
name: 'shadcn-ui',
|
||||
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'],
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,3 +1,4 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
import {
|
||||
FC,
|
||||
ReactElement,
|
||||
@@ -6,62 +7,55 @@ import {
|
||||
} from 'react';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
|
||||
type TButtonVariant =
|
||||
| 'primary'
|
||||
| 'secondary'
|
||||
| 'success'
|
||||
| 'danger'
|
||||
| 'text'
|
||||
| 'bordered';
|
||||
type TButtonSize = 'sm' | 'md' | 'lg';
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center font-[600] rounded-md px-[16px] py-[10px] transition-colors duration-200 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'bg-primary-500 hover:bg-primary-600 text-white shadow-md',
|
||||
secondary:
|
||||
'bg-white dark:bg-gray-800 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-gray-700 text-primary-500 dark:text-primary-400 shadow-md dark:shadow-gray-900/50 border dark:border-gray-700',
|
||||
text: 'bg-transparent hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-gray-800 text-primary-500 dark:text-primary-400',
|
||||
bordered:
|
||||
'border border-primary-500 dark:border-primary-400 hover:border-primary-600 dark:hover:border-primary-300 bg-transparent hover:text-primary-600 dark:hover:text-primary-300 hover:bg-gray-50 dark:hover:bg-gray-800 text-primary-500 dark:text-primary-400',
|
||||
success: 'bg-success-500 hover:bg-success-600 text-white shadow-md',
|
||||
danger:
|
||||
'bg-danger-100 dark:bg-danger-500/20 hover:bg-danger-200 dark:hover:bg-danger-500/30 text-danger-500 shadow-md dark:shadow-gray-900/50',
|
||||
},
|
||||
size: {
|
||||
sm: 'text-[12px] max-h-[36px]',
|
||||
md: 'text-[15px] max-h-[40px]',
|
||||
lg: 'text-[19px] max-h-[44px]',
|
||||
icon: 'h-9 w-9 p-0',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type TButtonProps = DetailedHTMLProps<
|
||||
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
> & {
|
||||
variant?: TButtonVariant;
|
||||
size?: TButtonSize;
|
||||
};
|
||||
|
||||
const variantClasses: Record<TButtonVariant, string> = {
|
||||
primary: 'bg-primary-500 hover:bg-primary-600 text-white shadow-md',
|
||||
secondary:
|
||||
'bg-white dark:bg-gray-800 hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-gray-700 text-primary-500 dark:text-primary-400 shadow-md dark:shadow-gray-900/50 border dark:border-gray-700',
|
||||
text: 'bg-transparent hover:text-primary-600 dark:hover:text-primary-400 hover:bg-gray-50 dark:hover:bg-gray-800 text-primary-500 dark:text-primary-400',
|
||||
bordered:
|
||||
'border border-primary-500 dark:border-primary-400 hover:border-primary-600 dark:hover:border-primary-300 bg-transparent hover:text-primary-600 dark:hover:text-primary-300 hover:bg-gray-50 dark:hover:bg-gray-800 text-primary-500 dark:text-primary-400',
|
||||
success: 'bg-success-500 hover:bg-success-600 text-white shadow-md',
|
||||
danger:
|
||||
'bg-danger-100 dark:bg-danger-500/20 hover:bg-danger-200 dark:hover:bg-danger-500/30 text-danger-500 shadow-md dark:shadow-gray-900/50',
|
||||
};
|
||||
|
||||
const sizeClasses: Record<TButtonSize, string> = {
|
||||
sm: 'text-[12px] max-h-[36px]',
|
||||
md: 'text-[15px] max-h-[40px]',
|
||||
lg: 'text-[19px] max-h-[44px]',
|
||||
};
|
||||
|
||||
const disabledClass = 'opacity-50 cursor-not-allowed';
|
||||
> &
|
||||
VariantProps<typeof buttonVariants>;
|
||||
|
||||
export const Button: FC<TButtonProps> = ({
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
variant,
|
||||
size,
|
||||
disabled,
|
||||
className,
|
||||
children,
|
||||
...rest
|
||||
}): ReactElement => {
|
||||
const mergedClassName = cn(
|
||||
'inline-flex items-center justify-center font-[600] rounded-md px-[16px] py-[10px]',
|
||||
'transition-colors duration-200 cursor-pointer',
|
||||
sizeClasses[size],
|
||||
variantClasses[variant],
|
||||
disabled && disabledClass,
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<button className={mergedClassName} disabled={disabled} {...rest}>
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size }), className)}
|
||||
disabled={disabled}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './card';
|
||||
|
||||
describe('Card', () => {
|
||||
it('renders card with content', () => {
|
||||
render(<Card>Card content</Card>);
|
||||
expect(screen.getByText('Card content')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
render(<Card className="custom-class">Content</Card>);
|
||||
expect(screen.getByText('Content')).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('renders full card composition', () => {
|
||||
render(
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Title</CardTitle>
|
||||
<CardDescription>Description</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>Body</CardContent>
|
||||
<CardFooter>Footer</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
|
||||
expect(screen.getByText('Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Description')).toBeInTheDocument();
|
||||
expect(screen.getByText('Body')).toBeInTheDocument();
|
||||
expect(screen.getByText('Footer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './card';
|
||||
|
||||
const meta: Meta<typeof Card> = {
|
||||
title: 'Components/Card',
|
||||
component: Card,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Card>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Card className="w-[350px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>Card description goes here.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p>Card content body.</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<p>Card footer</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
),
|
||||
};
|
||||
|
||||
export const Simple: Story = {
|
||||
render: () => (
|
||||
<Card className="w-[350px] p-6">
|
||||
<p>Simple card with padding.</p>
|
||||
</Card>
|
||||
),
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { cn } from '../lib';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
@@ -0,0 +1 @@
|
||||
export * from './card';
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from './dialog';
|
||||
|
||||
describe('Dialog', () => {
|
||||
it('opens dialog when trigger is clicked', async () => {
|
||||
render(
|
||||
<Dialog>
|
||||
<DialogTrigger>Open</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Dialog Title</DialogTitle>
|
||||
<DialogDescription>Dialog description</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText('Open'));
|
||||
|
||||
expect(screen.getByText('Dialog Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Dialog description')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from './dialog';
|
||||
|
||||
const meta: Meta<typeof Dialog> = {
|
||||
title: 'Components/Dialog',
|
||||
component: Dialog,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Dialog>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Dialog>
|
||||
<DialogTrigger>Open Dialog</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<DialogDescription>This action cannot be undone.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<button>Cancel</button>
|
||||
<button>Continue</button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
),
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import * as React from 'react';
|
||||
import { LuX } from 'react-icons/lu';
|
||||
import { cn } from '../lib';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
@@ -0,0 +1 @@
|
||||
export * from './dialog';
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerTrigger,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from './drawer';
|
||||
|
||||
describe('Drawer', () => {
|
||||
it('opens drawer when trigger is clicked', async () => {
|
||||
render(
|
||||
<Drawer>
|
||||
<DrawerTrigger>Open Drawer</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Drawer Title</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText('Open Drawer'));
|
||||
|
||||
expect(screen.getByText('Drawer Title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import {
|
||||
Drawer,
|
||||
DrawerTrigger,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerClose,
|
||||
} from './drawer';
|
||||
|
||||
const meta: Meta<typeof Drawer> = {
|
||||
title: 'Components/Drawer',
|
||||
component: Drawer,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Drawer>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Drawer>
|
||||
<DrawerTrigger>Open Drawer</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Drawer Title</DrawerTitle>
|
||||
<DrawerDescription>Drawer description goes here.</DrawerDescription>
|
||||
</DrawerHeader>
|
||||
<DrawerFooter>
|
||||
<DrawerClose>Close</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
),
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { Drawer as DrawerPrimitive } from 'vaul';
|
||||
import { cn } from '../lib';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
@@ -0,0 +1 @@
|
||||
export * from './drawer';
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Form,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormMessage,
|
||||
} from './form';
|
||||
|
||||
function TestForm() {
|
||||
const form = useForm({ defaultValues: { name: '' } });
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
describe('Form', () => {
|
||||
it('renders form with label and input', () => {
|
||||
render(<TestForm />);
|
||||
expect(screen.getByText('Name')).toBeInTheDocument();
|
||||
expect(screen.getByRole('textbox')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Form,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
} from './form';
|
||||
|
||||
const meta: Meta = {
|
||||
title: 'Components/Form',
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj;
|
||||
|
||||
function ExampleForm() {
|
||||
const form = useForm({ defaultValues: { username: '' } });
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<input placeholder="Enter username" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>This is your public display name.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <ExampleForm />,
|
||||
};
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
} from 'react-hook-form';
|
||||
import { cn } from '../lib';
|
||||
import { Label } from './label';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
import { Label } from '../label';
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './form';
|
||||
@@ -1,5 +1,10 @@
|
||||
export * from './button';
|
||||
export * from './card';
|
||||
export * from './dialog';
|
||||
export * from './drawer';
|
||||
export * from './form';
|
||||
export * from './input';
|
||||
export * from './label';
|
||||
export * from './select';
|
||||
export * from './textarea';
|
||||
export * from './select'
|
||||
export * from './toggle';
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
DetailedHTMLProps,
|
||||
FC,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './label';
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { Label } from './label';
|
||||
|
||||
describe('Label', () => {
|
||||
it('renders label text', () => {
|
||||
render(<Label>Username</Label>);
|
||||
expect(screen.getByText('Username')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies custom className', () => {
|
||||
render(<Label className="custom-class">Email</Label>);
|
||||
expect(screen.getByText('Email')).toHaveClass('custom-class');
|
||||
});
|
||||
|
||||
it('associates with input via htmlFor', () => {
|
||||
render(<Label htmlFor="email-input">Email</Label>);
|
||||
expect(screen.getByText('Email')).toHaveAttribute('for', 'email-input');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Label } from './label';
|
||||
|
||||
const meta: Meta<typeof Label> = {
|
||||
title: 'Components/Label',
|
||||
component: Label,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Label>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: 'Email address',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithInput: Story = {
|
||||
render: () => (
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<input id="email" type="email" placeholder="Enter your email" />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as LabelPrimitive from '@radix-ui/react-label';
|
||||
import * as React from 'react';
|
||||
import { cn } from '../lib';
|
||||
import { cn } from '@imphnen-frontend-service/utils';
|
||||
|
||||
function Label({
|
||||
className,
|
||||
@@ -16,13 +16,11 @@ export const RegisterMentorStep: FC<TRegisterMentorStep> = ({ step }): ReactElem
|
||||
|
||||
return (
|
||||
<div className="relative w-full px-8">
|
||||
{/* Garis dasar (abu/biru muda) */}
|
||||
<div className="absolute top-5 left-8 right-8 h-0.5 bg-blue-100 z-0" />
|
||||
|
||||
{/* Garis progress biru (dinamis) */}
|
||||
<div
|
||||
className="absolute top-5 left-8 h-0.5 bg-blue-500 z-0 transition-all duration-300"
|
||||
style={{ width: `calc(${progressPercent}% - 0.5rem)` }} // -0.5rem agar tidak overlap ke lingkaran
|
||||
style={{ width: `calc(${progressPercent}% - 0.5rem)` }}
|
||||
/>
|
||||
|
||||
<div className="flex justify-between relative z-10">
|
||||
@@ -32,7 +30,6 @@ export const RegisterMentorStep: FC<TRegisterMentorStep> = ({ step }): ReactElem
|
||||
|
||||
return (
|
||||
<div key={index} className="flex flex-col items-center flex-1 text-center">
|
||||
{/* Lingkaran angka */}
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 ${
|
||||
isActive
|
||||
@@ -42,7 +39,6 @@ export const RegisterMentorStep: FC<TRegisterMentorStep> = ({ step }): ReactElem
|
||||
>
|
||||
<span className="font-bold">{currentStep}</span>
|
||||
</div>
|
||||
{/* Label */}
|
||||
<span className="text-xs text-blue-600 mt-2 leading-tight">{label}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,8 @@ import {
|
||||
import { Button } from '../../atoms';
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { cn, For, useSession } from '@imphnen-frontend-service/utils';
|
||||
import { cn, For } from '@imphnen-frontend-service/utils';
|
||||
import { useSession } from '@imphnen-frontend-service/service';
|
||||
|
||||
type MenuItem = {
|
||||
label: string;
|
||||
@@ -274,20 +275,16 @@ export const BackofficeSidebar: FC<SidebarProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop Sidebar - visible on lg+, sticky */}
|
||||
<div className="hidden lg:block sticky top-0 h-screen overflow-y-auto shadow">
|
||||
{sidebarContent}
|
||||
</div>
|
||||
|
||||
{/* Mobile Sidebar - overlay */}
|
||||
{isOpen && (
|
||||
<div className="lg:hidden fixed inset-0 z-50">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 transition-opacity"
|
||||
onClick={onClose}
|
||||
/>
|
||||
{/* Sidebar */}
|
||||
<div className="fixed inset-y-0 left-0 z-50 transform transition-transform duration-300 ease-in-out">
|
||||
{sidebarContent}
|
||||
</div>
|
||||
|
||||
@@ -45,9 +45,6 @@ export const BackofficeWrapper: FC<TBackofficeWrapperProps> = ({
|
||||
</h1>
|
||||
|
||||
<div className="flex items-center gap-x-6">
|
||||
{/* <Button type="button" variant="secondary" className="max-h-full p-3">
|
||||
<Icon icon="mdi:bell-outline" className="size-6" />
|
||||
</Button> */}
|
||||
<div className="flex items-center gap-x-6">
|
||||
<div className="text-neutral-600 font-medium">
|
||||
<p className="text-p3">{user?.fullname || 'Full Name'}</p>
|
||||
|
||||
@@ -23,7 +23,6 @@ interface DataTableProps<T extends RowData> {
|
||||
columns?: ColumnDef<T, unknown>[];
|
||||
pageSize?: number;
|
||||
className?: string;
|
||||
// server-side pagination props
|
||||
manualPagination?: boolean;
|
||||
pageCount?: number;
|
||||
currentPage?: number;
|
||||
@@ -47,7 +46,6 @@ export const DataTable = <T extends RowData>({
|
||||
});
|
||||
const [sorting, setSorting] = React.useState<SortingState>([]);
|
||||
|
||||
// Update pagination state when pageSize prop changes
|
||||
React.useEffect(() => {
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
@@ -55,7 +53,6 @@ export const DataTable = <T extends RowData>({
|
||||
}));
|
||||
}, [pageSize]);
|
||||
|
||||
// Reset pagination when data changes to prevent out-of-bounds errors
|
||||
React.useEffect(() => {
|
||||
if (data.length > 0) {
|
||||
setPagination((prev) => ({
|
||||
@@ -65,11 +62,9 @@ export const DataTable = <T extends RowData>({
|
||||
}
|
||||
}, [data.length]);
|
||||
|
||||
// Memoize data and columns to prevent unnecessary re-renders
|
||||
const memoizedData = React.useMemo(() => data, [data]);
|
||||
const memoizedColumns = React.useMemo(() => columns, [columns]);
|
||||
|
||||
// Memoize table configuration to prevent recreation on every render
|
||||
const tableConfig = React.useMemo(() => {
|
||||
const config: TableOptions<T> = {
|
||||
data: memoizedData,
|
||||
@@ -84,7 +79,6 @@ export const DataTable = <T extends RowData>({
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
// server-side pagination config
|
||||
manualPagination,
|
||||
pageCount: manualPagination ? pageCount : undefined,
|
||||
};
|
||||
@@ -99,11 +93,9 @@ export const DataTable = <T extends RowData>({
|
||||
pageCount,
|
||||
]);
|
||||
|
||||
// Prefer external table instance if provided; otherwise create an internal one
|
||||
const internalTable = useReactTable(tableConfig);
|
||||
const t = table ?? internalTable;
|
||||
|
||||
// Handle empty data state
|
||||
const isEmpty = t.getRowModel().rows.length === 0;
|
||||
|
||||
return (
|
||||
@@ -186,7 +178,6 @@ export const DataTable = <T extends RowData>({
|
||||
</table>
|
||||
</div>
|
||||
{manualPagination && onPageChange && pageCount ? (
|
||||
// Server-side pagination controls with numbered pages
|
||||
<div className="flex items-center justify-center gap-10">
|
||||
<button
|
||||
className="disabled:opacity-50 cursor-pointer"
|
||||
@@ -211,7 +202,6 @@ export const DataTable = <T extends RowData>({
|
||||
|
||||
<div className="flex gap-4 items-baseline">
|
||||
{pageCount <= 8 ? (
|
||||
// Show all pages if 8 or fewer
|
||||
Array.from({ length: pageCount }, (_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
@@ -226,7 +216,6 @@ export const DataTable = <T extends RowData>({
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
// Show ellipsis for many pages
|
||||
<>
|
||||
<button
|
||||
onClick={() => onPageChange(1)}
|
||||
@@ -294,7 +283,6 @@ export const DataTable = <T extends RowData>({
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
// Client-side pagination (default)
|
||||
<Pagination table={t} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,8 @@ import { MenuOutlined } from '@ant-design/icons';
|
||||
import { FC, ReactElement, useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Button } from '../../atoms/button';
|
||||
import { useModalLogin, useSession } from '@imphnen-frontend-service/utils';
|
||||
import { useModalLogin } from '@imphnen-frontend-service/utils';
|
||||
import { useSession } from '@imphnen-frontend-service/service';
|
||||
|
||||
export const Navbar: FC = (): ReactElement => {
|
||||
const { session, signOut, isAuthenticated } = useSession();
|
||||
|
||||
+9
-11
@@ -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/ui',
|
||||
emptyOutDir: true,
|
||||
@@ -32,7 +26,6 @@ export default defineConfig(() => ({
|
||||
transformMixedEsModules: true,
|
||||
},
|
||||
lib: {
|
||||
// Could also be a dictionary or array of multiple entry points.
|
||||
entry: [
|
||||
'src/atoms/index.ts',
|
||||
'src/molecules/index.ts',
|
||||
@@ -40,13 +33,18 @@ export default defineConfig(() => ({
|
||||
],
|
||||
name: 'ui',
|
||||
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'],
|
||||
external: [
|
||||
'react',
|
||||
'react-dom',
|
||||
'react/jsx-runtime',
|
||||
'react-router-dom',
|
||||
'@ant-design/icons',
|
||||
'@imphnen-frontend-service/service',
|
||||
'@imphnen-frontend-service/utils',
|
||||
],
|
||||
},
|
||||
},
|
||||
test: {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import Cookies from 'js-cookie';
|
||||
|
||||
type TTokenItem = {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
const TOKEN_KEY = 'token';
|
||||
|
||||
type TStoredToken =
|
||||
| {
|
||||
token?: TTokenItem;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
export const SessionToken = {
|
||||
set: (val: TStoredToken) => {
|
||||
Cookies.set(TOKEN_KEY, JSON.stringify(val), {
|
||||
secure: true,
|
||||
sameSite: 'Strict',
|
||||
expires: 7,
|
||||
});
|
||||
},
|
||||
get: (): TStoredToken => {
|
||||
const token = Cookies.get(TOKEN_KEY);
|
||||
if (!token) return undefined;
|
||||
try {
|
||||
return JSON.parse(token);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
remove: () => {
|
||||
Cookies.remove(TOKEN_KEY);
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,2 @@
|
||||
export * from './use-query-state';
|
||||
export * from './use-modal-login';
|
||||
export * from './use-session';
|
||||
export * from './use-register';
|
||||
export * from './use-otp';
|
||||
export * from './use-send-otp';
|
||||
@@ -1,47 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { TLoginItem } from '@imphnen-frontend-service/service';
|
||||
import { SessionToken } from '../cookies';
|
||||
import { SessionUser } from '../local-storage';
|
||||
|
||||
export enum ESessionStatus {
|
||||
Authenticated = 'authenticated',
|
||||
Authenticating = 'authenticating',
|
||||
Unauthenticated = 'unauthenticated',
|
||||
}
|
||||
|
||||
type SessionState = {
|
||||
isLoading: boolean;
|
||||
session?: TLoginItem;
|
||||
status?: ESessionStatus;
|
||||
setLoading: (val: boolean) => void;
|
||||
setSession: (payload: TLoginItem) => void;
|
||||
clearSession: () => void;
|
||||
};
|
||||
|
||||
export const useAuthStore = create<SessionState>((set) => {
|
||||
const session = SessionToken.get();
|
||||
const user = SessionUser.get();
|
||||
const isAuthenticated = !!session;
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
session: isAuthenticated ? { token: session.token, user } : undefined,
|
||||
status: isAuthenticated
|
||||
? ESessionStatus.Authenticated
|
||||
: ESessionStatus.Unauthenticated,
|
||||
setLoading: (val) => set({ isLoading: val }),
|
||||
setSession: (data) => {
|
||||
SessionToken.set({ token: data.token });
|
||||
SessionUser.set(data.user);
|
||||
set({
|
||||
session: data,
|
||||
status: ESessionStatus.Authenticated,
|
||||
});
|
||||
},
|
||||
clearSession: () => {
|
||||
SessionToken.remove();
|
||||
SessionUser.remove();
|
||||
set({ session: undefined, status: ESessionStatus.Unauthenticated });
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, ReactNode, useContext, useState } from 'react';
|
||||
|
||||
interface ModalLoginContextType {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// OTP verification is not used with GitHub OAuth authentication
|
||||
// This hook is kept for backward compatibility but is not used
|
||||
export const useOtp = () => {
|
||||
return {
|
||||
otp: () => {
|
||||
console.warn('OTP verification is not used with GitHub OAuth authentication');
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
};
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
interface UseQueryStateOptions {
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Registration is handled through GitHub OAuth
|
||||
// This hook is kept for backward compatibility but is not used
|
||||
export const useRegister = () => {
|
||||
return {
|
||||
register: () => {
|
||||
console.warn('Registration is handled through GitHub OAuth');
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
};
|
||||
@@ -1,10 +0,0 @@
|
||||
// OTP is not used with GitHub OAuth authentication
|
||||
// This hook is kept for backward compatibility but is not used
|
||||
export const useSendOTP = () => {
|
||||
return {
|
||||
resendOTP: () => {
|
||||
console.warn('OTP is not used with GitHub OAuth authentication');
|
||||
},
|
||||
isLoading: false,
|
||||
};
|
||||
};
|
||||
@@ -2,8 +2,4 @@ export * from './react-query';
|
||||
export * from './react-router';
|
||||
export * from './tailwind-merge';
|
||||
export * from './hooks';
|
||||
export * from './local-storage';
|
||||
export * from './cookies';
|
||||
export * from './session';
|
||||
export * from './constants';
|
||||
export * from './logic';
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
export type TPermissionItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
type TRoleItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
permissions: TPermissionItem[];
|
||||
};
|
||||
|
||||
type TUserItem = {
|
||||
id: string;
|
||||
avatar: string;
|
||||
birthdate: string;
|
||||
email: string;
|
||||
fullname: string;
|
||||
gender: string;
|
||||
is_active: boolean;
|
||||
phone_number: string;
|
||||
role: TRoleItem;
|
||||
bio?: string;
|
||||
location?: string;
|
||||
skills?: string[];
|
||||
};
|
||||
|
||||
export const SessionUser = {
|
||||
set: (val?: TUserItem) => localStorage.setItem('users', JSON.stringify(val)),
|
||||
get: (): TUserItem | undefined => {
|
||||
const users = localStorage.getItem('users');
|
||||
return users ? JSON.parse(users) : undefined;
|
||||
},
|
||||
remove: () => localStorage.removeItem('users'),
|
||||
};
|
||||
@@ -4,18 +4,6 @@ export interface ForProps<T, U> {
|
||||
fallback?: React.ReactNode | null
|
||||
}
|
||||
|
||||
/**
|
||||
* A functional component that renders a list of children components from a given
|
||||
* array of data.
|
||||
*
|
||||
* @param data - The array of data to render
|
||||
* @param children - A function that takes the current item and index and returns
|
||||
* the child component to render
|
||||
* @param fallback - An optional fallback component to render when the data is empty
|
||||
*
|
||||
* @returns An array of rendered child components if the data is not empty, otherwise
|
||||
* the fallback component if it is provided, null otherwise
|
||||
*/
|
||||
export function For<T, U extends React.JSX.Element>({
|
||||
data,
|
||||
children,
|
||||
|
||||
@@ -4,16 +4,6 @@ export interface ShowProps {
|
||||
fallback?: React.ReactNode | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally renders the `children` component if `condition` is true. If
|
||||
* `condition` is false, renders the `fallback` component instead.
|
||||
*
|
||||
* If `fallback` is `null`, renders nothing.
|
||||
*
|
||||
* @param condition - The condition to check
|
||||
* @param children - The component to render if `condition` is true
|
||||
* @param fallback - The component to render if `condition` is false
|
||||
*/
|
||||
export function Show({ condition, children, fallback = null }: ShowProps) {
|
||||
return condition ? children : fallback
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ export function convertPagesToRoute(
|
||||
return 'loader' in result ? result.loader?.(args) : null;
|
||||
},
|
||||
async guard() {
|
||||
// Permission checking removed - always allow access
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
type TProps = PropsWithChildren<{
|
||||
permissions?: Array<string>;
|
||||
fallback?: ReactNode;
|
||||
}>;
|
||||
|
||||
export const Guard: FC<TProps> = (props): ReactNode => {
|
||||
// Permission checking removed - always allow access
|
||||
return props.children;
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './guard';
|
||||
@@ -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/utils',
|
||||
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: 'utils',
|
||||
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'],
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user