diff --git a/web/src/components/TimeAgo.jsx b/web/src/components/TimeAgo.jsx new file mode 100644 index 000000000..eafce61db --- /dev/null +++ b/web/src/components/TimeAgo.jsx @@ -0,0 +1,61 @@ +import { h } from 'preact'; + +const timeAgo = ({ time, dense = false }) => { + if (!time) return 'Invalid Time'; + try { + const currentTime = new Date(); + const pastTime = new Date(time); + const elapsedTime = currentTime - pastTime; + if (elapsedTime < 0) return 'Invalid Time'; + + const timeUnits = [ + { unit: 'ye', full: 'year', value: 31536000 }, + { unit: 'mo', full: 'month', value: 0 }, + { unit: 'day', full: 'day', value: 86400 }, + { unit: 'h', full: 'hour', value: 3600 }, + { unit: 'm', full: 'minute', value: 60 }, + { unit: 's', full: 'second', value: 1 }, + ]; + + let elapsed = elapsedTime / 1000; + if (elapsed < 60) { + return 'just now'; + } + + for (let i = 0; i < timeUnits.length; i++) { + // if months + if (i === 1) { + // Get the month and year for the time provided + const pastMonth = pastTime.getUTCMonth(); + const pastYear = pastTime.getUTCFullYear(); + + // get current month and year + const currentMonth = currentTime.getUTCMonth(); + const currentYear = currentTime.getUTCFullYear(); + + let monthDiff = (currentYear - pastYear) * 12 + (currentMonth - pastMonth); + + // check if the time provided is the previous month but not exceeded 1 month ago. + if (currentTime.getUTCDate() < pastTime.getUTCDate()) { + monthDiff--; + } + + if (monthDiff > 0) { + const unitAmount = monthDiff; + return `${unitAmount}${dense ? timeUnits[i].unit[0] : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; + } + } else if (elapsed >= timeUnits[i].value) { + const unitAmount = Math.floor(elapsed / timeUnits[i].value); + return `${unitAmount}${dense ? timeUnits[i].unit[0] : ` ${timeUnits[i].full}`}${dense ? '' : 's'} ago`; + } + } + } catch { + return 'Invalid Time'; + } +}; + +const TimeAgo = (props) => { + return {timeAgo({ ...props })}; +}; + +export default TimeAgo; diff --git a/web/src/icons/Clock.jsx b/web/src/icons/Clock.jsx new file mode 100644 index 000000000..e813e006d --- /dev/null +++ b/web/src/icons/Clock.jsx @@ -0,0 +1,24 @@ +import { h } from 'preact'; +import { memo } from 'preact/compat'; + +export function Clock({ className = 'h-6 w-6', stroke = 'currentColor', fill = 'none', onClick = () => {} }) { + return ( + + + + ); +} + +export default memo(Clock); diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index 2b79efe90..87ae63ec8 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -15,6 +15,7 @@ import { UploadPlus } from '../icons/UploadPlus'; import { Clip } from '../icons/Clip'; import { Zone } from '../icons/Zone'; import { Camera } from '../icons/Camera'; +import { Clock } from '../icons/Clock'; import { Delete } from '../icons/Delete'; import { Download } from '../icons/Download'; import Menu, { MenuItem } from '../components/Menu'; @@ -22,8 +23,9 @@ import CalendarIcon from '../icons/Calendar'; import Calendar from '../components/Calendar'; import Button from '../components/Button'; import Dialog from '../components/Dialog'; -import { fromUnixTime, intervalToDuration, formatDuration } from 'date-fns'; import MultiSelect from '../components/MultiSelect'; +import { formatUnixTimestampToDateTime, getDurationFromTimestamps } from '../utils/dateUtil'; +import TimeAgo from '../components/TimeAgo'; const API_LIMIT = 25; @@ -39,16 +41,6 @@ const monthsAgo = (num) => { return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime() / 1000; }; -const clipDuration = (start_time, end_time) => { - const start = fromUnixTime(start_time); - const end = fromUnixTime(end_time); - let duration = 'In Progress'; - if (end_time) { - duration = formatDuration(intervalToDuration({ start, end })); - } - return duration; -}; - export default function Events({ path, ...props }) { const apiHost = useApiHost(); const [searchParams, setSearchParams] = useState({ @@ -511,13 +503,19 @@ export default function Events({ path, ...props }) {
{event.sub_label ? `${event.label.replaceAll('_', ' ')}: ${event.sub_label.replaceAll('_', ' ')}` - : event.label.replaceAll('_', ' ')}{' '} + : event.label.replaceAll('_', ' ')} ({(event.top_score * 100).toFixed(0)}%)
-
- {new Date(event.start_time * 1000).toLocaleDateString(locale, { timeZone: timezone })}{' '} - {new Date(event.start_time * 1000).toLocaleTimeString(locale, { timeZone: timezone })} ( - {clipDuration(event.start_time, event.end_time)}) +
+ + {formatUnixTimestampToDateTime(event.start_time, locale, timezone)} +
+ - + +
+
+ ( {getDurationFromTimestamps(event.start_time, event.end_time)} ) +
diff --git a/web/src/utils/dateUtil.ts b/web/src/utils/dateUtil.ts index 336fd7825..39cc993b4 100644 --- a/web/src/utils/dateUtil.ts +++ b/web/src/utils/dateUtil.ts @@ -1,6 +1,7 @@ export const longToDate = (long: number): Date => new Date(long * 1000); export const epochToLong = (date: number): number => date / 1000; export const dateToLong = (date: Date): number => epochToLong(date.getTime()); +import { fromUnixTime, intervalToDuration, formatDuration } from 'date-fns'; const getDateTimeYesterday = (dateTime: Date): Date => { const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; @@ -14,3 +15,75 @@ const getNowYesterday = (): Date => { export const getNowYesterdayInLong = (): number => { return dateToLong(getNowYesterday()); }; + +/** + * This function takes in a unix timestamp, locale, timezone, + * and returns a dateTime string. + * If unixTimestamp is not provided, it returns 'Invalid time' + * @param unixTimestamp: number + * @param locale: string + * @param timezone: string + * @returns string - dateTime or 'Invalid time' if unixTimestamp is not provided + */ +export const formatUnixTimestampToDateTime = (unixTimestamp: number, locale: string, timezone: string): string => { + if (isNaN(unixTimestamp)) { + return 'Invalid time'; + } + try { + const date = new Date(unixTimestamp * 1000); + const formatter = new Intl.DateTimeFormat(locale, { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + timeZone: timezone, + }); + return formatter.format(date); + } catch (error) { + return 'Invalid time'; + } +}; + +interface DurationToken { + xSeconds: string; + xMinutes: string; + xHours: string; +} + +/** + * This function takes in start and end time in unix timestamp, + * and returns the duration between start and end time in hours, minutes and seconds. + * If end time is not provided, it returns 'In Progress' + * @param start_time: number - Unix timestamp for start time + * @param end_time: number|null - Unix timestamp for end time + * @returns string - duration or 'In Progress' if end time is not provided + */ +export const getDurationFromTimestamps = (start_time: number, end_time: number | null): string => { + if (isNaN(start_time)) { + return 'Invalid start time'; + } + let duration = 'In Progress'; + if (end_time !== null) { + if (isNaN(end_time)) { + return 'Invalid end time'; + } + const start = fromUnixTime(start_time); + const end = fromUnixTime(end_time); + const formatDistanceLocale: DurationToken = { + xSeconds: '{{count}}s', + xMinutes: '{{count}}m', + xHours: '{{count}}h', + }; + const shortEnLocale = { + formatDistance: (token: keyof DurationToken, count: number) => + formatDistanceLocale[token].replace('{{count}}', count.toString()), + }; + duration = formatDuration(intervalToDuration({ start, end }), { + format: ['hours', 'minutes', 'seconds'], + locale: shortEnLocale, + }); + } + return duration; +};