Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/schedule-visualization #127

Merged
merged 29 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6097065
feat: add link to view schedule in StopPane component
Ahmedhossamdev Nov 18, 2024
a537249
feat: add server-side loading for stop schedule
Ahmedhossamdev Nov 18, 2024
1d4be0b
feat: Add stop-for-schedule api
Ahmedhossamdev Nov 19, 2024
0bbe23d
feat: remove schedule loading for stop
Ahmedhossamdev Nov 19, 2024
ae45ba4
feat: add function to convert Unix timestamp to local time
Ahmedhossamdev Nov 19, 2024
bfb601d
refactor: Moved the formatTime to formatter.js
Ahmedhossamdev Nov 19, 2024
5e8476f
feat: Initial page for visualizing schedules
Ahmedhossamdev Nov 19, 2024
d888168
refactor: remove console log from convertUnixToTime function
Ahmedhossamdev Nov 20, 2024
b3fc0b4
feat: update schedule page to use formatTime for arrival times
Ahmedhossamdev Nov 20, 2024
63564ec
refactor: remove console log from groupStopTimesByHour function
Ahmedhossamdev Nov 20, 2024
1113e72
feat: Add fetch new schedules when select new date & improve the ui
Ahmedhossamdev Nov 20, 2024
52ac2d6
feat: Add schedule accordion component for displaying trip schedules
Ahmedhossamdev Nov 20, 2024
1327317
feat: Refactor i18n setup to use async function for locale registration
Ahmedhossamdev Nov 20, 2024
1874754
refactor: changed the file name
Ahmedhossamdev Nov 20, 2024
d2d4069
feat: Add StopDetailsHeader component for displaying stop information
Ahmedhossamdev Nov 20, 2024
ec7e9b4
feat: Integrate StopDetailsHeader component
Ahmedhossamdev Nov 20, 2024
4f2ffb3
feat: Add conditional rendering for AM/PM schedule availability in sc…
Ahmedhossamdev Nov 21, 2024
94b0965
feat: Update schedule fetching logic to handle date changes and initi…
Ahmedhossamdev Nov 21, 2024
2385cb0
feat: Enhance scheduleAccordionItem with improved table structure and…
Ahmedhossamdev Nov 21, 2024
f1ead02
feat: Initialize current date for schedule fetching and update select…
Ahmedhossamdev Nov 21, 2024
34425dc
feat: Update StopPane layout to position 'View Schedule' link at the …
Ahmedhossamdev Nov 21, 2024
3aa23d7
feat: Update StopPane layout to use flexbox for positioning 'View Sch…
Ahmedhossamdev Nov 22, 2024
8d45c2a
feat: Localize 'on time' status message
Ahmedhossamdev Nov 22, 2024
a20cc1f
feat: Add localized schedule messages for multiple languages
Ahmedhossamdev Nov 23, 2024
bda9446
feat: Remove scheduleAccordionItem component
Ahmedhossamdev Nov 23, 2024
c900c0f
refactor: Uppercase the first char (file-name)
Ahmedhossamdev Nov 23, 2024
abf5e02
feat: Localize StopDetailsHeader component text
Ahmedhossamdev Nov 23, 2024
1ce8463
feat: Localize 'View Schedule' text in StopPane component
Ahmedhossamdev Nov 23, 2024
f837084
feat: Localize schedule-related text
Ahmedhossamdev Nov 23, 2024
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
2 changes: 1 addition & 1 deletion src/components/ArrivalDeparture.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
};
} else if (scheduledDiff <= 0) {
return {
status: 'on time',
status: `${$t('status.on_time')}`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll still merge this, but is there any reason this isn't expressed as:

status: $t('status.on_time'),

isn't the outer template string redundant?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're right I will update it in the upcoming PR.

text: `${$t('status.arrives_on_time')}`,
color: 'text-green-500'
};
Expand Down
10 changes: 2 additions & 8 deletions src/components/oba/TripDetailsPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { onMount, onDestroy } from 'svelte';
import { faBus, faPersonWalking } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
import { convertUnixToTime } from '$lib/formatters';

export let stop;
export let tripId;
Expand All @@ -14,13 +15,6 @@
let interval;
let busPosition = 0;

function formatTime(seconds) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

if (!seconds) return '';
const date = new Date(seconds * 1000);
const utcDate = new Date(date.toUTCString().slice(0, -4));
return utcDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}

function calculateBusPosition() {
if (tripDetails && tripDetails.status && tripDetails.status.position) {
const { lat, lon } = tripDetails.status.position;
Expand Down Expand Up @@ -118,7 +112,7 @@
<div class="text-md font-semibold text-[#000000] dark:text-white">
{stopInfo[tripStop.stopId] ? stopInfo[tripStop.stopId].name : tripStop.stopId}
</div>
<div class="text-sm text-[#86858B]">{formatTime(tripStop.arrivalTime)}</div>
<div class="text-sm text-[#86858B]">{convertUnixToTime(tripStop.arrivalTime)}</div>
</div>
</div>
{/each}
Expand Down
114 changes: 114 additions & 0 deletions src/components/schedule-for-stop/ScheduleAccordionItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script>
import { createEventDispatcher } from 'svelte';
import { AccordionItem } from 'flowbite-svelte';
import { t } from 'svelte-i18n';

export let schedule;
export let expanded;

const dispatch = createEventDispatcher();

function handleToggle() {
dispatch('toggle');
}

function formatHour(hour) {
const hourInt = +hour;
if (hourInt === 0) return '12';
if (hourInt > 12) return hourInt - 12;
return hourInt;
}

function renderScheduleTable(schedule) {
const stopTimes = Object.entries(schedule.stopTimes);

const amTimes = stopTimes.filter(([hour]) => +hour < 12);
const pmTimes = stopTimes.filter(([hour]) => +hour >= 12);

return {
amTimes,
pmTimes
};
}

function extractMinutes(arrivalTime) {
return arrivalTime.replace(/[AP]M/, '').split(':')[1];
}
</script>

<AccordionItem open={expanded} on:click={handleToggle}>
<span slot="header" class="text-lg font-semibold text-gray-800">
{schedule.tripHeadsign}
</span>
<div class="overflow-x-auto">
<table class="mt-4 w-full table-auto rounded-lg border border-gray-200 shadow-lg">
<thead class="bg-gray-100 text-gray-800">
<tr>
<th class="cursor-pointer px-6 py-3 text-left">{$t('schedule_for_stop.hour')}</th>
<th class="cursor-pointer px-6 py-3 text-left">{$t('schedule_for_stop.minutes')}</th>
</tr>
</thead>
<tbody>
<tr class="bg-gray-50 hover:bg-gray-100">
<td colspan="2" class="px-6 py-3 font-semibold text-gray-700">AM</td>
</tr>
{#if renderScheduleTable(schedule).amTimes.length === 0}
<tr>
<td colspan="2" class="border px-6 py-3 text-center text-gray-500">
{$t('schedule_for_stop.no_am_schedules_available')}
</td>
</tr>
{:else}
{#each renderScheduleTable(schedule).amTimes as [hour, times]}
<tr class="hover:bg-gray-100">
<td
class="border px-6 py-3 text-center text-lg font-semibold"
title="Full Time: {hour}:{extractMinutes(times[0].arrivalTime)}"
>
{formatHour(hour)} <span class="text-sm text-gray-600">AM</span>
</td>
<td class="border px-6 py-3 text-lg">
{#each times as stopTime, index (index)}
<span>
{extractMinutes(stopTime.arrivalTime)}
{index < times.length - 1 ? ', ' : ''}
</span>
{/each}
</td>
</tr>
{/each}
{/if}

<tr class="bg-gray-50 hover:bg-gray-100">
<td colspan="2" class="px-6 py-3 font-semibold text-gray-700">PM</td>
</tr>
{#if renderScheduleTable(schedule).pmTimes.length === 0}
<tr>
<td colspan="2" class="border px-6 py-3 text-center text-gray-500">
{$t('schedule_for_stop.no_pm_schedules_available')}
</td>
</tr>
{:else}
{#each renderScheduleTable(schedule).pmTimes as [hour, times]}
<tr class="hover:bg-gray-100">
<td
class="border px-6 py-3 text-center text-lg font-semibold"
title="Full Time: {hour}:{extractMinutes(times[0].arrivalTime)}"
>
{formatHour(hour)} <span class="text-sm text-gray-600">PM</span>
</td>
<td class="border px-6 py-3 text-lg">
{#each times as stopTime, index (index)}
<span>
{extractMinutes(stopTime.arrivalTime)}
{index < times.length - 1 ? ', ' : ''}
</span>
{/each}
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
</AccordionItem>
35 changes: 35 additions & 0 deletions src/components/schedule-for-stop/StopDetailsHeader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script>
import { faBus, faMapMarkerAlt, faArrowRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
import { t } from 'svelte-i18n';
export let stopName;
export let stopId;
export let stopDirection;
</script>

<div class="mb-6 rounded-lg bg-white p-6 text-center shadow-lg">
<h1 class="flex items-center justify-center gap-2 text-3xl font-bold text-green-700">
<FontAwesomeIcon icon={faBus} />
{$t('schedule_for_stop.stop_details')}
</h1>
<p class="mt-2 flex items-center justify-center gap-2 text-lg text-gray-700">
<FontAwesomeIcon icon={faMapMarkerAlt} />
<strong>{$t('schedule_for_stop.stop_name')}:</strong>
{stopName} | <strong>{$t('schedule_for_stop.stop_id')}:</strong>
{stopId}
</p>
<p class="mt-2 flex items-center justify-center gap-2 text-lg text-gray-700">
<FontAwesomeIcon icon={faArrowRight} />
<strong>{$t('schedule_for_stop.direction')}:</strong>
{stopDirection}
</p>
<div class="mt-4 flex justify-center">
<a
href={`/stops/${stopId}`}
class="flex items-center justify-center gap-2 rounded-lg bg-green-500 px-3 py-1 text-white shadow hover:bg-green-600"
>
<FontAwesomeIcon icon={faMapMarkerAlt} />
{$t('schedule_for_stop.click_for_realtime')}
</a>
</div>
</div>
11 changes: 10 additions & 1 deletion src/components/stops/StopPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,21 @@
{#if arrivalsAndDepartures}
<div class="space-y-4">
<div>
<div class="flex flex-col gap-y-1 rounded-lg bg-[#1C1C1E] bg-opacity-80 p-4">
<div class="relative flex flex-col gap-y-1 rounded-lg bg-[#1C1C1E] bg-opacity-80 p-4">
<h1 class="h1 mb-0 text-white">{stop.name}</h1>
<h2 class="h2 mb-0 text-white">{$t('stop')} #{stop.id}</h2>
{#if routeShortNames()}
<h2 class="h2 mb-0 text-white">{$t('routes')}: {routeShortNames().join(', ')}</h2>
{/if}
<div class="mt-auto flex justify-end">
<a
href={`/stops/${stop.id}/schedule`}
class="inline-block rounded-lg border border-green-500 bg-green-500 px-3 py-1 text-sm font-medium text-white shadow-md transition duration-200 ease-in-out hover:bg-green-600"
target="_blank"
>
{$t('schedule_for_stop.view_schedule')}
</a>
</div>
</div>
</div>
{#if arrivalsAndDepartures.arrivalsAndDepartures.length === 0}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@ export function formatTime(dateString) {
hour12: true
});
}

export function convertUnixToTime(seconds) {
if (!seconds) return '';
const date = new Date(seconds * 1000);
const utcDate = new Date(date.toUTCString().slice(0, -4));
return utcDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
30 changes: 18 additions & 12 deletions src/lib/i18n.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { init, register, getLocaleFromNavigator } from 'svelte-i18n';

register('en', () => import('./../locales/en.json')); // English
register('es', () => import('./../locales/es.json')); // Spanish
register('pl', () => import('./../locales/pl.json')); // Polish
register('vi', () => import('./../locales/vi.json')); // Vietnamese
register('tl', () => import('./../locales/tl.json')); // Tagalog
register('so', () => import('./../locales/so.json')); // Somali
register('am', () => import('./../locales/am.json')); // Amharic
register('ar', () => import('./../locales/ar.json')); // Arabic
async function setup() {
register('en', () => import('./../locales/en.json')); // English
register('es', () => import('./../locales/es.json')); // Spanish
register('pl', () => import('./../locales/pl.json')); // Polish
register('vi', () => import('./../locales/vi.json')); // Vietnamese
register('tl', () => import('./../locales/tl.json')); // Tagalog
register('so', () => import('./../locales/so.json')); // Somali
register('am', () => import('./../locales/am.json')); // Amharic
register('ar', () => import('./../locales/ar.json')); // Arabic

init({
fallbackLocale: 'en',
initialLocale: getLocaleFromNavigator()
});
return await Promise.allSettled([
init({
fallbackLocale: 'en',
initialLocale: getLocaleFromNavigator()
})
]);
}

await setup();
18 changes: 18 additions & 0 deletions src/locales/am.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "የተሻሻለ ውሂብ",
"no_real_time_data": "ለዚህ ተሽከርካሪ በእሁድኛ ጊዜ ውሂብ አናስተውላም።",
"next_stop": "ቀጣይ ማቆሚያ:"
},
"schedule_for_stop": {
"view_schedule": "ጊዜ ሰንጠረዥን ይመልከቱ",
"no_am_schedules_available": "ምንም AM ጊዜ ሰንጠረዦች አልተገኙም",
"no_pm_schedules_available": "ምንም PM ጊዜ ሰንጠረዦች አልተገኙም",
"minutes": "ደቂቃዎች",
"hour": "ሰዓት",
"selected_date": "የተመረጠ ቀን",
"show_all_routes": "ሁሉንም መንገዶች አሳይ",
"collapse_all_routes": "ሁሉንም መንገዶች አጠፋ",
"route_schedules": "የመንገድ ጊዜ ሰንጠረዦች",
"no_schedules_available": "ምንም ጊዜ ሰንጠረዦች ለተመረጠው ቀን አልተገኙም።",
"stop_details": "የማቆሚያ ዝርዝሮች",
"select_date": "ቀን ይምረጡ",
"stop_id": "የማቆሚያ መታወቂያ",
"direction": "አቅጣጫ",
"click_for_realtime": "ለትክክለኛ ጊዜ መረጃ እዚህ ጠቅ ያድርጉ",
"stop_name": "የማቆሚያ ስም"
}
}
18 changes: 18 additions & 0 deletions src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "تم تحديث البيانات",
"no_real_time_data": "لا توجد بيانات في الوقت الحقيقي لهذه المركبة الآن.",
"next_stop": "المحطة التالية:"
},
"schedule_for_stop": {
"view_schedule": "عرض الجدول",
"no_am_schedules_available": "لا توجد جداول صباحية متاحة",
"no_pm_schedules_available": "لا توجد جداول مسائية متاحة",
"minutes": "دقائق",
"hour": "ساعة",
"selected_date": "التاريخ المحدد",
"show_all_routes": "عرض جميع المسارات",
"collapse_all_routes": "إخفاء جميع المسارات",
"route_schedules": "جداول المسارات",
"no_schedules_available": "لا توجد جداول متاحة للتاريخ المحدد.",
"stop_details": "تفاصيل المحطة",
"select_date": "اختر التاريخ",
"stop_id": "رقم المحطة",
"direction": "الاتجاه",
"click_for_realtime": "انقر هنا لمعلومات الوقت الفعلي",
"stop_name": "اسم المحطة"
}
}
18 changes: 18 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "Data updated",
"no_real_time_data": "We don't have real-time data for this vehicle now.",
"next_stop": "Next stop:"
},
"schedule_for_stop": {
"view_schedule": "View Schedule",
"no_am_schedules_available": "No AM schedules available",
"no_pm_schedules_available": "No PM schedules available",
"minutes": "Minutes",
"hour": "Hour",
"selected_date": "Selected date",
"show_all_routes": "Show All Routes",
"collapse_all_routes": "Collapse All Routes",
"route_schedules": "Route Schedules",
"no_schedules_available": "No schedules available for the selected date.",
"stop_details": "Stop Details",
"select_date": "Select Date",
"stop_id": "Stop ID",
"direction": "Direction",
"click_for_realtime": "Click here for real-time info",
"stop_name": "Stop Name"
}
}
18 changes: 18 additions & 0 deletions src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "Datos actualizados",
"no_real_time_data": "Actualmente no tenemos datos en tiempo real para este vehículo.",
"next_stop": "Próxima parada:"
},
"schedule_for_stop": {
"view_schedule": "Ver horario",
"no_am_schedules_available": "No hay horarios AM disponibles",
"no_pm_schedules_available": "No hay horarios PM disponibles",
"minutes": "Minutos",
"hour": "Hora",
"selected_date": "Fecha seleccionada",
"show_all_routes": "Mostrar todas las rutas",
"collapse_all_routes": "Colapsar todas las rutas",
"route_schedules": "Horarios de rutas",
"no_schedules_available": "No hay horarios disponibles para la fecha seleccionada.",
"stop_details": "Detalles de la parada",
"select_date": "Seleccionar fecha",
"stop_id": "ID de la parada",
"direction": "Dirección",
"click_for_realtime": "Haga clic aquí para información en tiempo real",
"stop_name": "Nombre de la parada"
}
}
18 changes: 18 additions & 0 deletions src/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "Dane zaktualizowane",
"no_real_time_data": "Obecnie nie mamy danych w czasie rzeczywistym dla tego pojazdu.",
"next_stop": "Następny przystanek:"
},
"schedule_for_stop": {
"view_schedule": "Zobacz rozkład",
"no_am_schedules_available": "Brak dostępnych rozkładów AM",
"no_pm_schedules_available": "Brak dostępnych rozkładów PM",
"minutes": "Minuty",
"hour": "Godzina",
"selected_date": "Wybrana data",
"show_all_routes": "Pokaż wszystkie trasy",
"collapse_all_routes": "Zwiń wszystkie trasy",
"route_schedules": "Rozkłady tras",
"no_schedules_available": "Brak dostępnych rozkładów na wybraną datę.",
"stop_details": "Szczegóły przystanku",
"select_date": "Wybierz datę",
"stop_id": "ID przystanku",
"direction": "Kierunek",
"click_for_realtime": "Kliknij tutaj, aby zobaczyć informacje w czasie rzeczywistym",
"stop_name": "Nazwa przystanku"
}
}
Loading
Loading