Improve gear expiry countdowns and hover details

This commit is contained in:
Matt Isenhower
2026-09-27 18:13:37 -07:00
parent a17ee37819
commit 42c0df51bf
5 changed files with 89 additions and 11 deletions

View File

@@ -14,7 +14,7 @@
"in": "in {time}",
"remaining": "{time} übrig",
"until": "Bis {time}",
"left": "{time} verbleibend",
"left": "Noch {time}",
"d": "{n}Tg | {n}Tg",
"h": "{n}Std | {n}Std",
"m": "{n}Min | {n}Min",

View File

@@ -65,12 +65,10 @@
<!-- Time left -->
<div class="absolute top-1 left-6">
<div class="inline-block text-xs bg-zinc-200/30 rounded-sm px-1 py-px font-semibold">
<SkeletonBlock v-if="loading" class="h-4 w-16" />
<template v-else>
{{ $t('time.left', { time: formatDurationHoursFromNow(props.gear.saleEndTime) }) }}
</template>
<div v-if="loading" class="inline-block text-xs bg-zinc-200/30 rounded-sm px-1 py-px font-semibold">
<SkeletonBlock class="h-4 w-16" />
</div>
<GearExpiry v-else :end-time="props.gear.saleEndTime" />
</div>
</div>
</template>
@@ -78,7 +76,7 @@
<script setup>
import SkeletonBlock from '@/components/loading/SkeletonBlock.vue';
import { computed } from 'vue';
import { formatDurationHoursFromNow } from '@/common/time';
import GearExpiry from './GearExpiry.vue';
const props = defineProps({
gear: Object,

View File

@@ -15,9 +15,7 @@
</template>
<template v-else>
<div class="flex">
<div class="inline-block text-xs bg-zinc-200/30 rounded-sm px-1 py-px font-semibold">
{{ $t('time.left', { time: formatDurationHoursFromNow(props.gear.saleEndTime) }) }}
</div>
<GearExpiry :end-time="props.gear.saleEndTime" />
</div>
<div class="flex items-center space-x-2">
@@ -104,7 +102,7 @@
import SkeletonBlock from '@/components/loading/SkeletonBlock.vue';
import { computed } from 'vue';
import SquidTape from '@/components/SquidTape.vue';
import { formatDurationHoursFromNow } from '@/common/time';
import GearExpiry from './GearExpiry.vue';
import { getGesotownGearUrl } from '@/common/links';
const props = defineProps({

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';
import { createSSRApp } from 'vue';
import { renderToString } from 'vue/server-renderer';
import { createPinia } from 'pinia';
import { createI18n } from 'vue-i18n';
import messages from '@/assets/i18n/en-US.json';
import { useTimeStore } from '@/stores/time.mjs';
import GearExpiry from './GearExpiry.vue';
describe('gear expiry labels', () => {
it.each([
[1, '1 second left'],
[45, '45 seconds left'],
[59, '59 seconds left'],
[60, '1 minute left'],
[3600, '1 hour left'],
])('shows the appropriate label with %i seconds left', async (seconds, label) => {
const endTime = '2026-09-28T00:00:00Z';
const app = createSSRApp(GearExpiry, { endTime });
const pinia = createPinia();
app.use(pinia);
useTimeStore(pinia).setNow(Date.parse(endTime) - seconds * 1000);
app.use(createI18n({
legacy: false,
locale: 'en-US',
messages: { 'en-US': messages },
datetimeFormats: {
'en-US': {
dateTimeShortWeekday: {
weekday: 'short',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
timeZone: 'UTC',
},
},
},
}));
const html = await renderToString(app);
expect(html).toContain(label);
expect(html).toContain('title="Until Mon, 9/28, 12:00 AM"');
});
});

View File

@@ -0,0 +1,34 @@
<template>
<span :title="deadline" class="inline-block text-xs bg-zinc-200/30 rounded-sm px-1 py-px font-semibold">
<template v-if="remainingSeconds < 60">
{{ $t('time.left', { time: $t('time.seconds', { n: remainingSeconds }, remainingSeconds) }) }}
</template>
<template v-else>
{{ $t('time.left', { time: formatDurationHoursFromNow(endTime) }) }}
</template>
</span>
</template>
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { formatDurationHoursFromNow } from '@/common/time';
import { useTimeStore } from '@/stores/time.mjs';
const props = defineProps({
endTime: { type: String, required: true },
});
const time = useTimeStore();
const { t, d } = useI18n();
const deadline = computed(() => t('time.until', {
time: d(props.endTime, 'dateTimeShortWeekday'),
}));
const remainingSeconds = computed(() => {
const remaining = Date.parse(props.endTime) - time.now;
return Math.max(0, Math.ceil(remaining / 1000));
});
</script>