---
title: "Upvote Overlord | Feature Request & Voting Board for Joomla Sites"
description: "Turn user feedback into ranked feature requests with voting, moderation, roadmaps, leaderboards and anti-spam tools for Joomla websites and communities."
url: "https://www.yoozecase.com/?item_id=8"
date: "2026-10-08T06:45:57+00:00"
language: "en-GB"
---

#  Upvote Overlord

Let them vote. You decide.

**Latest Version**

**Requires**

**Release date**

**Made for**
Joomla
**License**
GPL-2.0-or-later
**Price**

 [ Demo page ](https://www.yoozecase.com/upvote-overlord-demo)

Your visitors have opinions. Left unchecked, they scatter them across emails, DMs, and the void. Upvote Overlord summons them into one arena, ranks their demands by votes, and lets you rule the outcome. They beg for features. The crowd decides which are worthy. You ship, and you take the credit. Install it, and never wonder what your people want again, because they will tell you, in order, whether you asked or not.

 ![Upvote Overlord 1](https://www.yoozecase.com/files/upvote-overlord_1.png)

 [](#)

 [](#)

---

##  Description

Upvote Overlord. A feature-request board for those who prefer to rule.
So. You have a website, and you have users, and the users have ideas. Adorable. Until now those ideas arrived as scattered emails, buried comments, and the occasional angry tweet you pretended not to see. No structure. No ranking. No throne. That ends today.

Upvote Overlord gives your visitors a single arena to submit their requests and upvote each other's. The best ideas rise to the top on their own, vote by vote, while the weak ones sink where they belong. A top-voted box crowns the current favorite for all to see. Your people do the sorting for you. This is delegation, and it is beautiful.

You are not merely a spectator to this democracy. You are its Overlord. A frontend moderation toolbar lets you approve, hide, and command without ever touching the backend. Every ruling is written to an audit log, because a wise ruler remembers. Set a review deadline and the round closes itself, then reveals the winners with appropriate ceremony. Anti-spam and rate limiting keep the rabble and their bots in line, so the votes that count are the ones cast by real subjects.

When it is time to govern openly, flip the board into Roadmap view and every request marches into its column: Open, Planned, In Progress, Shipped, Declined. Your users watch their wishes advance through your kingdom in real time, which keeps them patient, and keeps them off your inbox. A backend dashboard module reports the state of your realm at a glance from the Joomla Control Panel.

And because an Overlord must look the part, the board arrives dressed for the occasion, with designed themes including Leaderboard Arena, Neon Stage, and Retro Pixel Arcade. It speaks six languages, so no corner of your empire is left out.

Your competitors are still reading feature requests one email at a time. You could be watching yours line up and salute. Install Upvote Overlord. Let them vote. You decide.

##  Feature request

 ### What is the next feature for this plugin?

Submit your idea or vote on existing ones.

  #### Top voted

   Website (leave empty)

'; } html += ''; if (oc.title) { html += '##### ' + escapeHtml(oc.title) + '

'; } if (oc.text) { html += '' + escapeHtml(oc.text).replace(/\\n/g, '
') + '

'; } html += '

'; } // Auto winner(s) - top N from the already vote-sorted features if (mode === 'auto_winner' || mode === 'both') { var n = oc.winnerCount | 0; if (n '; for (var i = 0; i ' + (w.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(w.image_url)%20+%20')

' : '') + '' + (i + 1) + '' + '' + escapeHtml(w.title) + '' + '' + getIcon(this.cfg.voteIcon) + ' ' + (w.votes | 0) + '' + ''; } html += ''; } } if (html === '') { this.elOutcome.hidden = true; return; } this.elOutcome.innerHTML = html; this.elOutcome.hidden = false; }; /** * Roadmap view (v3.2.0): renders the items as status columns, in the * canonical status order from cfg.statuses. Each column is an 12. inside \* the existing .yc-fb-list , holding its own inner  of items - \* this keeps the delegated click/change handlers on elList working \* unchanged. Unknown statuses fall back into the "open" column so no item \* can silently disappear. Empty columns are skipped when \* cfg.roadmapHideEmpty is set. \*/ Board.prototype.renderRoadmap = function (items) { var stats = (this.cfg.statuses && this.cfg.statuses.length) ? this.cfg.statuses : \['open', 'planned', 'in\_progress', 'shipped', 'declined'\]; var labels = this.cfg.statusLabels || {}; var groups = {}; var i, st; for (i = 0; i ' + '##### ' + '' + escapeHtml(labels\[sv\] || sv) + '' + arr.length + '' + '

    ' + '' + itemsHtml + '
    ' + '
'; } return html; }; Board.prototype.renderItem = function (f, index) { var disabled = this.cfg.isClosed ? 'disabled' : ''; var voted = f.voted ? ' yc-voted' : ''; var descHtml = f.description ? '' + escapeHtml(f.description).replace(/\\n/g, '
') + '

' : ''; var thumbHtml = f.image_url ? '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

' : ''; // Status workflow (v3.0.0): badge for any non-default status. The // roadmap view (v3.2.0) skips the badge - the column already // communicates the status, a badge would be redundant noise. var st = (f.status || 'open'); var labels = this.cfg.statusLabels || {}; var statusBadge = (!this.isRoadmap && st && st !== 'open' && labels[st]) ? '' + escapeHtml(labels\[st\]) + '' : ''; // Design themes (v3.1.0): arena / neon / pixel render extra per-item // furniture - rank number, avatar (image or initials), a "leader" pill // on rank 1, and a "votes" label. Classic/colour themes are untouched. var t = this.cfg.theme || 'light'; var themed = (t === 'arena' || t === 'neon' || t === 'pixel'); var rank = (typeof index === 'number') ? (index + 1) : 0; var rankHtml = '', leaderHtml = '', avatarHtml = '', votesLabel = ''; if (themed) { rankHtml = '' + pad2(rank) + ''; if (rank === 1) { leaderHtml = '' + escapeHtml(this.cfg.leaderLabel || 'Leader') + ''; } if (f.image_url) { avatarHtml = '![](https://www.yoozecase.com/'%20+%20escapeHtml(f.image_url)%20+%20')

'; } else { avatarHtml = '' + escapeHtml(initialsOf(f.title)) + '

'; } votesLabel = '' + escapeHtml(this.cfg.votesLabel || 'votes') + ''; } // Moderation overlay: subtle corner buttons, only when canModerate. // Buttons appear on hover/focus (styled in CSS). Even in customizer // preview we render them when canModerate is true so the moderator // can see what visitors with moderation rights will see. var modHtml = ''; if (this.cfg.canModerate) { var parts = []; var hideTitle = escapeHtml(this.t('hideTitle', 'Hide this feature')); var deleteTitle = escapeHtml(this.t('deleteTitle', 'Delete this feature')); if (this.cfg.allowSoftHide) { parts.push( '' ); } if (this.cfg.allowHardDelete) { parts.push( '' ); } if (parts.length || (this.cfg.statuses && this.cfg.statuses.length)) { var sParts = parts.slice(); if (this.cfg.statuses && this.cfg.statuses.length) { var opts = ''; for (var si = 0; si ' + escapeHtml(sl) + ''; } var setStatus = escapeHtml(this.t('setStatus', 'Set status')); sParts.push( '' ); } modHtml = '' + sParts.join('') + '

'; } } var mediaHtml = themed ? avatarHtml : thumbHtml; return ( '32. ' + leaderHtml + rankHtml + '' + mediaHtml + '' + '##### ' + escapeHtml(f.title) + '

    ' + statusBadge + descHtml + '

    ' + modHtml + '
' ); }; /** * Detects whether a JSON response looks like a CSRF/token rejection. * Two cases: * 1. com_ajax-level rejection (Joomla's auto-token-check on POST): top * level message contains the JINVALID_TOKEN_NOTICE text and HTTP * status is typically 403. * 2. Plugin-level rejection from requireToken(): payload.error === "Invalid token". */ Board.prototype.isTokenError = function (status, raw, payload) { if (status === 403) return true; var hay = ''; try { hay = JSON.stringify(raw || {}) + '|' + JSON.stringify(payload || {}); } catch (e) {} return /invalid (security )?token/i.test(hay); }; /** * Fetch a fresh CSRF token from the server. Updates this.cfg.csrfToken so * the next call (and any future calls in this Board's lifetime) use the * correct one. Cache-bust with a timestamp so no upstream CDN caches the * response. */ Board.prototype.fetchFreshToken = function () { var self = this; var url = this.cfg.ajaxUrl + '&task=token&_=' + Date.now(); return fetch(url, { method: 'GET', credentials: 'same-origin', headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' } }) .then(function (r) { return r.json(); }) .then(function (data) { var p = self.unwrapAjax(data); if (p && p.ok === true && typeof p.token === 'string' && p.token.length >= 16) { self.cfg.csrfToken = p.token; return p.token; } throw new Error('No fresh token in response'); }); }; /** * Generic POST-with-token helper. Builds FormData from a plain object, * adds the current token, posts. On 403 / token-error response, fetches a * fresh token and retries exactly once. * * @param {string} task The task name (appended to URL and body) * @param {object} fields Key-value form fields (no token) * @return {Promise} Resolves to the unwrapped payload on success. \* Rejects with Error containing .reason on failure. \*/ Board.prototype.postWithToken = function (task, fields) { var self = this; function attempt(token) { var fd = new FormData(); fd.append('task', task); for (var k in fields) { if (Object.prototype.hasOwnProperty.call(fields, k)) { fd.append(k, fields\[k\]); } } fd.append(token, '1'); return fetch(self.cfg.ajaxUrl + '&task=' + encodeURIComponent(task), { method: 'POST', credentials: 'same-origin', body: fd, headers: { 'Accept': 'application/json' } }).then(function (r) { return r.json().then(function (data) { return { status: r.status, raw: data, payload: self.unwrapAjax(data) }; }); }); } return attempt(this.cfg.csrfToken).then(function (res) { // Success path if (res.payload && res.payload.ok === true) { return res.payload; } // Token rejection? Fetch fresh and retry once. if (self.isTokenError(res.status, res.raw, res.payload)) { return self.fetchFreshToken().then(function (fresh) { return attempt(fresh).then(function (res2) { if (res2.payload && res2.payload.ok === true) { return res2.payload; } var err = new Error( (res2.payload && res2.payload.error) || (res2.raw && res2.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res2.payload || null; throw err; }); }); } // Some other error - surface it var err = new Error( (res.payload && res.payload.error) || (res.raw && res.raw.message) || self.t('requestFailed', 'Request failed.') ); err.payload = res.payload || null; throw err; }); }; Board.prototype.handleSubmit = function () { var self = this; if (isInBuilder()) { this.showStatus(this.t('previewSubmit', 'Preview mode - submissions are disabled in the YOOtheme builder. Try this on the published page.'), 'info'); return; } if (this.cfg.isClosed) { this.showStatus(this.cfg.closedMessage || this.t('votingClosed', 'Voting is closed.'), 'info'); return; } var title = (this.elTitle && this.elTitle.value || '').trim(); var desc = (this.elDesc && this.elDesc.value || '').trim(); // Client-side validation: minimum title length. Server re-validates. var minLen = (this.cfg.minTitleLength | 0) || 2; if (title.length for hide/delete, remember parent + sibling for rollback. var li = btn.closest('li'); var parentEl = li ? li.parentNode : null; var nextEl = li ? li.nextSibling : null; if (li && (action === 'hide' || action === 'delete')) { li.remove(); } btn.disabled = true; var task = action === 'hide' ? 'moderate\_hide' : action === 'delete' ? 'moderate\_delete' : action === 'restore' ? 'moderate\_restore' : ''; if (!task) { btn.disabled = false; return; } this.postWithToken(task, { feature\_id: featureId }) .then(function (payload) { // Success - keep the UI as-is (already removed) and refresh // so vote counts, top box, and hidden-features panel reflect // the new state. self.refresh(); if (self.hiddenOpen) { self.hiddenLoaded = false; // force a refetch self.fetchHiddenFeatures(); } var msg = action === 'delete' ? self.t('deleted', 'Feature deleted.') : action === 'hide' ? self.t('hidden', 'Feature hidden.') : action === 'restore' ? self.t('restored', 'Feature restored.') : self.t('done', 'Done.'); self.showStatus(msg, 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { // Rollback: re-insert the row if we removed it. if (li && parentEl) { if (nextEl) { parentEl.insertBefore(li, nextEl); } else { parentEl.appendChild(li); } } btn.disabled = false; self.showStatus(err && err.message ? err.message : self.t('modFailed', 'Moderation action failed.'), 'error'); }); }; Board.prototype.handleStatusChange = function (featureId, status, sel) { var self = this; if (!featureId || !status) return; sel.disabled = true; this.postWithToken('moderate\_status', { feature\_id: featureId, status: status }) .then(function () { self.refresh(); var label = (self.cfg.statusLabels && self.cfg.statusLabels\[status\]) ? self.cfg.statusLabels\[status\] : status; self.showStatus(self.tf('statusSet', 'Status: %s', label), 'success'); setTimeout(function () { self.hideStatus(); }, 3000); }) .catch(function (err) { sel.disabled = false; self.showStatus(err && err.message ? err.message : self.t('statusFailed', 'Could not set status.'), 'error'); }); }; /\*\* \* Toggle the hidden-features panel open/closed. First open triggers \* an AJAX fetch; subsequent toggles just hide/show the cached list. \*/ Board.prototype.toggleHiddenPanel = function () { if (!this.elHiddenList || !this.elHiddenLabel) return; var arrow = this.elHiddenToggle && this.elHiddenToggle.querySelector('.yc-fb-hidden-arrow'); if (this.hiddenOpen) { this.hiddenOpen = false; this.elHiddenList.hidden = true; this.elHiddenLabel.textContent = this.t('showHidden', 'Show hidden features'); if (arrow) arrow.classList.remove('yc-open'); return; } this.hiddenOpen = true; this.elHiddenList.hidden = false; this.elHiddenLabel.textContent = this.t('hideHidden', 'Hide hidden features'); if (arrow) arrow.classList.add('yc-open'); if (!this.hiddenLoaded) { this.fetchHiddenFeatures(); } }; /\*\* \* Fetch the hidden-features list via the moderator-only endpoint. \* Re-callable to refresh the list after a restore. \*/ Board.prototype.fetchHiddenFeatures = function () { var self = this; if (!this.elHiddenList) return; this.elHiddenList.innerHTML = '2. ' + escapeHtml(this.t('loading', 'Loading...')) + '
'; var url = this.cfg.ajaxUrl + '&task=moderate\_list\_hidden&board\_id=' + encodeURIComponent(this.cfg.boardId); fetch(url, { credentials: 'same-origin', headers: { 'Accept': 'application/json' } }) .then(function (r) { return r.json(); }) .then(function (data) { var payload = self.unwrapAjax(data); if (!payload || payload.ok !== true) { self.elHiddenList.innerHTML = '4. ' + escapeHtml((payload && payload.error) ? payload.error : self.t('hiddenLoadFailed', 'Failed to load hidden features.')) + '
'; return; } self.hiddenLoaded = true; var features = payload.features || \[\]; if (features.length === 0) { self.elHiddenList.innerHTML = '6. ' + escapeHtml(self.t('noHidden', 'No hidden features.')) + '
'; return; } var html = ''; for (var i = 0; i ' + escapeHtml(f.description).replace(/\\n/g, '
') + '' : ''; var restoreTitle = escapeHtml(self.t('restoreTitle', 'Restore this feature')); html += '9. ' + '' + '##### ' + escapeHtml(f.title) + '

    ' + descHtml + '' + escapeHtml(self.tf('votesMeta', '%s votes', f.votes | 0)) + '' + '

    ' + '' + '
'; } self.elHiddenList.innerHTML = html; }) .catch(function () { self.elHiddenList.innerHTML = '11. ' + escapeHtml(self.t('hiddenNetwork', 'Network error while loading hidden features.')) + '
'; }); }; function initAll(scope) { var roots = (scope || document).querySelectorAll('.yc-fb-root'); for (var i = 0; i ---

##  Changelog

---

 [ v3.2.0 Breaking 15/07/2026 ](#) Added- \*\*Roadmap view.\*\* New "Board view" setting on the Display tab: "List (sorted

- \*\*"Roadmap empty columns" setting\*\* (Display tab): hide status columns that
- \*\*36 new frontend language keys\*\* in all six languages (en-GB, nl-NL, de-DE,

 Changed- \*\*Frontend JavaScript is now fully localized (closes the YC.6 JS i18n gap).\*\*
- In the roadmap view, item cards no longer show the status badge - the column

 Fixed- Removed a duplicate `ERR\_NO\_BOARD` language key that shipped in all six
- Removed a duplicate `pad2()` helper definition in the frontend JS.

 [ v3.1.3 Breaking 15/07/2026 ](#) Changed- \*\*"Board ID mode" is now "Board scope"\*\*, and its options describe the result

- \*\*Board scope now appears above Board ID\*\* in the Content tab. It is the
- \*\*Board ID help text rewritten.\*\* It previously recommended mapping the field
- \*\*Vote icon moved from the Voting tab to the Display tab\*\*, next to Card style

 [ v3.1.2 Breaking 03/07/2026 ](#) Added- \*\*Board ID mode: "Auto (current page / item)".\*\* A new option on the element.

 Changed- Default is still Manual, so existing boards are unaffected. Auto is opt-in.

 [ v3.1.1 Breaking 03/07/2026 ](#) Added- \*\*Dynamic Board ID.\*\* The Board ID field is now dynamic-content mappable. Drop

 Changed- Board ID sanitisation now slugifies: any run of disallowed characters becomes

 [ v3.1.0 Breaking 30/06/2026 ](#) Added- \*\*Three design themes\*\*, selectable per board from the element's \*\*Theme\*\*

- \*\*Leaderboard Arena\*\* - light cream surface, coral accent, hard offset
- \*\*Neon Stage\*\* - dark, volt-lime accent, vote button that fills and glows
- \*\*Retro Pixel Arcade\*\* - dark purple, 8-bit type, thick pixel borders, hard
- Per-item furniture for these themes: a rank number (Arena), an avatar that
- \*\*Self-hosted web fonts\*\* under `media/plg\_system\_yc\_upvote\_overlord/fonts/`

 Changed- Fully backward-compatible: the default theme is unchanged; existing boards keep
- Next stages: v3.2.0 Roadmap view (status columns); v3.3.0 Comments.

 [ v3.0.0 Breaking 26/06/2026 ](#) Added- \*\*Status workflow.\*\* Every item now has a status: Open (default), Planned,

- \*\*Backend dashboard module (`mod\_yc\_upvote\_overlord`).\*\* A Joomla
- New AJAX endpoints on the plugin: `moderate\_status` (frontend moderator) and
- `status` column on `#\_\_yc\_upvote\_overlord\_features` (added on install and via

 Changed- Fully backward-compatible: existing items default to status Open.
- Next stages: v3.1.0 Roadmap view (status columns); v3.2.0 Comments.

 [ v2.2.3 26/06/2026 ](#) Fixed- \*\*"Unexpected token ... is not valid JSON" on the board.\*\* When another

 [ v2.2.2 26/06/2026 ](#) Changed- \*\*YC conformance pass\*\* (no functional changes). Brought the package to the

- The element icon is referenced via `${url:images/icon.svg}` rather than an
- Local CSS/JS assets are inlined via `file\_get\_contents()` on `\_\_DIR\_\_`-relative

 [ v2.2.1 10/06/2026 ](#) Fixed- \*\*"Unknown column 'image\_url'" error on the board (list/vote).\*\* On some

- \*\*Element settings: "Messages" and "Advanced" tabs were unclickable.\*\* With

 [ v2.2.0 10/06/2026 ](#) Added- \*\*Maker-defined candidates.\*\* A new "Candidates" tab lets you pre-populate the

- \*\*Closed-round outcome reveal.\*\* A new "Outcome" tab controls what appears once
- Candidate and decision images render as thumbnails in the list and outcome block.

 Changed- The features table gains `image\_url` and `seed\_key` columns plus a unique

 Fixed- Two-column layout wrapper was reusing the `.yc-fb-body` class already used by

## Schema

```json
{
    "@context": "https://schema.org",
    "@type": "BreadcrumbList",
    "itemListElement": [
        {
            "@type": "ListItem",
            "position": 1,
            "name": "Home",
            "item": "https://www.yoozecase.com"
        },
        {
            "@type": "ListItem",
            "position": 2,
            "name": "Extensions",
            "item": "https://www.yoozecase.com/extensions"
        },
        {
            "@type": "ListItem",
            "position": 3,
            "name": "Upvote Overlord",
            "item": "https://www.yoozecase.com/"
        }
    ]
}
```
