import { clsx } from 'clsx' import { twMerge } from 'tailwind-merge' import { i18n } from '@/lib/i18n' import { currentViewingLocale } from '@/lib/locales' import type { ClassValue } from 'clsx' import type { LocaleCode } from '@/lib/locales' export const toDate = (d: string | Date): Date => typeof d === 'string' ? new Date (d) : d export const cn = (...inputs: ClassValue[]) => twMerge (clsx (...inputs)) type DatePrecision = 'month' | 'day' | 'hour' | 'minute' | 'second' | null const dateFormatters = new Map () const ainuDateFormatters = new Map () const formatDateString = ( date: Date, unknown: DatePrecision, locale: LocaleCode, timeZone = 'Asia/Tokyo', ): string => { const key = `${timeZone}/${unknown}` if (Number.isNaN (date.getTime ())) return date.toString () if (locale === 'ja') { if (!dateFormatters.has (key)) { dateFormatters.set (key, new Intl.DateTimeFormat ('ja-JP-u-ca-japanese', { era: 'long', year: 'numeric', timeZone, month: unknown === 'month' ? undefined : 'long', day: unknown != null && ['month', 'day'].includes (unknown) ? undefined : 'numeric', weekday: unknown != null && ['month', 'day'].includes (unknown) ? undefined : 'short', hour: unknown == null || ['second', 'minute'].includes (unknown) ? 'numeric' : undefined, minute: unknown == null || unknown === 'second' ? 'numeric' : undefined, second: unknown == null ? 'numeric' : undefined })) } return dateFormatters.get (key)!.format (date) } if (!ainuDateFormatters.has (timeZone)) ainuDateFormatters.set (timeZone, new Intl.DateTimeFormat ('en-CA', { year: 'numeric', month: '2-digit', day: '2-digit', timeZone, hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23' })) const parts = ainuDateFormatters.get (timeZone)!.formatToParts (date) const part = (type: string) => parts.find (entry => entry.type === type)?.value ?? '' const omitDay = unknown != null && ['month', 'day'].includes (unknown) const dateParts = [part ('year'), ...(unknown === 'month' ? [] : [part ('month')]), ...(omitDay ? [] : [part ('day')])] const dateText = dateParts.join ('-') if (unknown != null && ['month', 'day', 'hour'].includes (unknown)) return dateText const timeParts = [part ('hour'), ...(unknown === 'minute' ? [] : [part ('minute')]), ...(unknown == null ? [part ('second')] : [])] return `${dateText}\u3000${timeParts.join (':')}` } export const dateString = ( value: string | Date, unknown: DatePrecision = null, locale?: LocaleCode, ): string => formatDateString (toDate (value), unknown, locale ?? currentViewingLocale ()) export const localisedDateString = ( value: string | Date, locale: LocaleCode, unknown: DatePrecision = null, timeZone?: string, ): string => formatDateString (toDate (value), unknown, locale, timeZone) export const originalCreatedAtString = ( f: string | Date | null, b: string | Date | null, locale: LocaleCode = 'ja', ): string => { const from = f ? toDate (f) : null const before = b ? toDate (b) : null const format = (value: Date, unknown: Parameters[1]) => localisedDateString (value, locale, unknown) const unknownLabel = (key: string) => i18n.t (`history:${key}`, { lng: locale }) const space = locale === 'ja' ? ' ' : '\u3000' const fromParts = from == null ? null : new Date (from.getTime () + 9 * 3_600_000) const beforeParts = before == null ? null : new Date (before.getTime () + 9 * 3_600_000) if (from && before && fromParts && beforeParts) { const diff = before.getTime () - from.getTime () if (diff <= 60_000 /* 1 分 */) return format (from, 'second') if (fromParts.getUTCMinutes () === 0 && beforeParts.getUTCMinutes () === 0) { if (Math.abs (diff - 3_600_000 /* 1 時間 */) < 60_000) return `${format (from, 'minute')}${space}(${unknownLabel ('unknownMinute')})` if (fromParts.getUTCHours () === 0 && beforeParts.getUTCHours () === 0) { if (Math.abs (diff - 86_400_000 /* 1 日 */) < 60_000) return `${format (from, 'hour')}${space}(${unknownLabel ('unknownTime')})` if (fromParts.getUTCDate () === 1 && beforeParts.getUTCDate () === 1) { if (2_332_800_000 /* 27 日 */ < diff && diff < 2_764_800_000 /* 32 日 */) return `${format (from, 'day')}${space}(${unknownLabel ('unknownDay')})` if (fromParts.getUTCMonth () === 0 && beforeParts.getUTCMonth () === 0 && (31_449_600_000 /* 364 日 */ <= diff && diff < 31_708_800_000 /* 367 日 */)) { return `${format (from, 'month')}${space}(${unknownLabel ('unknownMonthDay')})` } } } } } const rtn = ([from ? format (from, 'second') : '', '〜', before ? format (new Date (before.getTime () - 60_000), 'second') : ''] .filter (Boolean) .join (space)) return rtn === '〜' ? unknownLabel ('unknownDate') : rtn } export const msToTime = (ms: number): string => { const totalS = Math.trunc (ms / 1_000) const s = String (totalS % 60) const min = String (Math.trunc (totalS / 60) % 60) const h = Math.trunc (totalS / 3_600) const remainderMs = ms % 1_000 const base = (h > 0 ? `${ h }:${ min.padStart (2, '0') }:${ s.padStart (2, '0') }` : `${ min }:${ s.padStart (2, '0') }`) return remainderMs > 0 ? `${ base }.${ remainderMs.toString ().padStart (3, '0') }` : base } export const inputClass = (invalid?: boolean, className?: string): string => cn ('w-full rounded border p-2', (invalid ? ['border-red-500 bg-red-50 text-red-900', 'placeholder:text-red-300', 'focus:border-red-500 focus:outline-none focus:ring-2 focus:ring-red-200', 'dark:border-red-500 dark:bg-red-950/30 dark:text-red-100'] : ['border-gray-300', 'focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200']), className)