Skip to content
Merged
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
172 changes: 134 additions & 38 deletions app/pages/about.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { Role } from '#server/api/contributors.get'

const router = useRouter()
const canGoBack = useCanGoBack()

Expand Down Expand Up @@ -27,6 +29,22 @@ const pmLinks = {
}

const { data: contributors, status: contributorsStatus } = useLazyFetch('/api/contributors')

const governanceMembers = computed(
() => contributors.value?.filter(c => c.role !== 'contributor') ?? [],
)

const communityContributors = computed(
() => contributors.value?.filter(c => c.role === 'contributor') ?? [],
)

const roleLabels = computed(
() =>
({
steward: $t('about.team.role_steward'),
maintainer: $t('about.team.role_maintainer'),
}) as Partial<Record<Role, string>>,
)
</script>

<template>
Expand Down Expand Up @@ -139,56 +157,134 @@ const { data: contributors, status: contributorsStatus } = useLazyFetch('/api/co

<div>
<h2 class="text-lg text-fg-subtle uppercase tracking-wider mb-4">
{{
$t(
'about.contributors.title',
{ count: $n(contributors?.length ?? 0) },
contributors?.length ?? 0,
)
}}
{{ $t('about.team.title') }}
</h2>
<p class="text-fg-muted leading-relaxed mb-6">
{{ $t('about.contributors.description') }}
</p>

<!-- Contributors cloud -->
<div v-if="contributorsStatus === 'pending'" class="text-fg-subtle text-sm">
{{ $t('about.contributors.loading') }}
</div>
<div v-else-if="contributorsStatus === 'error'" class="text-fg-subtle text-sm">
{{ $t('about.contributors.error') }}
</div>
<div
v-else-if="contributors?.length"
class="grid grid-cols-[repeat(auto-fill,48px)] justify-center gap-2"
<!-- Governance: stewards + maintainers -->
<section
v-if="governanceMembers.length"
class="mb-12"
aria-labelledby="governance-heading"
>
<a
v-for="contributor in contributors"
:key="contributor.id"
:href="contributor.html_url"
target="_blank"
rel="noopener noreferrer"
class="group relative"
:aria-label="$t('about.contributors.view_profile', { name: contributor.login })"
<h3
id="governance-heading"
class="text-sm text-fg-subtle uppercase tracking-wider mb-4"
>
<div class="relative flex items-center">
{{ $t('about.team.governance') }}
</h3>

<ul class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 list-none p-0">
<li
v-for="person in governanceMembers"
:key="person.id"
class="relative flex items-center gap-3 p-3 border border-border rounded-lg hover:border-border-hover hover:bg-bg-muted transition-[border-color,background-color] duration-200 cursor-pointer focus-within:ring-2 focus-within:ring-offset-bg focus-within:ring-offset-2 focus-within:ring-fg/50"
>
<img
:src="`${contributor.avatar_url}&s=64`"
:alt="contributor.login"
width="32"
height="32"
class="w-12 h-12 rounded-lg ring-2 ring-transparent group-hover:ring-accent transition-all duration-200 ease-out hover:scale-125 will-change-transform"
:src="`${person.avatar_url}&s=80`"
:alt="`${person.login}'s avatar`"
class="w-12 h-12 rounded-md ring-1 ring-border shrink-0"
loading="lazy"
/>
<div class="min-w-0 flex-1">
<div class="font-mono text-sm text-fg truncate">
<NuxtLink
:to="person.html_url"
target="_blank"
class="decoration-none after:content-[''] after:absolute after:inset-0"
:aria-label="$t('about.contributors.view_profile', { name: person.login })"
>
@{{ person.login }}
Comment on lines +193 to +199
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 | 🟠 Major

Add rel="noopener noreferrer" for external profile links opened in a new tab.

On Line 193, target="_blank" without a safe rel exposes window.opener and enables tabnabbing.

🔒 Suggested fix
<NuxtLink
  :to="person.html_url"
  target="_blank"
+ rel="noopener noreferrer"
  class="decoration-none after:content-[''] after:absolute after:inset-0"
  :aria-label="$t('about.contributors.view_profile', { name: person.login })"
>
📝 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
<NuxtLink
:to="person.html_url"
target="_blank"
class="decoration-none after:content-[''] after:absolute after:inset-0"
:aria-label="$t('about.contributors.view_profile', { name: person.login })"
>
@{{ person.login }}
<NuxtLink
:to="person.html_url"
target="_blank"
rel="noopener noreferrer"
class="decoration-none after:content-[''] after:absolute after:inset-0"
:aria-label="$t('about.contributors.view_profile', { name: person.login })"
>
@{{ person.login }}

</NuxtLink>
</div>
<div class="text-xs text-fg-muted tracking-tight">
{{ roleLabels[person.role] ?? person.role }}
</div>
<LinkBase
v-if="person.sponsors_url"
:to="person.sponsors_url"
no-underline
no-external-icon
classicon="i-carbon:favorite"
class="relative z-10 text-xs text-fg-muted hover:text-pink-400 mt-0.5"
:aria-label="$t('about.team.sponsor_aria', { name: person.login })"
>
{{ $t('about.team.sponsor') }}
</LinkBase>
</div>
<span
class="pointer-events-none absolute -top-9 inset-is-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs px-2 py-1 shadow-lg opacity-0 scale-95 transition-all duration-150 group-hover:opacity-100 group-hover:scale-100"
dir="ltr"
class="i-carbon:launch rtl-flip w-3.5 h-3.5 text-fg-muted opacity-50 shrink-0 self-start mt-0.5"
aria-hidden="true"
/>
</li>
</ul>
</section>

<!-- Contributors cloud -->
<section aria-labelledby="contributors-heading">
<h3
id="contributors-heading"
class="text-sm text-fg-subtle uppercase tracking-wider mb-4"
>
{{
$t(
'about.contributors.title',
{ count: $n(communityContributors.length) },
communityContributors.length,
)
}}
</h3>

<div
v-if="contributorsStatus === 'pending'"
class="text-fg-subtle text-sm"
role="status"
>
{{ $t('about.contributors.loading') }}
</div>
<div
v-else-if="contributorsStatus === 'error'"
class="text-fg-subtle text-sm"
role="alert"
>
{{ $t('about.contributors.error') }}
</div>
<ul
v-else-if="communityContributors.length"
class="grid grid-cols-[repeat(auto-fill,48px)] justify-center gap-2 list-none p-0"
>
<li
v-for="contributor in communityContributors"
:key="contributor.id"
class="group relative"
>
<LinkBase
:to="contributor.html_url"
no-underline
no-external-icon
:aria-label="$t('about.contributors.view_profile', { name: contributor.login })"
>
@{{ contributor.login }}
</span>
</div>
</a>
</div>
<img
:src="`${contributor.avatar_url}&s=64`"
:alt="`${contributor.login}'s avatar`"
width="48"
height="48"
class="w-12 h-12 rounded-lg ring-2 ring-transparent group-hover:ring-accent transition-all duration-200 ease-out hover:scale-125 will-change-transform"
loading="lazy"
/>
<span
class="pointer-events-none absolute -top-9 inset-is-1/2 -translate-x-1/2 whitespace-nowrap rounded-md bg-gray-900 text-white dark:bg-gray-100 dark:text-gray-900 text-xs px-2 py-1 shadow-lg opacity-0 scale-95 transition-all duration-150 group-hover:opacity-100 group-hover:scale-100"
dir="ltr"
role="tooltip"
>
@{{ contributor.login }}
</span>
</LinkBase>
</li>
</ul>
</section>
</div>

<CallToAction />
Expand Down
10 changes: 9 additions & 1 deletion i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -810,8 +810,16 @@
"managers": "package managers"
}
},
"team": {
"title": "Team",
"governance": "Governance",
"role_steward": "steward",
"role_maintainer": "maintainer",
"sponsor": "sponsor",
"sponsor_aria": "Sponsor {name} on GitHub"
},
"contributors": {
"title": "{count} Contributor | {count} Contributors",
"title": "... and {count} more contributor | ... and {count} more contributors",
"description": "npmx is fully open source, built by an amazing community of contributors. Join us and let's build the npm browsing experience we always wanted, together.",
"loading": "Loading contributors...",
"error": "Failed to load contributors",
Expand Down
24 changes: 24 additions & 0 deletions i18n/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2434,6 +2434,30 @@
},
"additionalProperties": false
},
"team": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"governance": {
"type": "string"
},
"role_steward": {
"type": "string"
},
"role_maintainer": {
"type": "string"
},
"sponsor": {
"type": "string"
},
"sponsor_aria": {
"type": "string"
}
},
"additionalProperties": false
},
"contributors": {
"type": "object",
"properties": {
Expand Down
10 changes: 9 additions & 1 deletion lunaria/files/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -809,8 +809,16 @@
"managers": "package managers"
}
},
"team": {
"title": "Team",
"governance": "Governance",
"role_steward": "steward",
"role_maintainer": "maintainer",
"sponsor": "sponsor",
"sponsor_aria": "Sponsor {name} on GitHub"
},
"contributors": {
"title": "{count} Contributor | {count} Contributors",
"title": "... and {count} more contributor | ... and {count} more contributors",
"description": "npmx is fully open source, built by an amazing community of contributors. Join us and let's build the npm browsing experience we always wanted, together.",
"loading": "Loading contributors...",
"error": "Failed to load contributors",
Expand Down
10 changes: 9 additions & 1 deletion lunaria/files/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -809,8 +809,16 @@
"managers": "package managers"
}
},
"team": {
"title": "Team",
"governance": "Governance",
"role_steward": "steward",
"role_maintainer": "maintainer",
"sponsor": "sponsor",
"sponsor_aria": "Sponsor {name} on GitHub"
},
"contributors": {
"title": "{count} Contributor | {count} Contributors",
"title": "... and {count} more contributor | ... and {count} more contributors",
"description": "npmx is fully open source, built by an amazing community of contributors. Join us and let's build the npm browsing experience we always wanted, together.",
"loading": "Loading contributors...",
"error": "Failed to load contributors",
Expand Down
3 changes: 3 additions & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export default defineNuxtConfig({

runtimeConfig: {
sessionPassword: '',
github: {
orgToken: '',
},
// Upstash Redis for distributed OAuth token refresh locking in production
upstash: {
redisRestUrl: process.env.UPSTASH_KV_REST_API_URL || process.env.KV_REST_API_URL || '',
Expand Down
Loading
Loading