zemenay-blog
Version:
Zemenay Blog as a pluggable Next.js package (dedicated DB)
183 lines (144 loc) • 4.97 kB
text/typescript
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
export function generateSlug(title: string): string {
return title
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "") // Remove special characters
.replace(/[\s_-]+/g, "-") // Replace spaces and underscores with hyphens
.replace(/^-+|-+$/g, "") // Remove leading/trailing hyphens
}
export function generateExcerpt(content: string, maxLength = 160): string {
// Remove HTML tags
const plainText = content.replace(/<[^>]*>/g, "")
if (plainText.length <= maxLength) {
return plainText
}
// Find the last complete word within the limit
const truncated = plainText.substring(0, maxLength)
const lastSpaceIndex = truncated.lastIndexOf(" ")
if (lastSpaceIndex > 0) {
return truncated.substring(0, lastSpaceIndex) + "..."
}
return truncated + "..."
}
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "long",
day: "numeric",
}).format(date)
}
export function formatRelativeDate(date: Date | string): string {
const dateObj = typeof date === 'string' ? new Date(date) : date
// Check if the date is valid
if (isNaN(dateObj.getTime())) {
return "recently"
}
const now = new Date()
const diffInSeconds = Math.floor((now.getTime() - dateObj.getTime()) / 1000)
if (diffInSeconds < 60) {
return "just now"
}
const diffInMinutes = Math.floor(diffInSeconds / 60)
if (diffInMinutes < 60) {
return `${diffInMinutes} minute${diffInMinutes > 1 ? "s" : ""} ago`
}
const diffInHours = Math.floor(diffInMinutes / 60)
if (diffInHours < 24) {
return `${diffInHours} hour${diffInHours > 1 ? "s" : ""} ago`
}
const diffInDays = Math.floor(diffInHours / 24)
if (diffInDays < 7) {
return `${diffInDays} day${diffInDays > 1 ? "s" : ""} ago`
}
const diffInWeeks = Math.floor(diffInDays / 7)
if (diffInWeeks < 4) {
return `${diffInWeeks} week${diffInWeeks > 1 ? "s" : ""} ago`
}
const diffInMonths = Math.floor(diffInDays / 30)
if (diffInMonths < 12) {
return `${diffInMonths} month${diffInMonths > 1 ? "s" : ""} ago`
}
const diffInYears = Math.floor(diffInDays / 365)
return `${diffInYears} year${diffInYears > 1 ? "s" : ""} ago`
}
export function getReadingTime(content: string): number {
const wordsPerMinute = 200
const words = content.trim().split(/\s+/).length
const readingTime = Math.ceil(words / wordsPerMinute)
return Math.max(1, readingTime)
}
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 truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) {
return text
}
return text.substring(0, maxLength).trim() + "..."
}
export function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
export function validatePassword(password: string): {
isValid: boolean
errors: string[]
} {
const errors: string[] = []
if (password.length < 8) {
errors.push("Password must be at least 8 characters long")
}
if (!/[A-Z]/.test(password)) {
errors.push("Password must contain at least one uppercase letter")
}
if (!/[a-z]/.test(password)) {
errors.push("Password must contain at least one lowercase letter")
}
if (!/\d/.test(password)) {
errors.push("Password must contain at least one number")
}
return {
isValid: errors.length === 0,
errors,
}
}
export function sanitizeHtml(html: string): string {
// Basic HTML sanitization - in production, use a proper library like DOMPurify
return html
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
.replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, "")
.replace(/javascript:/gi, "")
.replace(/on\w+\s*=/gi, "")
}
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 Number.parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
}
export function generateRandomId(): string {
return Math.random().toString(36).substring(2) + Date.now().toString(36)
}
export function capitalizeFirst(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}
export function pluralize(count: number, singular: string, plural?: string): string {
if (count === 1) {
return `${count} ${singular}`
}
return `${count} ${plural || singular + "s"}`
}