diff --git a/desktop/components/navbar.html b/desktop/components/navbar.html new file mode 100644 index 0000000..438b1ee --- /dev/null +++ b/desktop/components/navbar.html @@ -0,0 +1,25 @@ +
+
+ + Logo + São Miguel Bus + +
+ Routes + Directions + Tours + Info + Premium + +
+
+
diff --git a/desktop/components/sidebar.html b/desktop/components/sidebar.html new file mode 100644 index 0000000..b2e0924 --- /dev/null +++ b/desktop/components/sidebar.html @@ -0,0 +1,10 @@ + diff --git a/desktop/index.html b/desktop/index.html index a2b19ca..9b4d4a3 100644 --- a/desktop/index.html +++ b/desktop/index.html @@ -41,6 +41,8 @@ + + @@ -136,383 +138,30 @@ -
-
-
-
-
- -
-
-
- -
- - - -
- -
- -
- - - - - - - -
- -
-
-
-
-
-

Download APP

- -
-
-
-
-
- - -
-
-
-
-
-
-
-
-

Encontrou Algo Incorreto?

-

Estamos abertos a qualquer sugestão ou para apenas conversar.

-
-
-
-
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- - -
-
-
-
- -
-
-
-
-
-
-
- -
-
-
-
- - -
- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + diff --git a/desktop/js/apiHandler.js b/desktop/js/apiHandler.js index a35b47d..5dcd8ba 100644 --- a/desktop/js/apiHandler.js +++ b/desktop/js/apiHandler.js @@ -16,7 +16,7 @@ document.addEventListener("DOMContentLoaded", function() { }); function fetchAndPopulateStops() { - const url = 'https://api.saomiguelbus.com/api/v1/stops'; + const url = 'https://api.saomiguelbus.com/api/v2/stops'; fetch(url, { method: 'GET', headers: { @@ -50,10 +50,10 @@ function searchRoutes(origin, destination, day, time) { document.getElementById('noRoutesMessage').style.display = 'none'; const parameters = getUrlParameters(origin, destination, day, time); - const url = 'https://api.saomiguelbus.com/api/v1/route?origin=' + parameters.origin - + '&destination=' + parameters.destination - + '&day=' + parameters.day - + '&start=' + parameters.time + const url = 'https://api.saomiguelbus.com/api/v2/route?origin=' + encodeURIComponent(parameters.origin) + + '&destination=' + encodeURIComponent(parameters.destination) + + '&day=' + encodeURIComponent(parameters.day) + + '&start=' + encodeURIComponent(parameters.time) fetchAndDisplayRoutes(url, parameters); // postToStats if not in localhost if (window.location.hostname != "localhost" && window.location.hostname != "127.0.0.1") @@ -82,7 +82,8 @@ function fetchAndDisplayRoutes(url, parameters) { } function postToStats(parameters) { - const url = `https://api.saomiguelbus.com/api/v1/stat?request=get_route&origin=${parameters.origin}&destination=${parameters.destination}&time=${parameters.time}&language=${LANG}&platform=web&day=${parameters.day}`; + const lang = (typeof currentLanguage !== 'undefined' && currentLanguage) ? currentLanguage : 'pt'; + const url = `https://api.saomiguelbus.com/api/v1/stat?request=get_route&origin=${encodeURIComponent(parameters.origin)}&destination=${encodeURIComponent(parameters.destination)}&time=${encodeURIComponent(parameters.time)}&language=${encodeURIComponent(lang)}&platform=web&day=${encodeURIComponent(parameters.day)}`; fetch(url, { method: 'POST', headers: { @@ -102,7 +103,11 @@ function postToStats(parameters) { function displayNoRoutesMessage(parameters) { const noRoutesDiv = document.getElementById('noRoutesMessage'); - noRoutesDiv.innerHTML = `

${LANGS[LANG].No_routes1} ${parameters.origin} ${LANGS[LANG].No_routes2} ${parameters.destination}

${LANGS[LANG].No_routes_subtitle}

`; + const message = (typeof t === 'function' ? t('noRoutesMessage') : 'No routes found from {origin} to {destination}.') + .replace('{origin}', parameters.origin) + .replace('{destination}', parameters.destination); + const subtitle = (typeof t === 'function' ? t('noRoutesSubtitle') : 'Try different parameters.'); + noRoutesDiv.innerHTML = `

${message}

${subtitle}

`; noRoutesDiv.style.display = 'block'; } @@ -167,7 +172,7 @@ function displayRoutes(routes, originStop) { // Generate HTML for the first and last stops and transfer information let stopsHtml = `
${firstStop[0]}: ${firstStop[1]}
-
${stopsArray.length > 2 ? `+${stopsArray.length - 2} ${stopsArray.length - 2 === 1 ? LANGS[LANG].Transfer : LANGS[LANG].Transfers}` : ''}
+
${stopsArray.length > 2 ? `+${stopsArray.length - 2} ${(typeof t==='function' ? (stopsArray.length - 2 === 1 ? t('transfer') : t('transfers')) : (stopsArray.length - 2 === 1 ? 'transfer' : 'transfers'))}` : ''}
${stopsArray.slice(1, stopsArray.length - 1).map(([stop, time]) => `
${stop}: ${time}
`).join('')}
diff --git a/desktop/js/app.js b/desktop/js/app.js new file mode 100644 index 0000000..79830bf --- /dev/null +++ b/desktop/js/app.js @@ -0,0 +1,68 @@ +// Desktop app bootstrap +(function(){ + async function bootstrap(){ + // Load navbar + sidebar and route + if (window.desktopRouter) { + await window.desktopRouter.router(); + } + + // Register SW (desktop only) + if ('serviceWorker' in navigator) { + window.addEventListener('load', function(){ + navigator.serviceWorker.register('/desktop/js/sw.js').catch(console.error); + }); + } + + // Mobile redirect safeguard + function checkScreenWidth(){ + if (window.innerWidth <= 769) { + if (typeof umami !== 'undefined') umami.track('desktop-redirect-to-mobile'); + window.location.href = '/'; + } + } + document.addEventListener('DOMContentLoaded', checkScreenWidth); + window.addEventListener('resize', checkScreenWidth); + } + + // Search page init called by router after page inject + window.initSearchPage = function(){ + // i18n + if (typeof loadTranslations === 'function') { + const langCookie = getCookie('language'); + const lang = langCookie || (navigator.language || 'pt').split('-')[0]; + loadTranslations(lang.startsWith('pt') ? 'pt' : lang); + } + + // Bind swap button + const swapBtn = document.getElementById('change'); + if (swapBtn) { + swapBtn.addEventListener('click', function(){ + const origin = document.getElementById('origin'); + const destination = document.getElementById('destination'); + const tmp = origin.value; origin.value = destination.value; destination.value = tmp; + if (typeof umami !== 'undefined') umami.track('desktop-swap-origin-destination'); + }); + } + + // Bind search button + const searchBtn = document.getElementById('btnSubmit'); + if (searchBtn) { + searchBtn.addEventListener('click', function(e){ + e.preventDefault(); + const origin = document.getElementById('origin').value; + const destination = document.getElementById('destination').value; + const day = document.getElementById('day').value; + const time = document.getElementById('time').value; + if (typeof searchRoutes === 'function') { + searchRoutes(origin, destination, day, time); + } + }); + } + + // Populate stops + initial ads + if (typeof fetchAndPopulateStops === 'function') fetchAndPopulateStops(); + if (typeof loadAdBanner === 'function') loadAdBanner('home'); + } + + document.addEventListener('DOMContentLoaded', bootstrap); +})(); diff --git a/desktop/js/i18n.js b/desktop/js/i18n.js new file mode 100644 index 0000000..a495f09 --- /dev/null +++ b/desktop/js/i18n.js @@ -0,0 +1,51 @@ +let availableLanguages = ['pt', 'en', 'es', 'de', 'fr', 'it', 'uk', 'zh']; +let currentLanguage = getCookie('language') || (availableLanguages.includes((navigator.language||'pt').split('-')[0]) ? (navigator.language||'pt').split('-')[0] : 'pt'); +let translations = {}; + +async function loadTranslations(lang) { + try { + const response = await fetch(`/locales/${lang}.json`); + translations = await response.json(); + currentLanguage = lang; + updatePageContent(); + } catch (error) { + console.error('Error loading translations:', error); + } +} + +function t(key, fallback){ + return translations[key] || fallback || key; +} + +function updatePageContent() { + const textElements = document.querySelectorAll('[data-i18n]'); + textElements.forEach(element => { + const key = element.getAttribute('data-i18n'); + const translation = t(key); + if (translation) { + if (translation.includes('<') && translation.includes('>')) { + element.innerHTML = translation; + } else { + element.innerText = translation; + } + } + }); + + const placeholderElements = document.querySelectorAll('[data-i18n-placeholder]'); + placeholderElements.forEach(element => { + const key = element.getAttribute('data-i18n-placeholder'); + const translation = t(key); + if (translation) element.placeholder = translation; + }); +} + +function changeLanguage(lang) { + loadTranslations(lang); + setCookie('language', lang, 30); + // Refresh i18n content only (no full reload) + setTimeout(updatePageContent, 0); +} + +document.addEventListener('DOMContentLoaded', () => { + loadTranslations(currentLanguage); +}); diff --git a/desktop/js/router.js b/desktop/js/router.js new file mode 100644 index 0000000..82f9a0f --- /dev/null +++ b/desktop/js/router.js @@ -0,0 +1,74 @@ +// Simple hash-based router for desktop SPA +(function(){ + const routes = { + '/search': '/desktop/pages/search.html', + '/directions': '/desktop/pages/directions.html', + '/tracking': '/desktop/pages/tracking.html', + '/tours': '/desktop/pages/tours.html', + '/info': '/desktop/pages/info.html', + '/premium': '/desktop/pages/premium.html' + }; + + async function loadComponent(selector, url) { + try { + const res = await fetch(url, { cache: 'no-cache' }); + const html = await res.text(); + const el = document.querySelector(selector); + if (el) el.innerHTML = html; + } catch (e) { + console.error('Failed to load', url, e); + } + } + + async function ensureShell() { + await Promise.all([ + loadComponent('#navbar', '/desktop/components/navbar.html'), + loadComponent('#sidebar', '/desktop/components/sidebar.html') + ]); + } + + async function router() { + if (!location.hash) { + location.hash = '#/search'; + return; + } + + const path = location.hash.replace('#', ''); + const page = routes[path] || routes['/search']; + + await ensureShell(); + + await loadComponent('#content', page); + + // After page load hooks + if (path === '/search' && typeof initSearchPage === 'function') { + initSearchPage(); + } + + // i18n update after DOM injected + if (typeof updatePageContent === 'function') { + updatePageContent(); + } + + // Track page view + if (typeof umami !== 'undefined') { + umami.track(`desktop-route-${path.slice(1)}-view`); + } + + setActiveNav(path); + } + + function setActiveNav(path){ + const links = document.querySelectorAll('[data-nav]'); + links.forEach(a => { + if (a.getAttribute('href') === `#${path}`) { + a.classList.add('text-green-600','font-semibold'); + } else { + a.classList.remove('text-green-600','font-semibold'); + } + }); + } + + window.addEventListener('hashchange', router); + window.desktopRouter = { router }; +})(); diff --git a/desktop/js/utils.js b/desktop/js/utils.js new file mode 100644 index 0000000..ceb18ec --- /dev/null +++ b/desktop/js/utils.js @@ -0,0 +1,20 @@ +function setCookie(name, value, days) { + const date = new Date(); + date.setTime(date.getTime() + (days*24*60*60*1000)); + const expires = "expires=" + date.toUTCString(); + document.cookie = name + "=" + encodeURIComponent(value) + ";" + expires + ";path=/"; +} +function getCookie(name) { + const decodedCookie = decodeURIComponent(document.cookie); + const ca = decodedCookie.split(';'); + name = name + '='; + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1); + if (c.indexOf(name) === 0) return c.substring(name.length, c.length); + } + return ""; +} +function deleteCookie(name){ + document.cookie = name + '=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;'; +} diff --git a/desktop/pages/directions.html b/desktop/pages/directions.html new file mode 100644 index 0000000..e419bba --- /dev/null +++ b/desktop/pages/directions.html @@ -0,0 +1,4 @@ +
+

Directions

+

Coming soon.

+
diff --git a/desktop/pages/info.html b/desktop/pages/info.html new file mode 100644 index 0000000..a96ff1f --- /dev/null +++ b/desktop/pages/info.html @@ -0,0 +1,4 @@ +
+

Info

+

Coming soon.

+
diff --git a/desktop/pages/premium.html b/desktop/pages/premium.html new file mode 100644 index 0000000..cb8b940 --- /dev/null +++ b/desktop/pages/premium.html @@ -0,0 +1,4 @@ +
+

Remove Ads

+

Choose your plan to remove all advertisements and enjoy an ad-free experience.

+
diff --git a/desktop/pages/search.html b/desktop/pages/search.html new file mode 100644 index 0000000..45901bd --- /dev/null +++ b/desktop/pages/search.html @@ -0,0 +1,41 @@ +
+

Search a Bus

+

Find the best route for you.

+ + + +
+ + + + +
diff --git a/desktop/pages/tours.html b/desktop/pages/tours.html new file mode 100644 index 0000000..4292d33 --- /dev/null +++ b/desktop/pages/tours.html @@ -0,0 +1,4 @@ +
+

Tours

+

Find tours on São Miguel.

+
diff --git a/desktop/pages/tracking.html b/desktop/pages/tracking.html new file mode 100644 index 0000000..10d6d37 --- /dev/null +++ b/desktop/pages/tracking.html @@ -0,0 +1,4 @@ +
+

Tracking

+

Coming soon.

+