# Leaderboards Pro

A premium leaderboard module for **NamelessMC v2.2.x**.

Leaderboards Pro adds a polished, public leaderboard page driven entirely by
**native NamelessMC data** — no external Minecraft plugins required for v1.0.0.
It ships with a glassmorphism / podium UI for DefaultRevamp, a full StaffCP
configuration panel, permissions, navbar + StaffCP integration, and a settings
table created automatically on install.

---

## What's new in v1.1

* **Appearance settings** — 9 configurable colours (primary, secondary, value, text, muted, card background, gold, silver, bronze) + an "auto-darken in dark mode" toggle, all from StaffCP, no CSS editing. Themed via one `Appearance` class so the page **and** widgets inherit the palette automatically.
* **Per-board display styles** — Podium / Compact List / **Auto** (podium at ≥3 entries, else list), stored per leaderboard.
* **Vote diagnostics** — StaffCP card showing the detected vote module, the data source found (if any), and a clear status message.
* **Homepage widgets** — Top Reputation, Top Posts, Top Reactions, Top Voters, Newest Members (enable in StaffCP → Widgets).
* **Future-proofing seams** for the v2.0 source platform: `display_style` column, `getValueFormat()`, `identifier_type` provider contract, and provider registration ready for DB-defined custom leaderboards.

## Features

* Public page with a 3-place podium + ranked list per board
* **Configurable public route** (default `/leaderboards`) — change it from the AdminCP to `/stats`, `/top`, `/rankings`, `/leaderboards-pro`, etc.
* **Admin panel** at `/panel/leaderboards` (permission `admincp.leaderboards`)
* Automatic **navbar** entry (🏆 `fa-trophy`) and **StaffCP** entry
* Built-in caching (5 min TTL) — no heavy queries on page load
* Responsive, light **and** dark-mode (DefaultRevamp `body.dark`) ready
* Extensible architecture for future plugin integrations

### Included leaderboards (v1.0.0 — native data only)

| Key           | Source                                                                 |
|---------------|------------------------------------------------------------------------|
| `reputation`  | Net reaction **score** received (forum + profile wall posts)¹          |
| `posts`       | Forum post count (`nl2_posts`)                                         |
| `reactions`   | Total reaction **count** received (forum + profile wall posts)         |
| `new_members` | Newest registered, non-banned members (`nl2_users.joined`)            |
| `votes`       | Top voters — **only shown if a Vote module is installed**²             |

¹ NamelessMC removed the legacy `users.reputation` column in 2.2.x
(migration `20230304231334`). "Reputation" is therefore derived from the native
reaction system using core's own scoring convention
(`type 2 = +1`, `type 0 = -1`, `type 3 = custom_score`). The `reactions`
leaderboard counts raw reactions, making the two boards meaningfully distinct.

² There is no single canonical "votes per user" table in NamelessMC core
(Partydragen's *Vote* module stores only vote sites; samerton's *VotingPlugin*
reads totals from an external plugin database). `VotesLeaderboard` therefore
detects a vote-type module by name **and** probes known native vote tables,
querying one only when its table and columns are verified to exist. If nothing
is found the leaderboard is hidden silently — no errors, no empty podium.

---

## Installation

1. Copy the contents of this package into your NamelessMC root:
   * `modules/LeaderboardsPro/` → `modules/LeaderboardsPro/`
   * `upload/custom/...` → `custom/...`
2. In StaffCP → **Modules**, enable **LeaderboardsPro**. The
   `nl2_leaderboards_settings` table and default rows are created on enable.
3. Visit **StaffCP → Leaderboards Pro** to enable/order/limit each board.

### Files

```
modules/LeaderboardsPro/
├── autoload.php                     # registers the module's class autoloader
├── init.php                         # builds Language + module instance
├── module.php                       # routes, install SQL, navbar, permissions
├── language/en_UK.php → en_UK.json  # see "Notes" below
├── classes/
│   ├── AbstractLeaderboard.php      # provider base class (+ safe query helpers)
│   ├── LeaderboardManager.php       # registry, settings, caching, decoration
│   └── Leaderboards/
│       ├── ReputationLeaderboard.php
│       ├── PostsLeaderboard.php
│       ├── ReactionsLeaderboard.php
│       ├── NewMembersLeaderboard.php
│       └── VotesLeaderboard.php
├── pages/
│   ├── leaderboards.php             # public controller
│   └── panel/leaderboards.php       # admin controller (token + flash messages)
└── assets/css/leaderboards.css      # premium scoped styles

custom/templates/DefaultRevamp/leaderboards_pro.tpl                 # public template
custom/panel_templates/Default/leaderboards/panel_leaderboards.tpl  # admin template
```

---

## Notes on NamelessMC 2.2.x conventions (deviations from the brief)

These changes were required for the module to actually load and run on a real
NamelessMC 2.2.x install:

* **Language file is `en_UK.json`, not `en_UK.php`.** The core `Language` class
  loads only `__lng__.json` files (`core/classes/Core/Language.php`); PHP
  language files are never read in 2.x. Keys use the `section/term` format.
* **Admin template lives in `custom/panel_templates/Default/`,** not under the
  DefaultRevamp theme. The StaffCP uses the *Default* panel template engine
  (Bootstrap/SB-Admin), and `displayTemplate()` resolves panel templates from
  there. The frontend template stays in `custom/templates/DefaultRevamp/`.
* **A `classes/` autoloader (`autoload.php`) is included.** First-party modules
  are pre-registered in Composer's classmap; third-party modules must register
  their own autoloader, which core loads before each module's `init.php`.

> ℹ️ The module's public template is named **`leaderboards_pro.tpl`** (not
> `leaderboards.tpl`) and uses the **`$LEADERBOARDS_LIST`** variable (not
> `$LEADERBOARDS`). This is deliberate: the stock Core "placeholder leaderboards"
> template ships as `leaderboards.tpl` in every theme and uses `$LEADERBOARDS` as
> a *string* heading. Sharing either name caused the page to print the literal
> word "Array". Using unique names eliminates that collision entirely.

### Theme compatibility & overriding the template

The public page renders **exactly like a native NamelessMC page** — a single
`displayTemplate()` call whose template includes `header.tpl` / `navbar.tpl` /
`footer.tpl` once (the same pattern every core page uses). There is no second
render path, so the layout can never appear twice. `LeaderboardsPro_Module::renderPublic()`:

1. **Active theme** — if `custom/templates/<ActiveTheme>/leaderboards_pro.tpl`
   exists, it renders via native `displayTemplate()`. This is the override point.
2. **Auto-provision** — if the active theme lacks the template, the bundled
   `DefaultRevamp/leaderboards_pro.tpl` is **copied into the active theme** (when
   that directory is writable) and then rendered through the same native
   `displayTemplate()` path. Custom/premium themes work with no manual setup, and
   theme developers can then customise the copied file. A note is logged.
3. **Standalone HTML** — only if the theme directory is not writable *and* no
   template is present, a self-contained, fully escaped HTML page is emitted.
   Plain and unthemed, but the visitor **never** sees a blank page, a PHP error,
   or a duplicated layout.

> The earlier "render the bundled template via a Smarty `file:` resource"
> approach was removed: it was the only non-native render path and could emit a
> second header/navbar/footer on themes that lacked the template.

**For theme developers / premium themes:** to fully restyle the page, copy
`custom/templates/DefaultRevamp/leaderboards_pro.tpl` into your theme directory
(`custom/templates/<YourTheme>/leaderboards_pro.tpl`) and edit it. The available
Smarty variables are `{$LEADERBOARDS_LIST}` (array of `{key,title,icon,entries[]}`,
each entry: `rank, username, avatar_url, profile_url, group_style, group_html[],
value`), plus the label variables assigned in `pages/leaderboards.php`. No PHP
changes are required.

### MinecraftThemesLab — first-class theme integration

Leaderboards Pro **automatically inherits the active MinecraftThemesLab preset**
(Default, Ender, Cyber, Forest, Inferno, Aqua…) with zero configuration. MTL
injects `:root{ --mtl-* }` on every page; we map our tokens onto theirs via a
3-layer cascade:

1. **L1 — defaults** (`.lbpro`, `body.dark .lbpro`): used on DefaultRevamp and
   any custom theme. Unchanged.
2. **L2 — MTL inheritance** (`body.mtl-theme .lbpro`): maps `--lbpro-*` to
   `var(--mtl-*)`. When the admin changes preset (or a preset token), the
   leaderboards **and widgets recolour instantly** — no rebuild, no action.
   Gold/silver/bronze stay independent (semantic medals).
3. **L3 — StaffCP overrides** (Appearance settings, emitted with `!important`):
   **always win** over both the preset and the defaults.

No PHP theme-detection is used — it's pure CSS cascade, so it also degrades
gracefully on themes that don't expose `--mtl-*`. No dedicated MTL template is
required: `renderPublic()` auto-provisions `leaderboards_pro.tpl` (and widgets
auto-provision `widgets/leaderboard_widget.tpl`) into the active theme, then
renders natively. Verified on DefaultRevamp (light/dark), MinecraftThemesLab
(Cyber preset), StaffCP override, widgets on both themes, and mobile — see
`docs/verify-*.png`.

### Public visibility rules (who appears on leaderboards)

To avoid surfacing members who should not be shown publicly, every board filters
users centrally in `LeaderboardManager::isHidden()`:

* **Banned** users (`isbanned`) are always excluded (all boards).
* **Private-profile** users (`private_profile`) are excluded — they opted out of
  public visibility.
* **Ghosted** users are excluded when a ghost-style boolean column
  (`ghost` / `ghosted` / `is_ghost` / `vanished`) is present on `nl2_users`.

**Limitations (be aware):**
* Filtering happens during decoration (after the SQL `LIMIT`); the manager
  over-fetches a buffer of **+50 rows** to backfill, so a board could in theory
  show fewer than the configured limit only if more than 50 of the top-ranked
  users are hidden — extremely unlikely in practice.
* Ghost detection is **best-effort**: if a Ghost-type module stores its hidden
  state outside `nl2_users` (a separate table or the session), it cannot be
  detected here.

### Configurable public route (Core compatibility)

NamelessMC Core registers `/leaderboards` **only when Minecraft integration is
enabled** (its placeholder-leaderboards feature). To guarantee compatibility for
every install, Leaderboards Pro's public route is **admin-configurable**:

* **StaffCP → Leaderboards Pro → Public route** — change `/leaderboards` to any
  safe path (`/stats`, `/top`, `/rankings`, `/leaderboards-pro`, …).
* The value is stored in the core **`nl2_settings`** table, module-scoped
  (`Settings::*(…, 'LeaderboardsPro')`). The route is read in the module
  constructor (memoised — one indexed query per request) so it can be registered
  before NamelessMC dispatches.
* Input is sanitised (allowed `[a-z0-9/_-]`, single leading slash, no trailing
  slash, `/panel*` blocked) on both **save** and **read**, so a bad value can
  never break routing.
* If the active route is still `/leaderboards` **and** Minecraft integration is
  on, the admin panel shows a **warning** recommending an alternative route.

**No migration is required** — `nl2_settings` is a core table that always exists,
and installs that upgrade without the setting fall back to `/leaderboards`
automatically (`LeaderboardsPro_Module::getPublicRoute()`), so existing sites
keep working unchanged.

---

## Extending (future-proofing)

Adding a new leaderboard — including future Minecraft plugin integrations
(Plan, LiteBans, EssentialsX, AureliumSkills, CMI) — is a single class plus one
registration line:

```php
class MyLeaderboard extends AbstractLeaderboard {
    protected string $_key = 'my_key';
    protected string $_icon = 'fas fa-bolt';
    public function getTitle(Language $language): string { return $language->get('leaderboards', 'my_key'); }
    public function isAvailable(): bool { return DB::getInstance()->showTables('my_table') > 0; }
    public function getRawData(int $limit): array {
        return $this->safeQuery('SELECT user_id, SUM(x) AS value FROM nl2_my_table GROUP BY user_id ORDER BY value DESC LIMIT ?', [$limit]);
    }
}
```

Register it in `LeaderboardManager::registerDefaultProviders()` and add a row to
`nl2_leaderboards_settings`. The manager handles caching, user decoration,
ordering and visibility automatically.

---

**Author:** Minecraft Themes Lab · **Version:** 1.0.0 · **Requires:** NamelessMC ≥ 2.2.0, PHP 8+
