Files
RenewCMS/web/src/components/SaveStatus.vue
T

104 lines
2.6 KiB
Vue

<script setup lang="ts">
import { t } from '@/i18n'
import { computed, onMounted, onUnmounted, ref } from 'vue'
const props = defineProps<{
state: 'clean' | 'dirty' | 'saving' | 'saved' | 'error' | 'offline'
savedAt?: Date | null
}>()
defineEmits<{ retry: [] }>()
const MINUTE_MS = 60 * 1000
const HOUR_MS = 60 * MINUTE_MS
const TICK_MS = 15000
const now = ref(Date.now())
let ticker: ReturnType<typeof setInterval> | null = null
onMounted(() => {
ticker = setInterval(() => {
now.value = Date.now()
}, TICK_MS)
})
onUnmounted(() => {
if (ticker !== null) {
clearInterval(ticker)
}
})
function formatClock(date: Date) {
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${hours}:${minutes}`
}
function formatElapsed(diffMs: number) {
const minutes = Math.floor(diffMs / MINUTE_MS)
if (minutes < 60) {
return t('{0} min', [minutes])
}
const hours = Math.floor(minutes / 60)
if (hours < 24) {
return t('{0} h', [hours])
}
return t('{0} d', [Math.floor(hours / 24)])
}
const savedText = computed(() => {
if (!props.savedAt) {
return t('Saved')
}
const diffMs = now.value - props.savedAt.getTime()
if (diffMs < HOUR_MS) {
return t('Saved at {0}', [formatClock(props.savedAt)])
}
return t('Saved {0} ago', [formatElapsed(diffMs)])
})
</script>
<template>
<span
aria-live="polite"
class="inline-flex shrink-0 items-center gap-1.5 text-sm whitespace-nowrap"
:class="{
'text-warning': state === 'dirty',
'text-error': state === 'error',
'text-muted': state !== 'dirty' && state !== 'error',
}"
>
<template v-if="state === 'clean' || state === 'saved'">
<UIcon name="i-lucide-check" class="size-3.5" />
<span>{{ savedText }}</span>
</template>
<template v-else-if="state === 'dirty'">
<UIcon name="i-lucide-circle-dot" class="size-3.5" />
<span>{{ t('Unsaved changes') }}</span>
</template>
<template v-else-if="state === 'saving'">
<UIcon name="i-lucide-loader-circle" class="size-3.5 animate-spin" />
<span>{{ t('Saving…') }}</span>
</template>
<template v-else-if="state === 'error'">
<UIcon name="i-lucide-circle-alert" class="size-3.5" />
<span>{{ t('Save failed') }}</span>
<UButton
size="xs"
color="neutral"
variant="outline"
icon="i-lucide-rotate-cw"
class="h-6 gap-1 px-2 text-xs text-highlighted"
@click="$emit('retry')"
>
{{ t('Retry') }}
</UButton>
</template>
<template v-else-if="state === 'offline'">
<UIcon name="i-lucide-wifi-off" class="size-3.5" />
<span>{{ t('Waiting for connection') }}</span>
</template>
</span>
</template>