Skip to content

Commit b5cdba8

Browse files
committed
chore: add build environment component
1 parent db1e9b2 commit b5cdba8

6 files changed

Lines changed: 227 additions & 29 deletions

File tree

app/components/AppFooter.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
<script setup lang="ts"></script>
12
<template>
23
<footer class="border-t border-border mt-auto">
34
<div class="container py-3 sm:py-8 flex flex-col gap-2 sm:gap-4 text-fg-subtle text-sm">
@@ -54,6 +55,7 @@
5455
<span class="sm:hidden">{{ $t('non_affiliation_disclaimer') }}</span>
5556
<span class="hidden sm:inline">{{ $t('trademark_disclaimer') }}</span>
5657
</p>
58+
<BuildEnvironment footer />
5759
</div>
5860
</footer>
5961
</template>

app/components/AppHeader.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ onKeyStroke(',', e => {
5252
aria-hidden="true"
5353
:alt="$t('alt_logo')"
5454
src="/logo.svg"
55+
width="96"
56+
height="96"
5557
class="w-8 h-8 rounded-lg"
5658
/>
5759
<span>npmx</span>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
footer?: boolean
4+
}>()
5+
6+
const buildInfo = useAppConfig().buildInfo
7+
const timeAgoOptions = useTimeAgoOptions()
8+
const buildTimeDate = new Date(buildInfo.time)
9+
const buildTimeAgo = useTimeAgo(buildTimeDate, timeAgoOptions)
10+
const isHydrated = shallowRef(false)
11+
12+
useNuxtApp().hook('app:suspense:resolve', () => {
13+
console.log('app:suspense:resolve')
14+
isHydrated.value = true
15+
})
16+
</script>
17+
<template>
18+
<div
19+
class="font-mono text-xs text-fg-muted flex items-center gap-2 motion-safe:animate-fade-in motion-safe:animate-fill-both"
20+
:class="footer ? 'mt-4 justify-start' : 'mb-8 justify-center'"
21+
style="animation-delay: 0.05s"
22+
>
23+
<!-- TODO: replace this with NuxtTime -->
24+
<ClientOnly>
25+
<i18n-t keypath="built_at">
26+
<time :datetime="String(buildTimeDate)" :title="$d(buildTimeDate, 'long')">{{
27+
buildTimeAgo
28+
}}</time>
29+
</i18n-t>
30+
</ClientOnly>
31+
<span>&middot;</span>
32+
<NuxtLink
33+
v-if="buildInfo.env === 'release'"
34+
external
35+
:href="`https://github.com/npmx-dev/npmx.dev/tag/v${buildInfo.version}`"
36+
target="_blank"
37+
class="hover:text-fg transition-colors"
38+
>
39+
v{{ buildInfo.version }}
40+
</NuxtLink>
41+
<span v-else class="tracking-wider">{{ buildInfo.env }}</span>
42+
43+
<template v-if="buildInfo.commit && buildInfo.branch !== 'release'">
44+
<span>&middot;</span>
45+
<NuxtLink
46+
external
47+
:href="`https://github.com/npmx-dev/npmx.dev/commit/${buildInfo.commit}`"
48+
target="_blank"
49+
class="hover:text-fg transition-colors"
50+
>
51+
{{ buildInfo.shortCommit }}
52+
</NuxtLink>
53+
</template>
54+
</div>
55+
</template>

app/composables/i18n.ts

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type { UseTimeAgoOptions } from '@vueuse/core'
2+
3+
/**
4+
* Global Intl formatter.
5+
* @public
6+
*/
7+
const formatter = Intl.NumberFormat()
8+
9+
/**
10+
* Composable to handle Intl number formatting.
11+
* @public
12+
*/
13+
export function formattedNumber(num: number, useFormatter: Intl.NumberFormat = formatter) {
14+
return useFormatter.format(num)
15+
}
16+
17+
/**
18+
* Composable to handle number formatting.
19+
* @public
20+
*/
21+
export function useHumanReadableNumber() {
22+
const { n, locale } = useI18n()
23+
24+
const fn = (num: number) => {
25+
return n(
26+
num,
27+
num < 10000 ? 'smallCounting' : num < 1000000 ? 'kiloCounting' : 'millionCounting',
28+
locale.value,
29+
)
30+
}
31+
32+
return {
33+
formatHumanReadableNumber: (num: MaybeRef<number>) => fn(unref(num)),
34+
formatNumber: (num: MaybeRef<number>) => n(unref(num), 'smallCounting', locale.value),
35+
formatPercentage: (num: MaybeRef<number>) => n(unref(num), 'percentage', locale.value),
36+
forSR: (num: MaybeRef<number>) => unref(num) > 10000,
37+
}
38+
}
39+
40+
/**
41+
* Composable to handle datetime formatting.
42+
* TODO: check if can replace with NuxtTime
43+
* @public
44+
*/
45+
export function useFormattedDateTime(
46+
value: MaybeRefOrGetter<string | number | Date | undefined | null>,
47+
options: Intl.DateTimeFormatOptions = { dateStyle: 'long', timeStyle: 'medium' },
48+
) {
49+
const { locale } = useI18n()
50+
const formatter = computed(() => Intl.DateTimeFormat(locale.value, options))
51+
return computed(() => {
52+
const v = toValue(value)
53+
return v ? formatter.value.format(new Date(v)) : ''
54+
})
55+
}
56+
57+
/**
58+
* Composable to handle time ago formatting.
59+
* TODO: check if can replace with NuxtTime
60+
* @public
61+
*/
62+
export function useTimeAgoOptions(short = false): UseTimeAgoOptions<false> {
63+
const { d, t, n: fnf, locale } = useI18n()
64+
const prefix = short ? 'short_' : ''
65+
66+
const fn = (n: number, past: boolean, key: string) => {
67+
return t(`time_ago_options.${prefix}${key}_${past ? 'past' : 'future'}`, n, {
68+
named: {
69+
v: fnf(n, 'smallCounting', locale.value),
70+
},
71+
})
72+
}
73+
74+
return {
75+
rounding: 'floor',
76+
showSecond: !short,
77+
updateInterval: short ? 60000 : 1000,
78+
messages: {
79+
justNow: t('time_ago_options.just_now'),
80+
// just return the value
81+
past: n => n,
82+
// just return the value
83+
future: n => n,
84+
second: (n, p) => fn(n, p, 'second'),
85+
minute: (n, p) => fn(n, p, 'minute'),
86+
hour: (n, p) => fn(n, p, 'hour'),
87+
day: (n, p) => fn(n, p, 'day'),
88+
week: (n, p) => fn(n, p, 'week'),
89+
month: (n, p) => fn(n, p, 'month'),
90+
year: (n, p) => fn(n, p, 'year'),
91+
invalid: '',
92+
},
93+
fullDateFormatter(date) {
94+
return d(date, short ? 'short' : 'long')
95+
},
96+
}
97+
}
98+
/**
99+
* Composable to handle file sizes formatting.
100+
* TODO: review if we need this composable
101+
* @public
102+
*/
103+
export function useFileSizeFormatter() {
104+
const { locale } = useI18n()
105+
106+
const formatters = computed(
107+
() =>
108+
[
109+
Intl.NumberFormat(locale.value, {
110+
style: 'unit',
111+
unit: 'megabyte',
112+
unitDisplay: 'narrow',
113+
maximumFractionDigits: 0,
114+
}),
115+
Intl.NumberFormat(locale.value, {
116+
style: 'unit',
117+
unit: 'kilobyte',
118+
unitDisplay: 'narrow',
119+
maximumFractionDigits: 0,
120+
}),
121+
] as const,
122+
)
123+
124+
const megaByte = 1024 * 1024
125+
126+
function formatFileSize(size: number) {
127+
return size >= megaByte
128+
? formatters.value[0].format(size / megaByte)
129+
: formatters.value[1].format(size / 1024)
130+
}
131+
132+
return { formatFileSize }
133+
}

app/pages/index.vue

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import { debounce } from 'perfect-debounce'
33
44
const router = useRouter()
5-
const buildInfo = useAppConfig().buildInfo
65
76
const searchQuery = ref('')
87
const searchInputRef = useTemplateRef('searchInputRef')
@@ -43,39 +42,15 @@ defineOgImageComponent('Default')
4342
aria-hidden="true"
4443
:alt="$t('alt_logo')"
4544
src="/logo.svg"
45+
width="48"
46+
height="48"
4647
class="w-12 h-12 sm:w-20 sm:h-20 md:w-24 md:h-24 rounded-2xl sm:rounded-3xl"
4748
/>
4849
<span class="pb-4">npmx</span>
4950
</h1>
5051

51-
<!-- Build info badge (moved below title) -->
52-
<div
53-
class="mb-8 font-mono text-xs text-fg-muted flex items-center justify-center gap-2 motion-safe:animate-fade-in motion-safe:animate-fill-both"
54-
style="animation-delay: 0.05s"
55-
>
56-
<NuxtLink
57-
v-if="buildInfo.env === 'release'"
58-
external
59-
:href="`https://github.com/npmx-dev/npmx.dev/tag/v${buildInfo.version}`"
60-
target="_blank"
61-
class="hover:text-fg transition-colors"
62-
>
63-
v{{ buildInfo.version }}
64-
</NuxtLink>
65-
<span v-else class="uppercase tracking-wider">{{ buildInfo.env }}</span>
66-
67-
<template v-if="buildInfo.commit && buildInfo.branch !== 'release'">
68-
<span>&middot;</span>
69-
<NuxtLink
70-
external
71-
:href="`https://github.com/npmx-dev/npmx.dev/commit/${buildInfo.commit}`"
72-
target="_blank"
73-
class="hover:text-fg transition-colors"
74-
>
75-
{{ buildInfo.shortCommit }}
76-
</NuxtLink>
77-
</template>
78-
</div>
52+
<!-- Build info badge -->
53+
<BuildEnvironment />
7954

8055
<p
8156
class="text-fg-muted text-lg sm:text-xl max-w-md mb-12 motion-safe:animate-slide-up motion-safe:animate-fill-both"

i18n/locales/en.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,5 +750,36 @@
750750
"empty": "No organizations found",
751751
"view_all": "View all"
752752
}
753+
},
754+
"time_ago_options": {
755+
"day_future": "in 0 days|tomorrow|in {n} days",
756+
"day_past": "0 days ago|yesterday|{n} days ago",
757+
"hour_future": "in 0 hours|in 1 hour|in {n} hours",
758+
"hour_past": "0 hours ago|1 hour ago|{n} hours ago",
759+
"just_now": "just now",
760+
"minute_future": "in 0 minutes|in 1 minute|in {n} minutes",
761+
"minute_past": "0 minutes ago|1 minute ago|{n} minutes ago",
762+
"month_future": "in 0 months|next month|in {n} months",
763+
"month_past": "0 months ago|last month|{n} months ago",
764+
"second_future": "just now|in {n} second|in {n} seconds",
765+
"second_past": "just now|{n} second ago|{n} seconds ago",
766+
"short_day_future": "in {n}d",
767+
"short_day_past": "{n}d",
768+
"short_hour_future": "in {n}h",
769+
"short_hour_past": "{n}h",
770+
"short_minute_future": "in {n}min",
771+
"short_minute_past": "{n}min",
772+
"short_month_future": "in {n}mo",
773+
"short_month_past": "{n}mo",
774+
"short_second_future": "in {n}s",
775+
"short_second_past": "{n}s",
776+
"short_week_future": "in {n}w",
777+
"short_week_past": "{n}w",
778+
"short_year_future": "in {n}y",
779+
"short_year_past": "{n}y",
780+
"week_future": "in 0 weeks|next week|in {n} weeks",
781+
"week_past": "0 weeks ago|last week|{n} weeks ago",
782+
"year_future": "in 0 years|next year|in {n} years",
783+
"year_past": "0 years ago|last year|{n} years ago"
753784
}
754785
}

0 commit comments

Comments
 (0)