diff --git a/app/components/BlueskyPostEmbed.client.vue b/app/components/BlueskyPostEmbed.client.vue new file mode 100644 index 000000000..32e3b982a --- /dev/null +++ b/app/components/BlueskyPostEmbed.client.vue @@ -0,0 +1,128 @@ + + + diff --git a/app/pages/recharging.vue b/app/pages/recharging.vue new file mode 100644 index 000000000..5d5df22fc --- /dev/null +++ b/app/pages/recharging.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/i18n/locales/en.json b/i18n/locales/en.json index c8c59c783..adcc6e675 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -1110,5 +1110,40 @@ "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": "{site} and {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": "the repo" + }, + "return": { + "title": "see you soon", + "p1": "we'll come back recharged and ready for the final push to March 3rd. {social} for updates.", + "social_link": "follow us on Bluesky", + "add_to_calendar": "remind me when Discord reopens" + }, + "stats": { + "contributors": "Contributors", + "commits": "Commits", + "pr": "PRs Merged", + "subtitle": { + "some": "some", + "all": "all" + } + } } } diff --git a/i18n/schema.json b/i18n/schema.json index 4827d567e..92163031c 100644 --- a/i18n/schema.json +++ b/i18n/schema.json @@ -3337,6 +3337,111 @@ }, "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" + }, + "what": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "p1": { + "type": "string" + }, + "dates": { + "type": "string" + }, + "p2": { + "type": "string" + }, + "garden": { + "type": "string" + } + }, + "additionalProperties": false + }, + "meantime": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "p1": { + "type": "string" + }, + "repo_link": { + "type": "string" + } + }, + "additionalProperties": false + }, + "return": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "p1": { + "type": "string" + }, + "social_link": { + "type": "string" + }, + "add_to_calendar": { + "type": "string" + } + }, + "additionalProperties": false + }, + "stats": { + "type": "object", + "properties": { + "contributors": { + "type": "string" + }, + "commits": { + "type": "string" + }, + "pr": { + "type": "string" + }, + "subtitle": { + "type": "object", + "properties": { + "some": { + "type": "string" + }, + "all": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, "$schema": { "type": "string" } diff --git a/lunaria/files/en-GB.json b/lunaria/files/en-GB.json index d9685809e..0949dc42c 100644 --- a/lunaria/files/en-GB.json +++ b/lunaria/files/en-GB.json @@ -1109,5 +1109,40 @@ "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": "{site} and {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": "the repo" + }, + "return": { + "title": "see you soon", + "p1": "we'll come back recharged and ready for the final push to March 3rd. {social} for updates.", + "social_link": "follow us on Bluesky", + "add_to_calendar": "remind me when Discord reopens" + }, + "stats": { + "contributors": "Contributors", + "commits": "Commits", + "pr": "PRs Merged", + "subtitle": { + "some": "some", + "all": "all" + } + } } } diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json index ffec3ed29..30a83e757 100644 --- a/lunaria/files/en-US.json +++ b/lunaria/files/en-US.json @@ -1109,5 +1109,40 @@ "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": "{site} and {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": "the repo" + }, + "return": { + "title": "see you soon", + "p1": "we'll come back recharged and ready for the final push to March 3rd. {social} for updates.", + "social_link": "follow us on Bluesky", + "add_to_calendar": "remind me when Discord reopens" + }, + "stats": { + "contributors": "Contributors", + "commits": "Commits", + "pr": "PRs Merged", + "subtitle": { + "some": "some", + "all": "all" + } + } } } diff --git a/nuxt.config.ts b/nuxt.config.ts index 47a66a94c..0c41f9bc5 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -149,6 +149,7 @@ export default defineNuxtConfig({ '/privacy': { prerender: true }, '/search': { isr: false, cache: false }, // never cache '/settings': { prerender: true }, + '/recharging': { prerender: true }, // proxy for insights '/_v/script.js': { proxy: 'https://npmx.dev/_vercel/insights/script.js' }, '/_v/view': { proxy: 'https://npmx.dev/_vercel/insights/view' }, diff --git a/server/api/repo-stats.get.ts b/server/api/repo-stats.get.ts new file mode 100644 index 000000000..0b998d179 --- /dev/null +++ b/server/api/repo-stats.get.ts @@ -0,0 +1,80 @@ +import { CACHE_MAX_AGE_ONE_HOUR } from '#shared/utils/constants' + +const REPO = 'npmx-dev/npmx.dev' +const GITHUB_HEADERS = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'npmx', +} as const + +interface GitHubSearchResponse { + total_count: number +} + +export interface RepoStats { + contributors: number + commits: number + pullRequests: number +} + +export default defineCachedEventHandler( + async (): Promise => { + const [contributorsCount, commitsCount, prsCount] = await Promise.all([ + fetchPageCount(`https://api.github.com/repos/${REPO}/contributors?per_page=1&anon=false`), + fetchPageCount(`https://api.github.com/repos/${REPO}/commits?per_page=1`), + fetchSearchCount('issues', `repo:${REPO} is:pr is:merged`), + ]) + + return { + contributors: contributorsCount, + commits: commitsCount, + pullRequests: prsCount, + } + }, + { + maxAge: CACHE_MAX_AGE_ONE_HOUR, + swr: true, + name: 'repo-stats', + getKey: () => 'repo-stats', + }, +) + +/** + * Count items by requesting a single result and reading the last page + * number from the Link header. + */ +async function fetchPageCount(url: string): Promise { + const response = await fetch(url, { headers: GITHUB_HEADERS }) + + if (!response.ok) { + throw createError({ statusCode: response.status, message: `Failed to fetch ${url}` }) + } + + const link = response.headers.get('link') + if (link) { + const match = link.match(/[?&]page=(\d+)>;\s*rel="last"/) + if (match?.[1]) { + return Number.parseInt(match[1], 10) + } + } + + // No Link header means only one page — count the response body + const body = (await response.json()) as unknown[] + return body.length +} + +/** + * Use the GitHub search API to get a total_count for issues/PRs. + */ +async function fetchSearchCount(type: 'issues', query: string): Promise { + const response = await fetch( + `https://api.github.com/search/${type}?q=${encodeURIComponent(query)}&per_page=1`, + { headers: GITHUB_HEADERS }, + ) + + if (!response.ok) { + throw createError({ statusCode: response.status, message: `Failed to fetch ${type} count` }) + } + + const data = (await response.json()) as GitHubSearchResponse + return data.total_count +} diff --git a/server/middleware/canonical-redirects.global.ts b/server/middleware/canonical-redirects.global.ts index 230ab3443..b6e9d1cba 100644 --- a/server/middleware/canonical-redirects.global.ts +++ b/server/middleware/canonical-redirects.global.ts @@ -25,6 +25,7 @@ const pages = [ '/privacy', '/search', '/settings', + '/recharging', ] const cacheControl = 's-maxage=3600, stale-while-revalidate=36000' diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts index 84caccdd2..23ffb915a 100644 --- a/test/nuxt/a11y.spec.ts +++ b/test/nuxt/a11y.spec.ts @@ -115,6 +115,7 @@ import { AppHeader, AppLogo, BaseCard, + BlueskyPostEmbed, BuildEnvironment, ButtonBase, LinkBase, @@ -2467,6 +2468,18 @@ describe('component accessibility audits', () => { }) }) + describe('BlueskyPostEmbed', () => { + it('should have no accessibility violations in pending state', async () => { + const component = await mountSuspended(BlueskyPostEmbed, { + props: { + uri: 'at://did:plc:u5zp7npt5kpueado77kuihyz/app.bsky.feed.post/3mejzn5mrcc2g', + }, + }) + const results = await runAxe(component) + expect(results.violations).toEqual([]) + }) + }) + describe('UserAvatar', () => { it('should have no accessibility violations', async () => { const component = await mountSuspended(UserAvatar, {