GamendWeb.CoreComponents (gamend_web v1.0.1215)

Copy Markdown View Source

Provides core UI components.

At first glance, this module may seem daunting, but its goal is to provide core building blocks for your application, such as tables, forms, and inputs. The components consist mostly of markup and are well-documented with doc strings and declarative assigns. You may customize and style them in any way you want, based on your application growth and needs.

The foundation for styling is Tailwind CSS, a utility-first CSS framework, augmented with daisyUI, a Tailwind CSS plugin that provides UI components and themes. Here are useful references:

  • daisyUI - a good place to get started and see the available components.

  • Tailwind CSS - the foundational framework we build on. You will use it for layout, sizing, flexbox, grid, and spacing.

  • Heroicons - see icon/1 for usage.

  • Phoenix.Component - the component system used by Phoenix. Some components, such as <.link> and <.form>, are defined there.

Summary

Functions

Renders a button with navigation support.

Renders the captcha widget, or nothing when the captcha is disabled.

The "there is nothing here" panel: an icon, a heading and a line of prose.

The grid card every entity list shares — leaderboards, tournaments, groups, quests. One recipe (bg-base-200, icon in the title, badges stacked top-right, muted two-line description) so the grids read as one family instead of four dialects.

An entity's icon: the uploaded icon_url when set, otherwise the typed default for its entity type (GamendWeb.Icons.default/1) — so every group, tournament, leaderboard, quest and notification has some icon without storing one.

A country flag.

Renders flash notices.

Renders a page header: the <h1> in the one style every page title uses (text-4xl font-black text-base-content/95), an optional subtitle and actions.

Renders a Heroicon.

Renders an input with label and error messages.

Renders a data list.

Social sign-in buttons for every enabled OAuth provider, preceded by an "or" divider. Renders nothing when no provider is enabled, so the auth forms need no branching of their own.

Renders a pagination bar with Prev/Next buttons, page info, and optional page-size selector.

Renders a table with generic styling.

Renders a stored-UTC timestamp for a human reader.

Translates an error message using gettext.

Translates the errors for a field from a keyword list of errors.

A user's avatar as a round image when they have one (profile_url), falling back to the generic person icon. Pass class for sizing, e.g. "w-5 h-5". If the image URL fails to load (provider not ready yet, expired CDN link, rate-limited avatar CDN), assets/js/avatar_fallback.js hides the broken image and reveals the same icon. That lives in a real script rather than an onerror attribute because the CSP here has no script-src 'unsafe-inline', so an inline handler is refused and the fallback would never fire.

Display label for a user in admin tables: username, then display name, then the raw id as a last resort. Accepts a loaded %User{}; nil or a not-loaded association renders "-". Surface the full id separately (e.g. a title attribute on the cell) so it stays available without cluttering the table.

The game's own line under a player's name — a rank, a title, a guild — read from a metadata path the host configures

The title text user_title/1 renders, or nil. Exposed for plain-text callers.

Functions

button(assigns)

Renders a button with navigation support.

Examples

<.button>Send!</.button>
<.button phx-click="go" variant="primary">Send!</.button>
<.button navigate={~p"/"}>Home</.button>

Attributes

  • class (:string)
  • variant (:string) - Must be one of "primary".
  • Global attributes are accepted. Supports all globals plus: ["href", "navigate", "patch", "method", "download", "name", "value", "disabled"].

Slots

  • inner_block (required)

captcha(assigns)

Renders the captcha widget, or nothing when the captcha is disabled.

Place it inside the form it guards: the widget writes its token into a hidden cf-turnstile-response input, which arrives in the phx-submit params at the top level rather than under the form's as. Verify it with GamendWeb.UserAuth.verify_captcha/2.

<.form for={@form} phx-submit="save">
  <.input field={@form[:email]} type="email" />
  <.captcha id="register_captcha" />
  <.button>Register</.button>
</.form>

id must be unique on the page — the login page renders two forms, and two widgets sharing an id would leave the second one unrendered.

Attributes

  • id (:string) (required)

empty_state(assigns)

The "there is nothing here" panel: an icon, a heading and a line of prose.

For a page that legitimately has no content — no changelog file, no results, an empty list — not for an error. Both halves of the copy are attributes, and both should be translated: the text on the changelog page was an English literal on every host for exactly as long as it was written inline.

Example

<.empty_state
  icon="hero-document-text"
  title={gettext("No results.")}
  text={gettext("Add a changelog file at CHANGELOG.md to display it here.")}
/>

Attributes

  • icon (:string) (required)
  • title (:string) (required)
  • text (:string) - Defaults to nil.

entity_card(assigns)

The grid card every entity list shares — leaderboards, tournaments, groups, quests. One recipe (bg-base-200, icon in the title, badges stacked top-right, muted two-line description) so the grids read as one family instead of four dialects.

<.entity_card
  navigate={~p"/leaderboards/#{group.slug}"}
  title={group.title}
  icon_url={group.icon_url}
  type={:leaderboard}
  description={group.description}
>
  <:badges>
    <span class="badge badge-success">{gettext("Active")}</span>
  </:badges>
</.entity_card>

With navigate the card is a <.link>; without it a <div>, and any phx-click/title in rest lands on it. class appends to the wrapper — state borders (border-success), cursor-pointer, and the like.

Attributes

  • title (:string) (required)
  • icon_url (:string) - Defaults to nil.
  • icon (:atom) - Defaults to nil.
  • type (:atom) (required) - Must be one of :group, :tournament, :leaderboard, :quest, or :notification.
  • description (:string) - Defaults to nil.
  • navigate (:string) - Defaults to nil.
  • class (:any) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • badges
  • inner_block

entity_icon(assigns)

An entity's icon: the uploaded icon_url when set, otherwise the typed default for its entity type (GamendWeb.Icons.default/1) — so every group, tournament, leaderboard, quest and notification has some icon without storing one.

Pass icon (any GamendWeb.Icons atom — the full heroicons catalog) to override the type default.

Examples

<.entity_icon icon_url={group.icon_url} type={:group} />
<.entity_icon icon_url={nil} type={:quest} icon={:fire} />

Attributes

  • icon_url (:string) - Defaults to nil.
  • icon (:atom) - Defaults to nil.
  • type (:atom) (required) - Must be one of :group, :tournament, :leaderboard, :quest, or :notification.
  • class (:any) - Defaults to "w-6 h-6".

flag(assigns)

A country flag.

An <img> rather than a CSS background: the flag-icons plugin inlined all 54 as base64 data URIs in the critical stylesheet, which made it 951 KB — 69% of it flags, blocking the first paint of every page including the ones showing none. As files the browser fetches only what is on screen, in parallel, each cached on its own.

Lazy by default because most of a page's flags sit in a closed dropdown. But lazy is the wrong default for a flag the reader sees at load — the navbar's locale button, a heading, a picker's selected value: the preload scanner skips lazy images, the fetch waits for layout, and the decode is async, so even a cached flag pops in a frame or two after the text beside it. That is the "flash" on every refresh. eager opts those few out: the browser fetches them with the HTML and paints them with the first frame.

Sized in em so the caller still controls it with a text-* class, exactly as the old .fi/.fis classes did.

No cache-busting query: a page carries up to 114 of these, and a content hash on each costs more in HTML than it saves. The artwork is fixed reference data — replace a flag by replacing the file and its year-long cache expires on its own, which is the one case where that is an acceptable wait.

Attributes

  • code (:any) (required) - ISO alpha-2 country code, or nil for none.
  • square (:boolean) - 1:1 box instead of 4:3. Defaults to false.
  • eager (:boolean) - visible at load: fetch and decode with the page. Defaults to false.
  • class (:any) - Defaults to nil.

flash(assigns)

Renders flash notices.

Examples

<.flash kind={:info} flash={@flash} />
<.flash kind={:info} phx-mounted={show("#flash")}>Welcome Back!</.flash>

Attributes

  • id (:string) - the optional id of flash container.
  • flash (:map) - the map of flash messages to display. Defaults to %{}.
  • title (:string) - Defaults to nil.
  • kind (:atom) - used for styling and flash lookup. Must be one of :info, or :error.
  • Global attributes are accepted. the arbitrary HTML attributes to add to the flash container.

Slots

  • inner_block - the optional inner block that renders the flash message.

header(assigns)

Renders a page header: the <h1> in the one style every page title uses (text-4xl font-black text-base-content/95), an optional subtitle and actions.

The inner block IS the title text — pass words, an icon, a badge; never another <h1>. A heading start tag inside an open heading is a parse error the browser recovers from by closing the outer one first, so the page ended up with an empty <h1> followed by the real one.

class extends the title (flex items-center gap-3 for an icon, break-all for an unbreakable string) — it does not replace the size, so every page stays the same size without each caller restating it.

Attributes

  • class (:any) - extra classes on the <h1>. Defaults to nil.
  • back (:string) - path to go up to; renders a Back button on the title's own line. Defaults to nil.
  • back_label (:string) - overrides the "Back" wording. Defaults to nil.

Slots

  • inner_block (required)
  • subtitle
  • actions

hide(js \\ %JS{}, selector)

icon(assigns)

Renders a Heroicon.

Heroicons come in three styles – outline, solid, and mini. By default, the outline style is used, but solid and mini may be applied by using the -solid and -mini suffix.

You can customize the size and colors of the icons by setting width, height, and background color classes.

Icons are extracted from the deps/heroicons directory and bundled within your compiled app.css by the shared plugin in apps/gamend_web/assets/vendor/heroicons.js.

Examples

<.icon name="hero-x-mark" />
<.icon name="hero-arrow-path" class="ms-1 size-3 motion-safe:animate-spin" />

Attributes

  • name (:string) (required)
  • class (:string) - Defaults to "size-4".

input(assigns)

Renders an input with label and error messages.

A Phoenix.HTML.FormField may be passed as argument, which is used to retrieve the input name, id, and values. Otherwise all attributes may be passed explicitly.

Types

This function accepts all HTML input types, considering that:

  • You may also set type="select" to render a <select> tag

  • type="checkbox" is used exclusively to render boolean values

  • For live file uploads, see Phoenix.Component.live_file_input/1

See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input for more information. Unsupported types, such as hidden and radio, are best written directly in your templates.

Examples

<.input field={@form[:email]} type="email" />
<.input name="my-input" errors={["oh no!"]} />

Attributes

  • id (:any) - Defaults to nil.
  • name (:any)
  • label (:string) - Defaults to nil.
  • value (:any)
  • type (:string) - Defaults to "text". Must be one of "checkbox", "color", "date", "datetime-local", "utc-datetime-local", "email", "file", "month", "number", "password", "search", "select", "tel", "text", "textarea", "time", "url", or "week".
  • field (Phoenix.HTML.FormField) - a form field struct retrieved from the form, for example: @form[:email].
  • errors (:list) - Defaults to [].
  • checked (:boolean) - the checked flag for checkbox inputs.
  • prompt (:string) - the prompt for select inputs. Defaults to nil.
  • options (:list) - the options to pass to Phoenix.HTML.Form.options_for_select/2.
  • multiple (:boolean) - the multiple flag for select inputs. Defaults to false.
  • class (:string) - the input class to use over defaults. Defaults to nil.
  • error_class (:string) - the input error class to use over defaults. Defaults to nil.
  • Global attributes are accepted. Supports all globals plus: ["accept", "autocomplete", "capture", "cols", "disabled", "form", "list", "max", "maxlength", "min", "minlength", "multiple", "pattern", "placeholder", "readonly", "required", "rows", "size", "step"].

list(assigns)

Renders a data list.

Examples

<.list>
  <:item title="Title">{@post.title}</:item>
  <:item title="Views">{@post.views}</:item>
</.list>

Slots

  • item (required) - Accepts attributes:
    • title (:string) (required)

oauth_buttons(assigns)

Social sign-in buttons for every enabled OAuth provider, preceded by an "or" divider. Renders nothing when no provider is enabled, so the auth forms need no branching of their own.

<.oauth_buttons label={gettext("Log in")} />

Attributes

  • label (:string) (required)

pagination(assigns)

Renders a pagination bar with Prev/Next buttons, page info, and optional page-size selector.

Renders nothing when the list fits on one page: two dead buttons and a "1 / 1" counter are noise on every short list in the app. The size selector survives a one-page list when a smaller size would actually split it — otherwise raising the size until everything fits would hide the only control that undoes it.

Attributes

  • page — current page number (required)
  • total_pages — total number of pages (required)
  • total_count — total number of items (optional, shown in info text)
  • page_size — current page size (optional, enables size selector when combined with on_page_size)
  • on_prev — event name for previous page (required)
  • on_next — event name for next page (required)
  • on_page_size — event name for page size change (optional, enables size selector)
  • page_sizes — list of page size options (default: [25, 50, 100, 200])
  • class — additional CSS classes for the container

Usage

<.pagination
  page={@page}
  total_pages={@total_pages}
  total_count={@count}
  page_size={@page_size}
  on_prev="prev_page"
  on_next="next_page"
  on_page_size="page_size"
/>

Attributes

  • page (:integer) (required)
  • total_pages (:integer) (required)
  • total_count (:integer) - Defaults to nil.
  • page_size (:integer) - Defaults to nil.
  • on_prev (:string) (required)
  • on_next (:string) (required)
  • on_page_size (:string) - Defaults to nil.
  • page_sizes (:list) - Defaults to [25, 50, 100, 200].
  • value (:map) - extra phx-value-* pairs sent with every event, e.g. which section the list belongs to. Defaults to %{}.
  • class (:string) - Defaults to nil.

show(js \\ %JS{}, selector)

table(assigns)

Renders a table with generic styling.

Examples

<.table id="users" rows={@users}>
  <:col :let={user} label="id">{user.id}</:col>
  <:col :let={user} label="username">{user.username}</:col>
</.table>

Attributes

  • id (:string) (required)
  • rows (:list) (required)
  • row_id (:any) - the function for generating the row id. Defaults to nil.
  • row_click (:any) - the function for handling phx-click on each row. Defaults to nil.
  • row_item (:any) - the function for mapping each row before calling the :col and :action slots. Defaults to &Function.identity/1.

Slots

  • col (required) - Accepts attributes:
    • label (:string)
  • action - the slot for showing user actions in the last table column.

timestamp(assigns)

Renders a stored-UTC timestamp for a human reader.

The server has no timezone database and no idea where the reader is, so it emits the instant in UTC and marks it; local_time.js rewrites the text in the viewer's own zone and locale once it runs. Without JS the UTC text stands, which is why it is labelled rather than left to look local.

format is "datetime" (default), "date", "time" or "full".

<.timestamp at={@user.inserted_at} />
<.timestamp at={@message.inserted_at} format="time" class="text-xs" />

Attributes

  • at (:any) (required) - a DateTime, or nil to render the dash.
  • format (:string) - Defaults to "datetime". Must be one of "datetime", "date", "time", or "full".
  • class (:string) - Defaults to nil.
  • empty (:string) - text shown when at is nil. Defaults to "-".

translate_error(arg)

Translates an error message using gettext.

translate_errors(errors, field)

Translates the errors for a field from a keyword list of errors.

user_avatar(assigns)

A user's avatar as a round image when they have one (profile_url), falling back to the generic person icon. Pass class for sizing, e.g. "w-5 h-5". If the image URL fails to load (provider not ready yet, expired CDN link, rate-limited avatar CDN), assets/js/avatar_fallback.js hides the broken image and reveals the same icon. That lives in a real script rather than an onerror attribute because the CSP here has no script-src 'unsafe-inline', so an inline handler is refused and the fallback would never fire.

crossorigin="anonymous" is load-bearing, not decoration: /play and /game/* are served cross-origin isolated for Godot's SharedArrayBuffer (see GamendWeb.Plugs.GameHeaders), and under Cross-Origin-Embedder-Policy: require-corp a cross-origin subresource is blocked unless it either sends Cross-Origin-Resource-Policy or is fetched in CORS mode. OAuth avatar CDNs (Google, Discord, Steam, Gravatar) send no CORP header but do send Access-Control-Allow-Origin: *, so asking for CORS mode is what makes them load on those pages. Same-origin and object-storage avatars are unaffected — buckets already need CORS for the presigned upload flow.

Attributes

  • user (:any) - Defaults to nil.
  • class (:string) - Defaults to "w-6 h-6".

user_display(user)

Display label for a user in admin tables: username, then display name, then the raw id as a last resort. Accepts a loaded %User{}; nil or a not-loaded association renders "-". Surface the full id separately (e.g. a title attribute on the cell) so it stays available without cluttering the table.

user_title(assigns)

The game's own line under a player's name — a rank, a title, a guild — read from a metadata path the host configures:

config :gamend_web, :user_title_meta_path, ["player", "rank"]

Core knows nothing about what a rank IS; it only knows where the host keeps it. Renders nothing when the path is unset, the user has no metadata, or the value is blank, so a fresh account shows a bare name rather than an empty badge. Takes a %User{}, a metadata map, or nil.

One component so every place that names a player — leaderboards, tournament rosters, party lists, profile cards — says the same thing about them, and a host that changes where the title lives changes one config key.

Attributes

  • user (:any) (required)
  • class (:string) - Defaults to "text-xs text-base-content/60".

user_title_text(user)

The title text user_title/1 renders, or nil. Exposed for plain-text callers.