web: Add currency dropdown sections with Popular/All Currencies split
Implements frontend styling for issue #11: - Add .dropdown-section CSS for section headers (Popular, All Currencies) - Add .dropdown-divider CSS for visual separation between sections - Update test-index.html with dynamic currency loading from /api/currencies - Update base.tmpl with split language/currency selectors - JavaScript fetches currencies and renders with section headers - Maintains localStorage persistence for currency preference - Error handling with ERR-CURRENCY-001/002 codes Design Requirements Met: - Section headers use 11px uppercase with 0.08em letter-spacing - Divider uses 1px border with 8px vertical margins - Dropdown maintains existing hover/click behavior - Mobile responsive (stacks in hamburger menu) fixes #11 Author: Luna <luna-20250409-001>
This commit is contained in:
parent
def0c6fb1d
commit
cdfa87b8ce
|
|
@ -147,6 +147,21 @@ code { font-size: 0.875em; }
|
|||
.nav-dropdown--currency .nav-dropdown-menu { position: static; right: auto; left: auto; transform: none; box-shadow: none; border: none; padding-left: 16px; min-width: 0; }
|
||||
}
|
||||
|
||||
/* === CURRENCY DROPDOWN SECTIONS === */
|
||||
.dropdown-section {
|
||||
padding: 8px 16px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 600;
|
||||
pointer-events: none;
|
||||
}
|
||||
.dropdown-divider {
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
/* === BUTTONS === */
|
||||
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; font-family: var(--font-sans); font-size: 0.875rem; font-weight: 600; padding: 0.625rem 1.25rem; border-radius: var(--radius-sm); border: 1px solid transparent; cursor: pointer; transition: all 100ms ease; text-align: center; text-decoration: none; }
|
||||
.btn-primary { background: var(--brand-black); color: #ffffff; border-color: var(--brand-black); }
|
||||
|
|
|
|||
|
|
@ -104,19 +104,20 @@
|
|||
</div>
|
||||
</div>
|
||||
<a href="/pricing" class="nav-link{{if eq .ActiveNav "pricing"}} active{{end}}">Pricing</a>
|
||||
<div class="nav-dropdown nav-dropdown--locale">
|
||||
<span class="nav-link nav-dropdown-trigger" id="localeTrigger">🌐 EN / $</span>
|
||||
<div class="nav-dropdown nav-dropdown--language">
|
||||
<span class="nav-link nav-dropdown-trigger" id="languageTrigger">🇺🇸 EN</span>
|
||||
<div class="nav-dropdown-menu nav-dropdown-menu--right">
|
||||
<div style="padding:8px 16px;font-size:11px;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:0.08em;">Language</div>
|
||||
<a href="/" class="nav-dropdown-item active" data-lang="en">🇺🇸 English</a>
|
||||
<a href="/de" class="nav-dropdown-item" data-lang="de">🇩🇪 Deutsch</a>
|
||||
<a href="/fr" class="nav-dropdown-item" data-lang="fr">🇫🇷 Français</a>
|
||||
<div style="border-top:1px solid var(--border);margin:8px 0;"></div>
|
||||
<div style="padding:8px 16px;font-size:11px;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:0.08em;">Currency</div>
|
||||
<a href="#" class="nav-dropdown-item active" data-currency="USD">USD $</a>
|
||||
<a href="#" class="nav-dropdown-item" data-currency="EUR">EUR €</a>
|
||||
<a href="#" class="nav-dropdown-item" data-currency="CHF">CHF</a>
|
||||
<a href="#" class="nav-dropdown-item" data-currency="GBP">GBP £</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-dropdown nav-dropdown--currency">
|
||||
<span class="nav-link nav-dropdown-trigger" id="currencyTrigger">$ USD</span>
|
||||
<div class="nav-dropdown-menu nav-dropdown-menu--right" id="currencyMenu">
|
||||
<!-- Currency options loaded dynamically from /api/currencies -->
|
||||
<a href="#" class="nav-dropdown-item active" data-currency="USD">$ USD</a>
|
||||
<a href="#" class="nav-dropdown-item" data-currency="EUR">€ EUR</a>
|
||||
</div>
|
||||
</div>
|
||||
<a href="#" class="nav-link btn btn-ghost">Sign in</a>
|
||||
|
|
@ -173,61 +174,140 @@
|
|||
<script>
|
||||
document.querySelectorAll('.nav-dropdown-trigger').forEach(t=>t.addEventListener('click',()=>t.parentElement.classList.toggle('open')));
|
||||
|
||||
// Locale selector
|
||||
// Language selector state management
|
||||
(function() {
|
||||
const localeTrigger = document.getElementById('localeTrigger');
|
||||
if (!localeTrigger) return;
|
||||
|
||||
const dropdown = localeTrigger.parentElement;
|
||||
const langTrigger = document.getElementById('languageTrigger');
|
||||
if (!langTrigger) return;
|
||||
|
||||
const dropdown = langTrigger.closest('.nav-dropdown--language');
|
||||
const langItems = dropdown.querySelectorAll('[data-lang]');
|
||||
const currencyItems = dropdown.querySelectorAll('[data-currency]');
|
||||
|
||||
// Load saved preferences
|
||||
const saved = JSON.parse(localStorage.getItem('clavitor-locale') || '{}');
|
||||
const currentLang = saved.lang || 'en';
|
||||
const currentCurrency = saved.currency || 'USD';
|
||||
|
||||
function updateDisplay() {
|
||||
const lang = dropdown.querySelector('[data-lang].active')?.dataset.lang || currentLang;
|
||||
const currency = dropdown.querySelector('[data-currency].active')?.dataset.currency || currentCurrency;
|
||||
const langFlags = { en: '🇺🇸', de: '🇩🇪', fr: '🇫🇷' };
|
||||
localeTrigger.textContent = `${langFlags[lang] || '🌐'} ${lang.toUpperCase()} / ${currency}`;
|
||||
}
|
||||
|
||||
// Set initial active states
|
||||
|
||||
// Load saved preference
|
||||
const savedLang = localStorage.getItem('preferredLanguage') || 'en';
|
||||
const langFlags = { en: '🇺🇸', de: '🇩🇪', fr: '🇫🇷' };
|
||||
|
||||
// Set initial active state
|
||||
langItems.forEach(el => {
|
||||
if (el.dataset.lang === currentLang) el.classList.add('active');
|
||||
else el.classList.remove('active');
|
||||
if (el.dataset.lang === savedLang) {
|
||||
el.classList.add('active');
|
||||
langTrigger.textContent = langFlags[savedLang] + ' ' + savedLang.toUpperCase();
|
||||
} else {
|
||||
el.classList.remove('active');
|
||||
}
|
||||
});
|
||||
currencyItems.forEach(el => {
|
||||
if (el.dataset.currency === currentCurrency) el.classList.add('active');
|
||||
else el.classList.remove('active');
|
||||
});
|
||||
updateDisplay();
|
||||
|
||||
|
||||
// Handle language selection
|
||||
langItems.forEach(el => el.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const lang = el.dataset.lang;
|
||||
const flag = el.textContent.trim().split(' ')[0];
|
||||
|
||||
langTrigger.textContent = flag + ' ' + lang.toUpperCase();
|
||||
langItems.forEach(i => i.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
localStorage.setItem('clavitor-locale', JSON.stringify({ lang: el.dataset.lang, currency: currentCurrency }));
|
||||
updateDisplay();
|
||||
localStorage.setItem('preferredLanguage', lang);
|
||||
|
||||
// Navigate to language path
|
||||
if (el.dataset.lang === 'en') window.location.href = '/';
|
||||
else window.location.href = '/' + el.dataset.lang;
|
||||
}));
|
||||
|
||||
// Handle currency selection
|
||||
currencyItems.forEach(el => el.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
currencyItems.forEach(i => i.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
localStorage.setItem('clavitor-locale', JSON.stringify({ lang: currentLang, currency: el.dataset.currency }));
|
||||
updateDisplay();
|
||||
// Refresh page to apply currency (or fetch rates via JS)
|
||||
window.location.reload();
|
||||
if (lang === 'en') window.location.href = '/';
|
||||
else window.location.href = '/' + lang;
|
||||
}));
|
||||
})();
|
||||
|
||||
// Currency selector - fetch from API and render with sections
|
||||
(function() {
|
||||
const currencyTrigger = document.getElementById('currencyTrigger');
|
||||
if (!currencyTrigger) return;
|
||||
|
||||
async function loadCurrencies() {
|
||||
const menu = document.getElementById('currencyMenu');
|
||||
if (!menu) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/currencies');
|
||||
if (!response.ok) throw new Error('ERR-CURRENCY-001: Failed to load currencies');
|
||||
const data = await response.json();
|
||||
|
||||
// Clear existing content
|
||||
menu.innerHTML = '';
|
||||
|
||||
// Render "Popular" section
|
||||
if (data.top && data.top.length > 0) {
|
||||
const popularHeader = document.createElement('div');
|
||||
popularHeader.className = 'dropdown-section';
|
||||
popularHeader.textContent = 'Popular';
|
||||
menu.appendChild(popularHeader);
|
||||
|
||||
data.top.forEach(currency => {
|
||||
const item = createCurrencyItem(currency, currencyTrigger);
|
||||
menu.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Divider between sections
|
||||
if (data.all && data.all.length > 0 && data.top && data.top.length > 0) {
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'dropdown-divider';
|
||||
menu.appendChild(divider);
|
||||
}
|
||||
|
||||
// Render "All Currencies" section
|
||||
if (data.all && data.all.length > 0) {
|
||||
const allHeader = document.createElement('div');
|
||||
allHeader.className = 'dropdown-section';
|
||||
allHeader.textContent = 'All Currencies';
|
||||
menu.appendChild(allHeader);
|
||||
|
||||
data.all.forEach(currency => {
|
||||
const item = createCurrencyItem(currency, currencyTrigger);
|
||||
menu.appendChild(item);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// ERR-CURRENCY-002: API unavailable - keep default fallback options
|
||||
console.error('ERR-CURRENCY-002: Currency API unavailable, using defaults');
|
||||
}
|
||||
}
|
||||
|
||||
function createCurrencyItem(currency, trigger) {
|
||||
const item = document.createElement('a');
|
||||
item.href = '#';
|
||||
item.className = 'nav-dropdown-item';
|
||||
item.setAttribute('data-currency', currency.code);
|
||||
item.textContent = (currency.symbol || '$') + ' ' + currency.code;
|
||||
|
||||
// Set active state based on current selection or saved preference
|
||||
const savedCurrency = localStorage.getItem('preferredCurrency') || 'USD';
|
||||
const currentText = trigger.textContent.trim();
|
||||
if (currentText.includes(currency.code) || savedCurrency === currency.code) {
|
||||
item.classList.add('active');
|
||||
if (savedCurrency === currency.code) {
|
||||
trigger.textContent = (currency.symbol || '$') + ' ' + currency.code;
|
||||
}
|
||||
}
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const code = item.getAttribute('data-currency');
|
||||
const symbol = currency.symbol || '$';
|
||||
trigger.textContent = symbol + ' ' + code;
|
||||
|
||||
// Update active state
|
||||
document.querySelectorAll('.nav-dropdown--currency .nav-dropdown-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
// Store preference
|
||||
localStorage.setItem('preferredCurrency', code);
|
||||
|
||||
// Refresh page to apply currency (or fetch rates via JS)
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// Load currencies on page load
|
||||
loadCurrencies();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -55,7 +55,8 @@
|
|||
</div>
|
||||
<div class="nav-dropdown nav-dropdown--currency">
|
||||
<span class="nav-link nav-dropdown-trigger" id="currencyTrigger">$ USD</span>
|
||||
<div class="nav-dropdown-menu nav-dropdown-menu--right">
|
||||
<div class="nav-dropdown-menu nav-dropdown-menu--right" id="currencyMenu">
|
||||
<!-- Currency options loaded dynamically from /api/currencies -->
|
||||
<a href="#" class="nav-dropdown-item active" data-currency="USD">$ USD</a>
|
||||
<a href="#" class="nav-dropdown-item" data-currency="EUR">€ EUR</a>
|
||||
</div>
|
||||
|
|
@ -145,17 +146,89 @@ document.querySelectorAll('.nav-dropdown--language .nav-dropdown-item').forEach(
|
|||
});
|
||||
});
|
||||
|
||||
// Currency selector state management
|
||||
document.querySelectorAll('.nav-dropdown--currency .nav-dropdown-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const currency = item.getAttribute('data-currency');
|
||||
const symbol = item.textContent.trim().split(' ')[0];
|
||||
document.getElementById('currencyTrigger').textContent = symbol + ' ' + currency;
|
||||
document.querySelectorAll('.nav-dropdown--currency .nav-dropdown-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
});
|
||||
});
|
||||
// Currency selector - fetch from API and render with sections
|
||||
async function loadCurrencies() {
|
||||
const menu = document.getElementById('currencyMenu');
|
||||
const trigger = document.getElementById('currencyTrigger');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/currencies');
|
||||
if (!response.ok) throw new Error('ERR-CURRENCY-001: Failed to load currencies');
|
||||
const data = await response.json();
|
||||
|
||||
// Clear existing content
|
||||
menu.innerHTML = '';
|
||||
|
||||
// Render "Popular" section
|
||||
if (data.top && data.top.length > 0) {
|
||||
const popularHeader = document.createElement('div');
|
||||
popularHeader.className = 'dropdown-section';
|
||||
popularHeader.textContent = 'Popular';
|
||||
menu.appendChild(popularHeader);
|
||||
|
||||
data.top.forEach(currency => {
|
||||
const item = createCurrencyItem(currency, trigger);
|
||||
menu.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// Divider between sections
|
||||
if (data.all && data.all.length > 0 && data.top && data.top.length > 0) {
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'dropdown-divider';
|
||||
menu.appendChild(divider);
|
||||
}
|
||||
|
||||
// Render "All Currencies" section
|
||||
if (data.all && data.all.length > 0) {
|
||||
const allHeader = document.createElement('div');
|
||||
allHeader.className = 'dropdown-section';
|
||||
allHeader.textContent = 'All Currencies';
|
||||
menu.appendChild(allHeader);
|
||||
|
||||
data.all.forEach(currency => {
|
||||
const item = createCurrencyItem(currency, trigger);
|
||||
menu.appendChild(item);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// ERR-CURRENCY-002: API unavailable - keep default fallback options
|
||||
console.error('ERR-CURRENCY-002: Currency API unavailable, using defaults');
|
||||
}
|
||||
}
|
||||
|
||||
function createCurrencyItem(currency, trigger) {
|
||||
const item = document.createElement('a');
|
||||
item.href = '#';
|
||||
item.className = 'nav-dropdown-item';
|
||||
item.setAttribute('data-currency', currency.code);
|
||||
item.textContent = (currency.symbol || '$') + ' ' + currency.code;
|
||||
|
||||
// Set active state based on current selection
|
||||
const currentText = trigger.textContent.trim();
|
||||
if (currentText.includes(currency.code)) {
|
||||
item.classList.add('active');
|
||||
}
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const code = item.getAttribute('data-currency');
|
||||
const symbol = currency.symbol || '$';
|
||||
trigger.textContent = symbol + ' ' + code;
|
||||
|
||||
// Update active state
|
||||
document.querySelectorAll('.nav-dropdown--currency .nav-dropdown-item').forEach(i => i.classList.remove('active'));
|
||||
item.classList.add('active');
|
||||
|
||||
// Store preference
|
||||
localStorage.setItem('preferredCurrency', code);
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
// Load currencies on page load
|
||||
loadCurrencies();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue