Skip to content

Commit 2804af0

Browse files
committed
feat: add (hidden) vacations page
1 parent 012c35e commit 2804af0

File tree

6 files changed

+351
-0
lines changed

6 files changed

+351
-0
lines changed

app/pages/vacations.vue

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
<script setup lang="ts">
2+
definePageMeta({
3+
name: 'vacations',
4+
})
5+
6+
useSeoMeta({
7+
title: () => `${$t('vacations.title')} - npmx`,
8+
description: () => $t('vacations.meta_description'),
9+
ogTitle: () => `${$t('vacations.title')} - npmx`,
10+
ogDescription: () => $t('vacations.meta_description'),
11+
twitterTitle: () => `${$t('vacations.title')} - npmx`,
12+
twitterDescription: () => $t('vacations.meta_description'),
13+
})
14+
15+
defineOgImageComponent('Default', {
16+
title: () => $t('vacations.title'),
17+
description: () => $t('vacations.meta_description'),
18+
})
19+
20+
const router = useRouter()
21+
const canGoBack = useCanGoBack()
22+
23+
// --- Cosy fireplace easter egg ---
24+
const logClicks = ref(0)
25+
const fireVisible = ref(false)
26+
function pokeLog() {
27+
logClicks.value++
28+
if (logClicks.value >= 3) {
29+
fireVisible.value = true
30+
}
31+
}
32+
33+
// Icons that tile across the banner, repeating to fill.
34+
// Classes must be written out statically so UnoCSS can detect them at build time.
35+
const icons = [
36+
'i-carbon:snowflake',
37+
'i-carbon:mountain',
38+
'i-carbon:tree',
39+
'i-carbon:cafe',
40+
'i-carbon:book',
41+
'i-carbon:music',
42+
'i-carbon:snowflake',
43+
'i-carbon:star',
44+
'i-carbon:moon',
45+
] as const
46+
47+
// --- .ics calendar reminder ---
48+
// Pick a random daytime hour (9–17) in the user's local timezone on Feb 22
49+
// so reminders are staggered and people don't all flood in at once.
50+
function downloadIcs() {
51+
const hour = 9 + Math.floor(Math.random() * 9) // 9..17
52+
const start = new Date(2026, 1, 22, hour, 0, 0) // month is 0-indexed
53+
const end = new Date(2026, 1, 22, hour + 1, 0, 0)
54+
55+
// Format as UTC for the .ics file
56+
const fmt = (d: Date) =>
57+
d
58+
.toISOString()
59+
.replace(/[-:]/g, '')
60+
.replace(/\.\d{3}/, '')
61+
62+
const uid = `npmx-vacations-${start.getTime()}@npmx.dev`
63+
64+
const ics = [
65+
'BEGIN:VCALENDAR',
66+
'VERSION:2.0',
67+
'PRODID:-//npmx//vacations//EN',
68+
'BEGIN:VEVENT',
69+
`DTSTART:${fmt(start)}`,
70+
`DTEND:${fmt(end)}`,
71+
`SUMMARY:npmx Discord is back!`,
72+
`DESCRIPTION:The npmx team is back from vacation. Time to rejoin! https://chat.npmx.dev`,
73+
'STATUS:CONFIRMED',
74+
`UID:${uid}`,
75+
'END:VEVENT',
76+
'END:VCALENDAR',
77+
].join('\r\n')
78+
79+
const blob = new Blob([ics], { type: 'text/calendar;charset=utf-8' })
80+
const url = URL.createObjectURL(blob)
81+
const a = document.createElement('a')
82+
a.href = url
83+
a.download = 'npmx-discord-reminder.ics'
84+
a.click()
85+
URL.revokeObjectURL(url)
86+
}
87+
</script>
88+
89+
<template>
90+
<main class="container flex-1 py-12 sm:py-16 overflow-x-hidden max-w-full">
91+
<article class="max-w-2xl mx-auto">
92+
<header class="mb-12">
93+
<div class="flex items-baseline justify-between gap-4 mb-4">
94+
<h1 class="font-mono text-3xl sm:text-4xl font-medium">
95+
{{ $t('vacations.heading') }}
96+
</h1>
97+
<button
98+
type="button"
99+
class="cursor-pointer inline-flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded focus-visible:outline-accent/70 shrink-0"
100+
@click="router.back()"
101+
v-if="canGoBack"
102+
>
103+
<span class="i-carbon:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
104+
<span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
105+
</button>
106+
</div>
107+
<p class="text-fg-muted text-lg">
108+
{{ $t('vacations.subtitle') }}
109+
</p>
110+
</header>
111+
112+
<!-- Icon banner — a single row of cosy icons, clipped to fill width -->
113+
<div
114+
class="relative mb-12 px-4 border border-border rounded-lg bg-bg-subtle overflow-hidden select-none"
115+
:aria-label="$t('vacations.illustration_alt')"
116+
role="img"
117+
>
118+
<div class="flex items-center gap-4 sm:gap-5 py-3 sm:py-4 w-max">
119+
<template v-for="n in 4" :key="`set-${n}`">
120+
<!-- Campsite icon — click it 3x to light the fire -->
121+
<button
122+
type="button"
123+
class="relative shrink-0 cursor-pointer rounded transition-transform duration-200 hover:scale-110 focus-visible:outline-accent/70 w-5 h-5 sm:w-6 sm:h-6"
124+
:aria-label="$t('vacations.poke_log')"
125+
@click="pokeLog"
126+
>
127+
<span
128+
class="absolute inset-0 i-carbon:fire w-5 h-5 sm:w-6 sm:h-6 text-orange-400 transition-opacity duration-400"
129+
:class="fireVisible ? 'opacity-100' : 'opacity-0'"
130+
/>
131+
<span
132+
class="absolute inset-0 i-carbon:campsite w-5 h-5 sm:w-6 sm:h-6 transition-colors duration-400"
133+
:class="fireVisible ? 'text-amber-700' : ''"
134+
/>
135+
</button>
136+
<span
137+
v-for="(icon, i) in icons"
138+
:key="`${n}-${i}`"
139+
class="shrink-0 w-5 h-5 sm:w-6 sm:h-6 opacity-40"
140+
:class="icon"
141+
/>
142+
</template>
143+
</div>
144+
</div>
145+
146+
<section class="prose prose-invert max-w-none space-y-8">
147+
<div>
148+
<p class="text-fg-muted leading-relaxed mb-4">
149+
<i18n-t keypath="vacations.intro.p1" tag="span" scope="global">
150+
<template #some>
151+
<span class="line-through decoration-fg">{{ $t('vacations.intro.some') }}</span>
152+
{{ ' ' }}
153+
<strong class="text-fg">{{ $t('vacations.intro.all') }}</strong>
154+
</template>
155+
</i18n-t>
156+
</p>
157+
<p class="text-fg-muted leading-relaxed">
158+
{{ $t('vacations.intro.p2') }}
159+
</p>
160+
</div>
161+
162+
<!-- What's happening -->
163+
<div>
164+
<h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4">
165+
{{ $t('vacations.what.title') }}
166+
</h2>
167+
<p class="text-fg-muted leading-relaxed mb-4">
168+
<i18n-t keypath="vacations.what.p1" tag="span" scope="global">
169+
<template #dates>
170+
<strong class="text-fg">{{ $t('vacations.what.dates') }}</strong>
171+
</template>
172+
</i18n-t>
173+
</p>
174+
<ul class="space-y-3 text-fg-muted list-none p-0">
175+
<li class="flex items-start gap-3">
176+
<span class="text-fg-subtle shrink-0 mt-1">&mdash;</span>
177+
<span>
178+
<i18n-t keypath="vacations.what.discord" tag="span" scope="global">
179+
<template #garden>
180+
<code class="font-mono text-fg text-sm">{{ $t('vacations.what.garden') }}</code>
181+
</template>
182+
</i18n-t>
183+
</span>
184+
</li>
185+
<li class="flex items-start gap-3">
186+
<span class="text-fg-subtle shrink-0 mt-1">&mdash;</span>
187+
<span>{{ $t('vacations.what.site') }}</span>
188+
</li>
189+
<li class="flex items-start gap-3">
190+
<span class="text-fg-subtle shrink-0 mt-1">&mdash;</span>
191+
<span>{{ $t('vacations.what.repo') }}</span>
192+
</li>
193+
</ul>
194+
</div>
195+
196+
<!-- In the meantime -->
197+
<div>
198+
<h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4">
199+
{{ $t('vacations.meantime.title') }}
200+
</h2>
201+
<p class="text-fg-muted leading-relaxed mb-4">
202+
<i18n-t keypath="vacations.meantime.p1" tag="span" scope="global">
203+
<template #repo>
204+
<LinkBase to="https://repo.npmx.dev">
205+
{{ $t('vacations.meantime.repo_link') }}
206+
</LinkBase>
207+
</template>
208+
</i18n-t>
209+
</p>
210+
<p class="text-fg-muted leading-relaxed">
211+
{{ $t('vacations.meantime.p2') }}
212+
</p>
213+
</div>
214+
215+
<!-- See you soon -->
216+
<div>
217+
<h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4">
218+
{{ $t('vacations.return.title') }}
219+
</h2>
220+
<p class="text-fg-muted leading-relaxed mb-4">
221+
{{ $t('vacations.return.p1') }}
222+
</p>
223+
<p class="text-fg-muted leading-relaxed mb-6">
224+
<i18n-t keypath="vacations.return.p2" tag="span" scope="global">
225+
<template #social>
226+
<LinkBase to="https://social.npmx.dev">
227+
{{ $t('vacations.return.social_link') }}
228+
</LinkBase>
229+
</template>
230+
</i18n-t>
231+
</p>
232+
233+
<!-- Add to calendar button -->
234+
<ButtonBase classicon="i-carbon:calendar" @click="downloadIcs">
235+
{{ $t('vacations.return.add_to_calendar') }}
236+
</ButtonBase>
237+
</div>
238+
</section>
239+
</article>
240+
</main>
241+
</template>

i18n/locales/en.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,5 +1105,41 @@
11051105
"p1": "If you encounter an accessibility barrier on {app}, please let us know by opening an issue on our {link}. We take these reports seriously and will do our best to address them.",
11061106
"link": "GitHub repository"
11071107
}
1108+
},
1109+
"vacations": {
1110+
"title": "on vacation",
1111+
"meta_description": "The npmx team is recharging. Discord reopens in a week.",
1112+
"heading": "recharging",
1113+
"subtitle": "… because it's a marathon, not a sprint.",
1114+
"illustration_alt": "a single row of cosy icons",
1115+
"poke_log": "Poke the campfire",
1116+
"intro": {
1117+
"p1": "We've been building npmx at a pace that has cost {some} of us sleep. That's not sustainable – and we don't want it to be the norm. So we're doing something about it: all of us are taking a week off. Together.",
1118+
"some": "some",
1119+
"all": "all",
1120+
"p2": "Go outside. Touch grass – or snow. Finish that book you started before npmx took over your evenings. Visit someone you've been meaning to visit. Do something that has nothing to do with code. We'll all be better for it."
1121+
},
1122+
"what": {
1123+
"title": "what's happening",
1124+
"p1": "Discord is closed {dates}.",
1125+
"dates": "February 14 – 21",
1126+
"discord": "All invite links are gone and channels are locked – except {garden}, which stays open for folks already there who want to keep hanging out.",
1127+
"garden": "#garden",
1128+
"site": "npmx.dev is still up. Browse packages as usual.",
1129+
"repo": "The repo is open. Issues and PRs are welcome – but expect us to be hands-off until we're back."
1130+
},
1131+
"meantime": {
1132+
"title": "in the meantime",
1133+
"p1": "You're more than welcome to keep poking around while we're away. The codebase is on {repo} – dig in, file issues, open PRs. We'll get to everything when we return.",
1134+
"repo_link": "GitHub",
1135+
"p2": "Just don't expect a fast review. We'll be somewhere near a cosy fireplace."
1136+
},
1137+
"return": {
1138+
"title": "see you soon",
1139+
"p1": "We'll come back recharged and ready for the final push to March 3rd. Things are going to get even more interesting after that – so let's make sure we're taking care of each other first.",
1140+
"p2": "Follow us {social} if you want updates in the meantime.",
1141+
"social_link": "on Bluesky",
1142+
"add_to_calendar": "remind me when Discord reopens"
1143+
}
11081144
}
11091145
}

lunaria/files/en-GB.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,5 +1104,41 @@
11041104
"p1": "If you encounter an accessibility barrier on {app}, please let us know by opening an issue on our {link}. We take these reports seriously and will do our best to address them.",
11051105
"link": "GitHub repository"
11061106
}
1107+
},
1108+
"vacations": {
1109+
"title": "on vacation",
1110+
"meta_description": "The npmx team is recharging. Discord reopens in a week.",
1111+
"heading": "recharging",
1112+
"subtitle": "… because it's a marathon, not a sprint.",
1113+
"illustration_alt": "a single row of cosy icons",
1114+
"poke_log": "Poke the campfire",
1115+
"intro": {
1116+
"p1": "We've been building npmx at a pace that has cost {some} of us sleep. That's not sustainable – and we don't want it to be the norm. So we're doing something about it: all of us are taking a week off. Together.",
1117+
"some": "some",
1118+
"all": "all",
1119+
"p2": "Go outside. Touch grass – or snow. Finish that book you started before npmx took over your evenings. Visit someone you've been meaning to visit. Do something that has nothing to do with code. We'll all be better for it."
1120+
},
1121+
"what": {
1122+
"title": "what's happening",
1123+
"p1": "Discord is closed {dates}.",
1124+
"dates": "February 14 – 21",
1125+
"discord": "All invite links are gone and channels are locked – except {garden}, which stays open for folks already there who want to keep hanging out.",
1126+
"garden": "#garden",
1127+
"site": "npmx.dev is still up. Browse packages as usual.",
1128+
"repo": "The repo is open. Issues and PRs are welcome – but expect us to be hands-off until we're back."
1129+
},
1130+
"meantime": {
1131+
"title": "in the meantime",
1132+
"p1": "You're more than welcome to keep poking around while we're away. The codebase is on {repo} – dig in, file issues, open PRs. We'll get to everything when we return.",
1133+
"repo_link": "GitHub",
1134+
"p2": "Just don't expect a fast review. We'll be somewhere near a cosy fireplace."
1135+
},
1136+
"return": {
1137+
"title": "see you soon",
1138+
"p1": "We'll come back recharged and ready for the final push to March 3rd. Things are going to get even more interesting after that – so let's make sure we're taking care of each other first.",
1139+
"p2": "Follow us {social} if you want updates in the meantime.",
1140+
"social_link": "on Bluesky",
1141+
"add_to_calendar": "remind me when Discord reopens"
1142+
}
11071143
}
11081144
}

lunaria/files/en-US.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,5 +1104,41 @@
11041104
"p1": "If you encounter an accessibility barrier on {app}, please let us know by opening an issue on our {link}. We take these reports seriously and will do our best to address them.",
11051105
"link": "GitHub repository"
11061106
}
1107+
},
1108+
"vacations": {
1109+
"title": "on vacation",
1110+
"meta_description": "The npmx team is recharging. Discord reopens in a week.",
1111+
"heading": "recharging",
1112+
"subtitle": "… because it's a marathon, not a sprint.",
1113+
"illustration_alt": "a single row of cosy icons",
1114+
"poke_log": "Poke the campfire",
1115+
"intro": {
1116+
"p1": "We've been building npmx at a pace that has cost {some} of us sleep. That's not sustainable – and we don't want it to be the norm. So we're doing something about it: all of us are taking a week off. Together.",
1117+
"some": "some",
1118+
"all": "all",
1119+
"p2": "Go outside. Touch grass – or snow. Finish that book you started before npmx took over your evenings. Visit someone you've been meaning to visit. Do something that has nothing to do with code. We'll all be better for it."
1120+
},
1121+
"what": {
1122+
"title": "what's happening",
1123+
"p1": "Discord is closed {dates}.",
1124+
"dates": "February 14 – 21",
1125+
"discord": "All invite links are gone and channels are locked – except {garden}, which stays open for folks already there who want to keep hanging out.",
1126+
"garden": "#garden",
1127+
"site": "npmx.dev is still up. Browse packages as usual.",
1128+
"repo": "The repo is open. Issues and PRs are welcome – but expect us to be hands-off until we're back."
1129+
},
1130+
"meantime": {
1131+
"title": "in the meantime",
1132+
"p1": "You're more than welcome to keep poking around while we're away. The codebase is on {repo} – dig in, file issues, open PRs. We'll get to everything when we return.",
1133+
"repo_link": "GitHub",
1134+
"p2": "Just don't expect a fast review. We'll be somewhere near a cosy fireplace."
1135+
},
1136+
"return": {
1137+
"title": "see you soon",
1138+
"p1": "We'll come back recharged and ready for the final push to March 3rd. Things are going to get even more interesting after that – so let's make sure we're taking care of each other first.",
1139+
"p2": "Follow us {social} if you want updates in the meantime.",
1140+
"social_link": "on Bluesky",
1141+
"add_to_calendar": "remind me when Discord reopens"
1142+
}
11071143
}
11081144
}

nuxt.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ export default defineNuxtConfig({
149149
'/privacy': { prerender: true },
150150
'/search': { isr: false, cache: false }, // never cache
151151
'/settings': { prerender: true },
152+
'/vacations': { prerender: true },
152153
// proxy for insights
153154
'/_v/script.js': { proxy: 'https://npmx.dev/_vercel/insights/script.js' },
154155
'/_v/view': { proxy: 'https://npmx.dev/_vercel/insights/view' },

server/middleware/canonical-redirects.global.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const pages = [
2525
'/privacy',
2626
'/search',
2727
'/settings',
28+
'/vacations',
2829
]
2930

3031
const cacheControl = 's-maxage=3600, stale-while-revalidate=36000'

0 commit comments

Comments
 (0)