Skip to content

Featured picture for cave entrances #1701

Description

@Paul-AUB

Feature: Featured picture for cave entrances

Context

The GrottoCenter frontend currently has no way to display an attractive image on an entrance's main page. Images exist in the system — they are attached to documents, which are themselves linked to entrances — but there is no mechanism to designate one as the visual representative of an entrance.

The goal is to allow editors to pick one image among a document's files and mark it as the featured picture of an entrance. This image would then be displayed:

  • on the entrance's main page (primary use case)
  • later, in web previews and listing cards (Open Graph, search results, map tooltips, etc.)

Business validation rule

The selected file must belong to a document that is already linked to the entrance. It is not valid to point to an arbitrary file from anywhere in the system. This constraint must be checked explicitly: when an editor sets a featured file, the API verifies that the file's parent document is among the documents associated with that entrance. If not, the request is rejected with a clear error.

Data model change

One nullable foreign key column is added to the entrance table, pointing to the file table. No new junction table is needed.

Table Change
t_entrance Add nullable FK id_featured_file → t_file(id)

API contract changes

GET /api/v1/entrances/:id — response shape

The entrance response gains a new top-level featuredFile field. When set, it is a fully populated file object (same shape as the file objects already present inside documents[].files[]). When not set, it is null.

{
  "id": 42,
  "name": "Entrance du Brudour",
  "featuredFile": {
    "id": 25,
    "fileName": "brudour-entrance.jpg",
    "completePath": "https://storage.grottocenter.org/files/brudour-entrance.jpg",
    "isValidated": true
  },
  "documents": [
    {
      "id": 10,
      "title": "Rapport 2021",
      "files": [
        { "id": 25, "fileName": "brudour-entrance.jpg", "completePath": "https://...", "isValidated": true },
        { "id": 26, "fileName": "coupe-transversale.pdf", "completePath": "https://...", "isValidated": true }
      ]
    }
  ]
}

The completePath field already contains the full public URL of the binary file — the frontend can use it directly in an <img src="..."> tag without any additional request.

PUT /api/v1/entrances/:id — request body additions

The existing update endpoint accepts two new optional fields:

Field Type Description
featuredFile integer | null ID of the file to set as featured. Must belong to a linked document. Pass null to clear.
{ "featuredFile": 25 }

Response: the updated entrance object (same shape as GET, with featuredFile populated).

Error cases:

  • 400 — the provided file ID does not belong to any document linked to this entrance.
  • 404 — the file ID does not exist.

No new endpoint is needed.


Typical frontend call sequence

Displaying the featured image (read path)

1. GET /api/v1/entrances/:id
   ↳ if response.featuredFile !== null
       → display response.featuredFile.completePath as <img>
     else
       → display placeholder / no image

The image URL is available immediately in the entrance response. No extra call.

Letting an editor pick a featured image (write path)

1. GET /api/v1/entrances/:id
   ↳ collect all files across documents[].files[]
   ↳ filter to image MIME types (jpg, png, webp…) — based on fileName extension or a future mimeType field
   ↳ pre-select response.featuredFile.id if already set

2. User picks a file from the list (or clears the selection)

3. PUT /api/v1/entrances/:id   { "featuredFile": <id> | null }
   ↳ on success: update local state with response.featuredFile
   ↳ on 400: show "This image is not linked to this entrance"

How the actual image is served

completePath is the direct public URL to the binary file on the storage backend. The frontend does not call any API endpoint to get the image bytes — it passes completePath directly to the browser as the src attribute of an <img> tag (or as an Open Graph og:image meta tag). The browser fetches the file from the storage URL independently.


Alternatives considered

A. Flag is_featured on the document–entrance junction table

Mark a document (not a file) as featured for a given entrance, and let the frontend pick the first image from that document.

  • Pro: no need to pick a specific file; works when a document has only one image.
  • Con: indirect — the frontend still has to iterate document files to find an image. No control over which image is shown when a document has multiple files. Adds a column to the junction table rather than the entrance itself.
  • Verdict: rejected — too indirect and gives less control to editors.

B. Auto-selection without storage

Return the first validated image file found among the entrance's linked documents, without storing any preference.

  • Pro: zero migration, zero model change.
  • Con: the result is non-deterministic (depends on insertion order), not controllable by editors, and can change unexpectedly when documents are added or removed.
  • Verdict: acceptable as a short-term fallback, not suitable as a permanent solution since it removes editorial control.

C. Dedicated junction table j_entrance_featured_file(id_entrance, id_file)

A separate table mapping entrances to their featured file.

  • Pro: clean separation, extensible to multiple featured images.
  • Con: over-engineered for a single nullable value; adds join complexity for no immediate benefit.
  • Verdict: rejected — premature abstraction.

D (chosen). Nullable FK id_featured_file on t_entrance

Direct, explicit, one column. Follows the same pattern as the existing nullable id_cave FK on the same table.

  • Pro: simple query, clear ownership, consistent with existing patterns.
  • Con: the file–document–entrance consistency constraint must be validated in application code (see business validation rule above).
  • Verdict: selected.

Metadata

Metadata

Assignees

No one assigned
    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions