feat(stats): split updated timestamp ticker
This commit is contained in:
@@ -520,12 +520,73 @@
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-page="stats"] [data-slot="hero-meta"] span {
|
[data-page="stats"] [data-slot="hero-meta-label"],
|
||||||
|
[data-page="stats"] [data-slot="hero-meta-empty"] {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="hero-meta-time"] {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="hero-meta-separator"] {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-right: 0.35em;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="hero-meta-ticker"] {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
min-width: 0;
|
||||||
|
height: 1.1em;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="hero-meta-ticker-track"] {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
transform: translateY(0);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="hero-meta-ticker"][data-ticking="true"] [data-slot="hero-meta-ticker-track"] {
|
||||||
|
animation: stats-hero-meta-ticker 680ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-page="stats"] [data-slot="hero-meta-ticker-item"] {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.1;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes stats-hero-meta-ticker {
|
||||||
|
0%,
|
||||||
|
18% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
82%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
[data-page="stats"] [data-slot="hero-meta-ticker"][data-ticking="true"] [data-slot="hero-meta-ticker-track"] {
|
||||||
|
animation: none;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 48rem) {
|
@media (min-width: 48rem) {
|
||||||
[data-page="stats"] [data-section="hero"] {
|
[data-page="stats"] [data-section="hero"] {
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
|||||||
@@ -117,11 +117,47 @@ export default function StatsHome() {
|
|||||||
|
|
||||||
function Hero(props: { updatedAt: string | null }) {
|
function Hero(props: { updatedAt: string | null }) {
|
||||||
const [timeZone, setTimeZone] = createSignal("UTC")
|
const [timeZone, setTimeZone] = createSignal("UTC")
|
||||||
onMount(() => setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"))
|
const [previousTimeZone, setPreviousTimeZone] = createSignal("UTC")
|
||||||
|
const [isTicking, setIsTicking] = createSignal(false)
|
||||||
|
const updatedAtParts = (timeZone: string) =>
|
||||||
|
props.updatedAt ? formatUpdatedAtParts(props.updatedAt, timeZone) : { date: "No rows yet", time: "" }
|
||||||
|
const previousUpdatedAt = createMemo(() => updatedAtParts(previousTimeZone()))
|
||||||
|
const currentUpdatedAt = createMemo(() => updatedAtParts(timeZone()))
|
||||||
|
const currentUpdatedLabel = createMemo(() =>
|
||||||
|
props.updatedAt ? `Updated ${formatUpdatedAtLabel(currentUpdatedAt())}` : "No rows yet",
|
||||||
|
)
|
||||||
|
const isDateTicking = createMemo(() => isTicking() && previousUpdatedAt().date !== currentUpdatedAt().date)
|
||||||
|
const isTimeTicking = createMemo(() => isTicking() && previousUpdatedAt().time !== currentUpdatedAt().time)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!props.updatedAt) return
|
||||||
|
const nextTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"
|
||||||
|
if (nextTimeZone === "UTC") return
|
||||||
|
if (
|
||||||
|
formatUpdatedAtLabel(formatUpdatedAtParts(props.updatedAt, nextTimeZone)) ===
|
||||||
|
formatUpdatedAtLabel(updatedAtParts("UTC"))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
const timeouts: number[] = []
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setPreviousTimeZone(timeZone())
|
||||||
|
setTimeZone(nextTimeZone)
|
||||||
|
setIsTicking(true)
|
||||||
|
timeouts.push(
|
||||||
|
window.setTimeout(() => {
|
||||||
|
setPreviousTimeZone(nextTimeZone)
|
||||||
|
setIsTicking(false)
|
||||||
|
}, 720),
|
||||||
|
)
|
||||||
|
}, 480),
|
||||||
|
)
|
||||||
|
onCleanup(() => timeouts.forEach((timeout) => window.clearTimeout(timeout)))
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section data-section="hero">
|
<section data-section="hero">
|
||||||
<p data-slot="hero-meta">
|
<p data-slot="hero-meta" aria-live="polite" aria-atomic="true" aria-label={currentUpdatedLabel()}>
|
||||||
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
|
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
@@ -130,7 +166,28 @@ function Hero(props: { updatedAt: string | null }) {
|
|||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>{props.updatedAt ? `Updated ${formatUpdatedAt(props.updatedAt, timeZone())}` : "No rows yet"}</span>
|
{props.updatedAt ? (
|
||||||
|
<>
|
||||||
|
<span data-slot="hero-meta-label" aria-hidden="true">
|
||||||
|
Updated
|
||||||
|
</span>
|
||||||
|
<span data-slot="hero-meta-time" aria-hidden="true">
|
||||||
|
<HeroMetaTickerPart
|
||||||
|
previous={previousUpdatedAt().date}
|
||||||
|
current={currentUpdatedAt().date}
|
||||||
|
ticking={isDateTicking()}
|
||||||
|
/>
|
||||||
|
<span data-slot="hero-meta-separator">,</span>
|
||||||
|
<HeroMetaTickerPart
|
||||||
|
previous={previousUpdatedAt().time}
|
||||||
|
current={currentUpdatedAt().time}
|
||||||
|
ticking={isTimeTicking()}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span data-slot="hero-meta-empty">No rows yet</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div data-slot="hero-canvas">
|
<div data-slot="hero-canvas">
|
||||||
<div data-slot="hero-pattern" aria-hidden="true" />
|
<div data-slot="hero-pattern" aria-hidden="true" />
|
||||||
@@ -144,6 +201,17 @@ function Hero(props: { updatedAt: string | null }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function HeroMetaTickerPart(props: { previous: string; current: string; ticking: boolean }) {
|
||||||
|
return (
|
||||||
|
<span data-slot="hero-meta-ticker" data-ticking={props.ticking}>
|
||||||
|
<span data-slot="hero-meta-ticker-track">
|
||||||
|
<span data-slot="hero-meta-ticker-item">{props.previous}</span>
|
||||||
|
<span data-slot="hero-meta-ticker-item">{props.current}</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function StatsLoading() {
|
function StatsLoading() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -185,17 +253,27 @@ function EmptyState(props: { title: string; description: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatUpdatedAt(value: string, timeZone: string) {
|
function formatUpdatedAtParts(value: string, timeZone: string) {
|
||||||
const date = new Date(value)
|
const date = new Date(value)
|
||||||
if (Number.isNaN(date.getTime())) return "just now"
|
if (Number.isNaN(date.getTime())) return { date: "just now", time: "" }
|
||||||
return new Intl.DateTimeFormat("en", {
|
return {
|
||||||
month: "short",
|
date: new Intl.DateTimeFormat("en", {
|
||||||
day: "numeric",
|
month: "short",
|
||||||
hour: "numeric",
|
day: "numeric",
|
||||||
minute: "2-digit",
|
timeZone,
|
||||||
timeZone,
|
}).format(date),
|
||||||
timeZoneName: "short",
|
time: new Intl.DateTimeFormat("en", {
|
||||||
}).format(date)
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
timeZone,
|
||||||
|
timeZoneName: "short",
|
||||||
|
}).format(date),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUpdatedAtLabel(value: { date: string; time: string }) {
|
||||||
|
if (!value.time) return value.date
|
||||||
|
return `${value.date}, ${value.time}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function TopModelsSection(props: { data: StatsHomeData["usage"] }) {
|
function TopModelsSection(props: { data: StatsHomeData["usage"] }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user