mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
143 lines
5.2 KiB
Svelte
143 lines
5.2 KiB
Svelte
<script lang="ts">
|
|
import { type ComponentProps } from 'svelte';
|
|
import { capitalize } from '$lib/helpers/string';
|
|
import {
|
|
timeFromNow,
|
|
toLocaleDateTime,
|
|
toLocalDateTimeISO,
|
|
toISOString
|
|
} from '$lib/helpers/date';
|
|
import { Badge, InteractiveText, Layout, Popover, Typography } from '@appwrite.io/pink-svelte';
|
|
import { menuOpen } from '$lib/components/menu/store';
|
|
|
|
export let time: string | null = null;
|
|
export let placement: ComponentProps<Popover>['placement'] = 'bottom';
|
|
|
|
function timeDifference(dateString: string): string {
|
|
const SECONDS_IN_MINUTE = 60;
|
|
const SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60;
|
|
const SECONDS_IN_DAY = SECONDS_IN_HOUR * 24;
|
|
const SECONDS_IN_MONTH = SECONDS_IN_DAY * 30;
|
|
|
|
const now = new Date();
|
|
const pastDate = new Date(dateString);
|
|
|
|
if (isNaN(pastDate.getTime())) return 'Invalid time';
|
|
|
|
const diffInSeconds = Math.floor((now.getTime() - pastDate.getTime()) / 1000);
|
|
|
|
const timeParts = [
|
|
{ label: 'month', value: Math.floor(diffInSeconds / SECONDS_IN_MONTH) },
|
|
{ label: 'day', value: Math.floor(diffInSeconds / SECONDS_IN_DAY) % 30 },
|
|
{ label: 'hour', value: Math.floor(diffInSeconds / SECONDS_IN_HOUR) % 24 },
|
|
{ label: 'minute', value: Math.floor(diffInSeconds / SECONDS_IN_MINUTE) % 60 },
|
|
{ label: 'second', value: diffInSeconds % 60 }
|
|
];
|
|
|
|
const month = timeParts[0].value;
|
|
const day = timeParts[1].value;
|
|
const hour = timeParts[2].value;
|
|
const second = timeParts[4].value;
|
|
|
|
let outputParts: { label: string; value: number }[];
|
|
|
|
if (month === 0 && day === 0 && hour === 0 && second > 0) {
|
|
// Seconds exist but no months, days, hours — show minutes + seconds
|
|
outputParts = timeParts.slice(3).filter((unit) => unit.value > 0);
|
|
} else {
|
|
// Normal logic — top 3 non-zero units, no seconds.
|
|
outputParts = timeParts
|
|
.filter((unit) => unit.label !== 'second' && unit.value > 0)
|
|
.slice(0, 3);
|
|
}
|
|
|
|
const formattedTime = outputParts
|
|
.map((unit) => `${unit.value} ${unit.label}${unit.value > 1 ? 's' : ''}`)
|
|
.join(', ');
|
|
|
|
return formattedTime ? `${formattedTime} ago` : 'Just now';
|
|
}
|
|
|
|
let isMouseOverTooltip = false;
|
|
function hidePopover(hideTooltip: () => void, timeout = true) {
|
|
if (!timeout) {
|
|
isMouseOverTooltip = false;
|
|
return hideTooltip();
|
|
}
|
|
|
|
setTimeout(() => {
|
|
if (!isMouseOverTooltip) {
|
|
hideTooltip();
|
|
}
|
|
}, 150);
|
|
}
|
|
|
|
$: timeToString = time ? timeDifference(time) : 'Invalid time';
|
|
</script>
|
|
|
|
{#if time}
|
|
<Popover let:show let:hide {placement} portal>
|
|
<button
|
|
on:mouseenter={() => {
|
|
if (!$menuOpen) {
|
|
setTimeout(show, 150);
|
|
}
|
|
}}
|
|
on:mouseleave={() => hidePopover(hide)}>
|
|
<slot>{capitalize(timeFromNow(time))}</slot>
|
|
</button>
|
|
|
|
<div
|
|
let:hide
|
|
slot="tooltip"
|
|
role="tooltip"
|
|
style:padding="1rem"
|
|
style:margin="-1rem"
|
|
on:mouseenter={() => (isMouseOverTooltip = true)}
|
|
on:mouseleave={() => hidePopover(hide, false)}>
|
|
<Layout.Stack gap="s" alignContent="flex-start">
|
|
<!-- `Raw time` as per design -->
|
|
<Typography.Caption color="--fgcolor-neutral-tertiary" variant="400">
|
|
{#if $$slots.title}
|
|
<slot name="title" />
|
|
{:else}
|
|
{timeToString}
|
|
{/if}
|
|
</Typography.Caption>
|
|
|
|
<!-- `Absolute time` as per design -->
|
|
<Layout.Stack gap="xxs">
|
|
<Layout.Stack
|
|
direction="row"
|
|
alignItems="center"
|
|
alignContent="center"
|
|
justifyContent="space-between">
|
|
<InteractiveText
|
|
isVisible
|
|
variant="copy"
|
|
text={toLocaleDateTime(time, false, 'UTC')}
|
|
value={toISOString(time)} />
|
|
|
|
<Badge variant="secondary" content="UTC" size="xs" />
|
|
</Layout.Stack>
|
|
|
|
<Layout.Stack
|
|
direction="row"
|
|
alignItems="center"
|
|
alignContent="center"
|
|
justifyContent="space-between">
|
|
<InteractiveText
|
|
isVisible
|
|
variant="copy"
|
|
text={toLocaleDateTime(time)}
|
|
value={toLocalDateTimeISO(time)} />
|
|
<Badge variant="secondary" content="Local" size="xs" />
|
|
</Layout.Stack>
|
|
</Layout.Stack>
|
|
</Layout.Stack>
|
|
</div>
|
|
</Popover>
|
|
{:else}
|
|
<Typography.Text>{time === null || time === undefined ? 'null' : time}</Typography.Text>
|
|
{/if}
|