@v4/code-of-conduct

The In-Arena Code of Conduct incident report: a signed-in fan picks a game, describes what happened and it is emailed to ELB.

2026-11-07

Overview

@v4/code-of-conduct is the In-Arena Code of Conduct incident report, which replaces the third-party form at inform.euroleague.net. A signed-in fan chooses the game they attended, says what happened, attaches photos or videos, and the report is emailed to ELB. The design is ELB – Web – Content, section 29238:5802. Club-only.

The page is apps/web/src/app/[locale]/code-of-conduct/page.tsx (noindex), and its layout loads the ghost-basketball theme part, because the game cards are basketball components.

The three entries

Entry Contents
@v4/code-of-conduct The screens — sign-in card, game picker, report, sent — and their parts (stepper, fields, tick box, drop zone, file rows, notices), all plain React over ghost-core, ghost-content and ghost-basketball; the rules; the sample season
@v4/code-of-conduct/page CodeOfConductPage, the client page body: asks /api/auth/session who is signed in and shows the sign-in card or the form
@v4/code-of-conduct/server The three route factories the app mounts under app/api/code-of-conduct/, and the email

The screens carry no sign-in code, so Storybook (Code of Conduct/*) renders exactly what the site does, on the sample season.

The endpoints

Route What it does
GET /api/code-of-conduct/games One season's played games, with the competitions and seasons offered (EuroLeague and EuroCup, the newest two seasons each), from the Competition Engine. The engine filters by round and one club only, so venue, team and date are filtered in the browser
GET /api/code-of-conduct/reporter The signed-in reporter's name and email, read on the server from the session's ID token (readSessionProfile in @v4/cortex-sso/server). 401 signed out
POST /api/code-of-conduct/report Send. Takes the game, the description and the files as a multipart form; the reporter is always the session's, never the form's. Checks everything the form checks again, then emails the report. Answers 401 signed out, 422 for a profile with no email, 400 for a report that breaks a rule, 413 for a file over 50 MB, 503 when sending is not set up and 502 when the email failed

The rules, the same in the browser and on the server (src/report.ts): a description of 20 to 2,000 characters, the consent box ticked, at most five files of JPG, PNG, HEIC, MP4 or MOV, up to 50 MB each. A multipart body sends each line break as CRLF, so the server counts it once.

The email

renderIncidentEmail writes the report in the agreed format: the template attached to the ticket, whose *|merge_tags|* (reporter, game, round, date, venue, description, files) it fills by their own names, plus the same content as plain text and HTML. The game is dated in ELB's time zone (Europe/Madrid), the reply goes to the reporter, and the files travel as attachments. The template's "Open report in the browser" link is left out: reports are not stored.

Sending is a seam, deliver. No email provider is chosen yet, so:

  • Locally (NODE_ENV not production), the email is written to the server log instead (logIncidentEmail) and the form goes through to its confirmation.
  • Everywhere else, Send answers 503 and the form shows its "couldn't send" notice with Try again.

The ticket names the recipients: three ELB addresses for production, one testing address for dev and staging. They are configured with the provider.

Running it locally

The sample season stands in for the Competition Engine wherever the engine cannot be reached at all, locally only; production shows the load error instead, never made-up games. A timeout is not "cannot be reached", so point the engine at a closed port to get the sample at once:

COMPETITION_ENGINE_BASE_URL=http://127.0.0.1:9 pnpm --filter @v4/web exec next dev --webpack -p 3000

Signing in needs http://localhost:3000 (the only address Cortex stage returns to) and the CORTEX_SSO_* settings. With sign-on off, the form opens with a placeholder reporter, and on a local site Send logs the email as that reporter.

What it changed elsewhere

  • ghost-basketball Game: layout="small" keeps the Small card at every width (the report's game panel); action.trailingIcon and action.accessibleLabel for the card's own button.
  • ghost-core FilterDropdownTrigger: ariaHasPopup={false} for a trigger that opens an inline panel ("More filters") rather than a list.
  • cortex-sso: readSessionProfile, the signed-in visitor's first name, last name and email from the session's ID token, for a server route.