Data

Leaderboard

People ranked by score with medals for the top three, shared places for ties, and your own row kept in view.

#data#list#social#user#no-js

Demo

Preview
  1. 1st:Ada Lovelace42 answers1,280pts
  2. 2nd:Katherine Johnson38 answers1,105pts
  3. 2nd:Alan Turing35 answers1,105pts
  4. 4th:Margaret Hamilton27 answers940pts
  5. 5th:Hedy Lamarr19 answers720pts
  6. 7th:Linus TorvaldsYou11 answers480pts
  1. 1st:Mika19.75min
  2. 2nd:Sol21.4min
  3. 3rd:Ade24.1min

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/leaderboard.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/leaderboard.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/leaderboard.qwe
let leaderboard_demo_people = [    { id: "u3", name: "Katherine Johnson", score: 1105, meta: "38 answers" },    { id: "u1", name: "Ada Lovelace", score: 1280, meta: "42 answers", href: "#ada" },    { id: "u5", name: "Hedy Lamarr", score: 720, meta: "19 answers" },    { id: "u2", name: "Alan Turing", score: 1105, meta: "35 answers" },    { id: "u4", name: "Margaret Hamilton", score: 940, meta: "27 answers" },    { id: "u6", name: "Grace Hopper", score: 610, meta: "15 answers" },    { id: "u7", name: "Linus Torvalds", score: 480, meta: "11 answers" },]view LeaderboardDemo() =    <div.demo-row style="align-items: flex-start; gap: 24px">        <div style="flex: 1 1 300px; max-width: 420px">            <Leaderboard id="leaderboard-demo-week" label="Top answerers this week" unit="pts" limit={5} you="u7" items={leaderboard_demo_people}/>        </div>        <div style="flex: 1 1 240px; max-width: 320px">            <Leaderboard id="leaderboard-demo-race" label="5 km, fastest first" order="asc" medals={false} unit="min" items={[                { name: "Sol", score: 21.4 }, { name: "Mika", score: 19.75 }, { name: "Ade", score: 24.1 },            ]}/>        </div>    </div>

Source

ui/leaderboard.qwe
// Leaderboard: people (or teams) ranked by score, highest first, with medals for// the top three, a picture or initials, and the score on the right; equal scores// share a place (1, 2, 2, 4). The row that is you can be picked out.////   <Leaderboard label="This week" unit="pts" you="u7" items={[//       { id: "u1", name: "Ada Lovelace", score: 1280, meta: "42 answers" },//       { id: "u7", name: "Grace Hopper", score: 990, avatar: "/img/grace.jpg" },//   ]}/>//// items: maps of { name, score, id?, avatar? (a picture's URL), meta? (a line under// the name), href? (the name links there) }, in any order: they are sorted by// score (order "asc" puts the lowest first, for times or strokes; "none" keeps// the order given). unit: after each score ("pts", "XP"). limit: show only the// first so many (you still show below them, after a gap, if you are further// down). you: the id (or name) of the row to pick out, with "You" beside it.// medals: false shows numbers for the top three too. label: the list's name for// screen readers. id: the element's id. It is an <ol>; each row says its place.import ui/theme.qwe use ui_clslet leaderboard_css = css`.ui-leaderboard { margin: 0; padding: 4px; list-style: none; border: 1px solid var(--ui-border); border-radius: var(--ui-radius);  background: var(--ui-surface); font: 14px/1.4 var(--ui-font); color: var(--ui-fg); }.ui-leaderboard-row { display: flex; align-items: center; gap: 12px; padding: 8px 12px; border-radius: var(--ui-radius-sm); }.ui-leaderboard-row + .ui-leaderboard-row { box-shadow: 0 -1px 0 var(--ui-border); }.ui-leaderboard-top { background: color-mix(in srgb, var(--ui-warning-soft) 45%, transparent); }.ui-leaderboard-you, .ui-leaderboard-you + .ui-leaderboard-row { box-shadow: none; }.ui-leaderboard-you { background: var(--ui-accent-soft); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ui-accent) 35%, transparent); }.ui-leaderboard-rank { flex: none; width: 30px; text-align: center; color: var(--ui-muted); font-weight: 600; font-variant-numeric: tabular-nums; }.ui-leaderboard-medal { font-size: 20px; line-height: 1; }.ui-leaderboard-pic { position: relative; flex: none; display: grid; place-items: center; width: 36px; height: 36px; overflow: hidden;  border-radius: 50%; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 13px; font-weight: 600; }.ui-leaderboard-pic img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }.ui-leaderboard-who { flex: 1; min-width: 0; display: grid; }.ui-leaderboard-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; color: inherit; text-decoration: none; }a.ui-leaderboard-name:hover { color: var(--ui-accent); text-decoration: underline; }a.ui-leaderboard-name:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-leaderboard-meta { color: var(--ui-muted); font-size: 12px; }.ui-leaderboard-tag { margin-left: 6px; padding: 1px 6px; border-radius: 999px; background: var(--ui-accent); color: var(--ui-accent-fg);  font-size: 11px; font-weight: 600; vertical-align: 1px; }.ui-leaderboard-score { flex: none; font-weight: 650; font-variant-numeric: tabular-nums; }.ui-leaderboard-unit { margin-left: 3px; color: var(--ui-muted); font-size: 12px; font-weight: 500; }.ui-leaderboard-gap { padding: 0 12px; color: var(--ui-muted); text-align: center; letter-spacing: .2em; }`fn leaderboard_initials(name: str) -> str {    let words = split(trim(name), " ") | filter((w) => w != "")    upper(len(words) == 0 ? "?" : (len(words) == 1 ? slice(words[0], 0, 2) : words[0][0] + words[-1][0]))}// "1st", "2nd", "3rd", "4th", "11th", "22nd".fn leaderboard_ordinal(n: int) -> str {    let tens = n % 100    let end = tens >= 11 && tens <= 13 ? "th" : match n % 10 { 1 => "st", 2 => "nd", 3 => "rd", _ => "th" }    `${n}${end}`}// The items sorted, each with its place: { item, rank, key }; equal scores share a place.fn leaderboard_ranked(items: list, order: str) -> list {    // Ties keep the order they were given in, either way.    let sorted = order == "none" ? items : order == "asc" ? sort_by(items, (it) => it.score) : sort_by(items, (it) => 0 - it.score)    for [i, it] in enumerate(sorted) => {        let first = index_of(sorted | map((x) => x.score), it.score) ?? i        { item: it, rank: order == "none" ? i + 1 : first + 1, key: str(it.id ?? it.name) }    }}view Leaderboard(items: list, unit: str? = null, limit: int? = null, you = null, order: str = "desc", medals: bool = true,                 label: str? = null, id: str? = null, class: str? = null) {    style global leaderboard_css    let all = leaderboard_ranked(items, order)    let top = limit != null ? take(all, limit) : all    let me = you == null ? null : find(all, (r) => r.key == str(you))    let extra = me != null && index_of(top | map((r) => r.key), me.key) == null    let row = (r: map) => {        let it = r.item        let mine = me != null && r.key == me.key        <li class={ui_cls(["ui-leaderboard-row", medals && r.rank <= 3 && "ui-leaderboard-top", mine && "ui-leaderboard-you"])} data-rank={r.rank}            aria-current={mine ? "true" : null}>            <span.ui-leaderboard-rank>                {medals && r.rank <= 3                    ? <span.ui-leaderboard-medal aria-hidden="true">{match r.rank { 1 => "🥇", 2 => "🥈", _ => "🥉" }}</span>                    : <span aria-hidden="true">{r.rank}</span>}                <span.ui-sr>{leaderboard_ordinal(r.rank)}:</span>            </span>            <span.ui-leaderboard-pic aria-hidden="true">                {leaderboard_initials(it.name ?? "")}                {it.avatar != null && <img src={it.avatar} alt="" loading="lazy"/>}            </span>            <span.ui-leaderboard-who>                <span>                    {it.href != null ? <a.ui-leaderboard-name href={it.href}>{it.name}</a> : <span.ui-leaderboard-name>{it.name}</span>}                    {mine && <span.ui-leaderboard-tag>You</span>}                </span>                {it.meta != null && <span.ui-leaderboard-meta>{it.meta}</span>}            </span>            <span.ui-leaderboard-score>                {type_of(it.score) == "int" || type_of(it.score) == "float" ? format_number(it.score, "en-US") : str(it.score)}                {unit != null && <span.ui-leaderboard-unit>{unit}</span>}            </span>        </li>    }    <ol class={ui_cls(["ui-leaderboard", class])} id={id} aria-label={label}>        {for r in top => row(r)}        {extra && <li.ui-leaderboard-gap aria-hidden="true">···</li>}        {extra && row(me)}    </ol>}
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On Leaderboard: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.