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:
1 parent
f68d97188c
commit
3f4461c65c
231 files changed
+6062
-39166
No files matched your search
@@ -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';
|
||||
File renamed without changes.
@@ -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>
|
||||
),
|
||||
};
|
||||
Loaded 50 of 231 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user