feat: Passwordless cross-device authentication

- Arkitektur: docs/auth/passwordless-architecture.md
- Backend: iom/quixzoom-auth-service/ (FastAPI + Redis)
- Webb: quixzoom-market-pages/se/login/ (QR-kod + polling)
- App: iom/quixzoom-app/src/features/auth/ (push + deep links)

Flöde: QR-kod → app-godkännande → webb-inloggad
This commit is contained in:
Bernt
2026-07-07 07:11:50 +00:00
parent 4aa984ad74
commit 6989a98d75
61843 changed files with 5491611 additions and 872231 deletions
+136
View File
@@ -0,0 +1,136 @@
// Academy Language Switcher
// Växlar mellan engelska och svenska
class LanguageSwitcher {
constructor() {
this.currentLang = localStorage.getItem('academyLang') || 'en';
this.translations = {
en: {
title: 'quiXzoom Academy',
subtitle: 'Master the skills to become a top-earning Zoomer',
module: 'Module',
lesson: 'Lesson',
back: '← Back',
academy: 'Academy',
gettingStarted: 'Getting Started',
photoQuality: 'Photo Quality',
missionTypes: 'Mission Types',
advancedTechniques: 'Advanced Techniques',
earningMore: 'Earning More',
proZoomer: 'Pro Zoomer',
welcome: 'Welcome to quiXzoom',
firstMission: 'Your First Mission',
photoBasics: 'Photo Basics',
submittingWork: 'Submitting Your Work',
lightingAngles: 'Lighting & Angles',
compositionRules: 'Composition Rules',
focusClarity: 'Focus & Clarity',
commonMistakes: 'Common Mistakes',
infrastructure: 'Infrastructure',
propertyRealEstate: 'Property & Real Estate',
commercialRetail: 'Commercial & Retail',
gpsLocation: 'GPS & Location Data',
multipleAngles: 'Multiple Angles',
documentationStandards: 'Documentation Standards',
missionSelection: 'Mission Selection',
efficiencyTips: 'Efficiency Tips',
buildingReputation: 'Building Reputation',
qualityAssurance: 'Quality Assurance',
troubleshooting: 'Troubleshooting',
safetyEthics: 'Safety & Ethics',
graduation: 'Graduation',
footer: 'quiXzoom Academy — Learn. Earn. Grow.',
switchToSwedish: '🇸🇪 Svenska',
switchToEnglish: '🇬🇧 English'
},
sv: {
title: 'quiXzoom Academy',
subtitle: 'Bemästra färdigheterna för att bli en topputbildad Zoomer',
module: 'Modul',
lesson: 'Lektion',
back: '← Tillbaka',
academy: 'Academy',
gettingStarted: 'Komma Igång',
photoQuality: 'Fotokvalitet',
missionTypes: 'Uppdragstyper',
advancedTechniques: 'Avancerade Tekniker',
earningMore: 'Tjäna Mer',
proZoomer: 'Pro Zoomer',
welcome: 'Välkommen till quiXzoom',
firstMission: 'Ditt Första Uppdrag',
photoBasics: 'Fotobasics',
submittingWork: 'Skicka In Ditt Arbete',
lightingAngles: 'Ljus & Vinklar',
compositionRules: 'Kompositionsregler',
focusClarity: 'Fokus & Klarhet',
commonMistakes: 'Vanliga Misstag',
infrastructure: 'Infrastruktur',
propertyRealEstate: 'Fastighet & Bostad',
commercialRetail: 'Kommersiell & Detaljhandel',
gpsLocation: 'GPS & Platsdata',
multipleAngles: 'Flera Vinklar',
documentationStandards: 'Dokumentationsstandarder',
missionSelection: 'Uppdragsval',
efficiencyTips: 'Effektivitetstips',
buildingReputation: 'Bygga Rykte',
qualityAssurance: 'Kvalitetssäkring',
troubleshooting: 'Felsökning',
safetyEthics: 'Säkerhet & Etik',
graduation: 'Examination',
footer: 'quiXzoom Academy — Lär. Tjäna. Väx.',
switchToSwedish: '🇸🇪 Svenska',
switchToEnglish: '🇬🇧 English'
}
};
}
init() {
this.renderSwitcher();
this.applyLanguage();
}
renderSwitcher() {
const switcher = document.createElement('div');
switcher.className = 'language-switcher';
switcher.innerHTML = `
<button class="lang-btn ${this.currentLang === 'en' ? 'active' : ''}" onclick="langSwitcher.setLanguage('en')">
🇬🇧 EN
</button>
<button class="lang-btn ${this.currentLang === 'sv' ? 'active' : ''}" onclick="langSwitcher.setLanguage('sv')">
🇸🇪 SV
</button>
`;
// Lägg till i nav
const nav = document.querySelector('.nav');
if (nav) {
nav.appendChild(switcher);
}
}
setLanguage(lang) {
this.currentLang = lang;
localStorage.setItem('academyLang', lang);
location.reload();
}
applyLanguage() {
const t = this.translations[this.currentLang];
// Uppdatera titlar
document.querySelectorAll('[data-i18n]').forEach(el => {
const key = el.dataset.i18n;
if (t[key]) {
el.textContent = t[key];
}
});
}
getText(key) {
return this.translations[this.currentLang][key] || key;
}
}
// Initiera
const langSwitcher = new LanguageSwitcher();
document.addEventListener('DOMContentLoaded', () => langSwitcher.init());