(function(mw, $) {
var skin = mw.config.get('skin');
var userName = mw.config.get('wgUserName');
function injectHeaderLink() {
if (userName === null) return;
mw.loader.using(['mediawiki.util']).done(function() {
if (skin === 'vector') {
var $personalMenu = $('#p-personal ul');
if (!$personalMenu.length) {
setTimeout(injectHeaderLink, 250);
return;
}
if ($('#pt-travelmap').length) return;
mw.util.addPortletLink(
'p-personal',
mw.util.getUrl('User:' + userName + '/Visited'),
'My Travel Map',
'pt-travelmap',
'View or update your travel map',
null,
'#pt-preferences'
);
}
if (skin === 'vector-2022') {
if ($('#pt-travelmap-text').length) return;
var $bell = $('#pt-notifications-notice, #pt-notifications-alert').first();
if (!$bell.length) {
setTimeout(injectHeaderLink, 250);
return;
}
var $textLink = $('<li id="pt-travelmap-text" class="mw-list-item">' +
'<a href="' + mw.util.getUrl('User:' + userName + '/Visited') + '" title="My Travel Map" class="cdx-button cdx-button--weight-quiet" style="border:none !important; background:transparent !important; box-shadow:none !important; padding: 0 10px;">' +
'<span class="travel-tracker-text-label" style="font-size: 0.85rem; font-weight: 500; letter-spacing: 0.01em;">my travel map</span>' +
'</a></li>');
$bell.before($textLink);
mw.util.addCSS(
'#pt-travelmap-text .cdx-button { min-height: 32px; display: flex; align-items: center; justify-content: center; } ' +
'.travel-tracker-text-label { color: #54595d; } ' +
'.skin-theme-clientpref-night .travel-tracker-text-label { color: #eaecf0 !important; } ' +
'@media (prefers-color-scheme: dark) { .client-js.skin-theme-clientpref-os .travel-tracker-text-label { color: #eaecf0 !important; } }'
);
}
});
}
injectHeaderLink();
var pageName = mw.config.get('wgPageName');
if (mw.config.get('wgNamespaceNumber') !== 2 || !mw.config.get('wgTitle').match(/\/Visited$/)) return;
mw.loader.using(['mediawiki.api', 'mediawiki.util']).done(function() {
var api = new mw.Api();
var dataModule = 'Module:Countries.json';
var isExists = mw.config.get('wgArticleId') > 0;
var continentNames = {
"af": "Africa", "as": "Asia", "eu": "Europe",
"na": "North America", "oc": "Oceania", "sa": "South America", "an": "Antarctica"
};
if (!isExists) {
var $contentArea = $('#mw-content-text');
if ($contentArea.length) {
$contentArea.empty().append(
'<div class="mw-message-box mw-message-box-notice" style="border-radius: 8px; border-left-width: 5px;">' +
'<strong>Welcome to your Travel Tracker!</strong><br>' +
'Select the countries/territories you have visited below to generate your personalised map. Make sure to hit the "save changes" button to generate your map.' +
'</div>' +
'<div id="visited-tool-placeholder"></div>'
);
}
}
api.get({
action: 'query', prop: 'revisions', titles: [pageName, dataModule],
rvprop: 'content', formatversion: 2
}).done(function(res) {
var pages = res.query.pages;
var pageData = pages.find(p => p.title.replace(/ /g, '_') === pageName);
var moduleData = pages.find(p => p.title === dataModule);
if (!moduleData || !moduleData.revisions) return;
var countries = JSON.parse(moduleData.revisions[0].content);
var content = (pageData && pageData.revisions) ? pageData.revisions[0].content : '';
var match = content.match(/{{VisitedMap\|([^}]*)}}/);
var savedQids = match ? match[1].split(',').map(s => s.trim()) : [];
startBuilding(countries, savedQids);
});
function startBuilding(countries, savedQids) {
var $placeholder = $('#visited-tool-placeholder');
if (!$placeholder.length) {
setTimeout(function() { startBuilding(countries, savedQids); }, 200);
return;
}
buildUI($placeholder, countries, savedQids);
}
function buildUI($placeholder, countries, savedQids) {
var $statsBar = $('<div style="background: var(--background-color-neutral-subtle, #f8f9fa); border: 1px solid var(--border-color-base, #eaecf0); border-radius: 8px; padding: 15px 20px; margin: 20px 0; display: flex; justify-content: space-between; align-items: center; font-family: sans-serif;"></div>');
var $counterText = $('<span style="font-size: 1.1em; font-weight: 600; color: var(--color-emphasized, #202122);"></span>');
$statsBar.append($counterText);
$placeholder.empty().append($statsBar);
var updateCounter = function() {
var count = $('#visited-tool-placeholder input:checked').length;
$counterText.text('Update your travel map – ' + count + ' countries/territories visited');
};
var grouped = {};
countries.forEach(function(c) {
var cont = c.c || "ot";
if (!grouped[cont]) grouped[cont] = [];
grouped[cont].push(c);
});
var sortedCodes = Object.keys(grouped).sort(function(a, b) {
return (continentNames[a] || a).localeCompare(continentNames[b] || b);
});
sortedCodes.forEach(function(code) {
var name = continentNames[code] || "Other territories";
var $section = $('<div class="continent-card" style="margin-bottom: 25px; background: var(--background-color-base, #fff); border: 1px solid var(--border-color-base, #eaecf0); border-radius: 8px; overflow: hidden;">');
$section.append('<div style="background: var(--background-color-neutral-subtle, #f8f9fa); padding: 10px 15px; font-weight: bold; border-bottom: 1px solid var(--border-color-base, #eaecf0); color: var(--color-subtle, #54595d);">' + name + '</div>');
var $list = $('<div style="column-count: 4; column-gap: 20px; padding: 15px;">');
grouped[code].sort((a, b) => a.n.localeCompare(b.n)).forEach(function(c) {
var isChecked = savedQids.indexOf(c.q) !== -1 ? 'checked' : '';
var $item = $('<div style="transition: background 0.2s; border-radius: 4px; padding: 4px 8px; break-inside: avoid;">' +
'<label style="display: flex; align-items: center; cursor: pointer; font-size: 0.9em; font-family: sans-serif; color: var(--color-base, #202122);">' +
'<input type="checkbox" value="' + c.q + '" ' + isChecked + ' style="margin-right: 8px; cursor: pointer;"> ' + c.n + '</label></div>');
$item.find('input').on('change', updateCounter);
$item.hover(function() { $(this).css('background', 'var(--background-color-progressive-subtle, #f0f4ff)'); }, function() { $(this).css('background', 'transparent'); });
$list.append($item);
});
$section.append($list);
$placeholder.append($section);
});
var $saveBtn = $('<button id="save-visited">Save changes</button>').css({
'background-color': 'var(--background-color-progressive, #36c)', 'color': '#fff', 'border': 'none', 'border-radius': '4px',
'padding': '10px 24px', 'font-size': '1em', 'font-weight': '600', 'font-family': 'sans-serif',
'cursor': 'pointer', 'transition': 'background 0.2s', 'box-shadow': '0 2px 4px rgba(0,0,0,0.1)'
});
$statsBar.append($saveBtn);
$placeholder.append('<div id="save-status" style="text-align: right; margin-top: 5px; font-size: 0.85em; font-weight: bold; min-height: 1.2em; font-family: sans-serif;"></div>');
updateCounter();
$saveBtn.on('mouseenter', function() { $(this).css('background-color', 'var(--background-color-progressive--hover, #447ff5)'); });
$saveBtn.on('mouseleave', function() { $(this).css('background-color', 'var(--background-color-progressive, #36c)'); });
$saveBtn.on('click', function(e) {
e.preventDefault();
var selected = [];
$('#visited-tool-placeholder input:checked').each(function() { selected.push($(this).val()); });
var $btn = $(this);
$btn.addClass('mw-ui-disabled').css('background-color', 'var(--background-color-disabled, #a2a9b1)').text('Saving...');
api.postWithToken('csrf', {
action: 'edit', title: pageName, text: '{{VisitedMap|' + selected.join(',') + '}}',
summary: (isExists ? 'Updating' : 'Creating') + ' travel map via [[Wikivoyage:Travel Tracker|TravelTracker gadget]]'
}).done(function() {
$('#save-status').text('Success! Reloading...').css('color', 'var(--color-success, #00af89)');
setTimeout(function() { location.reload(); }, 800);
});
});
}
});
})(mediaWiki, jQuery);