|
|
import { type ClassValue, clsx } from "clsx" |
|
|
import { twMerge } from "tailwind-merge" |
|
|
|
|
|
export function cn(...inputs: ClassValue[]) { |
|
|
return twMerge(clsx(inputs)) |
|
|
} |
|
|
|
|
|
export function formatDate(date: Date | string): string { |
|
|
const d = new Date(date) |
|
|
const now = new Date() |
|
|
const diff = now.getTime() - d.getTime() |
|
|
|
|
|
|
|
|
if (diff < 60000) { |
|
|
return 'Just now' |
|
|
} |
|
|
|
|
|
|
|
|
if (diff < 3600000) { |
|
|
const minutes = Math.floor(diff / 60000) |
|
|
return `${minutes}m ago` |
|
|
} |
|
|
|
|
|
|
|
|
if (diff < 86400000) { |
|
|
const hours = Math.floor(diff / 3600000) |
|
|
return `${hours}h ago` |
|
|
} |
|
|
|
|
|
|
|
|
if (diff < 604800000) { |
|
|
const days = Math.floor(diff / 86400000) |
|
|
return `${days}d ago` |
|
|
} |
|
|
|
|
|
|
|
|
return d.toLocaleDateString() |
|
|
} |
|
|
|
|
|
export function formatTime(date: Date | string): string { |
|
|
const d = new Date(date) |
|
|
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) |
|
|
} |
|
|
|
|
|
export function formatFileSize(bytes: number): string { |
|
|
if (bytes === 0) return '0 Bytes' |
|
|
|
|
|
const k = 1024 |
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'] |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)) |
|
|
|
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] |
|
|
} |
|
|
|
|
|
export function getFileIcon(mimeType: string): string { |
|
|
if (mimeType.startsWith('image/')) return 'πΌοΈ' |
|
|
if (mimeType.startsWith('video/')) return 'π₯' |
|
|
if (mimeType.startsWith('audio/')) return 'π΅' |
|
|
if (mimeType.includes('pdf')) return 'π' |
|
|
if (mimeType.includes('word')) return 'π' |
|
|
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'π' |
|
|
if (mimeType.includes('powerpoint') || mimeType.includes('presentation')) return 'π½οΈ' |
|
|
if (mimeType.includes('zip') || mimeType.includes('rar') || mimeType.includes('archive')) return 'π¦' |
|
|
return 'π' |
|
|
} |
|
|
|
|
|
export function generateAvatar(name: string): string { |
|
|
const colors = [ |
|
|
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', |
|
|
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9' |
|
|
] |
|
|
|
|
|
const initials = name |
|
|
.split(' ') |
|
|
.map(word => word[0]) |
|
|
.join('') |
|
|
.toUpperCase() |
|
|
.slice(0, 2) |
|
|
|
|
|
const colorIndex = name.charCodeAt(0) % colors.length |
|
|
const color = colors[colorIndex] |
|
|
|
|
|
return `data:image/svg+xml,${encodeURIComponent(` |
|
|
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"> |
|
|
<circle cx="20" cy="20" r="20" fill="${color}"/> |
|
|
<text x="20" y="25" text-anchor="middle" fill="white" font-family="Arial" font-size="14" font-weight="bold"> |
|
|
${initials} |
|
|
</text> |
|
|
</svg> |
|
|
`)}` |
|
|
} |
|
|
|
|
|
export function debounce<T extends (...args: any[]) => any>( |
|
|
func: T, |
|
|
wait: number |
|
|
): (...args: Parameters<T>) => void { |
|
|
let timeout: NodeJS.Timeout | null = null |
|
|
|
|
|
return (...args: Parameters<T>) => { |
|
|
if (timeout) { |
|
|
clearTimeout(timeout) |
|
|
} |
|
|
|
|
|
timeout = setTimeout(() => { |
|
|
func(...args) |
|
|
}, wait) |
|
|
} |
|
|
} |
|
|
|
|
|
export function throttle<T extends (...args: any[]) => any>( |
|
|
func: T, |
|
|
limit: number |
|
|
): (...args: Parameters<T>) => void { |
|
|
let inThrottle: boolean |
|
|
|
|
|
return (...args: Parameters<T>) => { |
|
|
if (!inThrottle) { |
|
|
func(...args) |
|
|
inThrottle = true |
|
|
setTimeout(() => inThrottle = false, limit) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
export function isValidEmail(email: string): boolean { |
|
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ |
|
|
return emailRegex.test(email) |
|
|
} |
|
|
|
|
|
export function isValidUsername(username: string): boolean { |
|
|
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/ |
|
|
return usernameRegex.test(username) |
|
|
} |
|
|
|
|
|
export function truncateText(text: string, maxLength: number): string { |
|
|
if (text.length <= maxLength) return text |
|
|
return text.slice(0, maxLength) + '...' |
|
|
} |
|
|
|
|
|
export function copyToClipboard(text: string): Promise<void> { |
|
|
if (navigator.clipboard) { |
|
|
return navigator.clipboard.writeText(text) |
|
|
} else { |
|
|
|
|
|
const textArea = document.createElement('textarea') |
|
|
textArea.value = text |
|
|
document.body.appendChild(textArea) |
|
|
textArea.focus() |
|
|
textArea.select() |
|
|
|
|
|
try { |
|
|
document.execCommand('copy') |
|
|
return Promise.resolve() |
|
|
} catch (err) { |
|
|
return Promise.reject(err) |
|
|
} finally { |
|
|
document.body.removeChild(textArea) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
export function downloadFile(url: string, filename: string): void { |
|
|
const link = document.createElement('a') |
|
|
link.href = url |
|
|
link.download = filename |
|
|
document.body.appendChild(link) |
|
|
link.click() |
|
|
document.body.removeChild(link) |
|
|
} |
|
|
|
|
|
export function isImageFile(file: File): boolean { |
|
|
return file.type.startsWith('image/') |
|
|
} |
|
|
|
|
|
export function isVideoFile(file: File): boolean { |
|
|
return file.type.startsWith('video/') |
|
|
} |
|
|
|
|
|
export function isAudioFile(file: File): boolean { |
|
|
return file.type.startsWith('audio/') |
|
|
} |
|
|
|
|
|
export function getInitials(name: string): string { |
|
|
return name |
|
|
.split(' ') |
|
|
.map(word => word[0]) |
|
|
.join('') |
|
|
.toUpperCase() |
|
|
.slice(0, 2) |
|
|
} |
|
|
|