This commit is contained in:
chamikaJ
2025-04-17 18:28:54 +05:30
parent f583291d8a
commit 8825b0410a
2837 changed files with 241385 additions and 127578 deletions

View File

@@ -0,0 +1,41 @@
import {
differenceInSeconds,
differenceInMinutes,
differenceInHours,
differenceInDays,
differenceInWeeks,
differenceInMonths,
differenceInYears,
formatDistanceToNow,
} from 'date-fns';
import { enUS, es, pt } from 'date-fns/locale';
import { getLanguageFromLocalStorage } from './language-utils';
export function calculateTimeDifference(timestamp: string | Date): string {
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
const localeString = getLanguageFromLocalStorage();
const locale = localeString === 'en' ? enUS : localeString === 'es' ? es : pt;
const now = new Date();
const diffInSeconds = differenceInSeconds(now, date);
if (diffInSeconds < 60) {
return 'Just now';
}
const distanceFunctions = [
differenceInYears,
differenceInMonths,
differenceInWeeks,
differenceInDays,
differenceInHours,
differenceInMinutes,
];
for (const distanceFunction of distanceFunctions) {
if (distanceFunction(now, date) > 0) {
return formatDistanceToNow(date, { addSuffix: true, locale });
}
}
return 'Just now';
}

View File

@@ -0,0 +1,10 @@
import { formatDistanceToNow } from "date-fns";
import { enUS, es, pt } from "date-fns/locale";
import { getLanguageFromLocalStorage } from "./language-utils";
export function calculateTimeGap(timestamp: string | Date): string {
const localeString = getLanguageFromLocalStorage();
const locale = localeString === 'en' ? enUS : localeString === 'es' ? es : pt;
const date = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
return formatDistanceToNow(date, { addSuffix: true, locale });
}

View File

@@ -0,0 +1,13 @@
import { tasksApiService } from "@/api/tasks/tasks.api.service";
import logger from "./errorLogger";
export const checkTaskDependencyStatus = async (taskId: string, statusId: string) => {
if (!taskId || !statusId) return false;
try {
const res = await tasksApiService.getTaskDependencyStatus(taskId, statusId);
return res.done ? res.body.can_continue : false;
} catch (error) {
logger.error('Error checking task dependency status:', error);
return false;
}
};

View File

@@ -0,0 +1,3 @@
export const tagBackground = (color: string): string => {
return `${color}1A`; // 1A is 10% opacity in hex
};

View File

@@ -0,0 +1,12 @@
import dayjs from 'dayjs';
import { getLanguageFromLocalStorage } from './language-utils';
export const currentDateString = (): string => {
const date = dayjs();
const localeString = getLanguageFromLocalStorage();
const locale = localeString === 'en' ? 'en' : localeString === 'es' ? 'es' : 'pt';
const todayText =
localeString === 'en' ? 'Today is' : localeString === 'es' ? 'Hoy es' : 'Hoje é';
return `${todayText} ${date.locale(locale).format('dddd, MMMM DD, YYYY')}`;
};

View File

@@ -0,0 +1,32 @@
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
// Initialize the relativeTime plugin
dayjs.extend(relativeTime);
/**
* Formats a date to a relative time string (e.g., "2 hours ago", "a day ago")
* This mimics the Angular fromNow pipe functionality
*
* @param date - The date to format (string, Date, or dayjs object)
* @returns A string representing the relative time
*/
export const fromNow = (date: string | Date | dayjs.Dayjs): string => {
if (!date) return '';
return dayjs(date).fromNow();
};
/**
* Formats a date to a specific format
*
* @param date - The date to format (string, Date, or dayjs object)
* @param format - The format string (default: 'YYYY-MM-DD')
* @returns A formatted date string
*/
export const formatDate = (
date: string | Date | dayjs.Dayjs,
format: string = 'YYYY-MM-DD'
): string => {
if (!date) return '';
return dayjs(date).format(format);
};

View File

@@ -0,0 +1,25 @@
export const durationDateFormat = (date: Date | null | string | undefined): string => {
if (!date) return '-';
const givenDate = new Date(date);
const currentDate = new Date();
const diffInMilliseconds = currentDate.getTime() - givenDate.getTime();
const diffInDays = Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24));
const diffInMonths =
currentDate.getMonth() -
givenDate.getMonth() +
12 * (currentDate.getFullYear() - givenDate.getFullYear());
const diffInYears = currentDate.getFullYear() - givenDate.getFullYear();
if (diffInYears > 0) {
return diffInYears === 1 ? '1 year ago' : `${diffInYears} years ago`;
} else if (diffInMonths > 0) {
return diffInMonths === 1 ? '1 month ago' : `${diffInMonths} months ago`;
} else if (diffInDays > 0) {
return diffInDays === 1 ? '1 day ago' : `${diffInDays} days ago`;
} else {
return 'Today';
}
};

View File

@@ -0,0 +1,153 @@
export type LogLevel = 'info' | 'success' | 'warning' | 'error' | 'debug';
interface LogStyles {
title: string;
text: string;
background?: string;
}
interface LogOptions {
showTimestamp?: boolean;
collapsed?: boolean;
level?: LogLevel;
}
class ConsoleLogger {
private readonly isProduction = import.meta.env.PROD;
private readonly styles: Record<LogLevel, LogStyles> = {
info: {
title: 'color: #1890ff; font-weight: bold; font-size: 12px;',
text: 'color: #1890ff; font-size: 12px;',
background: 'background: transparent; padding: 2px 5px; border-radius: 2px;',
},
success: {
title: 'color: #52c41a; font-weight: bold; font-size: 12px;',
text: 'color: #52c41a; font-size: 12px;',
background: 'background: transparent; padding: 2px 5px; border-radius: 2px;',
},
warning: {
title: 'color: #faad14; font-weight: bold; font-size: 12px;',
text: 'color: #faad14; font-size: 12px;',
background: 'background: transparent; padding: 2px 5px; border-radius: 2px;',
},
error: {
title: 'color: #ff4d4f; font-weight: bold; font-size: 12px;',
text: 'color: #ff4d4f; font-size: 12px;',
background: 'background: transparent; padding: 2px 5px; border-radius: 2px;',
},
debug: {
title: 'color: #722ed1; font-weight: bold; font-size: 12px;',
text: 'color: #722ed1; font-size: 12px;',
background: 'background: transparent; padding: 2px 5px; border-radius: 2px;',
},
};
// Private helper methods
private getTimestamp(): string {
return new Date().toISOString();
}
private formatValue(value: unknown): unknown {
if (value instanceof Error) {
const { name, message, stack } = value;
return { name, message, stack };
}
return value;
}
private formatLogMessage(title: string, showTimestamp: boolean, styles: LogStyles): string {
const timestamp = showTimestamp ? `[${this.getTimestamp()}] ` : '';
return `%c${timestamp}${title}`;
}
private logObjectData(data: Record<string, unknown>, styles: LogStyles): void {
for (const [key, value] of Object.entries(data)) {
console.log(`%c${key}:`, styles.title, this.formatValue(value));
}
}
private log(
title: string,
data: unknown,
{ showTimestamp = true, collapsed = false, level = 'info' }: LogOptions = {}
): void {
if (this.isProduction) return;
const styles = this.styles[level];
const logMethod = collapsed ? console.groupCollapsed : console.group;
const formattedMessage = this.formatLogMessage(title, showTimestamp, styles);
logMethod(formattedMessage, styles.background ?? styles.title);
if (data !== null) {
if (typeof data === 'object' && data !== null) {
this.logObjectData(data as Record<string, unknown>, styles);
} else {
console.log(`%cValue:`, styles.title, this.formatValue(data));
}
}
console.groupEnd();
}
// Public logging methods
public info(title: string, data: unknown = null, options?: Omit<LogOptions, 'level'>): void {
this.log(title, data, { ...options, level: 'info' });
}
public success(title: string, data: unknown = null, options?: Omit<LogOptions, 'level'>): void {
this.log(title, data, { ...options, level: 'success' });
}
public warning(title: string, data: unknown = null, options?: Omit<LogOptions, 'level'>): void {
this.log(title, data, { ...options, level: 'warning' });
}
public error(title: string, data: unknown = null, options?: Omit<LogOptions, 'level'>): void {
this.log(title, data, { ...options, level: 'error' });
}
public debug(title: string, data: unknown = null, options?: Omit<LogOptions, 'level'>): void {
this.log(title, data, { ...options, level: 'debug' });
}
// Table logging
public table(title: string, data: unknown[] = []): void {
if (this.isProduction) return;
console.group(`%c${title}`, this.styles.info.title);
if (data.length > 0) {
console.table(data);
}
console.groupEnd();
}
// Performance logging
public time(label: string): void {
if (this.isProduction) return;
console.time(label);
}
public timeEnd(label: string): void {
if (this.isProduction) return;
console.timeEnd(label);
}
// Group logging
public group(title: string, collapsed = false): void {
if (this.isProduction) return;
const method = collapsed ? console.groupCollapsed : console.group;
method(`%c${title}`, this.styles.info.title);
}
public groupEnd(): void {
if (this.isProduction) return;
console.groupEnd();
}
}
// Create default instance
const logger = new ConsoleLogger();
export default logger;

View File

@@ -0,0 +1,13 @@
// function to fetch data
export const fetchData = async (
url: string,
setState: React.Dispatch<React.SetStateAction<any[]>>
) => {
try {
const response = await fetch(url);
const data = await response.json();
setState(data);
} catch (error) {
console.error(`Error fetching data from ${url}:`, error);
}
};

View File

@@ -0,0 +1,7 @@
export const getBase64 = (file: File): Promise<string | ArrayBuffer | null> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = error => reject(error);
});

View File

@@ -0,0 +1,12 @@
import { format } from 'date-fns';
import { enUS, es, pt } from 'date-fns/locale';
import { getLanguageFromLocalStorage } from './language-utils';
export const formatDateTimeWithLocale = (dateString: string): string => {
if (!dateString) return '';
const date = new Date(dateString);
const localeString = getLanguageFromLocalStorage();
const locale = localeString === 'en' ? enUS : localeString === 'es' ? es : pt;
return format(date, 'MMM d, yyyy, h:mm:ss a', { locale });
};

View File

@@ -0,0 +1,10 @@
export const getInitialTheme = () => {
try {
return (
localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
);
} catch {
return 'light';
}
};

View File

@@ -0,0 +1,21 @@
import { TaskPriorityType } from '../types/task.types';
type ThemeMode = 'light' | 'dark';
const priorityColors = {
light: {
low: '#c2e4d0',
medium: '#f9e3b1',
high: '#f6bfc0',
},
dark: {
low: '#75c997',
medium: '#fbc84c',
high: '#f37070',
},
};
export const getPriorityColor = (priority: string, themeMode: ThemeMode): string => {
const colors = priorityColors[themeMode];
return colors[priority as TaskPriorityType];
};

View File

@@ -0,0 +1,21 @@
import { TaskStatusType } from '../types/task.types';
type ThemeMode = 'light' | 'dark';
const statusColors = {
light: {
todo: '#d8d7d8',
doing: '#c0d5f6',
done: '#c2e4d0',
},
dark: {
todo: '#a9a9a9',
doing: '#70a6f3',
done: '#75c997',
},
};
export const getStatusColor = (status: string, themeMode: ThemeMode): string => {
const colors = statusColors[themeMode];
return colors[status as TaskStatusType];
};

View File

@@ -0,0 +1,35 @@
import dayjs from 'dayjs';
import { getLanguageFromLocalStorage } from './language-utils';
export const greetingString = (name: string): string => {
const date = dayjs();
const hours = date.hour();
let greet;
if (hours < 12) greet = 'morning';
else if (hours >= 12 && hours < 16) greet = 'afternoon';
else if (hours >= 16 && hours < 24) greet = 'evening';
const language = getLanguageFromLocalStorage();
let greetingPrefix = 'Hi';
let greetingSuffix = 'Good';
let morning = 'morning';
let afternoon = 'afternoon';
let evening = 'evening';
if (language === 'es') {
greetingPrefix = 'Hola';
greetingSuffix = 'Buen';
morning = 'mañana';
afternoon = 'tarde';
evening = 'noche';
} else if (language === 'pt') {
greetingPrefix = 'Olá';
greetingSuffix = 'Bom';
morning = 'manhã';
afternoon = 'tarde';
evening = 'noite';
}
return `${greetingPrefix} ${name}, ${greetingSuffix} ${greet}!`;
};

View File

@@ -0,0 +1,37 @@
import { ILanguageType, Language } from '@/features/i18n/localesSlice';
const STORAGE_KEY = 'i18nextLng';
/**
* Gets the user's browser language and returns it if supported, otherwise returns English
* @returns The detected supported language or English as fallback
*/
export const getDefaultLanguage = (): ILanguageType => {
const browserLang = navigator.language.split('-')[0];
if (Object.values(Language).includes(browserLang as Language)) {
return browserLang as ILanguageType;
}
return Language.EN;
};
export const DEFAULT_LANGUAGE: ILanguageType = getDefaultLanguage();
/**
* Gets the current language from local storage
* @returns The stored language or default language if not found
*/
export const getLanguageFromLocalStorage = (): ILanguageType => {
const savedLng = localStorage.getItem(STORAGE_KEY);
if (Object.values(Language).includes(savedLng as Language)) {
return savedLng as ILanguageType;
}
return DEFAULT_LANGUAGE;
};
/**
* Saves the current language to local storage
* @param lng Language to save
*/
export const saveLanguageInLocalStorage = (lng: ILanguageType): void => {
localStorage.setItem(STORAGE_KEY, lng);
};

View File

@@ -0,0 +1,17 @@
// these functions are utility functions which are use for save data and get data from local storage
export const getJSONFromLocalStorage = (name: string) => {
const storedItem = localStorage.getItem(name);
return storedItem ? JSON.parse(storedItem) : null;
};
export const saveJSONToLocalStorage = (name: string, item: unknown) => {
localStorage.setItem(name, JSON.stringify(item));
};
export const saveToLocalStorage = (name: string, item: string) => {
localStorage.setItem(name, item);
};
export const getFromLocalStorage = (name: string) => {
return localStorage.getItem(name);
};

View File

@@ -0,0 +1,11 @@
import { MixpanelConfig } from '@/types/mixpanel.types';
import mixpanel from 'mixpanel-browser';
export const initMixpanel = (token: string, config: MixpanelConfig = {}): void => {
mixpanel.init(token, {
debug: import.meta.env.VITE_APP_ENV !== 'production',
track_pageview: true,
persistence: 'localStorage',
...config,
});
};

View File

@@ -0,0 +1,24 @@
import { DATE_FORMAT_OPTIONS } from '@/shared/constants';
import { IProjectViewModel } from '@/types/project/projectViewModel.types';
interface DateRange {
startDate: string | null;
endDate: string | null;
}
export const formatDateRange = ({ startDate, endDate }: DateRange): string => {
const formattedStart = startDate
? new Date(startDate).toLocaleDateString('en-US', DATE_FORMAT_OPTIONS)
: 'N/A';
const formattedEnd = endDate
? new Date(endDate).toLocaleDateString('en-US', DATE_FORMAT_OPTIONS)
: 'N/A';
return `Start date: ${formattedStart}\nEnd date: ${formattedEnd}`;
};
export const getTaskProgressTitle = (data: IProjectViewModel): string => {
if (!data.all_tasks_count) return 'No tasks available.';
if (data.all_tasks_count === data.completed_tasks_count) return 'All tasks completed.';
return `${data.completed_tasks_count || 0}/${data.all_tasks_count || 0} tasks completed.`;
};

View File

@@ -0,0 +1,11 @@
import { PROJECT_STATUS_ICON_MAP } from '@/shared/constants';
import React from 'react';
export function getStatusIcon(statusIcon: string, colorCode: string) {
return React.createElement(
PROJECT_STATUS_ICON_MAP[statusIcon as keyof typeof PROJECT_STATUS_ICON_MAP],
{
style: { fontSize: 16, color: colorCode },
}
);
}

View File

@@ -0,0 +1,35 @@
import DOMPurify from 'dompurify';
/**
* Sanitizes user input to prevent XSS attacks
*
* @param input - The user input string to sanitize
* @param options - Optional configuration for DOMPurify
* @returns Sanitized string
*/
export const sanitizeInput = (input: string, options?: DOMPurify.Config): string => {
if (!input) return '';
// Default options for plain text inputs (strip all HTML)
const defaultOptions: DOMPurify.Config = {
ALLOWED_TAGS: [],
ALLOWED_ATTR: [],
};
return DOMPurify.sanitize(input, options || defaultOptions);
};
/**
* Sanitizes a string for use in HTML contexts (allows some basic tags)
*
* @param input - The input containing HTML to sanitize
* @returns Sanitized HTML string
*/
export const sanitizeHtml = (input: string): string => {
if (!input) return '';
return DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
});
};

View File

@@ -0,0 +1,3 @@
export const getDayName = (date: Date) => {
return date.toLocaleDateString('en-US', { weekday: 'long' }); // Returns `Monday`, `Tuesday`, etc.
};

View File

@@ -0,0 +1,33 @@
import { ILocalSession } from '@/types/auth/local-session.types';
export const WORKLENZ_SESSION_ID = import.meta.env.VITE_WORKLENZ_SESSION_ID;
const storage: Storage = localStorage;
export function setSession(user: ILocalSession): void {
storage.setItem(WORKLENZ_SESSION_ID, btoa(unescape(encodeURIComponent(JSON.stringify(user)))));
// storage.setItem(WORKLENZ_SESSION_ID, btoa(JSON.stringify(user)));
}
export function getUserSession(): ILocalSession | null {
try {
return JSON.parse(atob(<string>storage.getItem(WORKLENZ_SESSION_ID)));
} catch (e) {
return null;
}
}
export function hasSession() {
return !!storage.getItem(WORKLENZ_SESSION_ID);
}
export function deleteSession() {
storage.removeItem(WORKLENZ_SESSION_ID);
}
export function getRole() {
const session = getUserSession();
if (!session) return 'Unknown';
if (session.owner) return 'Owner';
if (session.is_admin) return 'Admin';
return 'Member';
}

View File

@@ -0,0 +1,24 @@
export const simpleDateFormat = (date: Date | string | null): string => {
if (!date) return '';
// convert ISO string date to Date object if necessary
const dateObj = typeof date === 'string' ? new Date(date) : date;
// check if the date is valid
if (isNaN(dateObj.getTime())) return '';
const options: Intl.DateTimeFormatOptions = {
month: 'short',
day: 'numeric',
};
const currentYear = new Date().getFullYear();
const inputYear = dateObj.getFullYear();
// add year to the format if it's not the current year
if (inputYear !== currentYear) {
options.year = 'numeric';
}
return new Intl.DateTimeFormat('en-US', options).format(dateObj);
};

View File

@@ -0,0 +1,34 @@
export const sortByBooleanField = <T extends Record<string, any>>(
data: T[],
field: keyof T,
prioritizeTrue: boolean = true
) => {
return [...data].sort((a, b) => {
const aValue = !!a[field];
const bValue = !!b[field];
if (aValue === bValue) return 0;
if (prioritizeTrue) {
return aValue ? -1 : 1;
} else {
return !aValue ? -1 : 1;
}
});
};
export const sortBySelection = (data: Array<{ selected?: boolean }>) =>
sortByBooleanField(data, 'selected');
export const sortByPending = (data: Array<{ pending_invitation?: boolean }>) =>
sortByBooleanField(data, 'pending_invitation', false);
export const sortTeamMembers = (data: Array<{ selected?: boolean; pending_invitation?: boolean; is_pending?: boolean }>) => {
return sortByBooleanField(
sortByBooleanField(
sortByBooleanField(data, 'is_pending', false),
'pending_invitation',
false
),
'selected'
);
};

View File

@@ -0,0 +1,12 @@
import { configureStore } from '@reduxjs/toolkit';
import authReducer from '@/features/auth/authSlice';
import userReducer from '@/features/user/userSlice';
export const mockStore = (preloadedState = {}) => {
return configureStore({
reducer: {
auth: authReducer,
user: userReducer,
},
preloadedState,
});
};

View File

@@ -0,0 +1,6 @@
type ThemeMode = 'light' | 'dark';
// this utility for toggle any colors with the theme
export const themeWiseColor = (defaultColor: string, darkColor: string, themeMode: ThemeMode) => {
return themeMode === 'dark' ? darkColor : defaultColor;
};

View File

@@ -0,0 +1,13 @@
import dayjs from 'dayjs';
export function formatDate(date: Date): string {
return dayjs(date).format('MMM DD, YYYY');
}
export function buildTimeString(hours: number, minutes: number, seconds: number) {
const h = hours > 0 ? `${hours}h` : '';
const m = `${minutes}m`;
const s = `${seconds}s`;
return `${h} ${m} ${s}`.trim();
}

View File

@@ -0,0 +1,7 @@
export const timeZoneCurrencyMap: { [key: string]: string } = {
'America/New_York': 'USD', // United States Dollar
'Europe/London': 'GBP', // British Pound
'Asia/Tokyo': 'JPY', // Japanese Yen
'Asia/Colombo': 'Rs', // Sri Lankan Rupee
'Asia/Kolkata': 'INR', // Indian Rupee
};

View File

@@ -0,0 +1,3 @@
export const toCamelCase = (str: string) => {
return str.toLowerCase().replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase());
};

View File

@@ -0,0 +1,9 @@
export function toQueryString(obj: any) {
const query = [];
for (const key in obj) {
if (typeof obj[key] !== undefined && obj[key] !== null) {
query.push(`${key}=${obj[key]}`);
}
}
return '?' + query.join('&');
}

View File

@@ -0,0 +1,12 @@
export const validateEmail = (email: string): boolean => {
if (!email) return false;
// Check if the email has basic format with @ and domain part
if (!email.includes('@') || email.endsWith('@') || email.split('@').length !== 2) {
return false;
}
const EMAIL_REGEXP =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return EMAIL_REGEXP.test(email);
};