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
241 changes: 241 additions & 0 deletions app/pages/vacations.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
<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 ---
// 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)

// Format as UTC for the .ics file
const fmt = (d: Date) =>
d
.toISOString()
.replace(/[-:]/g, '')
.replace(/\.\d{3}/, '')

const uid = `npmx-vacations-${start.getTime()}@npmx.dev`

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 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="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>
</div>
<p class="text-fg-muted text-lg">
{{ $t('vacations.subtitle') }}
</p>
</header>

<!-- 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="img"
>
<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"
/>
</template>
</div>
</div>

<section class="prose prose-invert max-w-none space-y-8">
<div>
<p class="text-fg-muted leading-relaxed mb-4">
<i18n-t keypath="vacations.intro.p1" tag="span" scope="global">
<template #some>
<span class="line-through decoration-fg">{{ $t('vacations.intro.some') }}</span>
{{ ' ' }}
<strong class="text-fg">{{ $t('vacations.intro.all') }}</strong>
</template>
</i18n-t>
</p>
<p class="text-fg-muted leading-relaxed">
{{ $t('vacations.intro.p2') }}
</p>
</div>

<!-- 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>
<ul class="space-y-3 text-fg-muted list-none p-0">
<li class="flex items-start gap-3">
<span class="text-fg-subtle shrink-0 mt-1">&mdash;</span>
<span>
<i18n-t keypath="vacations.what.discord" tag="span" scope="global">
<template #garden>
<code class="font-mono text-fg text-sm">{{ $t('vacations.what.garden') }}</code>
</template>
</i18n-t>
</span>
</li>
<li class="flex items-start gap-3">
<span class="text-fg-subtle shrink-0 mt-1">&mdash;</span>
<span>{{ $t('vacations.what.site') }}</span>
</li>
<li class="flex items-start gap-3">
<span class="text-fg-subtle shrink-0 mt-1">&mdash;</span>
<span>{{ $t('vacations.what.repo') }}</span>
</li>
</ul>
</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 mb-4">
<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>
<p class="text-fg-muted leading-relaxed">
{{ $t('vacations.meantime.p2') }}
</p>
</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-4">
{{ $t('vacations.return.p1') }}
</p>
<p class="text-fg-muted leading-relaxed mb-6">
<i18n-t keypath="vacations.return.p2" 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>
36 changes: 36 additions & 0 deletions i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1105,5 +1105,41 @@
"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": "… because it's a marathon, not a sprint.",
"illustration_alt": "a single row of cosy icons",
"poke_log": "Poke the campfire",
"intro": {
"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.",
"some": "some",
"all": "all",
"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."
},
"what": {
"title": "what's happening",
"p1": "Discord is closed {dates}.",
"dates": "February 14 – 21",
"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.",
"garden": "#garden",
"site": "npmx.dev is still up. Browse packages as usual.",
"repo": "The repo is open. Issues and PRs are welcome – but expect us to be hands-off until we're back."
},
"meantime": {
"title": "in the meantime",
"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.",
"repo_link": "GitHub",
"p2": "Just don't expect a fast review. We'll be somewhere near a cosy fireplace."
},
"return": {
"title": "see you soon",
"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.",
"p2": "Follow us {social} if you want updates in the meantime.",
"social_link": "on Bluesky",
"add_to_calendar": "remind me when Discord reopens"
}
}
}
108 changes: 108 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3322,6 +3322,114 @@
},
"additionalProperties": false
},
"vacations": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"meta_description": {
"type": "string"
},
"heading": {
"type": "string"
},
"subtitle": {
"type": "string"
},
"illustration_alt": {
"type": "string"
},
"poke_log": {
"type": "string"
},
"intro": {
"type": "object",
"properties": {
"p1": {
"type": "string"
},
"some": {
"type": "string"
},
"all": {
"type": "string"
},
"p2": {
"type": "string"
}
},
"additionalProperties": false
},
"what": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"p1": {
"type": "string"
},
"dates": {
"type": "string"
},
"discord": {
"type": "string"
},
"garden": {
"type": "string"
},
"site": {
"type": "string"
},
"repo": {
"type": "string"
}
},
"additionalProperties": false
},
"meantime": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"p1": {
"type": "string"
},
"repo_link": {
"type": "string"
},
"p2": {
"type": "string"
}
},
"additionalProperties": false
},
"return": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"p1": {
"type": "string"
},
"p2": {
"type": "string"
},
"social_link": {
"type": "string"
},
"add_to_calendar": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"$schema": {
"type": "string"
}
Expand Down
Loading
Loading