MediaWiki:Common.js: Difference between revisions
Created page with "(function () { if (localStorage.getItem("ageVerified") === "yes") { return; } var overlay = document.createElement("div"); overlay.style.position = "fixed"; overlay.style.top = "0"; overlay.style.left = "0"; overlay.style.width = "100%"; overlay.style.height = "100%"; overlay.style.background = "rgba(0,0,0,0.95)"; overlay.style.color = "white"; overlay.style.zIndex = "99999"; overlay.style.display = "flex"; ov..." |
Add geo dashboard script: Users by Country with day/week/month periods, activity chart, world map |
||
| (36 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
// Hero logo image styling - white + orange glow | |||
(function() { | |||
var s = document.createElement('style'); | |||
s.textContent = '#ax-hero-logo img { filter: hue-rotate(200deg) saturate(8) brightness(1.6) drop-shadow(0 0 20px rgba(255,120,0,0.9)) drop-shadow(0 0 50px rgba(255,80,0,0.5)); max-width:520px; width:80%; height:auto; display:inline-block; }'; | |||
document.head.appendChild(s); | |||
})(); | |||
// Knowledge Areas grid layout fixer | |||
(function() { | |||
function fixKnowledgeGrid() { | |||
var heroDiv = document.getElementById('ax-sexual-health-hero'); | |||
if (!heroDiv) return; | |||
var firstCard = heroDiv.closest('.ax-card'); | |||
if (!firstCard) return; | |||
var gridContainer = firstCard.parentElement; | |||
if (!gridContainer) return; | |||
gridContainer.style.display = 'grid'; | |||
gridContainer.style.gridTemplateColumns = 'repeat(2, 1fr)'; | |||
gridContainer.style.gap = '14px'; | |||
// Collect cards by hero IDs | |||
var heroIds = ['ax-sexual-health-hero','ax-dating-hero','ax-kink-hero','ax-culture-hero','ax-fashion-hero','ax-community-hero','ax-drugs-hero','ax-life-hero']; | |||
var allKnowledgeCards = []; | |||
heroIds.forEach(function(id) { | |||
var h = document.getElementById(id); | |||
if (h) { var c = h.closest('.ax-card'); if (c) allKnowledgeCards.push(c); } | |||
}); | |||
// Find remaining 3 knowledge cards by keyword match (Community, Drugs, Life Planning) | |||
// These have no hero images. Identify by checking they are NOT Start Learning or Featured | |||
var keywords = []; | |||
Array.from(document.querySelectorAll('.ax-card')).forEach(function(c) { | |||
if (allKnowledgeCards.indexOf(c) >= 0) return; | |||
var txt = c.innerText || ''; | |||
keywords.forEach(function(kw) { | |||
if (txt.indexOf(kw) >= 0 && allKnowledgeCards.indexOf(c) < 0) { | |||
allKnowledgeCards.push(c); | |||
} | |||
}); | |||
}); | |||
// Move all knowledge cards into grid | |||
allKnowledgeCards.forEach(function(c) { | |||
if (c.parentElement !== gridContainer) gridContainer.appendChild(c); | |||
}); | |||
} | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', fixKnowledgeGrid); | |||
} else { | |||
fixKnowledgeGrid(); | |||
} | |||
})(); | |||
// Sexual Health hero image | |||
(function() { | |||
var style = document.createElement('style'); | |||
style.textContent = '#ax-sexual-health-hero { background-image: url("https://alphax.wiki/images/c/c2/Sexual_health.png"); background-size: cover; background-position: center center; } #ax-dating-hero { background-image: url("https://alphax.wiki/images/6/6c/Dating_and_relationships.png"); background-size: cover; background-position: center center; } #ax-kink-hero { background-image: url("https://alphax.wiki/images/e/e5/Kink_BDSM_Hero.png"); background-size: cover; background-position: center center; } #ax-culture-hero { background-image: url("https://alphax.wiki/images/2/25/Culture_History_Politics_Hero.png"); background-size: cover; background-position: center center; } #ax-fashion-hero { background-image: url("https://alphax.wiki/images/4/4b/Fashion_Visual_Signaling_Hero.png"); background-size: cover; background-position: center top; } #ax-community-hero { background-image: url("https://alphax.wiki/images/e/ed/Community_Identity_Hero.png"); background-size: cover; background-position: center center; } #ax-drugs-hero { background-image: url("https://alphax.wiki/images/c/c7/Drugs_Party_Culture_Hero.jpg"); background-size: cover; background-position: center center; } #ax-life-hero { background-image: url("https://alphax.wiki/images/7/74/Life_Planning_Hero.jpg"); background-size: cover; background-position: center top; }'; | |||
document.head.appendChild(style); | |||
})(); | |||
(function () { | (function () { | ||
if (localStorage. | const KEY = "alphax_entry_consent"; | ||
return; | const ANALYTICS_KEY = "alphax_analytics_consent"; | ||
const MAX_AGE = 30 * 24 * 60 * 60 * 1000; | |||
const GA_ID = "G-XXXXXXXXXX"; | |||
const saved = localStorage.getItem(KEY); | |||
if (saved && (Date.now() - Number(saved) < MAX_AGE)) { | |||
const analyticsConsent = localStorage.getItem(ANALYTICS_KEY); | |||
if (analyticsConsent === "granted") { | |||
loadGoogleAnalytics(); | |||
} | |||
return; | |||
} | |||
const style = document.createElement("style"); | |||
style.innerHTML = ` | |||
#age-overlay{ | |||
position:fixed; | |||
inset:0; | |||
background:rgba(0,0,0,0.85); | |||
backdrop-filter:blur(8px); | |||
display:flex; | |||
align-items:center; | |||
justify-content:center; | |||
z-index:999999; | |||
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial; | |||
} | |||
#age-box{ | |||
background:#0f0f12; | |||
border:1px solid rgba(255,255,255,0.08); | |||
border-radius:20px; | |||
padding:40px 34px; | |||
max-width:520px; | |||
width:90%; | |||
color:white; | |||
box-shadow:0 25px 80px rgba(0,0,0,0.6); | |||
text-align:center; | |||
} | |||
#age-box h2{ | |||
margin-top:0; | |||
font-size:32px; | |||
margin-bottom:18px; | |||
} | |||
#age-text{ | |||
color:#cfcfd6; | |||
line-height:1.6; | |||
margin-bottom:28px; | |||
} | |||
#checks{ | |||
text-align:left; | |||
margin-bottom:28px; | |||
} | |||
#checks label{ | |||
display:block; | |||
margin:12px 0; | |||
font-size:15px; | |||
color:#e4e4ea; | |||
cursor:pointer; | |||
line-height:1.5; | |||
} | |||
#checks input{ | |||
margin-right:10px; | |||
} | |||
#optional-title{ | |||
margin-top:22px; | |||
margin-bottom:8px; | |||
font-size:13px; | |||
font-weight:700; | |||
letter-spacing:0.04em; | |||
text-transform:uppercase; | |||
color:#9ca3af; | |||
} | |||
#buttons{ | |||
display:flex; | |||
gap:14px; | |||
justify-content:center; | |||
margin-top:10px; | |||
} | |||
#enter{ | |||
background:#34d27a; | |||
color:#05110b; | |||
border:none; | |||
padding:14px 26px; | |||
border-radius:10px; | |||
font-size:16px; | |||
font-weight:700; | |||
cursor:pointer; | |||
opacity:0.4; | |||
} | |||
#enter.active{ | |||
opacity:1; | |||
} | |||
#exit{ | |||
background:#e74c3c; | |||
border:none; | |||
color:white; | |||
padding:14px 22px; | |||
border-radius:10px; | |||
font-size:16px; | |||
cursor:pointer; | |||
} | |||
#links{ | |||
margin-top:22px; | |||
font-size:13px; | |||
opacity:0.8; | |||
} | |||
#links a{ | |||
color:#bbb; | |||
margin:0 10px; | |||
text-decoration:none; | |||
} | |||
#links a:hover{ | |||
color:white; | |||
} | |||
`; | |||
document.head.appendChild(style); | |||
const overlay = document.createElement("div"); | |||
overlay.id = "age-overlay"; | |||
overlay.innerHTML = ` | |||
<div id="age-box"> | |||
<h2>18+ Access</h2> | |||
<div id="age-text"> | |||
This website contains adult educational content.<br> | |||
You must confirm the following to enter. | |||
</div> | |||
<div id="checks"> | |||
<label> | |||
<input type="checkbox" id="c1"> | |||
I confirm that I am at least 18 years old | |||
</label> | |||
<label> | |||
<input type="checkbox" id="c2"> | |||
I agree to the Terms & Conditions | |||
</label> | |||
<label> | |||
<input type="checkbox" id="c3"> | |||
I agree to the Privacy Policy | |||
</label> | |||
<div id="optional-title">Optional</div> | |||
<label> | |||
<input type="checkbox" id="c4"> | |||
Allow anonymous analytics to help improve the website | |||
</label> | |||
</div> | |||
<div id="buttons"> | |||
<button id="enter">Enter</button> | |||
<button id="exit">Exit</button> | |||
</div> | |||
<div id="links"> | |||
<a href="/index.php?title=Terms_and_Conditions">T&C</a> | |||
<a href="/index.php?title=Datenschutz">Privacy Policy</a> | |||
</div> | |||
</div> | |||
`; | |||
document.body.appendChild(overlay); | |||
const c1 = document.getElementById("c1"); | |||
const c2 = document.getElementById("c2"); | |||
const c3 = document.getElementById("c3"); | |||
const c4 = document.getElementById("c4"); | |||
const enter = document.getElementById("enter"); | |||
function checkAll(){ | |||
if (c1.checked && c2.checked && c3.checked) { | |||
enter.classList.add("active"); | |||
enter.disabled = false; | |||
} else { | |||
enter.classList.remove("active"); | |||
enter.disabled = true; | |||
} | |||
} | |||
c1.onchange = checkAll; | |||
c2.onchange = checkAll; | |||
c3.onchange = checkAll; | |||
enter.disabled = true; | |||
function loadGoogleAnalytics() { | |||
if (window.__gaLoaded) return; | |||
window.__gaLoaded = true; | |||
window.dataLayer = window.dataLayer || []; | |||
function gtag() { | |||
dataLayer.push(arguments); | |||
} | |||
window.gtag = gtag; | |||
gtag("consent", "default", { | |||
analytics_storage: "granted" | |||
}); | |||
const script = document.createElement("script"); | |||
script.async = true; | |||
script.src = "https://www.googletagmanager.com/gtag/js?id=" + encodeURIComponent(GA_ID); | |||
document.head.appendChild(script); | |||
gtag("js", new Date()); | |||
gtag("config", GA_ID, { | |||
anonymize_ip: true | |||
}); | |||
} | |||
enter.onclick = function(){ | |||
localStorage.setItem(KEY, String(Date.now())); | |||
if (c4.checked) { | |||
localStorage.setItem(ANALYTICS_KEY, "granted"); | |||
loadGoogleAnalytics(); | |||
} else { | |||
localStorage.setItem(ANALYTICS_KEY, "denied"); | |||
} | |||
overlay.remove(); | |||
}; | |||
document.getElementById("exit").onclick = function(){ | |||
window.location.href = "https://google.com"; | |||
}; | |||
})(); | |||
/* ================================================ */ | |||
/* ADMIN CONTROL PANEL - AlphaX Wiki */ | |||
/* ================================================ */ | |||
(function() { | |||
if (mw.config.get('wgPageName') !== 'AlphaX:Admin_Control_Panel') return; | |||
mw.loader.using(['mediawiki.api'], function() { | |||
var css = [ | |||
'#axcp-root *{box-sizing:border-box}', | |||
'#axcp-root{font-family:system-ui,-apple-system,sans-serif;background:#0D0D0D;color:#fff;margin:-8px -20px;padding:0;border-radius:12px;overflow:hidden}', | |||
'.axcp-hdr{background:#1A1A1A;border-bottom:1px solid #2E2E2E;padding:24px 28px 20px}', | |||
'.axcp-ttl{font-size:22px;font-weight:700;color:#fff;margin-bottom:3px}', | |||
'.axcp-sub{font-size:11px;color:#555;text-transform:uppercase;letter-spacing:.1em}', | |||
'.axcp-sr{display:flex;gap:12px;margin:16px 0 0;flex-wrap:wrap}', | |||
'.axcp-sc{background:#0D0D0D;border:1px solid #2E2E2E;border-radius:12px;padding:14px 18px;flex:1;min-width:90px}', | |||
'.axcp-sv{font-size:24px;font-weight:700;color:#FF6600;line-height:1}', | |||
'.axcp-sl{font-size:10px;color:#555;text-transform:uppercase;letter-spacing:.08em;margin-top:4px}', | |||
'.axcp-ctrl{display:flex;gap:10px;padding:12px 28px;background:#111;border-bottom:1px solid #2E2E2E;flex-wrap:wrap;align-items:center}', | |||
'.axcp-inp{flex:1;min-width:160px;background:#1A1A1A;border:1px solid #2E2E2E;border-radius:8px;padding:8px 13px;color:#fff;font-size:13px;outline:none}', | |||
'.axcp-inp:focus{border-color:rgba(255,102,0,.5)}', | |||
'.axcp-sel{background:#1A1A1A;border:1px solid #2E2E2E;border-radius:8px;padding:8px 11px;color:#fff;font-size:12px;outline:none;cursor:pointer;max-width:200px}', | |||
'.axcp-sel:focus{border-color:rgba(255,102,0,.5)}', | |||
'.axcp-cnt{background:rgba(255,102,0,.12);border:1px solid rgba(255,102,0,.3);color:#FF6600;border-radius:6px;padding:4px 12px;font-size:12px;font-weight:600;white-space:nowrap}', | |||
'.axcp-rbtn{background:linear-gradient(135deg,#FF6600,#FF8533);border:none;border-radius:8px;color:#fff;padding:9px 16px;font-size:12px;font-weight:700;cursor:pointer}', | |||
'.axcp-rbtn:hover{opacity:.85}', | |||
'.axcp-tw{overflow-x:auto;padding:0 28px 32px;background:#0D0D0D}', | |||
'table.axcp-t{width:100%;border-collapse:collapse;margin-top:14px;font-size:13px}', | |||
'table.axcp-t thead th{background:#111;color:#555;text-transform:uppercase;letter-spacing:.07em;font-size:10px;font-weight:600;padding:10px 12px;border-bottom:1px solid #2E2E2E;cursor:pointer;white-space:nowrap;user-select:none}', | |||
'table.axcp-t thead th:hover{color:#FF6600}', | |||
'table.axcp-t thead th.sa::after{content:" u2191";color:#FF6600}', | |||
'table.axcp-t thead th.sd::after{content:" u2193";color:#FF6600}', | |||
'table.axcp-t td{padding:8px 12px;border-bottom:1px solid rgba(255,255,255,.03);vertical-align:middle}', | |||
'table.axcp-t tr:hover td{background:rgba(255,102,0,.04)}', | |||
'table.axcp-t tr:last-child td{border-bottom:none}', | |||
'.axcp-al{color:#fff;text-decoration:none;font-weight:500;font-size:13px}', | |||
'.axcp-al:hover{color:#FF6600}', | |||
'.axcp-cp{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600;white-space:nowrap}', | |||
'.axcp-st{display:inline-block;padding:2px 9px;border-radius:20px;font-size:11px;font-weight:600}', | |||
'.s-stu{background:rgba(255,60,60,.15);color:#ff7070;border:1px solid rgba(255,60,60,.25)}', | |||
'.s-sho{background:rgba(255,166,0,.15);color:#ffb020;border:1px solid rgba(255,166,0,.25)}', | |||
'.s-med{background:rgba(61,220,132,.15);color:#3ddc84;border:1px solid rgba(61,220,132,.25)}', | |||
'.s-lon{background:rgba(100,160,255,.15);color:#78b0ff;border:1px solid rgba(100,160,255,.25)}', | |||
'.axcp-n{text-align:right;font-variant-numeric:tabular-nums;color:#777}', | |||
'.axcp-bw{background:#1A1A1A;border-radius:3px;height:5px;width:60px;display:inline-block;vertical-align:middle;margin-left:6px;overflow:hidden}', | |||
'.axcp-bf{height:5px;border-radius:3px;background:linear-gradient(to right,#FF6600,#FF8533)}', | |||
'.axcp-load{text-align:center;padding:80px 40px;color:#555;font-size:15px;background:#0D0D0D}', | |||
'.axcp-prog{color:#FF6600;font-size:13px;margin-top:10px}', | |||
'.axcp-z{color:#333}', | |||
'.axcp-hi{color:#FF6600;font-weight:600}', | |||
'.axcp-wt{color:#3ddc84;font-weight:600}' | |||
].join(''); | |||
var sEl = document.createElement('style'); | |||
sEl.textContent = css; | |||
document.head.appendChild(sEl); | |||
var contentEl = document.querySelector('#mw-content-text .mw-parser-output') || document.getElementById('mw-content-text'); | |||
contentEl.innerHTML = '<div id="axcp-root"><div class="axcp-load"><div style="font-size:28px;margin-bottom:14px">⚙</div><div>Loading article database...</div><div class="axcp-prog" id="axcp-prog">Fetching page list...</div></div></div>'; | |||
var root = document.getElementById('axcp-root'); | |||
var allData = []; | |||
var sortCol = 'title', sortDir = 1, filterText = '', filterCat = '', filterSt = ''; | |||
var CC = { | |||
'Sexual Health': 'rgba(255,100,100,.18)|#ff8080', | |||
'Dating, Sex & Relationships': 'rgba(255,182,60,.18)|#ffb83c', | |||
'Kink & BDSM': 'rgba(180,80,255,.18)|#c864ff', | |||
'Culture, History & Politics': 'rgba(80,140,255,.18)|#6496ff', | |||
'Fashion & Visual Signaling': 'rgba(255,220,60,.18)|#ffdc3c', | |||
'Community & Identity': 'rgba(61,220,132,.18)|#3ddc84', | |||
'Drugs, Party Culture & Harm Reduction':'rgba(255,120,40,.18)|#ff8028', | |||
'Life Planning': 'rgba(80,200,255,.18)|#50c8ff' | |||
}; | |||
var KCATS = Object.keys(CC); | |||
function catSty(cat) { | |||
var s = CC[cat] || 'rgba(120,120,120,.18)|#888', p = s.split('|'); | |||
return 'background:'+p[0]+';color:'+p[1]+';border:1px solid '+p[1]+';'; | |||
} | |||
function wSt(w) { | |||
if (w < 150) return ['stu','Stub']; | |||
if (w < 500) return ['sho','Short']; | |||
if (w < 1500) return ['med','Medium']; | |||
return ['lon','Long']; | |||
} | |||
function fDate(iso) { | |||
if (!iso) return '<span class="axcp-z">-</span>'; | |||
var d = new Date(iso); | |||
return d.toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}); | |||
} | |||
function fKb(b) { | |||
if (!b) return '<span class="axcp-z">-</span>'; | |||
return (b/1024).toFixed(1)+' <span style="color:#444;font-size:10px">KB</span>'; | |||
} | |||
function esc(s) { | |||
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |||
} | |||
function setP(msg) { var el = document.getElementById('axcp-prog'); if (el) el.textContent = msg; } | |||
function render(data) { | |||
var fd = data.filter(function(r) { | |||
var mt = !filterText || r.title.toLowerCase().indexOf(filterText)>=0 || (r.cat||'').toLowerCase().indexOf(filterText)>=0; | |||
var mc = !filterCat || r.cat === filterCat; | |||
var ms = !filterSt || wSt(r.words||0)[0] === filterSt; | |||
return mt && mc && ms; | |||
}); | |||
fd.sort(function(a,b) { | |||
var av=a[sortCol]||0, bv=b[sortCol]||0; | |||
if (typeof av==='number'&&typeof bv==='number') return sortDir*(av-bv); | |||
return sortDir*String(av).localeCompare(String(bv)); | |||
}); | |||
var mxW = Math.max.apply(null,[1].concat(fd.map(function(r){return r.words||0;}))); | |||
var totW = fd.reduce(function(s,r){return s+(r.words||0);},0); | |||
var avgW = fd.length ? Math.round(totW/fd.length) : 0; | |||
var totR = fd.reduce(function(s,r){return s+(r.revisions||0);},0); | |||
var nocat = fd.filter(function(r){return !r.cat;}).length; | |||
var cats={}; | |||
data.forEach(function(r){if(r.cat)cats[r.cat]=1;}); | |||
var catList=Object.keys(cats).sort(); | |||
var h=''; | |||
h+='<div class="axcp-hdr">'; | |||
h+='<div class="axcp-ttl">⚙ Admin Control Panel</div>'; | |||
h+='<div class="axcp-sub">AlphaX Wiki • Article Database • Live Data</div>'; | |||
h+='<div class="axcp-sr">'; | |||
h+='<div class="axcp-sc"><div class="axcp-sv">'+fd.length+'</div><div class="axcp-sl">Articles</div></div>'; | |||
h+='<div class="axcp-sc"><div class="axcp-sv">'+avgW.toLocaleString()+'</div><div class="axcp-sl">Avg Words</div></div>'; | |||
h+='<div class="axcp-sc"><div class="axcp-sv">'+totW.toLocaleString()+'</div><div class="axcp-sl">Total Words</div></div>'; | |||
h+='<div class="axcp-sc"><div class="axcp-sv">'+totR.toLocaleString()+'</div><div class="axcp-sl">Total Revisions</div></div>'; | |||
h+='<div class="axcp-sc"><div class="axcp-sv">'+catList.length+'</div><div class="axcp-sl">Categories</div></div>'; | |||
h+='<div class="axcp-sc"><div class="axcp-sv" style="color:'+(nocat>0?'#ff7070':'#3ddc84')+'">'+nocat+'</div><div class="axcp-sl">Uncategorised</div></div>'; | |||
h+='</div></div>'; | |||
h+='<div class="axcp-ctrl">'; | |||
h+='<input class="axcp-inp" id="axcps" type="text" placeholder="Search articles or categories..." value="'+esc(filterText)+'">'; | |||
h+='<select class="axcp-sel" id="axcpc"><option value="">All Categories</option>'; | |||
catList.forEach(function(c){h+='<option value="'+esc(c)+'"'+(filterCat===c?' selected':'')+'>'+esc(c)+'</option>';}); | |||
h+='</select>'; | |||
h+='<select class="axcp-sel" id="axcpf"><option value="">All Statuses</option>'; | |||
[['stu','Stub'],['sho','Short'],['med','Medium'],['lon','Long']].forEach(function(s){ | |||
h+='<option value="'+s[0]+'"'+(filterSt===s[0]?' selected':'')+'>'+s[1]+'</option>'; | |||
}); | |||
h+='</select>'; | |||
h+='<span class="axcp-cnt">'+fd.length+' articles</span>'; | |||
h+='<button class="axcp-rbtn" id="axcpr">↻ Refresh Data</button>'; | |||
h+='</div>'; | |||
h+='<div class="axcp-tw"><table class="axcp-t"><thead><tr>'; | |||
[{k:'title',l:'Article'},{k:'cat',l:'Category'},{k:'subcat',l:'Subcategory'}, | |||
{k:'words',l:'Words'},{k:'size',l:'Size (KB)'},{k:'status',l:'Status'}, | |||
{k:'touched',l:'Last Edited'},{k:'revisions',l:'Revisions'}, | |||
{k:'watchers',l:'Watchers'},{k:'links',l:'Inbound Links'} | |||
].forEach(function(c){ | |||
var cl=sortCol===c.k?(sortDir===1?'sa':'sd'):''; | |||
h+='<th class="'+cl+'" data-col="'+c.k+'">'+c.l+'</th>'; | |||
}); | |||
h+='</tr></thead><tbody>'; | |||
if (!fd.length) { | |||
h+='<tr><td colspan="10" style="text-align:center;padding:50px;color:#333">No articles match your filters.</td></tr>'; | |||
} | |||
fd.forEach(function(r){ | |||
var st=wSt(r.words||0); | |||
var bp=mxW>0?Math.round(((r.words||0)/mxW)*60):0; | |||
h+='<tr>'; | |||
h+='<td><a class="axcp-al" href="/wiki/'+encodeURIComponent((r.title||'').replace(/ /g,'_'))+'" target="_blank">'+esc(r.title)+'</a></td>'; | |||
h+='<td>'+(r.cat?'<span class="axcp-cp" style="'+catSty(r.cat)+'">'+esc(r.cat)+'</span>':'<span class="axcp-z">-</span>')+'</td>'; | |||
h+='<td style="color:#555;font-size:12px">'+esc(r.subcat||'-')+'</td>'; | |||
h+='<td class="axcp-n">'+(r.words||0).toLocaleString()+'<span class="axcp-bw"><span class="axcp-bf" style="width:'+bp+'px"></span></span></td>'; | |||
h+='<td class="axcp-n">'+fKb(r.size)+'</td>'; | |||
h+='<td><span class="axcp-st s-'+st[0]+'">'+st[1]+'</span></td>'; | |||
h+='<td style="color:#555;white-space:nowrap;font-size:12px">'+fDate(r.touched)+'</td>'; | |||
h+='<td class="axcp-n '+(r.revisions>20?'axcp-hi':'')+'">'+(r.revisions||0)+'</td>'; | |||
h+='<td class="axcp-n '+(r.watchers>0?'axcp-wt':'axcp-z')+'">'+(r.watchers>0?'👁 '+r.watchers:'-')+'</td>'; | |||
h+='<td class="axcp-n '+(r.links>5?'axcp-hi':'')+'">'+(r.links||0)+'</td>'; | |||
h+='</tr>'; | |||
}); | |||
h+='</tbody></table></div>'; | |||
root.innerHTML=h; | |||
var si=document.getElementById('axcps'); | |||
if(si)si.addEventListener('input',function(){filterText=this.value.toLowerCase();render(allData);}); | |||
var ci=document.getElementById('axcpc'); | |||
if(ci)ci.addEventListener('change',function(){filterCat=this.value;render(allData);}); | |||
var fi=document.getElementById('axcpf'); | |||
if(fi)fi.addEventListener('change',function(){filterSt=this.value;render(allData);}); | |||
var ri=document.getElementById('axcpr'); | |||
if(ri)ri.addEventListener('click',function(){allData=[];loadData();}); | |||
document.querySelectorAll('table.axcp-t thead th').forEach(function(th){ | |||
th.addEventListener('click',function(){ | |||
var col=this.getAttribute('data-col'); | |||
if(sortCol===col){sortDir*=-1;}else{sortCol=col;sortDir=1;} | |||
render(allData); | |||
}); | |||
}); | |||
} | |||
function apiFetch(url) { | |||
return fetch(url).then(function(r){return r.json();}); | |||
} | |||
function loadData() { | |||
root.innerHTML='<div class="axcp-load"><div style="font-size:28px;margin-bottom:14px">⚙</div><div>Loading article database...</div><div class="axcp-prog" id="axcp-prog">Step 1/4 -- Fetching page list...</div></div>'; | |||
var pages=[], map={}, ids=[]; | |||
function fetchPages(apc) { | |||
var u='/api.php?action=query&list=allpages&apnamespace=0&aplimit=500&format=json'; | |||
if(apc) u+='&apcontinue='+encodeURIComponent(apc); | |||
return apiFetch(u).then(function(d){ | |||
if(!d||!d.query)return; | |||
pages=pages.concat(d.query.allpages||[]); | |||
var cont=d.continue&&d.continue.apcontinue; | |||
if(cont)return fetchPages(cont); | |||
}); | |||
} | |||
// Step 2: info + categories (no revision params to avoid conflicts) | |||
function fetchInfoChunk(i) { | |||
if(i>=ids.length) return Promise.resolve(); | |||
var chunk=ids.slice(i,i+50).join('|'); | |||
return apiFetch('/api.php?action=query&pageids='+chunk+'&prop=info|categories&inprop=watchers&cllimit=15&format=json').then(function(d){ | |||
if(!d||!d.query||!d.query.pages){return fetchInfoChunk(i+50);} | |||
Object.keys(d.query.pages).forEach(function(pid){ | |||
var pg=d.query.pages[pid]; if(!map[pid])return; | |||
map[pid].size=pg.length||0; | |||
map[pid].touched=pg.touched||''; | |||
map[pid].watchers=pg.watchers!=null?Number(pg.watchers):0; | |||
if(pg.categories){pg.categories.forEach(function(c){ | |||
var cn=c.title.replace('Category:',''); | |||
if(KCATS.indexOf(cn)>=0){if(!map[pid].cat)map[pid].cat=cn;} | |||
else if(cn.length<60&&cn.toLowerCase().indexOf('stub')<0){if(!map[pid].subcat)map[pid].subcat=cn;} | |||
});} | |||
}); | |||
return fetchInfoChunk(i+50); | |||
}); | |||
} | |||
// Step 3: revision counts | |||
function fetchRevChunk(i) { | |||
if(i>=ids.length) return Promise.resolve(); | |||
var chunk=ids.slice(i,i+20).join('|'); | |||
return apiFetch('/api.php?action=query&pageids='+chunk+'&prop=revisions&rvprop=ids&rvlimit=max&format=json').then(function(d){ | |||
if(!d||!d.query||!d.query.pages){return fetchRevChunk(i+20);} | |||
Object.keys(d.query.pages).forEach(function(pid){ | |||
var pg=d.query.pages[pid]; if(!map[pid])return; | |||
map[pid].revisions=(pg.revisions||[]).length; | |||
}); | |||
return fetchRevChunk(i+20); | |||
}); | |||
} | |||
// Step 4: word counts + inbound links | |||
function fetchContentChunk(i) { | |||
if(i>=ids.length) return Promise.resolve(); | |||
var chunk=ids.slice(i,i+8).join('|'); | |||
return apiFetch('/api.php?action=query&pageids='+chunk+'&prop=revisions|linkshere&rvprop=content&rvslots=main&lhnamespace=0&lhlimit=max&format=json').then(function(d){ | |||
if(!d||!d.query||!d.query.pages){return fetchContentChunk(i+8);} | |||
Object.keys(d.query.pages).forEach(function(pid){ | |||
var pg=d.query.pages[pid]; if(!map[pid])return; | |||
map[pid].links=(pg.linkshere||[]).length; | |||
if(!pg.revisions||!pg.revisions[0])return; | |||
if(!pg.revisions||!pg.revisions[0])return; | |||
var slot=pg.revisions[0].slots?pg.revisions[0].slots.main:pg.revisions[0]; | |||
var raw=slot['*']||slot.content||''; | |||
// Remove template names but keep parameter text | |||
var c=raw.replace(/\{\{[A-Za-z][^|\}]*\|?/g,'').replace(/\}\}/g,' '); | |||
c=c.replace(/\[\[File:[^\]]+\]\]/gi,' '); | |||
c=c.replace(/\[\[(?:[^\]|]+\|)?([^\]]+)\]\]/g,'$1'); | |||
c=c.replace(/<[^>]+>/g,' ').replace(/<!--[^>]*-->/g,' '); | |||
c=c.replace(/={2,}[^=]+=={2,}/g,' '); | |||
c=c.replace(/[|!=*#;:{}\/\[\]]/g,' '); | |||
c=c.replace(/\s+/g,' ').trim(); | |||
map[pid].words=c?c.split(/\s+/).filter(function(w){return w.length>2;}).length:0; | |||
}); | |||
return fetchContentChunk(i+8); | |||
}); | |||
} | |||
fetchPages(null).then(function(){ | |||
setP('Step 2/4 -- Fetching page info & categories for '+pages.length+' articles...'); | |||
pages.forEach(function(p){map[p.pageid]={title:p.title,cat:'',subcat:'',words:0,size:0,touched:'',revisions:0,watchers:0,links:0};}); | |||
ids=pages.map(function(p){return p.pageid;}); | |||
return fetchInfoChunk(0); | |||
}).then(function(){ | |||
setP('Step 3/4 -- Counting revisions...'); | |||
return fetchRevChunk(0); | |||
}).then(function(){ | |||
setP('Step 4/4 -- Calculating word counts & inbound links...'); | |||
return fetchContentChunk(0); | |||
}).then(function(){ | |||
allData=Object.values(map); | |||
render(allData); | |||
}).catch(function(e){ | |||
root.innerHTML='<div class="axcp-load" style="color:#ff7070">⚠ Error: '+e.message+'</div>'; | |||
}); | |||
} | } | ||
loadData(); | |||
}); | |||
})(); | |||
// Category Grid Component - background images for ax-cat-grid | |||
(function() { | |||
// Map of card ID -> background image URL and fallback gradient | |||
var catImages = { | |||
'ax-cat-img-1': { url: 'https://alphax.wiki/images/4/43/Sexual_Health_Hero.jpg', pos: 'center center' }, | |||
'ax-cat-img-2': { url: 'https://alphax.wiki/images/e/e5/Kink_BDSM_Hero.png', pos: 'center center' }, | |||
'ax-cat-img-3': { gradient: 'linear-gradient(135deg, #0D1B2A 0%, #1B3A4B 40%, #0D2B3E 100%)' }, | |||
'ax-cat-img-4': { gradient: 'linear-gradient(135deg, #1A0D0D 0%, #3A1A0D 50%, #2B1A0D 100%)' }, | |||
'ax-cat-img-5': { url: 'https://alphax.wiki/images/0/0b/Dating_Sex_Relationships_Hero.png', pos: 'center 30%' }, | |||
'ax-cat-img-6': { gradient: 'linear-gradient(135deg, #1A0D1A 0%, #2E0D3A 50%, #1A0D2B 100%)' }, | |||
'ax-cat-img-7': { url: 'https://alphax.wiki/images/2/25/Culture_History_Politics_Hero.png', pos: 'center center' }, | |||
'ax-cat-img-8': { gradient: 'linear-gradient(135deg, #0D1A0D 0%, #0D2B1A 50%, #0D1A2B 100%)' } | |||
}; | |||
function applyCatImages() { | |||
Object.keys(catImages).forEach(function(id) { | |||
var el = document.getElementById(id); | |||
if (!el) return; | |||
var cfg = catImages[id]; | |||
if (cfg.url) { | |||
el.style.backgroundImage = 'url("' + cfg.url + '")'; | |||
el.style.backgroundSize = 'cover'; | |||
el.style.backgroundPosition = cfg.pos || 'center center'; | |||
} else if (cfg.gradient) { | |||
el.style.backgroundImage = cfg.gradient; | |||
} | |||
}); | |||
} | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', applyCatImages); | |||
} else { | |||
applyCatImages(); | |||
} | |||
})(); | |||
document.getElementById(" | // Add Impressum link to footer places row | ||
(function() { | |||
function addImpressumToFooter() { | |||
// Find the footer-places list | |||
var footerPlaces = document.getElementById('footer-places'); | |||
if (!footerPlaces) return; | |||
// Check if Impressum link already exists | |||
if (document.getElementById('footer-places-impressum')) return; | |||
// Create the new list item | |||
var li = document.createElement('li'); | |||
li.id = 'footer-places-impressum'; | |||
var a = document.createElement('a'); | |||
a.href = '/Impressum'; | |||
a.textContent = 'Impressum'; | |||
li.appendChild(a); | |||
footerPlaces.appendChild(li); | |||
} | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', addImpressumToFooter); | |||
} else { | |||
addImpressumToFooter(); | |||
} | |||
})(); | |||
// ===== Permanent Favicon ===== | |||
(function() { | |||
document.querySelectorAll('link[rel="icon"], link[rel="shortcut icon"]').forEach(function(el) { | |||
el.parentNode.removeChild(el); | |||
}); | |||
var link = document.createElement('link'); | |||
link.rel = 'icon'; | |||
link.type = 'image/png'; | |||
link.href = 'https://alphax.wiki/images/2/26/Favicon.png'; | |||
document.head.appendChild(link); | |||
})(); | |||
// ===================================================== | |||
// Users by Country Counter - Admin Page Widget | |||
// ===================================================== | |||
(function() { | |||
'use strict'; | |||
if (mw.config.get('wgPageName') !== 'User:Admin') return; | |||
var langToCountry = { | |||
'en': '🇺🇸 English-speaking', 'de': '🇩🇪 Germany / German-speaking', | |||
'fr': '🇫🇷 France / French-speaking', 'es': '🇪🇸 Spain / Spanish-speaking', | |||
'pt': '🇵🇹 Portugal / Portuguese-speaking', 'it': '🇮🇹 Italy / Italian-speaking', | |||
'nl': '🇳🇱 Netherlands / Dutch-speaking', 'pl': '🇵🇱 Poland / Polish-speaking', | |||
'ru': '🇷🇺 Russia / Russian-speaking', 'ja': '🇯🇵 Japan', | |||
'zh': '🇨🇳 China / Chinese-speaking', 'ar': '🇸🇦 Arabic-speaking', | |||
'tr': '🇹🇷 Turkey / Turkish-speaking', 'sv': '🇸🇪 Sweden / Swedish-speaking', | |||
'da': '🇩🇰 Denmark / Danish-speaking', 'fi': '🇫🇮 Finland / Finnish-speaking', | |||
'nb': '🇳🇴 Norway / Norwegian-speaking', 'cs': '🇨🇿 Czech Republic', | |||
'hu': '🇭🇺 Hungary', 'ro': '🇷🇴 Romania', 'el': '🇬🇷 Greece / Greek-speaking', | |||
'ko': '🇰🇷 Korea / Korean-speaking', 'uk': '🇺🇦 Ukraine / Ukrainian-speaking', | |||
'he': '🇮🇱 Israel / Hebrew-speaking', 'id': '🇮🇩 Indonesia', | |||
'vi': '🇻🇳 Vietnam', 'th': '🇹🇭 Thailand', 'hi': '🇮🇳 India / Hindi-speaking', | |||
'bn': '🇧🇩 Bangladesh / Bengali-speaking', 'fa': '🇮🇷 Iran / Persian-speaking' | |||
}; | |||
function renderResults(langCounts, totalUsers) { | |||
var container = document.getElementById('country-user-counter'); | |||
if (!container) return; | |||
var sorted = Object.keys(langCounts) | |||
.filter(function(l) { return langCounts[l] > 0; }) | |||
.sort(function(a, b) { return langCounts[b] - langCounts[a]; }); | |||
var html = '<style>'; | |||
html += '#cuc-wrap{font-family:sans-serif}'; | |||
html += '#cuc-stats{display:flex;gap:20px;margin-bottom:14px;flex-wrap:wrap}'; | |||
html += '.cuc-stat-card{background:#222;border:1px solid #444;border-radius:6px;padding:10px 18px;min-width:120px;text-align:center}'; | |||
html += '.cuc-stat-num{font-size:1.8em;font-weight:bold;color:#e07000}'; | |||
html += '.cuc-stat-lbl{font-size:0.8em;color:#aaa;margin-top:2px}'; | |||
html += '#cuc-table{width:100%;border-collapse:collapse;margin-top:4px}'; | |||
html += '#cuc-table th{background:#2a2a2a;color:#e07000;padding:8px 12px;text-align:left;border:1px solid #444;font-size:0.9em}'; | |||
html += '#cuc-table td{padding:7px 12px;border:1px solid #2e2e2e;color:#ddd;font-size:0.9em}'; | |||
html += '#cuc-table tr:nth-child(even) td{background:#1c1c1c}'; | |||
html += '#cuc-table tr:hover td{background:#252525}'; | |||
html += '.cuc-bar-wrap{background:#111;border-radius:3px;height:14px;overflow:hidden}'; | |||
html += '.cuc-bar{height:14px;background:linear-gradient(90deg,#e07000,#ff9a00);border-radius:3px;transition:width 0.4s ease}'; | |||
html += '.cuc-no-data{background:#1a1a1a;border:1px dashed #444;border-radius:6px;padding:16px;color:#aaa;line-height:1.6}'; | |||
html += '.cuc-no-data code{background:#222;padding:2px 6px;border-radius:3px;color:#e07000;font-size:0.9em}'; | |||
html += '#cuc-timestamp{color:#555;font-size:0.8em;margin-top:10px}'; | |||
html += '</style>'; | |||
html += '<div id="cuc-wrap">'; | |||
// Stats cards | |||
html += '<div id="cuc-stats">'; | |||
html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + totalUsers + '</div><div class="cuc-stat-lbl">Total Users</div></div>'; | |||
html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + sorted.length + '</div><div class="cuc-stat-lbl">Countries / Regions</div></div>'; | |||
var locatedUsers = sorted.reduce(function(sum, l) { return sum + langCounts[l]; }, 0); | |||
html += '<div class="cuc-stat-card"><div class="cuc-stat-num">' + locatedUsers + '</div><div class="cuc-stat-lbl">Users with Location</div></div>'; | |||
html += '</div>'; | |||
if (sorted.length === 0) { | |||
html += '<div class="cuc-no-data">'; | |||
html += '<strong>📍 No country data available yet.</strong><br>'; | |||
html += 'This counter tracks users by country using the <strong>Babel extension</strong> language categories.<br>'; | |||
html += 'Users can declare their language/country by adding babel tags to their user page:<br><br>'; | |||
html += '<code>{{#babel:en|de|fr}}</code><br><br>'; | |||
html += 'Once users add babel tags, this counter will automatically display their country distribution.<br>'; | |||
html += 'Currently tracking <strong>' + totalUsers + ' registered users</strong> on this wiki.'; | |||
html += '</div>'; | |||
} else { | |||
var maxCount = langCounts[sorted[0]] || 1; | |||
html += '<table id="cuc-table"><thead><tr>'; | |||
html += '<th style="width:40px">#</th>'; | |||
html += '<th>Country / Region</th>'; | |||
html += '<th style="width:70px;text-align:center">Users</th>'; | |||
html += '<th style="width:200px">Distribution</th>'; | |||
html += '</tr></thead><tbody>'; | |||
sorted.forEach(function(lang, idx) { | |||
var count = langCounts[lang]; | |||
var label = langToCountry[lang] || ('🌐 ' + lang.toUpperCase()); | |||
var pct = Math.round((count / locatedUsers) * 100); | |||
var barWidth = Math.round((count / maxCount) * 180); | |||
html += '<tr>'; | |||
html += '<td style="color:#666;text-align:center">' + (idx + 1) + '</td>'; | |||
html += '<td>' + label + '</td>'; | |||
html += '<td style="text-align:center;font-weight:bold;color:#e07000">' + count + '</td>'; | |||
html += '<td><div class="cuc-bar-wrap"><div class="cuc-bar" style="width:' + barWidth + 'px"></div></div></td>'; | |||
html += '</tr>'; | |||
}); | |||
html += '</tbody></table>'; | |||
} | |||
html += '<div id="cuc-timestamp">Last updated: ' + new Date().toLocaleString() + '</div>'; | |||
html += '</div>'; | |||
container.innerHTML = html; | |||
} | |||
function buildCounter() { | |||
var container = document.getElementById('country-user-counter'); | |||
if (!container) return; | |||
container.innerHTML = '<p style="color:#aaa;font-style:italic">⏳ Loading user statistics by country...</p>'; | |||
var allUsers = []; | |||
var langCounts = {}; | |||
var langs = Object.keys(langToCountry); | |||
function fetchUsers(aufrom) { | |||
var params = { action: 'query', list: 'allusers', aulimit: 500, format: 'json' }; | |||
if (aufrom) params.aufrom = aufrom; | |||
return $.getJSON(mw.util.wikiScript('api'), params).then(function(data) { | |||
var users = (data.query && data.query.allusers) || []; | |||
allUsers = allUsers.concat(users); | |||
if (data['continue'] && data['continue'].aufrom) return fetchUsers(data['continue'].aufrom); | |||
}); | |||
} | |||
function fetchLangCat(lang) { | |||
return $.getJSON(mw.util.wikiScript('api'), { | |||
action: 'query', list: 'categorymembers', | |||
cmtitle: 'Category:User_' + lang, | |||
cmtype: 'page', cmnamespace: 2, cmlimit: 500, format: 'json' | |||
}).then(function(data) { | |||
var m = (data.query && data.query.categorymembers) || []; | |||
if (m.length > 0) langCounts[lang] = (langCounts[lang] || 0) + m.length; | |||
}).catch(function() {}); | |||
} | |||
fetchUsers().then(function() { | |||
var d = $.Deferred().resolve().promise(); | |||
langs.forEach(function(lang) { d = d.then(function() { return fetchLangCat(lang); }); }); | |||
return d; | |||
}).then(function() { | |||
renderResults(langCounts, allUsers.length); | |||
}).catch(function(e) { | |||
container.innerHTML = '<p style="color:#f66">⚠️ Error loading data: ' + String(e) + '</p>'; | |||
}); | |||
} | |||
$(document).ready(function() { buildCounter(); }); | |||
})(); | |||
// ===================================================== | |||
// End: Users by Country Counter | |||
// ===================================================== | |||
// ===== Article Overview Page - Category/Subcategory/Title/Words/Focus Table ===== | |||
(function() { | |||
if (mw.config.get('wgPageName') !== 'Article_Overview') return; | |||
var apiBase = mw.util.wikiScript('api'); | |||
var allRows = []; | |||
var tableBody = null; | |||
var statusSpan = null; | |||
$(document).ready(function() { | |||
var wrapper = document.getElementById('article-overview-wrapper'); | |||
if (!wrapper) return; | |||
// Build controls row with button + status | |||
var controls = document.createElement('div'); | |||
controls.style.marginBottom = '14px'; | |||
var btn = document.createElement('button'); | |||
btn.textContent = 'Download CSV'; | |||
btn.style.cssText = 'background-color:#e07b00;color:#fff;padding:9px 20px;border:none;border-radius:5px;font-size:14px;font-weight:bold;cursor:pointer;letter-spacing:0.3px;'; | |||
btn.addEventListener('click', downloadCSV); | |||
controls.appendChild(btn); | |||
statusSpan = document.createElement('span'); | |||
statusSpan.style.cssText = 'margin-left:14px;font-size:13px;color:#aaa;vertical-align:middle;'; | |||
statusSpan.textContent = 'Loading…'; | |||
controls.appendChild(statusSpan); | |||
wrapper.appendChild(controls); | |||
// Build table | |||
var table = document.createElement('table'); | |||
table.style.cssText = 'width:100%;border-collapse:collapse;font-size:13px;'; | |||
var thead = document.createElement('thead'); | |||
var headerRow = document.createElement('tr'); | |||
headerRow.style.cssText = 'background-color:#2a2a2a;color:#f0a500;text-align:left;'; | |||
var cols = ['Category', 'Subcategory', 'Title', 'Words', 'Focus']; | |||
cols.forEach(function(colName, i) { | |||
var th = document.createElement('th'); | |||
th.textContent = colName; | |||
th.style.cssText = 'padding:9px 12px;border:1px solid #444;' + (colName === 'Words' ? 'text-align:right;' : ''); | |||
headerRow.appendChild(th); | |||
}); | |||
thead.appendChild(headerRow); | |||
table.appendChild(thead); | |||
tableBody = document.createElement('tbody'); | |||
tableBody.innerHTML = '<tr><td colspan="5" style="padding:12px;text-align:center;color:#aaa;font-style:italic;">Loading articles… please wait.</td></tr>'; | |||
table.appendChild(tableBody); | |||
wrapper.appendChild(table); | |||
buildTable(); | |||
}); | |||
function getCategoryMembers(catName, cmtype, continueParam, accumulated, callback) { | |||
var params = { | |||
action: 'query', list: 'categorymembers', | |||
cmtitle: 'Category:' + catName, | |||
cmlimit: 500, cmtype: cmtype, format: 'json' | |||
}; | }; | ||
if (continueParam) params.cmcontinue = continueParam; | |||
$.getJSON(apiBase, params).done(function(data) { | |||
var members = (data.query && data.query.categorymembers) ? data.query.categorymembers : []; | |||
members.forEach(function(m){ accumulated.push(m); }); | |||
if (data.continue && data.continue.cmcontinue) { | |||
getCategoryMembers(catName, cmtype, data.continue.cmcontinue, accumulated, callback); | |||
} else { | |||
callback(accumulated); | |||
} | |||
}).fail(function(){ callback(accumulated); }); | |||
} | |||
function escHtml(str) { | |||
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); | |||
} | |||
function renderTable(rows) { | |||
if (!tableBody) return; | |||
if (!rows || rows.length === 0) { | |||
tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No articles found.</td></tr>'; | |||
return; | |||
} | |||
var html = ''; | |||
rows.forEach(function(row, i) { | |||
var bg = i % 2 === 0 ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.07)'; | |||
html += '<tr style="background:' + bg + ';">'; | |||
html += '<td style="padding:6px 10px;border:1px solid #444;">' + escHtml(row.category) + '</td>'; | |||
html += '<td style="padding:6px 10px;border:1px solid #444;">' + escHtml(row.sub) + '</td>'; | |||
html += '<td style="padding:6px 10px;border:1px solid #444;"><a href="' + mw.util.getUrl(row.title) + '">' + escHtml(row.title) + '</a></td>'; | |||
html += '<td style="padding:6px 10px;border:1px solid #444;text-align:right;">' + escHtml(String(row.words)) + '</td>'; | |||
html += '<td style="padding:6px 10px;border:1px solid #444;">' + escHtml(row.focus) + '</td>'; | |||
html += '</tr>'; | |||
}); | |||
tableBody.innerHTML = html; | |||
} | |||
function buildTable() { | |||
if (statusSpan) statusSpan.textContent = 'Fetching categories…'; | |||
$.getJSON(apiBase, { action:'query', list:'allcategories', aclimit:500, format:'json' }) | |||
.done(function(data) { | |||
var topCats = (data.query && data.query.allcategories) ? data.query.allcategories.map(function(c){ return c['*']; }) : []; | |||
var rows = []; | |||
var totalCats = topCats.length; | |||
var processed = 0; | |||
if (totalCats === 0) { | |||
if (tableBody) tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#aaa;">No categories found.</td></tr>'; | |||
if (statusSpan) statusSpan.textContent = ''; | |||
return; | |||
} | |||
function checkDone() { | |||
processed++; | |||
if (statusSpan) statusSpan.textContent = 'Loading… (' + processed + '/' + totalCats + ' categories)'; | |||
if (processed === totalCats) { | |||
rows.sort(function(a,b){ | |||
var ca = a.category.toLowerCase(), cb = b.category.toLowerCase(); | |||
if (ca < cb) return -1; if (ca > cb) return 1; | |||
var sa = a.sub.toLowerCase(), sb = b.sub.toLowerCase(); | |||
if (sa < sb) return -1; if (sa > sb) return 1; | |||
return a.title.localeCompare(b.title); | |||
}); | |||
allRows = rows; | |||
renderTable(rows); | |||
if (statusSpan) statusSpan.textContent = rows.length + ' article entries loaded.'; | |||
} | |||
} | |||
topCats.forEach(function(catName) { | |||
getCategoryMembers(catName, 'page|subcat', null, [], function(members) { | |||
var pages = members.filter(function(m){ return m.ns === 0; }); | |||
var subcats = members.filter(function(m){ return m.ns === 14; }); | |||
if (pages.length === 0 && subcats.length === 0) { | |||
checkDone(); return; | |||
} | |||
var subTotal = pages.length + subcats.length; | |||
var subProcessed = 0; | |||
function subDone() { | |||
subProcessed++; | |||
if (subProcessed >= subTotal) checkDone(); | |||
} | |||
pages.forEach(function(page) { | |||
rows.push({ category: catName, sub: '—', title: page.title, words: 'n/a', focus: catName }); | |||
subDone(); | |||
}); | |||
subcats.forEach(function(subcat) { | |||
var subName = subcat.title.replace('Category:',''); | |||
getCategoryMembers(subName, 'page', null, [], function(subPages) { | |||
subPages.filter(function(m){ return m.ns === 0; }).forEach(function(page) { | |||
rows.push({ category: catName, sub: subName, title: page.title, words: 'n/a', focus: catName }); | |||
}); | |||
subDone(); | |||
}); | |||
}); | |||
}); | |||
}); | |||
}) | |||
.fail(function() { | |||
if (tableBody) tableBody.innerHTML = '<tr><td colspan="5" style="padding:10px;text-align:center;color:#f55;">Error loading data. Check API access.</td></tr>'; | |||
}); | |||
} | |||
function downloadCSV() { | |||
if (!allRows || allRows.length === 0) { | |||
alert('No data to download yet. Please wait for the table to finish loading.'); | |||
return; | |||
} | |||
var csv = 'Category,Subcategory,Title,Words,Focus\n'; | |||
allRows.forEach(function(row) { | |||
csv += ['category','sub','title','words','focus'].map(function(k){ | |||
return '"' + String(row[k]).replace(/"/g,'""') + '"'; | |||
}).join(',') + '\n'; | |||
}); | |||
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); | |||
var url = URL.createObjectURL(blob); | |||
var a = document.createElement('a'); | |||
a.href = url; a.download = 'article-overview.csv'; | |||
document.body.appendChild(a); a.click(); | |||
document.body.removeChild(a); | |||
URL.revokeObjectURL(url); | |||
} | |||
})(); | |||
// ═══════════════════════════════════════════════════════════ | |||
// GEO DASHBOARD – Users by Country (Day / Week / Month) | |||
// ═══════════════════════════════════════════════════════════ | |||
(function () { | |||
if (!document.getElementById('geo-dashboard')) return; | |||
var API = '/api.php'; | |||
function isoDate(d) { return d.toISOString().slice(0, 10); } | |||
function startOf(period) { | |||
var now = new Date(); | |||
if (period === 'day') return new Date(now.getFullYear(), now.getMonth(), now.getDate()); | |||
if (period === 'week') { var d = new Date(now); d.setDate(d.getDate() - d.getDay()); d.setHours(0,0,0,0); return d; } | |||
if (period === 'month') return new Date(now.getFullYear(), now.getMonth(), 1); | |||
return new Date(0); | |||
} | |||
var COUNTRY_MAP = { | |||
'en':'🇺🇸 English / US','de':'🇩🇪 German','fr':'🇫🇷 French','es':'🇪🇸 Spanish', | |||
'it':'🇮🇹 Italian','pt':'🇧🇷 Portuguese','ru':'🇷🇺 Russian','ja':'🇯🇵 Japanese', | |||
'zh':'🇨🇳 Chinese','ko':'🇰🇷 Korean','ar':'🇸🇦 Arabic','nl':'🇳🇱 Dutch', | |||
'pl':'🇵🇱 Polish','sv':'🇸🇪 Swedish','no':'🇳🇴 Norwegian','da':'🇩🇰 Danish', | |||
'fi':'🇫🇮 Finnish','tr':'🇹🇷 Turkish','cs':'🇨🇿 Czech','hu':'🇭🇺 Hungarian', | |||
'ro':'🇷🇴 Romanian','uk':'🇺🇦 Ukrainian','vi':'🇻🇳 Vietnamese','th':'🇹🇭 Thai', | |||
'id':'🇮🇩 Indonesian','ms':'🇲🇾 Malay','he':'🇮🇱 Hebrew','fa':'🇮🇷 Persian', | |||
'hi':'🇮🇳 Hindi','bn':'🇧🇩 Bengali' | |||
}; | |||
function fetchUsers() { | |||
return fetch(API + '?action=query&list=allusers&aulimit=500&format=json') | |||
.then(function(r) { return r.json(); }) | |||
.then(function(d) { return d.query.allusers || []; }); | |||
} | |||
function fetchChanges() { | |||
return fetch(API + '?action=query&list=recentchanges&rclimit=500&rcprop=user%7Ctimestamp&format=json') | |||
.then(function(r) { return r.json(); }) | |||
.then(function(d) { return d.query.recentchanges || []; }); | |||
} | |||
function fetchBabel(username) { | |||
return fetch(API + '?action=query&titles=User:' + encodeURIComponent(username) + '&prop=categories&format=json') | |||
.then(function(r) { return r.json(); }) | |||
.then(function(d) { | |||
var pages = (d.query && d.query.pages) ? d.query.pages : {}; | |||
var langs = []; | |||
Object.values(pages).forEach(function(p) { | |||
(p.categories || []).forEach(function(c) { | |||
var m = c.title.match(/Category:User ([a-z]{2,3})/i); | |||
if (m) langs.push(m[1].toLowerCase()); | |||
}); | |||
}); | |||
return langs; | |||
}); | |||
} | |||
function drawMap(activeLangs) { | |||
var svg = document.getElementById('geo-world-map'); | |||
if (!svg) return; | |||
svg.innerHTML = ''; | |||
var bg = document.createElementNS('http://www.w3.org/2000/svg','rect'); | |||
bg.setAttribute('width','900'); bg.setAttribute('height','440'); bg.setAttribute('fill','#0d1f33'); | |||
svg.appendChild(bg); | |||
var continents = [ | |||
'M80,100 L240,95 L255,200 L220,230 L170,235 L110,220 L75,170 Z', | |||
'M155,240 L220,235 L235,360 L195,385 L155,370 L138,320 Z', | |||
'M385,90 L480,85 L490,160 L450,175 L400,170 L378,140 Z', | |||
'M390,175 L460,170 L470,320 L430,345 L388,335 L375,280 Z', | |||
'M490,80 L750,75 L760,230 L700,250 L520,240 L485,180 Z', | |||
'M640,280 L740,275 L748,355 L700,368 L638,355 Z', | |||
'M230,50 L290,45 L295,90 L255,95 L228,82 Z' | |||
]; | |||
continents.forEach(function(d) { | |||
var path = document.createElementNS('http://www.w3.org/2000/svg','path'); | |||
path.setAttribute('d', d); path.setAttribute('fill','#2a3a2a'); | |||
path.setAttribute('stroke','#1a2a1a'); path.setAttribute('stroke-width','1'); | |||
svg.appendChild(path); | |||
}); | |||
var shapes = { | |||
'de':'M440,145 L455,140 L465,148 L462,165 L448,170 L438,162 Z', | |||
'fr':'M415,152 L430,148 L438,162 L430,175 L415,172 L408,162 Z', | |||
'es':'M390,170 L415,168 L415,185 L395,190 L385,182 Z', | |||
'it':'M450,168 L462,165 L468,180 L460,195 L450,188 L445,178 Z', | |||
'gb':'M418,135 L428,132 L430,145 L420,148 L414,142 Z', | |||
'ru':'M460,120 L560,110 L580,135 L540,145 L465,148 Z', | |||
'us':'M120,150 L220,148 L225,190 L200,200 L115,195 Z', | |||
'cn':'M620,150 L680,145 L685,175 L650,185 L615,178 Z', | |||
'jp':'M695,155 L708,152 L710,165 L700,168 L693,163 Z', | |||
'br':'M195,230 L240,225 L245,275 L215,282 L188,270 Z', | |||
'in':'M580,175 L615,170 L618,210 L595,218 L575,205 Z', | |||
'au':'M660,280 L720,275 L725,320 L690,328 L655,315 Z' | |||
}; | }; | ||
Object.keys(shapes).forEach(function(cc) { | |||
var isActive = activeLangs.some(function(l) { return l.startsWith(cc); }); | |||
var path = document.createElementNS('http://www.w3.org/2000/svg','path'); | |||
path.setAttribute('d', shapes[cc]); | |||
path.setAttribute('fill', isActive ? '#ff6600' : '#3a4a3a'); | |||
path.setAttribute('stroke','#555'); path.setAttribute('stroke-width','1'); | |||
path.setAttribute('opacity', isActive ? '0.9' : '0.5'); | |||
svg.appendChild(path); | |||
}); | |||
var labels = [{x:160,y:175,t:'North America'},{x:450,y:130,t:'Europe'},{x:620,y:165,t:'Asia'}, | |||
{x:430,y:265,t:'Africa'},{x:192,y:305,t:'South America'},{x:693,y:325,t:'Oceania'}]; | |||
labels.forEach(function(loc) { | |||
var text = document.createElementNS('http://www.w3.org/2000/svg','text'); | |||
text.setAttribute('x',loc.x); text.setAttribute('y',loc.y); | |||
text.setAttribute('fill','#444'); text.setAttribute('font-size','10'); | |||
text.setAttribute('font-family','sans-serif'); text.setAttribute('text-anchor','middle'); | |||
text.textContent = loc.t; svg.appendChild(text); | |||
}); | |||
} | |||
function renderActivityBars(daily) { | |||
var wrap = document.getElementById('geo-activity-bars'); | |||
var labelWrap = document.getElementById('geo-activity-labels'); | |||
if (!wrap) return; | |||
var days = []; | |||
for (var i = 29; i >= 0; i--) { | |||
var d = new Date(); d.setDate(d.getDate() - i); days.push(isoDate(d)); | |||
} | |||
var max = Math.max.apply(null, days.map(function(d){ return daily[d]||0; }).concat([1])); | |||
var bHtml = '', lHtml = ''; | |||
days.forEach(function(day, idx) { | |||
var val = daily[day] || 0; | |||
var h = Math.max(3, Math.round((val/max)*70)); | |||
var isToday = idx === 29; | |||
var col = isToday ? '#ff6600' : (val > 0 ? '#cc5500' : '#222'); | |||
bHtml += '<div title="' + day + ': ' + val + ' edits" style="flex:1;background:' + col + ';height:' + h + 'px;border-radius:2px 2px 0 0;min-width:4px;"></div>'; | |||
lHtml += '<div style="flex:1;text-align:center;white-space:nowrap;">' + (idx%5===0||isToday ? day.slice(5) : '') + '</div>'; | |||
}); | |||
wrap.innerHTML = bHtml; labelWrap.innerHTML = lHtml; | |||
} | |||
function renderTable(rows, total, period) { | |||
var tbody = document.getElementById('geo-table-body'); | |||
var titleEl = document.getElementById('geo-table-title'); | |||
if (!tbody) return; | |||
var labels = {day:'Today', week:'This Week', month:'This Month', all:'All Time'}; | |||
if (titleEl) titleEl.textContent = 'Users by Country – ' + (labels[period]||'All Time'); | |||
if (!rows.length) { | |||
tbody.innerHTML = '<tr><td colspan="5" style="padding:20px;text-align:center;color:#555;">No activity for this period.</td></tr>'; | |||
return; | |||
} | |||
var html = ''; | |||
rows.forEach(function(row, i) { | |||
var bar = '<div style="display:inline-block;background:#ff6600;height:8px;border-radius:2px;width:' + Math.round(row.pct) + '%;min-width:2px;vertical-align:middle;"></div><span style="color:#666;font-size:0.8em;margin-left:6px;">' + row.pct.toFixed(1) + '%</span>'; | |||
html += '<tr style="background:' + (i%2===0?'#1a1a1a':'#1e1e1e') + ';border-top:1px solid #222;">' + | |||
'<td style="padding:10px 16px;color:#666;">' + (i+1) + '</td>' + | |||
'<td style="padding:10px 16px;">' + (COUNTRY_MAP[row.lang]||('🌐 '+row.lang)) + '</td>' + | |||
'<td style="padding:10px 16px;text-align:right;font-weight:bold;color:#ff6600;">' + row.users + '</td>' + | |||
'<td style="padding:10px 16px;">' + bar + '</td>' + | |||
'<td style="padding:10px 16px;text-align:right;color:#aaa;">' + row.edits + '</td></tr>'; | |||
}); | |||
tbody.innerHTML = html; | |||
} | |||
var cachedUsers = null, cachedChanges = null, cachedBabel = {}, currentPeriod = 'day'; | |||
function showPeriod(period) { | |||
currentPeriod = period; | |||
['day','week','month','all'].forEach(function(p) { | |||
var btn = document.getElementById('geo-btn-'+p); | |||
if (!btn) return; | |||
if (p === period) { | |||
btn.style.background='#ff6600'; btn.style.color='#fff'; | |||
btn.style.borderColor='#ff6600'; btn.style.fontWeight='bold'; | |||
} else { | |||
btn.style.background='#333'; btn.style.color='#eee'; | |||
btn.style.borderColor='#555'; btn.style.fontWeight='normal'; | |||
} | |||
}); | |||
if (!cachedChanges || !cachedUsers) return; | |||
var cutoff = period === 'all' ? '1970-01-01' : startOf(period).toISOString(); | |||
var userEdits = {}; | |||
cachedChanges.forEach(function(c) { | |||
if (c.timestamp >= cutoff) userEdits[c.user] = (userEdits[c.user]||0)+1; | |||
}); | |||
var langUsers = {}, langEdits = {}; | |||
cachedUsers.forEach(function(u) { | |||
var langs = cachedBabel[u.name] || []; | |||
if (!langs.length) langs = ['unknown']; | |||
var edits = userEdits[u.name] || 0; | |||
var lang = langs[0]; | |||
langUsers[lang] = (langUsers[lang]||0)+1; | |||
langEdits[lang] = (langEdits[lang]||0)+edits; | |||
}); | |||
var total = cachedUsers.length; | |||
var cEl = document.getElementById('geo-countries'); | |||
if (cEl) cEl.textContent = Object.keys(langUsers).filter(function(l){return l!=='unknown';}).length; | |||
var rows = Object.keys(langUsers).map(function(lang) { | |||
return {lang:lang, users:langUsers[lang], edits:langEdits[lang]||0, pct:total>0?(langUsers[lang]/total*100):0}; | |||
}).sort(function(a,b) { | |||
if (a.lang==='unknown') return 1; if (b.lang==='unknown') return -1; return b.users-a.users; | |||
}); | |||
renderTable(rows, total, period); | |||
drawMap(rows.filter(function(r){return r.lang!=='unknown';}).map(function(r){return r.lang;})); | |||
} | |||
window.geoShowPeriod = showPeriod; | |||
function boot() { | |||
var el = document.getElementById('geo-dashboard'); | |||
if (!el) return; | |||
Promise.all([fetchUsers(), fetchChanges()]).then(function(results) { | |||
cachedUsers = results[0]; cachedChanges = results[1]; | |||
var now = new Date(); | |||
var tEl = document.getElementById('geo-total'); | |||
if (tEl) tEl.textContent = cachedUsers.length; | |||
var updEl = document.getElementById('geo-updated'); | |||
if (updEl) updEl.textContent = 'Updated: ' + now.toLocaleTimeString(); | |||
var dStart = startOf('day').toISOString(); | |||
var wStart = startOf('week').toISOString(); | |||
var mStart = startOf('month').toISOString(); | |||
var aDay=new Set(), aWeek=new Set(), aMonth=new Set(); | |||
var daily = {}; | |||
cachedChanges.forEach(function(c) { | |||
if (c.timestamp >= dStart) aDay.add(c.user); | |||
if (c.timestamp >= wStart) aWeek.add(c.user); | |||
if (c.timestamp >= mStart) aMonth.add(c.user); | |||
var day = c.timestamp.slice(0,10); | |||
daily[day] = (daily[day]||0)+1; | |||
}); | |||
var adEl = document.getElementById('geo-active-day'); | |||
var awEl = document.getElementById('geo-active-week'); | |||
var amEl = document.getElementById('geo-active-month'); | |||
if (adEl) adEl.textContent = aDay.size; | |||
if (awEl) awEl.textContent = aWeek.size; | |||
if (amEl) amEl.textContent = aMonth.size; | |||
renderActivityBars(daily); | |||
var babelPromises = cachedUsers.map(function(u) { | |||
return fetchBabel(u.name).then(function(langs) { cachedBabel[u.name] = langs; }); | |||
}); | |||
Promise.all(babelPromises).then(function() { showPeriod('day'); }); | |||
// wire up buttons | |||
['day','week','month','all'].forEach(function(p) { | |||
var btn = document.getElementById('geo-btn-'+p); | |||
if (btn) btn.addEventListener('click', function() { showPeriod(p); }); | |||
}); | |||
}).catch(function(e) { console.error('GEO dashboard error', e); }); | |||
} | |||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', boot); | |||
} else { | |||
setTimeout(boot, 100); | |||
} | |||
})(); | })(); | ||