Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions desktop/components/navbar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<div class="w-full bg-white border-b sticky top-0 z-40">
<div class="max-w-7xl mx-auto px-4 h-14 flex items-center justify-between">
<a href="#/search" class="flex items-center space-x-2" data-nav>
<img src="/static/img/logo-playstore.png" alt="Logo" class="w-6 h-6"/>
<span class="text-sm sm:text-base font-semibold">São Miguel Bus</span>
</a>
<div class="flex items-center space-x-4">
<a href="#/search" class="text-sm" data-nav data-i18n="navBarSearchLabel">Routes</a>
<a href="#/directions" class="text-sm" data-nav data-i18n="navBarRoutesLabel">Directions</a>
<a href="#/tours" class="text-sm" data-nav data-i18n="navBarToursLabel">Tours</a>
<a href="#/info" class="text-sm" data-nav data-i18n="navBarInfoLabel">Info</a>
<a href="#/premium" class="text-sm text-yellow-600 font-semibold" data-nav data-i18n="navBarAdvertLabel">Premium</a>
<select id="languageSelect" class="text-sm border rounded px-2 py-1" onchange="changeLanguage(this.value)" aria-label="Language">
<option value="pt">🇵🇹 PT</option>
<option value="en">🇬🇧 EN</option>
<option value="es">🇪🇸 ES</option>
<option value="fr">🇫🇷 FR</option>
<option value="de">🇩🇪 DE</option>
<option value="it">🇮🇹 IT</option>
<option value="uk">🇺🇦 UK</option>
<option value="zh">🇨🇳 ZH</option>
</select>
</div>
</div>
</div>
10 changes: 10 additions & 0 deletions desktop/components/sidebar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<aside class="h-[calc(100vh-56px)] sticky top-14 p-4 border-r hidden lg:block">
<nav class="space-y-2">
<a href="#/search" class="block hover:text-green-600" data-nav>🚌 <span data-i18n="navBarSearchLabel">Routes</span></a>
<a href="#/directions" class="block hover:text-green-600" data-nav>🗺️ <span data-i18n="navBarRoutesLabel">Directions</span></a>
<a href="#/tracking" class="block hover:text-green-600" data-nav>📌 <span>Tracking</span></a>
<a href="#/tours" class="block hover:text-green-600" data-nav>🎯 <span data-i18n="navBarToursLabel">Tours</span></a>
<a href="#/info" class="block hover:text-green-600" data-nav>ℹ️ <span data-i18n="navBarInfoLabel">Info</span></a>
<a href="#/premium" class="block hover:text-yellow-600 font-semibold" data-nav>👑 <span data-i18n="premium">PREMIUM</span></a>
</nav>
</aside>
405 changes: 27 additions & 378 deletions desktop/index.html

Large diffs are not rendered by default.

21 changes: 13 additions & 8 deletions desktop/js/apiHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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: {
Expand All @@ -102,7 +103,11 @@ function postToStats(parameters) {

function displayNoRoutesMessage(parameters) {
const noRoutesDiv = document.getElementById('noRoutesMessage');
noRoutesDiv.innerHTML = `<div class="container" data-umami-event="desktop-no-routes-message-displayed"><div class="row"><div class="col-xs-12"><h3>${LANGS[LANG].No_routes1} <b>${parameters.origin}</b> ${LANGS[LANG].No_routes2} <b>${parameters.destination}</b></h3><p>${LANGS[LANG].No_routes_subtitle}</p></div></div></div>`;
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 = `<div class="container" data-umami-event="desktop-no-routes-message-displayed"><div class="row"><div class="col-xs-12"><h3>${message}</h3><p>${subtitle}</p></div></div></div>`;
noRoutesDiv.style.display = 'block';
}

Expand Down Expand Up @@ -167,7 +172,7 @@ function displayRoutes(routes, originStop) {
// Generate HTML for the first and last stops and transfer information
let stopsHtml = `
<div class="stop"><b>${firstStop[0]}</b>: ${firstStop[1]}</div>
<div class="transfer" id="transfer-info" data-umami-event="desktop-transfer-info"> <span class="arrow-icon">⬇</span> ${stopsArray.length > 2 ? `<span class="transfer-info">+${stopsArray.length - 2} ${stopsArray.length - 2 === 1 ? LANGS[LANG].Transfer : LANGS[LANG].Transfers}</span>` : ''} </div>
<div class="transfer" id="transfer-info" data-umami-event="desktop-transfer-info"> <span class="arrow-icon">⬇</span> ${stopsArray.length > 2 ? `<span class="transfer-info">+${stopsArray.length - 2} ${(typeof t==='function' ? (stopsArray.length - 2 === 1 ? t('transfer') : t('transfers')) : (stopsArray.length - 2 === 1 ? 'transfer' : 'transfers'))}</span>` : ''} </div>
<div class="intermediate-stops" style="max-height: 0; overflow: hidden; transition: max-height 0.5s ease-out;">
${stopsArray.slice(1, stopsArray.length - 1).map(([stop, time]) => `<div class="stop"><b style="margin-left: 10px">${stop}</b>: ${time}</div>`).join('')}
</div>
Expand Down
68 changes: 68 additions & 0 deletions desktop/js/app.js
Original file line number Diff line number Diff line change
@@ -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);
})();
51 changes: 51 additions & 0 deletions desktop/js/i18n.js
Original file line number Diff line number Diff line change
@@ -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);
});
74 changes: 74 additions & 0 deletions desktop/js/router.js
Original file line number Diff line number Diff line change
@@ -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 };
})();
20 changes: 20 additions & 0 deletions desktop/js/utils.js
Original file line number Diff line number Diff line change
@@ -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=/;';
}
4 changes: 4 additions & 0 deletions desktop/pages/directions.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<section class="p-4">
<h1 class="text-2xl font-bold mb-4">Directions</h1>
<p class="text-gray-600">Coming soon.</p>
</section>
4 changes: 4 additions & 0 deletions desktop/pages/info.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<section class="p-4">
<h1 class="text-2xl font-bold mb-4">Info</h1>
<p class="text-gray-600">Coming soon.</p>
</section>
4 changes: 4 additions & 0 deletions desktop/pages/premium.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<section class="p-4">
<h1 class="text-2xl font-bold mb-4" data-i18n="removeAdsTitle">Remove Ads</h1>
<p class="text-gray-600" data-i18n="pricingDescription">Choose your plan to remove all advertisements and enjoy an ad-free experience.</p>
</section>
41 changes: 41 additions & 0 deletions desktop/pages/search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<section class="p-4">
<h1 class="text-2xl font-bold mb-4" data-i18n="bannerTitle">Search a Bus</h1>
<p class="text-gray-600 mb-6" data-i18n="bannerSubtitle">Find the best route for you.</p>

<form class="grid grid-cols-1 lg:grid-cols-12 gap-3" role="search" aria-label="Bus Search">
<div class="lg:col-span-4">
<label for="origin" class="block text-sm" data-i18n="originLabel">From</label>
<input id="origin" list="origin-stops" class="w-full border rounded px-3 py-2" data-i18n-placeholder="originPlaceholder" placeholder="Choose your starting point..." />
<datalist id="origin-stops"></datalist>
</div>
<div class="lg:col-span-1 flex items-end">
<button id="change" type="button" class="w-full border rounded px-3 py-2">⇅</button>
</div>
<div class="lg:col-span-4">
<label for="destination" class="block text-sm" data-i18n="destinationLabel">To</label>
<input id="destination" list="destination-stops" class="w-full border rounded px-3 py-2" data-i18n-placeholder="destinationPlaceholder" placeholder="Choose your destination..." />
<datalist id="destination-stops"></datalist>
</div>
<div class="lg:col-span-2">
<label for="day" class="block text-sm" data-i18n="dayLabel">Day of the Week</label>
<select id="day" class="w-full border rounded px-3 py-2">
<option value="1" data-i18n="weekday">Weekdays only</option>
<option value="2" data-i18n="saturday">Saturdays only</option>
<option value="3" data-i18n="sunday">Sundays only</option>
</select>
</div>
<div class="lg:col-span-1">
<label for="time" class="block text-sm" data-i18n="timeLabel">What time?</label>
<input id="time" type="time" class="w-full border rounded px-3 py-2" data-i18n-placeholder="timePlaceholder" placeholder="Select time (optional)" />
</div>
<div class="lg:col-span-12">
<button id="btnSubmit" class="bg-green-600 text-white rounded px-4 py-2" data-i18n="searchButton">Search</button>
</div>
</form>

<div id="placeHolderForAd" class="my-4"></div>

<div id="noRoutesMessage" class="hidden"></div>
<div id="favouriteRoutesContainer" class="hidden"></div>
<div id="routesContainer" class="hidden"></div>
</section>
4 changes: 4 additions & 0 deletions desktop/pages/tours.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<section class="p-4">
<h1 class="text-2xl font-bold mb-4" data-i18n="toursTitle">Tours</h1>
<p class="text-gray-600" data-i18n="toursSubtitle">Find tours on São Miguel.</p>
</section>
4 changes: 4 additions & 0 deletions desktop/pages/tracking.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<section class="p-4">
<h1 class="text-2xl font-bold mb-4">Tracking</h1>
<p class="text-gray-600">Coming soon.</p>
</section>