Files
ProxMenux/web/app/[locale]/docs/about/contributors/page.tsx
T
MacRimi fc41b04e87 Pre-release 1.2.7: OCI fixes, 12 new catalog applications and docs updates
Last changes before version 1.2.7.

OCI manager Apps
- Multi-container applications start while one of their containers is
  being backed up: the start hook waits until vzdump releases it and does
  not start it twice. The DHCP client of a dependency no longer keeps the
  lock of the hook, so later starts do not hang, and the watchdog waits
  while a dependency is locked. Installing or updating ProxMenux replaces
  the published copies of the hook already in place.
- Images built from scratch with a numeric user, such as ddns-updater,
  install: the container is created from a copy of the image that adds
  the /etc/passwd and /etc/group Proxmox VE needs, and the application
  runs with the user the image declares. The downloaded image stays
  unchanged. An image with a named user and no user database stops
  before anything is created, with a message that says why.

Catalog
- Twelve new applications: MDBListarr, CrossWatch, Free Games Claimer
  Remaster, Cloudreve, Poznote and AceStream HTTP Proxy, suggested in
  discussion #440, and the Dockur images of Chrony, Dnsmasq, Tor,
  Stunnel, strfry and Statping. The catalog lists 347 applications.
- Their prompts, labels and notes are translated into the seven
  languages of the menus.

Documentation
- OCI manager: architecture, lifecycle and stacks pages revised, with
  larger diagrams and new screenshots.
- Contributors: the page now thanks those developing, reviewing,
  translating and testing ProxMenux today and those who helped in its
  first steps, with new avatars.

Tests for the stack start hook, the watchdog, the user database check
of OCI images and the Tor installation summary.
2026-10-25 13:17:54 +01:00

149 lines
9.3 KiB
TypeScript

import type { Metadata } from "next"
import type { ReactNode } from "react"
import Image from "next/image"
import { getTranslations, setRequestLocale } from "next-intl/server"
import { BookOpen, Code2, ExternalLink, FlaskRound, GitPullRequest, Heart, Languages, Youtube } from "lucide-react"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
const REPOSITORY = "https://github.com/MacRimi/ProxMenux"
const activeContributors = ["Vaso73", "f3rs3n", "jcastro"] as const
// Human authors of merged PRs and code contributions, checked on 2026-10-25.
// Alphabetical, without commit rankings. Ongoing contributors are credited above.
const codeContributors = [
"byGarcia", "cod378", "detlefs", "fklnc94", "MrCaringi",
"pbr85", "pespinel", "phyrexia", "riri-314", "yeager",
]
const earlyContributors = [
{ name: "MALOW", avatar: "malow.png" },
{ name: "Segarra", avatar: "segarra.png" },
{ name: "Aprilia", avatar: "aprilia.png" },
{ name: "Jonatan Castro", avatar: "jonatancastro.png", github: "jcastro", youtube: "https://www.youtube.com/@JonatanCastro" },
{ name: "Kamunhas", avatar: "Kamunhas.png" },
{ name: "heriberto", avatar: "heriberto.png" },
{ name: "JF_Car", avatar: "JF_Car.png" },
{ name: "rafapuerta", avatar: "rafapuerta.png" },
{ name: "JcMinarro", avatar: "JcMinarro.png" },
]
function contributionsUrl(login: string) {
return REPOSITORY + "/pulls?q=" + encodeURIComponent("is:pr is:merged author:" + login)
}
function External({ href, children, className = "" }: { href: string; children: ReactNode; className?: string }) {
return <a href={href} target="_blank" rel="noopener noreferrer" className={"rounded focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-600 " + className}>{children}</a>
}
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.about.contributors.meta" })
return {
title: t("title"), description: t("description"),
openGraph: {
title: t("ogTitle"), description: t("ogDescription"), type: "article",
url: "https://proxmenux.com/" + locale + "/docs/about/contributors/",
},
}
}
export default async function ContributorsPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.about.contributors" })
const ways = [
{ key: "code", Icon: Code2 }, { key: "testing", Icon: FlaskRound },
{ key: "translations", Icon: Languages }, { key: "docs", Icon: BookOpen },
] as const
const heading = "mb-3 text-2xl font-semibold text-gray-900"
const paragraph = "mb-6 leading-relaxed text-gray-700"
return (
<div>
<DocHeader title={t("header.title")} description={t("header.description")} section={t("header.section")} estimatedMinutes={3} />
<section aria-labelledby="ongoing-contributors" className="mt-10">
<h2 id="ongoing-contributors" className={heading}>{t("active.heading")}</h2>
<p className={paragraph}>{t("active.intro")}</p>
<div className="grid gap-4 md:grid-cols-3">
{activeContributors.map((login) => (
<article key={login} className="flex min-w-0 flex-col rounded-xl border border-blue-100 bg-gradient-to-b from-blue-50/70 to-white p-5">
<External href={"https://github.com/" + login} className="group flex items-center gap-3">
<Image src={"/avatars/contributors/" + login + ".png"} alt="" width={64} height={64} className="h-16 w-16 shrink-0 rounded-full border border-white object-cover shadow-sm" />
<h3 className="min-w-0 break-words text-lg font-semibold text-gray-900 group-hover:text-blue-700">{login}<ExternalLink className="ml-1.5 inline h-3.5 w-3.5 text-gray-400" aria-hidden="true" /></h3>
</External>
<p className="mb-5 mt-4 flex-1 text-sm leading-6 text-gray-700">{t("active.people." + login)}</p>
<External href={contributionsUrl(login)} className="inline-flex items-center gap-2 self-start text-sm font-medium text-blue-700 hover:underline">
<GitPullRequest className="h-4 w-4 shrink-0" aria-hidden="true" />{t("code.viewWork")}<span className="sr-only">: {login}</span>
</External>
</article>
))}
</div>
</section>
<section aria-labelledby="code-contributors" className="mt-12">
<h2 id="code-contributors" className={heading}>{t("code.heading")}</h2>
<p className={paragraph}>{t("code.intro")}</p>
<ul className="grid list-none gap-3 p-0 sm:grid-cols-2 lg:grid-cols-3">
{codeContributors.map((login) => (
<li key={login} className="flex min-w-0 items-center gap-3 rounded-lg border border-gray-200 p-3">
<Image src={"/avatars/contributors/" + login + ".png"} alt="" width={40} height={40} className="h-10 w-10 shrink-0 rounded-full object-cover" />
<div className="min-w-0">
<External href={"https://github.com/" + login} className="break-words text-sm font-semibold text-gray-900 hover:text-blue-700 hover:underline">{login}</External>
<External href={contributionsUrl(login)} className="block text-xs leading-5 text-blue-700 hover:underline">{t("code.viewWork")}<span className="sr-only">: {login}</span></External>
</div>
</li>
))}
</ul>
<div className="mt-5 flex flex-wrap gap-x-6 gap-y-3 text-sm font-medium">
<External href={REPOSITORY + "/graphs/contributors"} className="inline-flex items-center gap-1.5 text-blue-700 hover:underline">{t("code.history")}<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" /></External>
<External href={REPOSITORY + "/pulls?q=is%3Apr+is%3Amerged"} className="inline-flex items-center gap-1.5 text-blue-700 hover:underline">{t("code.merged")}<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" /></External>
</div>
</section>
<section aria-labelledby="community-contributors" className="mt-12 rounded-xl border border-gray-200 bg-gray-50 p-5 sm:p-6">
<h2 id="community-contributors" className={heading}>{t("community.heading")}</h2>
<p className="mb-3 leading-relaxed text-gray-700">{t("community.body")}</p>
<p className="m-0 leading-relaxed text-gray-700">{t("community.languages")}</p>
</section>
<section aria-labelledby="early-contributors" className="mt-12">
<h2 id="early-contributors" className={heading}>{t("testers.heading")}</h2>
<p className={paragraph}>{t("testers.intro")}</p>
<ul className="grid list-none grid-cols-2 gap-x-4 gap-y-6 p-0 sm:grid-cols-3 lg:grid-cols-5">
{earlyContributors.map((person) => (
<li key={person.name} className="min-w-0 text-center">
<Image src={"/avatars/" + person.avatar} alt="" width={64} height={64} className="mx-auto h-16 w-16 rounded-full border border-gray-200 object-cover" />
<h3 className="mb-0 mt-2 break-words text-sm font-semibold text-gray-900">
{person.github ? <External href={"https://github.com/" + person.github} className="hover:text-blue-700 hover:underline">{person.name}</External> : person.name}
</h3>
{person.youtube && <External href={person.youtube} className="mt-1 inline-flex items-center gap-1 text-xs text-red-700 hover:underline"><Youtube className="h-3.5 w-3.5" aria-hidden="true" />{t("testers.youtube")}</External>}
</li>
))}
</ul>
</section>
<section aria-labelledby="join-contributors" className="mt-12 border-t border-gray-200 pt-8">
<h2 id="join-contributors" className={heading}>{t("contribute.heading")}</h2>
<p className={paragraph}>{t("contribute.intro")}</p>
<ul className="grid list-none gap-4 p-0 sm:grid-cols-2">
{ways.map(({ key, Icon }) => (
<li key={key} className="flex gap-3">
<Icon className="mt-1 h-5 w-5 shrink-0 text-blue-700" aria-hidden="true" />
<div><h3 className="m-0 text-base font-semibold text-gray-900">{t("contribute.ways." + key + ".title")}</h3><p className="mb-0 mt-1 text-sm leading-6 text-gray-700">{t("contribute.ways." + key + ".body")}</p></div>
</li>
))}
</ul>
<div className="mt-6 flex flex-wrap gap-3">
<External href={REPOSITORY + "/blob/main/CONTRIBUTING.md"} className="inline-flex items-center gap-2 rounded-lg bg-blue-700 px-4 py-2.5 text-sm font-semibold text-white hover:bg-blue-800">{t("contribute.guide")}<ExternalLink className="h-4 w-4" aria-hidden="true" /></External>
<External href={REPOSITORY + "/discussions"} className="inline-flex items-center gap-2 rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-semibold text-gray-800 hover:bg-gray-50">{t("contribute.discuss")}<ExternalLink className="h-4 w-4" aria-hidden="true" /></External>
</div>
</section>
<Callout variant="tip" title={t("thanks.title")} className="mt-8">
<p className="flex items-start gap-2"><Heart className="mt-0.5 h-4 w-4 shrink-0" aria-hidden="true" /><span>{t("thanks.body")}</span></p>
</Callout>
</div>
)
}