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

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

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