Skip to content
34 changes: 34 additions & 0 deletions src/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ html.dark .theme-icon-dark {

.card {
flex: 1;
position: relative;
Comment thread
m-aciek marked this conversation as resolved.
}

.card:target {
Expand Down Expand Up @@ -179,6 +180,39 @@ ul.links-row li:not(:first-child)::before {
margin-right: 0.5ch;
}

/* Pin button on user-language cards */

.pin-btn {
position: absolute;
top: 0.4rem;
right: 0.4rem;
background: none;
border: none;
padding: 0.25rem;
cursor: pointer;
color: var(--card-link-color);
border-radius: 4px;
line-height: 0;
transition: background-color 0.2s;
}

.pin-btn:hover {
background-color: var(--card-title-hover-bg);
}

.pin-btn .pin-icon-unpinned {
display: none;
}

.pin-btn.unpinned .pin-icon-pinned {
display: none;
}

.pin-btn.unpinned .pin-icon-unpinned {
display: inline;
opacity: 0.5;
}

/* ------------------------------ Index ------------------------------------- */

.progress {
Expand Down
2 changes: 1 addition & 1 deletion templates/base.html.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
rel="stylesheet"
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB"
crossorigin="anonymous">
<link href="style.css?version=3" rel="stylesheet">
<link href="style.css?version=4" rel="stylesheet">
</head>
<body>
<script>
Expand Down
107 changes: 107 additions & 0 deletions templates/index.html.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,112 @@
updateProgressBarVisibility();

window.addEventListener('resize', updateProgressBarVisibility);


(function () {
const userLangs = Array.from(navigator.languages || []).map(lang => lang.toLowerCase());
const row = document.querySelector('.row');
if (!row || !userLangs.length) return;

// Capture the original column order (completion-based sort from server) before any changes.
const originalCols = Array.from(row.children);

// Find the first matching card column for each user language preference.
const userLangToCol = new Map();
for (const lang of userLangs) {
const langBase = lang.split('-')[0];
const card = row.querySelector(`[id="${lang}"]`) || row.querySelector(`[id="${langBase}"]`);
if (card) {
const col = card.closest('.col-12');
if (col && col.parentElement === row) {
userLangToCol.set(lang, col);
}
}
}
if (!userLangToCol.size) return;

// Deduplicate: keep one entry per unique column in navigator.languages priority order.
const uniqueUserCols = [];
const seenCols = new Set();
for (const lang of userLangs) {
const col = userLangToCol.get(lang);
if (col && !seenCols.has(col)) {
const cardEl = col.querySelector('[id]');
if (!cardEl) continue;
uniqueUserCols.push({ cardId: cardEl.id, col });
seenCols.add(col);
}
}

// localStorage helpers – store the set of explicitly unpinned card IDs.
const LS_KEY = 'dashboard-unpinned';
function getUnpinned() {
try { return new Set(JSON.parse(localStorage.getItem(LS_KEY) || '[]')); }
catch { return new Set(); }
}
function saveUnpinned(set) {
try { localStorage.setItem(LS_KEY, JSON.stringify([...set])); }
catch {}
}

// SVG icons: filled = pinned, outline = unpinned.
// bi-pin-angle-fill: both paths together produce the solid filled pin.
const PIN_FILL = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pin-angle-fill" viewBox="0 0 16 16">
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146" />
</svg>`;
// bi-pin-angle: single outer-frame path produces a clean outline pin.
const PIN_OUTLINE = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pin-angle" viewBox="0 0 16 16">
<path d="M9.828.722a.5.5 0 0 1 .354.146l4.95 4.95a.5.5 0 0 1 0 .707c-.48.48-1.072.588-1.503.588-.177 0-.335-.018-.46-.039l-3.134 3.134a6 6 0 0 1 .16 1.013c.046.702-.032 1.687-.72 2.375a.5.5 0 0 1-.707 0l-2.829-2.828-3.182 3.182c-.195.195-1.219.902-1.414.707s.512-1.22.707-1.414l3.182-3.182-2.828-2.829a.5.5 0 0 1 0-.707c.688-.688 1.673-.767 2.375-.72a6 6 0 0 1 1.013.16l3.134-3.133a3 3 0 0 1-.04-.461c0-.43.108-1.022.589-1.503a.5.5 0 0 1 .353-.146m.122 2.112v-.002zm0-.002v.002a.5.5 0 0 1-.122.51L6.293 6.878a.5.5 0 0 1-.511.12H5.78l-.014-.004a5 5 0 0 0-.288-.076 5 5 0 0 0-.765-.116c-.422-.028-.836.008-1.175.15l5.51 5.509c.141-.34.177-.753.149-1.175a5 5 0 0 0-.192-1.054l-.004-.013v-.001a.5.5 0 0 1 .12-.512l3.536-3.535a.5.5 0 0 1 .532-.115l.096.022c.087.017.208.034.344.034q.172.002.343-.04L9.927 2.028q-.042.172-.04.343a1.8 1.8 0 0 0 .062.46z" />
</svg>`;

// Inject pin button into each matching card.
for (const { cardId, col } of uniqueUserCols) {
const card = col.querySelector('.card');
const btn = document.createElement('button');
btn.className = 'pin-btn';
btn.dataset.cardId = cardId;
btn.innerHTML = `<span class="pin-icon-pinned">${PIN_FILL}</span><span class="pin-icon-unpinned">${PIN_OUTLINE}</span>`;
card.appendChild(btn);

btn.addEventListener('click', function () {
const unpinned = getUnpinned();
if (unpinned.has(cardId)) {
unpinned.delete(cardId);
} else {
unpinned.add(cardId);
}
saveUnpinned(unpinned);
reorder();
});
}

function reorder() {
const unpinned = getUnpinned();

// Pinned user-language columns in navigator.languages priority order.
const pinnedCols = uniqueUserCols
.filter(({ cardId }) => !unpinned.has(cardId))
.map(({ col }) => col);

// Remaining columns in original server-side sort order.
const pinnedSet = new Set(pinnedCols);
const restCols = originalCols.filter(col => !pinnedSet.has(col));

[...pinnedCols, ...restCols].forEach(col => row.appendChild(col));

// Sync button appearance and accessible label.
for (const { cardId, col } of uniqueUserCols) {
const btn = col.querySelector('.pin-btn');
if (btn) {
const isPinned = !unpinned.has(cardId);
btn.classList.toggle('unpinned', !isPinned);
btn.title = isPinned ? 'Unpin' : 'Pin';
btn.setAttribute('aria-label', isPinned ? 'Unpin this language card' : 'Pin this language card');
}
}
}

reorder();
})();
</script>
{% endblock extrascript %}
Loading