import { format, formatDistanceToNow } from "date-fns"; /** * Parse a date string, ensuring UTC dates are properly converted to local time. * Handles both UTC dates (with 'Z' suffix) and dates without timezone info. * * @param dateString - ISO date string, potentially without timezone info * @returns Date object in local timezone */ const parseDate = (dateString: string): Date => { if (!dateString || typeof dateString !== 'string') { throw new Error('Invalid date string'); } // If the string already has 'Z' or timezone offset, new Date() will handle it correctly const hasTimezone = dateString.includes('Z') || dateString.includes('+') || /[+-]\d{2}:\d{2}$/.test(dateString); if (!hasTimezone && dateString.includes('T')) { // Date string without timezone - assume UTC and add 'Z' // Format: "2026-01-15T06:55:00" -> "2026-01-15T06:55:00Z" // Format: "2026-01-15T06:55:00.123" -> "2026-01-15T06:55:00.123Z" const datePart = dateString.split('.')[0]; // Remove milliseconds if present if (datePart.length >= 19) { // Ensure we have at least "YYYY-MM-DDTHH:mm:ss" dateString = dateString.replace(datePart, datePart + 'Z'); } } const date = new Date(dateString); // Validate the date is valid 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); // Format in user's local timezone 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); // Format relative time in user's local timezone 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 }; };