Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 280 additions & 0 deletions app/pages/holiday.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
<script setup lang="ts">
definePageMeta({
name: 'vacations',
})
useSeoMeta({
title: () => `${$t('vacations.title')} - npmx`,
description: () => $t('vacations.meta_description'),
ogTitle: () => `${$t('vacations.title')} - npmx`,
ogDescription: () => $t('vacations.meta_description'),
twitterTitle: () => `${$t('vacations.title')} - npmx`,
twitterDescription: () => $t('vacations.meta_description'),
})
defineOgImageComponent('Default', {
title: () => $t('vacations.title'),
description: () => $t('vacations.meta_description'),
})
const router = useRouter()
const canGoBack = useCanGoBack()
// --- Cosy fireplace easter egg ---
const logClicks = ref(0)
const fireVisible = ref(false)
function pokeLog() {
logClicks.value++
if (logClicks.value >= 3) {
fireVisible.value = true
}
}
// Icons that tile across the banner, repeating to fill.
// Classes must be written out statically so UnoCSS can detect them at build time.
const icons = [
'i-carbon:snowflake',
'i-carbon:mountain',
'i-carbon:tree',
'i-carbon:cafe',
'i-carbon:book',
'i-carbon:music',
'i-carbon:snowflake',
'i-carbon:star',
'i-carbon:moon',
] as const
// --- .ics calendar reminder ---
// Format as UTC for the .ics file
const fmt = (d: Date) =>
d
.toISOString()
.replace(/[-:]/g, '')
.replace(/\.\d{3}/, '')
// Pick a random daytime hour (9–17) in the user's local timezone on Feb 22
// so reminders are staggered and people don't all flood in at once.
function downloadIcs() {
const hour = 9 + Math.floor(Math.random() * 9) // 9..17
const start = new Date(2026, 1, 22, hour, 0, 0) // month is 0-indexed
const end = new Date(2026, 1, 22, hour + 1, 0, 0)
const uid = `npmx-vacations-${start.getTime()}@npmx.dev`
const ics = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//npmx//holiday//EN',
'BEGIN:VEVENT',
`DTSTART:${fmt(start)}`,
`DTEND:${fmt(end)}`,
`SUMMARY:npmx Discord is back!`,
`DESCRIPTION:The npmx team is back from vacation. Time to rejoin! https://chat.npmx.dev`,
'STATUS:CONFIRMED',
`UID:${uid}`,
'END:VEVENT',
'END:VCALENDAR',
].join('\r\n')
Comment on lines 65 to 78
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider adding DTSTAMP for ICS spec compliance.

Per RFC 5545, DTSTAMP is a required property for VEVENT. While most calendar apps are lenient, some strict parsers may reject the file without it.

🛠️ Suggested fix
   const ics = [
     'BEGIN:VCALENDAR',
     'VERSION:2.0',
     'PRODID:-//npmx//vacations//EN',
     'BEGIN:VEVENT',
+    `DTSTAMP:${fmt(new Date())}`,
     `DTSTART:${fmt(start)}`,
     `DTEND:${fmt(end)}`,
     `SUMMARY:npmx Discord is back!`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ics = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//npmx//vacations//EN',
'BEGIN:VEVENT',
`DTSTART:${fmt(start)}`,
`DTEND:${fmt(end)}`,
`SUMMARY:npmx Discord is back!`,
`DESCRIPTION:The npmx team is back from vacation. Time to rejoin! https://chat.npmx.dev`,
'STATUS:CONFIRMED',
`UID:${uid}`,
'END:VEVENT',
'END:VCALENDAR',
].join('\r\n')
const ics = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//npmx//vacations//EN',
'BEGIN:VEVENT',
`DTSTAMP:${fmt(new Date())}`,
`DTSTART:${fmt(start)}`,
`DTEND:${fmt(end)}`,
`SUMMARY:npmx Discord is back!`,
`DESCRIPTION:The npmx team is back from vacation. Time to rejoin! https://chat.npmx.dev`,
'STATUS:CONFIRMED',
`UID:${uid}`,
'END:VEVENT',
'END:VCALENDAR',
].join('\r\n')

const blob = new Blob([ics], { type: 'text/calendar;charset=utf-8' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'npmx-discord-reminder.ics'
a.click()
URL.revokeObjectURL(url)
}
</script>

<template>
<main class="container flex-1 py-12 sm:py-16 overflow-x-hidden max-w-full">
<article class="max-w-2xl mx-auto">
<header class="mb-12">
<div class="max-w-2xl mx-auto py-8 bg-none flex justify-center">
<!-- Icon / Illustration -->
<div class="relative inline-block">
<div class="absolute inset-0 bg-accent/20 blur-3xl rounded-full" aria-hidden="true" />
<span class="relative text-8xl sm:text-9xl animate-bounce-slow inline-block">🏖️</span>
</div>
</div>
<div class="flex items-baseline justify-between gap-4 mb-4">
<h1 class="font-mono text-3xl sm:text-4xl font-medium">
{{ $t('vacations.heading') }}
</h1>
<button
type="button"
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"
@click="router.back()"
v-if="canGoBack"
>
<span class="i-carbon:arrow-left rtl-flip w-4 h-4" aria-hidden="true" />
<span class="sr-only sm:not-sr-only">{{ $t('nav.back') }}</span>
</button>
Comment on lines +105 to +113
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove per-button focus-visible utility classes.

These buttons should rely on the global focus-visible styling rather than inline utilities.

🧹 Suggested diff
-            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"
+            class="cursor-pointer inline-flex items-center gap-2 font-mono text-sm text-fg-muted hover:text-fg transition-colors duration-200 rounded shrink-0"
...
-              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"
+              class="relative shrink-0 cursor-pointer rounded transition-transform duration-200 hover:scale-110 w-5 h-5 sm:w-6 sm:h-6"

Based on learnings: In the npmx.dev project, focus-visible styling for button and select elements is implemented globally in app/assets/main.css with the rule: button:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }. Do not apply per-element inline utility classes like focus-visible:outline-accent/70 on these elements in Vue templates or components.

Also applies to: 176-179

</div>
<i18n-t
keypath="vacations.subtitle"
tag="p"
scope="global"
class="text-fg-muted text-lg sm:text-xl"
>
<template #some>
<span class="line-through decoration-fg">{{
$t('vacations.stats.subtitle.some')
}}</span>
{{ ' ' }}
<strong class="text-fg">{{ $t('vacations.stats.subtitle.all') }}</strong>
</template>
</i18n-t>
</header>

<div
class="grid grid-cols-3 justify-center gap-4 sm:gap-8 mb-8 py-8 border-y border-border/50"
>
<div class="space-y-1 text-center">
<div class="font-mono text-2xl sm:text-3xl font-bold text-fg">
{{ $t('vacations.stats.contributors_text') }}
</div>
<div class="text-xs sm:text-sm text-fg-subtle uppercase tracking-wider">
{{ $t('vacations.stats.contributors') }}
</div>
</div>
<div class="space-y-1 text-center">
<div class="font-mono text-2xl sm:text-3xl font-bold text-fg">
{{ $t('vacations.stats.commits_text') }}
</div>
<div class="text-xs sm:text-sm text-fg-subtle uppercase tracking-wider">
{{ $t('vacations.stats.commits') }}
</div>
</div>
<div class="space-y-1 text-center">
<div class="font-mono text-2xl sm:text-3xl font-bold text-fg">
{{ $t('vacations.stats.pr_text') }}
</div>
<div class="text-xs sm:text-sm text-fg-subtle uppercase tracking-wider">
{{ $t('vacations.stats.pr') }}
</div>
</div>
</div>

<section class="prose prose-invert max-w-none space-y-8">
<!-- What's happening -->
<div>
<h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4">
{{ $t('vacations.what.title') }}
</h2>
<p class="text-fg-muted leading-relaxed mb-4">
<i18n-t keypath="vacations.what.p1" tag="span" scope="global">
<template #dates>
<strong class="text-fg">{{ $t('vacations.what.dates') }}</strong>
</template>
</i18n-t>
</p>
<p class="text-fg-muted leading-relaxed mb-4">
<i18n-t keypath="vacations.what.p2" tag="span" scope="global">
<template #garden>
<code class="font-mono text-fg text-sm">{{ $t('vacations.what.garden') }}</code>
</template>
</i18n-t>
</p>
</div>

<!-- In the meantime -->
<div>
<h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4">
{{ $t('vacations.meantime.title') }}
</h2>
<p class="text-fg-muted leading-relaxed">
<i18n-t keypath="vacations.meantime.p1" tag="span" scope="global">
<template #repo>
<LinkBase to="https://repo.npmx.dev">
{{ $t('vacations.meantime.repo_link') }}
</LinkBase>
</template>
</i18n-t>
</p>
</div>

<!-- Icon banner — a single row of cosy icons, clipped to fill width -->
<div
class="relative mb-12 px-4 border border-border rounded-lg bg-bg-subtle overflow-hidden select-none"
:aria-label="$t('vacations.illustration_alt')"
role="group"
>
<div class="flex items-center gap-4 sm:gap-5 py-3 sm:py-4 w-max">
<template v-for="n in 4" :key="`set-${n}`">
<!-- Campsite icon — click it 3x to light the fire -->
<button
type="button"
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"
:aria-label="$t('vacations.poke_log')"
@click="pokeLog"
>
<span
class="absolute inset-0 i-carbon:fire w-5 h-5 sm:w-6 sm:h-6 text-orange-400 transition-opacity duration-400"
:class="fireVisible ? 'opacity-100' : 'opacity-0'"
/>
<span
class="absolute inset-0 i-carbon:campsite w-5 h-5 sm:w-6 sm:h-6 transition-colors duration-400"
:class="fireVisible ? 'text-amber-700' : ''"
/>
</button>
<span
v-for="(icon, i) in icons"
:key="`${n}-${i}`"
class="shrink-0 w-5 h-5 sm:w-6 sm:h-6 opacity-40"
:class="icon"
aria-hidden="true"
/>
</template>
</div>
</div>

<!-- See you soon -->
<div>
<h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4">
{{ $t('vacations.return.title') }}
</h2>
<p class="text-fg-muted leading-relaxed mb-6">
<i18n-t keypath="vacations.return.p1" tag="span" scope="global">
<template #social>
<LinkBase to="https://social.npmx.dev">
{{ $t('vacations.return.social_link') }}
</LinkBase>
</template>
</i18n-t>
</p>

<!-- Add to calendar button -->
<ButtonBase classicon="i-carbon:calendar" @click="downloadIcs">
{{ $t('vacations.return.add_to_calendar') }}
</ButtonBase>
</div>
</section>
</article>
</main>
</template>

<style scoped>
.animate-bounce-slow {
animation: bounce 3s infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-bounce-slow {
animation: none;
}
}
@keyframes bounce {
0%,
100% {
transform: translateY(-5%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: translateY(0);
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
</style>
38 changes: 38 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1110,5 +1110,43 @@
"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.",
"link": "GitHub repository"
}
},
"vacations": {
"title": "on vacation",
"meta_description": "The npmx team is recharging. Discord reopens in a week.",
"heading": "recharging",
"subtitle": "we've been building npmx at a pace that has cost {some} of us sleep. we don't want that to be the norm! so we are taking a week off. together.",
"illustration_alt": "a single row of cosy icons",
"poke_log": "Poke the campfire",
"what": {
"title": "what's happening",
"p1": "discord is closed {dates}.",
"dates": "February 14 – 21",
"p2": "all invite links are gone and channels are locked – except {garden}, which stays open for folks who want to keep hanging out.",
"garden": "#garden"
},
"meantime": {
"title": "in the meantime",
"p1": "npmx.dev and the {repo} stay open – dig in, file issues, open PRs. we'll get to everything when we're back. just don't expect a fast review. we'll be somewhere near a cosy fireplace.",
"repo_link": "GitHub"
},
"return": {
"title": "see you soon",
"p1": "we'll come back recharged and ready for the final push to March 3rd. follow us {social} for updates.",
"social_link": "on Bluesky",
"add_to_calendar": "remind me when Discord reopens"
},
"stats": {
"contributors": "Contributors",
"contributors_text": "160+",
"commits": "Commits",
"commits_text": "1.1k+",
"pr": "PRs Merged",
"pr_text": "900+",
"subtitle": {
"some": "some",
"all": "all"
}
}
}
}
Loading
Loading