Format date displayed in Releases section (#14183)
Closes #14177 I noticed a `formatDisplayDate` file being used in `twenty-website` and created the same one for `twenty-front` as well. Unit tests for the same have been added. <img width="1438" height="770" alt="image" src="https://github.com/user-attachments/assets/5aa6eef5-19c5-4108-bf3f-c582d0ca1b59" /> <img width="1275" height="785" alt="image" src="https://github.com/user-attachments/assets/20a87ba6-fca4-472c-a691-362901505303" /> --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
committed by
GitHub
parent
40251d34ec
commit
7a999c8476
@@ -1,16 +1,24 @@
|
||||
/* eslint-disable @nx/workspace-explicit-boolean-predicates-in-if */
|
||||
import { isDate, isNumber, isString } from '@sniptt/guards';
|
||||
import { differenceInCalendarDays, formatDistanceToNow } from 'date-fns';
|
||||
import {
|
||||
differenceInCalendarDays,
|
||||
formatDistance,
|
||||
formatDistanceToNow,
|
||||
type Locale,
|
||||
} from 'date-fns';
|
||||
import { DateTime } from 'luxon';
|
||||
import moize from 'moize';
|
||||
|
||||
import { DateFormat } from '@/localization/constants/DateFormat';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { CustomError } from '@/error-handler/CustomError';
|
||||
import { i18n } from '@lingui/core';
|
||||
import { plural, t } from '@lingui/core/macro';
|
||||
import { logError } from './logError';
|
||||
|
||||
export const DEFAULT_DATE_LOCALE = 'en-EN';
|
||||
const getLuxonLocale = () => {
|
||||
return SOURCE_LOCALE === 'en' ? 'en-US' : SOURCE_LOCALE;
|
||||
};
|
||||
|
||||
export const parseDate = (dateToParse: Date | string | number) => {
|
||||
if (dateToParse === 'now') return DateTime.fromJSDate(new Date());
|
||||
@@ -44,7 +52,7 @@ export const parseDate = (dateToParse: Date | string | number) => {
|
||||
);
|
||||
}
|
||||
|
||||
return formattedDate.setLocale(DEFAULT_DATE_LOCALE);
|
||||
return formattedDate.setLocale(getLuxonLocale());
|
||||
};
|
||||
|
||||
const isSameDay = (a: DateTime, b: DateTime): boolean =>
|
||||
@@ -73,40 +81,33 @@ export const beautifyExactDateTime = (
|
||||
|
||||
export const beautifyExactDate = (dateToBeautify: Date | string | number) => {
|
||||
const isToday = isSameDay(parseDate(dateToBeautify), DateTime.local());
|
||||
const dateFormat = isToday ? "'Today'" : 'DD';
|
||||
return formatDate(dateToBeautify, dateFormat);
|
||||
if (isToday) {
|
||||
return t`Today`;
|
||||
}
|
||||
return formatDate(dateToBeautify, 'DD');
|
||||
};
|
||||
|
||||
export const beautifyPastDateRelativeToNow = (
|
||||
pastDate: Date | string | number,
|
||||
locale?: Locale,
|
||||
) => {
|
||||
try {
|
||||
const parsedDate = parseDate(pastDate);
|
||||
const now = new Date();
|
||||
const diffInSeconds = Math.abs(
|
||||
(now.getTime() - parsedDate.toJSDate().getTime()) / 1000,
|
||||
);
|
||||
|
||||
// For very recent times (less than 30 seconds), show "now"
|
||||
if (diffInSeconds < 30) {
|
||||
return t`now`;
|
||||
}
|
||||
|
||||
return formatDistanceToNow(parsedDate.toJSDate(), {
|
||||
addSuffix: true,
|
||||
}).replace('less than a minute ago', 'now');
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
export const beautifyPastDateAbsolute = (pastDate: Date | string | number) => {
|
||||
try {
|
||||
const parsedPastDate = parseDate(pastDate);
|
||||
|
||||
const hoursDiff = parsedPastDate.diffNow('hours').negate().hours;
|
||||
|
||||
if (hoursDiff <= 24) {
|
||||
return parsedPastDate.toFormat('HH:mm');
|
||||
} else if (hoursDiff <= 7 * 24) {
|
||||
return parsedPastDate.toFormat('cccc - HH:mm');
|
||||
} else if (hoursDiff <= 365 * 24) {
|
||||
return parsedPastDate.toFormat('MMMM d - HH:mm');
|
||||
} else {
|
||||
return parsedPastDate.toFormat('dd/MM/yyyy - HH:mm');
|
||||
}
|
||||
locale,
|
||||
includeSeconds: true,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
return '';
|
||||
@@ -133,96 +134,54 @@ export const beautifyDateDiff = (
|
||||
date: string,
|
||||
dateToCompareWith?: string,
|
||||
short = false,
|
||||
locale?: Locale,
|
||||
) => {
|
||||
// For simple cases, use date-fns which has excellent locale support
|
||||
if (!short && isDefined(locale)) {
|
||||
const fromDate = new Date(date);
|
||||
const toDate = dateToCompareWith ? new Date(dateToCompareWith) : new Date();
|
||||
return formatDistance(fromDate, toDate, { locale });
|
||||
}
|
||||
|
||||
// Manual implementation for complex cases or when locale is not available
|
||||
const dateDiff = DateTime.fromISO(date).diff(
|
||||
dateToCompareWith ? DateTime.fromISO(dateToCompareWith) : DateTime.now(),
|
||||
['years', 'days'],
|
||||
);
|
||||
|
||||
const years = Math.floor(dateDiff.years);
|
||||
const days = Math.floor(dateDiff.days);
|
||||
|
||||
let result = '';
|
||||
if (dateDiff.years) result = result + `${dateDiff.years} year`;
|
||||
if (![0, 1].includes(dateDiff.years)) result = result + 's';
|
||||
if (short && dateDiff.years) return result;
|
||||
if (dateDiff.years && dateDiff.days) result = result + ' and ';
|
||||
if (dateDiff.days) result = result + `${Math.floor(dateDiff.days)} day`;
|
||||
if (![0, 1].includes(dateDiff.days)) result = result + 's';
|
||||
|
||||
if (years !== 0) {
|
||||
result = plural(Math.abs(years), {
|
||||
one: `${years} ${t`year`}`,
|
||||
other: `${years} ${t`years`}`,
|
||||
});
|
||||
|
||||
if (short) return result;
|
||||
}
|
||||
|
||||
if (years !== 0 && days !== 0) {
|
||||
result += ` ${t`and`} `;
|
||||
}
|
||||
|
||||
if (days !== 0) {
|
||||
const daysPart = plural(Math.abs(days), {
|
||||
one: `${days} ${t`day`}`,
|
||||
other: `${days} ${t`days`}`,
|
||||
});
|
||||
result += daysPart;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const getMonthLabels = () => {
|
||||
const formatter = new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
timeZone: 'UTC',
|
||||
});
|
||||
|
||||
return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
||||
.map((month) => {
|
||||
const monthZeroFilled = month < 10 ? `0${month}` : month;
|
||||
return new Date(`2017-${monthZeroFilled}-01T00:00:00+00:00`);
|
||||
})
|
||||
.map((date) => formatter.format(date));
|
||||
};
|
||||
|
||||
export const getMonthLabelsMemoized = moize(getMonthLabels);
|
||||
|
||||
export const formatISOStringToHumanReadableDateTime = (date: string) => {
|
||||
const monthLabels = getMonthLabelsMemoized();
|
||||
|
||||
if (!isDefined(monthLabels)) {
|
||||
return formatToHumanReadableDateTime(date);
|
||||
}
|
||||
|
||||
const year = date.slice(0, 4);
|
||||
const month = date.slice(5, 7);
|
||||
|
||||
const monthLabel = monthLabels[parseInt(month, 10) - 1];
|
||||
|
||||
const jsDate = new Date(date);
|
||||
|
||||
const day = jsDate.getDate();
|
||||
|
||||
const hours = `0${jsDate.getHours()}`.slice(-2);
|
||||
|
||||
const minutes = `0${jsDate.getMinutes()}`.slice(-2);
|
||||
|
||||
return `${day} ${monthLabel} ${year} - ${hours}:${minutes}`;
|
||||
};
|
||||
|
||||
export const formatISOStringToHumanReadableDate = (date: string) => {
|
||||
const monthLabels = getMonthLabelsMemoized();
|
||||
|
||||
if (!isDefined(monthLabels)) {
|
||||
return formatToHumanReadableDate(date);
|
||||
}
|
||||
|
||||
const year = date.slice(0, 4);
|
||||
const month = date.slice(5, 7);
|
||||
const day = date.slice(8, 10);
|
||||
|
||||
const monthLabel = monthLabels[parseInt(month, 10) - 1];
|
||||
|
||||
return `${day} ${monthLabel} ${year}`;
|
||||
};
|
||||
|
||||
export const formatToHumanReadableDate = (date: Date | string) => {
|
||||
const parsedJSDate = parseDate(date).toJSDate();
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}).format(parsedJSDate);
|
||||
};
|
||||
|
||||
export const formatToHumanReadableDateTime = (date: Date | string) => {
|
||||
const parsedJSDate = parseDate(date).toJSDate();
|
||||
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
}).format(parsedJSDate);
|
||||
return i18n.date(parsedJSDate, { dateStyle: 'medium' });
|
||||
};
|
||||
|
||||
export const getDateFormatString = (
|
||||
|
||||
Reference in New Issue
Block a user