16 Commits

Author SHA1 Message Date
pigui
63d4410296 Show proposals and votes from TaskStream on a project's page
The most voted bug reports and ideas, the link to send one while the
board takes them, and how many want each epic on the roadmap. The mock
API serves a showcase for the catalogue so every section can be seen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:21:40 +02:00
pigui
a3783222c8 Show a project's roadmap, releases and first issues from TaskStream
When a project has a board in TaskStream, its page lists the epics with
their dates and progress, the releases with their notes, the sprint under
way and the tickets meant for newcomers, linking to the board. TaskStream
being slow or down only leaves those sections out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:13:58 +02:00
pigui
bca3e4bcc6 Show who signs a post and its tags, and read the blog by tag
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 11:20:57 +02:00
pigui
e5d91d673a Put the blog where people look for it
The blog was reachable only from a link in the footer, which is to say
hardly at all. Once there is a post:

- "Blog" joins the header menu, on desktop and in the mobile menu, on
  every page.
- The landing shows the three latest posts under "Novedades", with a
  link to the rest.

The post card moves into its own component, post-card, shared by the
blog (image beside the text) and the landing (image above).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 23:02:55 +02:00
pigui
240ad149e7 Bring the landing to life as it scrolls
- Each section assembles itself on arrival: heading first, then what it
  holds, and a grid's cards one after another — instead of the whole
  section fading in as one block.
- The hero enters on load, piece by piece, in CSS, so it plays with or
  without JavaScript; the stats come last and count up from zero.
- The hero's lights drift and fade as it scrolls away, and the header
  gains a shadow once the page moves — scroll-driven animations, no
  listeners; browsers without them simply do not animate.

The count only runs if the page hydrates before the stats appear at
0.8s: after that the figure has been read, and dropping it to zero to
count again would be a flicker. Server-rendered HTML carries the real
numbers and nothing hidden, and prefers-reduced-motion turns all of it
off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:21:48 +02:00
pigui
fea60a168c Add a blog, built from pages
A post is a page in the panel's "Blog" category, so it is written,
translated, scheduled and reviewed like any page, and the MCP server
can publish one the same way. Nothing new in the API.

- /blog (/en/blog, /ca/blog): posts newest first, with image, date and
  excerpt.
- A post shows its date and leads back to the blog; its structured data
  is a BlogPosting.
- /blog/rss.xml?lang=xx: one feed per language, announced in <head>.
- Posts stay out of the header and the footer, which links the blog
  once there is any post instead of growing with every one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:45:08 +02:00
pigui
86b7485b32 Tell search engines and link previews what each page is
- /sitemap.xml, from the API's /slugs plus the landing and listings:
  every address in every language, with hreflang and x-default. A 503
  when the API is down, rather than a sitemap missing half the site.
- /robots.txt, naming the sitemap on whatever domain this is served on.
- Slugs renamed in the panel now redirect. The API has always kept the
  old address the moment one changed; middleware/redirects.ts answers
  it with a 301, from a list kept for a minute and through outages.
- og:url, og:image (the record's own picture, the logo otherwise) and
  schema.org JSON-LD: Organization on the landing; WebPage,
  CreativeWork, Person or Service elsewhere. Shared through core/head.
- The proxy-derived origin moves to core/origin.ts, used by the page
  shell, the landing and both server routes.

In development only /api/* reaches Nitro, so the two files 404 under
`npm start`; they were checked against a production build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:21:59 +02:00
pigui
399b8d5450 Add a page for every way to help
/colabora (/en/contribute, /ca/col-labora) — the calls' own section, so
it lists what /colabora/{slug} are — brings the open calls, the good
first issues across the collective's repositories, and a five-step
guide from picking a task to having it reviewed. With nothing open and
nothing labelled, the guide and the other ways to help are still there.
The landing's "contribute" section links to it.

Also fixes the landing's issue list, which @pi/ui's `li + li` margin
spaced twice over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:14:16 +02:00
pigui
daacc2c5d6 Give each service its own page
A service now has its address, like a project: icon, tagline, what the
collective does and how, the projects it was delivered on, what the
people it was for said, and the technologies behind it. The landing's
service cards link to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:10:19 +02:00
pigui
06eeefc144 Give the tech radar its own page
/radar (/en/radar, /ca/radar) shows every technology the collective has
placed, by ring: logo, category, what it is, why it sits there, its
website and how many projects use it. The landing's radar summary links
to it.

It is the first listing — a page this site composes rather than one
record's — so resolveRoute learns about those (LISTINGS), and builds
their addresses and hreflang alternates from the same table.

Logos sit on a light tile in both themes. A brand's mark is not ours to
recolour, and Rust's, which is black, would vanish in dark mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:07:19 +02:00
pigui
b5681a12ac Give each call its own page, and a form to answer it
A call now has its address — description, what the volunteer gets, the
commitment added up, level, closing date, project, technologies — and
the application form right there. The landing's call card leads to it
instead of scrolling to the general form, and shows the level in the
visitor's language rather than the API's `mid` or `any`.

The form is the landing's, with a `callSlug` that hides the kind
selector and posts to the new /api/calls/{slug}/apply handler: the API
refuses a `kind` on an application, which is always an offer of time.
Both handlers share one check, core/submission.ts, now tested on its
own.

A call that is filled or closed keeps its page — a link to it may
still be out there — but shows why there is no form, and where to
write instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 15:02:06 +02:00
pigui
439f951f2e Give projects and members their own pages
The API has always served a project's and a member's detail, with
`path` and `alternates` in three languages, and the site had nowhere to
show it. A project now has its page — tagline, status, technologies,
description, technical decisions, what was learned, gallery, client,
dates, lead, team, repositories and open calls — and so does a member:
headline, bio, links, technologies and projects. Cards on the landing
link to both.

Addresses follow the API's rule read backwards (core/routes.ts):
pages at the root, records under a section named in their language,
the default language unprefixed. One catch-all route serves them all.
Not a route per shape: Analog registers each loader as an internal
Nitro route, and Nitro's router cannot hold `/:prefix/:slug` beside
`/:prefix/:section/:slug` — the first became unreachable.

The frame around a record (header, footer, lights, 404/503, title,
description, canonical, hreflang) is one component now, PageShell, and
each type only brings its body, all sharing one layout partial.

Also fixes chips that stepped down one row after the first, on the
landing's cards too: @pi/ui's reset gives every `li + li` a top margin,
right for prose and wrong for a row spaced by `gap`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 14:57:13 +02:00
pigui
80a1924519 Serve the pages the panel publishes
The landing was one address. It now also answers on the pages the panel
publishes -- "Sobre nosotros" and the like -- at the addresses the API
itself chooses: `path` and `alternates` put the default language
unprefixed and every other one under its code, so `/sobre-nosotros`,
`/en/about-us`, `/ca/qui-som` are the routes. `core/api.ts#contentRoute`
is the single rule for that shape, and `/es/…` is deliberately not one
of them, so a page has one address rather than two.

Two routes share one load function, because Analog wants a `.server.ts`
beside each `.page.ts`: `[slug]` for the default language, and
`[locale]/[slug]` for the rest.

Whether a slug exists is the API's answer, not something the route table
knows, so the 404 middleware lets page-shaped addresses through and
`ui/content-page.ts` sets the status itself from `ngOnInit` -- the same
`injectResponse()` the landing uses for its 503, and `ngOnInit` because
an input has no value in the constructor. Body HTML comes from the
panel's editor and goes through Angular's sanitiser.

The header shows the featured pages beside the section anchors; the
footer lists them all. On a page the language switcher follows the API's
`alternates`, and a language with no translation leads to the landing in
that language instead of a 404.

Verified: lint, 20 unit tests, build, and 6 e2e -- including a new one
that a page-shaped address the API does not know answers 404 with
`robots: noindex`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 09:55:10 +02:00
pigui
68ad014df0 Stop publishing a host port
`docker compose up` failed with "port is already allocated": 3000 was
taken by an unrelated project on the same machine. Publishing it was
never needed — in production the only thing that has to reach this
service is the reverse proxy, which does so over the compose network as
`landing:3000`.

`expose` replaces `ports`, and a commented `ports` block stays for the
cases that do want a host binding. LANDING_PORT is kept but re-labelled
as only mattering then.

Verified: the container binds nothing on the host, answers 200 with the
full page from another container on the network, and stays healthy —
its health check was always internal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:59:42 +02:00
pigui
a226cbddbd Fail the image build when the Nexus token is missing
`docker compose build` without NEXUS_TOKEN produced a Nexus 401 forty
seconds in, worded as a credentials problem when it was really an unset
variable. The mount's `required=true` asserts the secret was *declared*,
not that it holds anything, so an empty one sailed past it and npm sent
an empty `_authToken`.

The install moves to tools/docker-install-deps.sh, which checks the
secret before touching the network and fails in under a second with the
three ways to supply it. It is a file rather than inline shell because
BuildKit echoes an inline command twice on failure and buries the
message.

Also ignore .env. It carries NEXUS_TOKEN, compose reads it at build
time, and it was not gitignored — .env.example stays tracked. Nothing
had been committed: the only prior commit predates all of this.

Verified: empty token fails in 0.2s with the message; .env and the
environment variable both build; the image carries no trace of the
token and serves the full page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:56:11 +02:00
pigui
3f930bb46a Initial commit 2026-08-30 21:11:59 +02:00