MediaWiki:ClamComponent.js: Difference between revisions
Jump to navigation
Jump to search
RobertWesner (talk | contribs) Add server status to main page. |
RobertWesner (talk | contribs) m Remove now obsolete 7AM UTC status. |
||
| Line 47: | Line 47: | ||
font-weight: bold; | font-weight: bold; | ||
&[data-status="online"]::after { | &[data-status="online"]::after { | ||
color: #11cc0e; | color: #11cc0e; | ||
| Line 151: | Line 147: | ||
if ((await statsClient.server()).unique_players) status = 'online'; | if ((await statsClient.server()).unique_players) status = 'online'; | ||
} catch (e) {} | } catch (e) {} | ||
component('br.cc.-server-status', () => | component('br.cc.-server-status', () => | ||
Latest revision as of 08:48, 15 October 2026
(() => {
/**
* Reusable XSS-safe html-builder DSL.
* Taken from Roberts userscripts so we can avoid insecure innerHtml nonsense.
*
* Example:
* $builder('div')
* .id('foo')
* .className('a b c')
* .onBuildText('<b>Test</b>')
* .build()
*
* Result:
* ```
* <div id="foo" class="a b c">
* <b>Test</b>
* </div>;
* ```
*
* @license MIT
* @author Robert Wesner
* @see https://github.com/RobertWesner/YouTube-Play-All/blob/main/script.user.js
*/
const { $builder, $style } = (()=>{const $builder=t=>(t=>{const e=t.match(/^[a-zA-Z0-9-]+/);if(!e)throw"Invalid tag supplied to parseQuery.";const n=e[0],r=(t=>{let e=[];const getOperations=()=>{const t=[...e.flat()];return e=[],t},n=new Proxy(t,{get(r,a,s){const P=t=>(...e)=>(t(...e),n),PBO=t=>P(((...n)=>e.push((()=>t(...n)))));switch(a){case"build":return()=>(getOperations().forEach((e=>e(t))),t);case"buildWithSync":return async()=>{for(const e of getOperations()){const n=e(t);n&&"function"==typeof n.then&&await n}return t};case"addClass":return P((e=>t.classList.add(e)));case"onBuild":return P((t=>e.push(t)));case"onBuildAppend":return PBO(((...e)=>t.append(...e)));case"onBuildText":return PBO((e=>t.textContent=e));case"on":return P(((e,n)=>t.addEventListener(e,n)))}const c=["hidden","style"];return e=>(!c.includes(a)&&a in t?t[a]=e:t.setAttribute(a.replaceAll("_","-"),e),n)}});return n})(document.createElement(n));if(!t.match(/^[a-zA-Z0-9-]+$/)){const e=(t=>t.match(/^[a-zA-Z0-9-]+((?:[#.][a-zA-Z0-9-]+)+)?((?:\[[a-zA-Z0-9-]+(?:=".*?")?])+)?/))(t),n=e[1]??"",a=e[2]??"";n.matchAll(/([#.])([a-zA-Z0-9-]+)/g).forEach((([t,e,n])=>{({"#":r.id,".":r.addClass})[e](n)})),a.matchAll(/\[([a-zA-Z0-9-]+)(?:="(.*?)")?]/g).forEach((([t,e,n])=>{r[e](n)}))}return r})(t);return{$builder,$style:(t,e)=>document.head.insertAdjacentElement("beforeend",$builder(`style#${t}`).onBuild((t=>t.textContent=e)).build())}})();
const $bold = text => $builder('b').onBuildText(text).build();
$style('clam-components', `
.clam-component---online-badge {
border: 1px solid #2c303624;
background-color: white;
color: white;
padding: 0.12em 0.4em;
border-radius: 0.32em;
font-weight: bold;
&[data-online="true"]::after {
color: #11cc0e;
content: '● online';
}
&[data-online="false"]::after {
color: #cc140e;
content: '● offline';
}
}
.clam-component---server-status {
font-weight: bold;
&[data-status="online"]::after {
color: #11cc0e;
content: '● online';
}
&[data-status="offline"]::after {
color: #cc140e;
content: '● offline';
}
}
.clam-component---mc-text > .cc-mc-text {
&[data-color="0"] { color: #000000 }
&[data-color="1"] { color: #0000AA }
&[data-color="2"] { color: #00AA00 }
&[data-color="3"] { color: #00AAAA }
&[data-color="4"] { color: #AA0000 }
&[data-color="5"] { color: #AA00AA }
&[data-color="6"] { color: #FFAA00 }
&[data-color="7"] { color: #AAAAAA }
&[data-color="8"] { color: #555555 }
&[data-color="9"] { color: #5555FF }
&[data-color="a"] { color: #55FF55 }
&[data-color="b"] { color: #55FFFF }
&[data-color="c"] { color: #FF5555 }
&[data-color="d"] { color: #FF55FF }
&[data-color="e"] { color: #FFFF55 }
&[data-color="f"] { color: #FFFFFF }
}
.clam-component---stats {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.6em;
float: right;
padding: 0.6em;
background-color: #69696912;
border: 1px solid #80808080;
border-radius: 1em;
}
`);
const mcText = raw => $builder('span.clam-component---mc-text')
.onBuildAppend(
...raw
.split('§')
.map(part =>
$builder('span.cc-mc-text')
.data_color(part.charAt(0))
.onBuildText(part.slice(1))
.build(),
),
)
.build();
const date = timestamp => new Date(timestamp)
.toISOString()
.replace('T', ' ')
.slice(0, 16);
const time = seconds => {
const days = Math.floor(seconds / 86400000);
const hours = Math.floor((seconds % 86400000) / 3600000);
const minutes = Math.floor((seconds % 3600000) / 60000);
let result = '';
if (days) result += days + 'd ';
if (hours) result += hours + 'h ';
if (minutes) result += minutes + 'm';
return result.trim();
};
const route = (path, callable) => {
const matches = location.pathname.match(path);
if (matches) callable(...matches.slice(1));
};
const component = (selector, callable) =>
document.querySelectorAll(selector)
.forEach(element => {
const argsMatches = element.className.match(/\(([^()]+?)\)/);
const args = argsMatches
? Object.fromEntries(
argsMatches[1]
.split(',')
.map(arg => {
const parts = arg.trim().split('=', 2);
return [parts[0], parts[1] ?? true];
}),
) : {};
element.replaceWith(callable(args));
});
route(/^\/index.php\/Main_Page$/, async () => {
let status = 'offline';
try {
if ((await statsClient.server()).unique_players) status = 'online';
} catch (e) {}
component('br.cc.-server-status', () =>
$builder('span.clam-component---server-status')
.data_status(status)
.build(),
);
});
route(/^\/index.php\/User:([^\/]+)/, async username => {
const stats = await statsClient.player(username);
component('br.cc.-online', () =>
$builder('span.clam-component---online-badge')
.data_online(stats.online)
.build(),
);
component('br.cc.-stats', args => {
const element = (title, value) => $builder('div').onBuildAppend($bold(title), ': ', value).build();
return $builder('div.clam-component---stats')
.onBuildAppend(...[
$builder('b').onBuildAppend(...[
mcText(stats.prefix),
stats.name,
args.online && $builder('span.clam-component---online-badge')
.style('margin-left: 0.48em')
.data_online(stats.online)
.build(),
].filter(_ => !!_)).build(),
args.skin === 'face' && $builder(`img[src="https://minotar.net/avatar/${stats.uuid}/128.png"]`).build(),
args.skin === 'full' && $builder(`img[src="https://api.mineatar.io/body/full/${stats.uuid}?scale=8#.png"]`).build(),
$builder('div.cc-stats-container')
.onBuildAppend(...[
args.firstJoin && element('Joined', date(stats.first_join)),
args.lastSeen && element(
'Seen',
stats.online
? $builder('span').style("color: #11cc0e").onBuildText('online').build()
: date(stats.last_join),
),
args.playTime && element('Played', time(stats.playtime)),
args.score && element('Score', stats.overall.toFixed(2)),
args.balance && element('Balance', stats.balance.toFixed(2)),
args.blocksPlaced && element('Blocks placed', stats.blocks_placed),
args.blocksBroken && element('Blocks broken', stats.blocks_broken),
args.blocksTraveled && element('Traveled', Math.floor(stats.blocks_traveled)),
args.deaths && element('Deaths', stats.deaths),
args.playersKilled && element('Players killed', stats.players_killed),
args.mobsKilled && element('Mobs killed', stats.mobs_killed),
args.damageDealt && element('Damage dealt', stats.damage_dealt),
args.damageTaken && element('Damage taken', stats.damage_taken),
].filter(_ => !!_))
.build(),
].filter(_ => !!_))
.build();
});
});
})();