164 行
5.9 KiB
TypeScript
164 行
5.9 KiB
TypeScript
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<string, Intl.DateTimeFormat> ()
|
|
const ainuDateFormatters = new Map<string, Intl.DateTimeFormat> ()
|
|
|
|
|
|
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<typeof dateString>[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)
|