|
|
import { format, formatDistanceToNow } from "date-fns"; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const parseDate = (dateString: string): Date => { |
|
|
if (!dateString || typeof dateString !== 'string') { |
|
|
throw new Error('Invalid date string'); |
|
|
} |
|
|
|
|
|
|
|
|
const hasTimezone = dateString.includes('Z') || |
|
|
dateString.includes('+') || |
|
|
/[+-]\d{2}:\d{2}$/.test(dateString); |
|
|
|
|
|
if (!hasTimezone && dateString.includes('T')) { |
|
|
|
|
|
|
|
|
|
|
|
const datePart = dateString.split('.')[0]; |
|
|
if (datePart.length >= 19) { |
|
|
dateString = dateString.replace(datePart, datePart + 'Z'); |
|
|
} |
|
|
} |
|
|
|
|
|
const date = new Date(dateString); |
|
|
|
|
|
|
|
|
if (isNaN(date.getTime())) { |
|
|
throw new Error(`Invalid date: ${dateString}`); |
|
|
} |
|
|
|
|
|
return date; |
|
|
}; |
|
|
|
|
|
export const formatDate = (dateString: string | null | undefined): string => { |
|
|
if (!dateString) return "N/A"; |
|
|
try { |
|
|
const date = parseDate(dateString); |
|
|
|
|
|
return format(date, "MMM d, yyyy 'at' h:mm a"); |
|
|
} catch { |
|
|
return dateString; |
|
|
} |
|
|
}; |
|
|
|
|
|
export const formatRelativeDate = (dateString: string | null | undefined): string => { |
|
|
if (!dateString) return "N/A"; |
|
|
try { |
|
|
const date = parseDate(dateString); |
|
|
|
|
|
return formatDistanceToNow(date, { addSuffix: true }); |
|
|
} catch { |
|
|
return dateString; |
|
|
} |
|
|
}; |
|
|
|
|
|
export const formatNiche = (niche: string): string => { |
|
|
return niche |
|
|
.split("_") |
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) |
|
|
.join(" "); |
|
|
}; |
|
|
|
|
|
export const formatGenerationMethod = (method: string | null | undefined): string => { |
|
|
if (!method) return "Original"; |
|
|
if (method === "angle_concept_matrix") return "Matrix"; |
|
|
return method |
|
|
.split("_") |
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) |
|
|
.join(" "); |
|
|
}; |
|
|
|
|
|
export const truncateText = (text: string, maxLength: number): string => { |
|
|
if (text.length <= maxLength) return text; |
|
|
return text.slice(0, maxLength) + "..."; |
|
|
}; |
|
|
|
|
|
const getApiBase = () => process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; |
|
|
|
|
|
const isAbsoluteUrl = (url: string) => |
|
|
typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://")); |
|
|
|
|
|
export const getImageUrl = ( |
|
|
imageUrl: string | null | undefined, |
|
|
filename: string | null | undefined, |
|
|
r2Url?: string | null |
|
|
): string | null => { |
|
|
const best = r2Url ?? imageUrl; |
|
|
if (best && isAbsoluteUrl(best)) return best; |
|
|
if (filename) return `${getApiBase()}/images/${filename}`; |
|
|
if (imageUrl && typeof imageUrl === "string" && imageUrl.startsWith("/")) |
|
|
return `${getApiBase()}${imageUrl}`; |
|
|
return null; |
|
|
}; |
|
|
|
|
|
export const getImageUrlFallback = ( |
|
|
imageUrl: string | null | undefined, |
|
|
filename: string | null | undefined, |
|
|
r2Url?: string | null |
|
|
): { primary: string | null; fallback: string | null } => { |
|
|
const apiBase = getApiBase(); |
|
|
const best = r2Url ?? imageUrl; |
|
|
const primary = |
|
|
best && isAbsoluteUrl(best) ? best : null; |
|
|
const fromFilename = filename ? `${apiBase}/images/${filename}` : null; |
|
|
const fromRelative = |
|
|
imageUrl && typeof imageUrl === "string" && imageUrl.startsWith("/") |
|
|
? `${apiBase}${imageUrl}` |
|
|
: null; |
|
|
const fallback = fromFilename ?? fromRelative ?? null; |
|
|
return { primary, fallback }; |
|
|
}; |
|
|
|