Skip to content

Commit

Permalink
Feat/view-all-routes (#70)
Browse files Browse the repository at this point in the history
Fixes #64 

* feat: Add API endpoint to fetch all routes
* feat: Add LoadingSpinner component
* feat: Add RouteItem component
* feat: Add routesStore and dataFetched for caching and store the routes data
* feat: Add ViewAllRoutesModal component and functionality
* feat: Add new translations
* refactor: Update SearchPane component

- Adjust the zoom level when panning the map
- Add event listener for routeSelectedFromModal event
- Update the layout and styling of the component
- Add a button to view all available routes
- Add translations for search related texts

* refactor: Update StopPane component with LoadingSpinner
* feat: Add ViewAllRoutesModal component and functionality
* refactor: Add src path
* refactor: Add routes data preloading
* refactor: Update routes GET endpoint to use the preload routes
* refactor: Remove unused routesStore and dataFetched variables
* refactor: Add "all_routes" translation to language files
* refactor: route fetching logic
* refactor: display color logic
* chore: linting and formatting
  • Loading branch information
Ahmedhossamdev authored Oct 29, 2024
1 parent f96bd48 commit 4f50414
Show file tree
Hide file tree
Showing 16 changed files with 349 additions and 39 deletions.
25 changes: 25 additions & 0 deletions src/components/LoadingSpinner.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<script>
import { t } from 'svelte-i18n';
</script>

<div
class="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-[#1C1C1E] bg-opacity-80"
>
<div class="flex items-center text-white">
<svg
class="-ml-1 mr-3 h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{$t('loading')}...
</div>
</div>
29 changes: 29 additions & 0 deletions src/components/RouteItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<script>
import { createEventDispatcher } from 'svelte';
export let route;
const dispatch = createEventDispatcher();
function handleClick() {
dispatch('routeClick', { route });
}
function getDisplayRouteName() {
if (route.shortName && route.longName) {
return `${route.shortName} - ${route.longName}`;
} else if (route.shortName && route.description) {
return `${route.shortName} - ${route.description}`;
} else if (!route.shortName && (route.longName || route.description)) {
return `${route.agencyInfo.name} - ${route.longName || route.description}`;
}
}
</script>

<button
type="button"
class="route-item flex w-full items-center justify-between border-b border-gray-200 bg-[#f9f9f9] p-4 text-left hover:bg-[#e9e9e9] focus:outline-none dark:border-[#313135] dark:bg-[#1c1c1c] dark:text-white dark:hover:bg-[#363636]"
on:click={handleClick}
>
<div class="text-lg font-semibold" style="color: #{route.color}">
{getDisplayRouteName(route)}
</div>
</button>
130 changes: 130 additions & 0 deletions src/components/navigation/ViewAllRoutesModal.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
<script>
import LoadingSpinner from '$components/LoadingSpinner.svelte';
import RouteItem from '$components/RouteItem.svelte';
import { onMount } from 'svelte';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n';
let routes = [];
let filteredRoutes = [];
let query = '';
let loading = false;
const dispatch = createEventDispatcher();
onMount(async () => {
await fetchRoutes();
});
async function fetchRoutes() {
try {
loading = true;
const response = await fetch('/api/oba/routes');
const data = await response.json();
if (response.ok) {
routes = data.routes;
filterRoutes();
} else {
console.error('Failed to fetch routes:', data.error);
routes = [];
filteredRoutes = [];
}
} catch (error) {
console.error('Error fetching routes:', error);
routes = [];
filteredRoutes = [];
} finally {
loading = false;
}
}
async function handleSearch(event) {
query = event.target.value;
filterRoutes();
}
function handleRouteClick(event) {
const { route } = event.detail;
dispatch('routeSelected', { route });
}
function filterRoutes() {
const lowerCaseQuery = query.toLowerCase();
filteredRoutes = routes.filter((route) => {
const shortName = route.shortName?.toLowerCase();
const longNameOrDescription = (route.longName || route.description || '').toLowerCase();
const agencyName = route.agencyInfo?.name?.toLowerCase();
return (
shortName?.includes(lowerCaseQuery) ||
longNameOrDescription.includes(lowerCaseQuery) ||
agencyName?.includes(lowerCaseQuery)
);
});
}
</script>
<div>
{#if loading}
<LoadingSpinner />
{/if}
{#if routes.length > 0}
<div class="h-25 rounded-lg bg-[#1C1C1E] bg-opacity-80 p-4">
<h1 class="mb-6 text-center text-2xl font-bold text-white">{$t('search.all_routes')}</h1>
</div>
<div class="p-4">
<div class="relative mb-4">
<input
type="text"
placeholder={$t('search.search_for_routes')}
class="w-full rounded-lg border border-gray-300 p-2 pl-10 text-gray-700 placeholder-gray-500 dark:border-gray-700 dark:text-gray-900 dark:placeholder-gray-900"
bind:value={query}
on:input={handleSearch}
/>
<svg
class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform text-gray-500 dark:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
</div>
<div class="scrollbar-hidden fixed-height relative mt-4 max-h-96 overflow-y-auto">
{#if filteredRoutes.length > 0}
{#each filteredRoutes as route}
<RouteItem {route} on:routeClick={handleRouteClick} />
{/each}
{:else}
<div class="flex h-full items-center justify-center text-gray-400 dark:text-gray-500">
{$t('search.no_routes_found')}
</div>
{/if}
</div>
</div>
{/if}
</div>
<style>
.scrollbar-hidden {
scrollbar-width: none;
-ms-overflow-style: none;
}
.scrollbar-hidden::-webkit-scrollbar {
display: none;
}
.fixed-height {
height: 500px;
}
</style>
25 changes: 3 additions & 22 deletions src/components/oba/StopPane.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script>
import ArrivalDeparture from '../ArrivalDeparture.svelte';
import { onMount } from 'svelte';
import TripDetailsModal from '../navigation/TripDetailsModal.svelte';
import LoadingSpinner from '$components/LoadingSpinner.svelte';
import { onMount } from 'svelte';
import { createEventDispatcher } from 'svelte';
import '$lib/i18n.js';
Expand Down Expand Up @@ -91,27 +92,7 @@

<div>
{#if loading}
<div
class="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-[#1C1C1E] bg-opacity-80"
>
<div class="flex items-center text-white">
<svg
class="-ml-1 mr-3 h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{$t('loading')}...
</div>
</div>
<LoadingSpinner />
{/if}

{#if error}
Expand Down
35 changes: 26 additions & 9 deletions src/components/search/SearchPane.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script>
import SearchField from '$components/search/SearchField.svelte';
import SearchResultItem from '$components/search/SearchResultItem.svelte';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, onMount } from 'svelte';
import { compassDirection } from '$lib/formatters';
import { prioritizedRouteTypeForDisplay } from '$config/routeConfig';
import { faMapPin, faSignsPost } from '@fortawesome/free-solid-svg-icons';
Expand Down Expand Up @@ -45,30 +45,24 @@
clearResults();
const response = await fetch(`/api/oba/stops-for-route/${route.id}`);
const stopsForRoute = await response.json();
const stops = stopsForRoute.data.references.stops;
const polylinesData = stopsForRoute.data.entry.polylines;
for (const polylineData of polylinesData) {
const shape = polylineData.points;
let polyline;
polyline = mapProvider.createPolyline(shape);
polylines.push(polyline);
}
await showStopsOnRoute(stops);
currentIntervalId = await fetchAndUpdateVehicles(route.id, mapProvider);
const midpoint = calculateMidpoint(stopsForRoute.data.references.stops);
mapProvider.panTo(midpoint.lat, midpoint.lng);
mapProvider.setZoom(13);
mapProvider.setZoom(12);
dispatch('routeSelected', { route, stopsForRoute, stops, polylines, currentIntervalId });
}
Expand All @@ -86,6 +80,10 @@
query = results.detail.query;
}
function handleViewAllRoutes() {
dispatch('viewAllRoutes');
}
function clearResults() {
if (polylines) {
dispatch('clearResults', polylines);
Expand All @@ -99,12 +97,18 @@
mapProvider.clearVehicleMarkers();
clearInterval(currentIntervalId);
}
onMount(() => {
window.addEventListener('routeSelectedFromModal', (event) => {
handleRouteClick(event.detail.route);
});
});
</script>

<div
class="bg-blur-sm flex w-96 justify-between rounded-lg border-gray-500 bg-white/90 px-4 shadow-lg dark:bg-black dark:text-white dark:shadow-lg dark:shadow-gray-200/10"
>
<div class="flex w-full flex-col gap-y-4 py-4">
<div class="flex w-full flex-col gap-y-2 py-4">
<SearchField value={query} on:searchResults={handleSearchResults} />

{#if query}
Expand Down Expand Up @@ -148,5 +152,18 @@
{/each}
{/if}
</div>

<div class="mt-0 sm:mt-0">
<button
type="button"
class="text-sm font-medium text-green-600 underline hover:text-green-400 focus:outline-none"
on:click={handleViewAllRoutes}
>
{$t('search.click_here')}
</button>
<span class="text-sm font-medium text-black dark:text-white">
{$t('search.for_a_list_of_available_routes')}</span
>
</div>
</div>
</div>
47 changes: 47 additions & 0 deletions src/hooks.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import oba from '$lib/obaSdk.js';

let routesCache = null;

async function fetchRoutesData() {
try {
const agenciesResponse = await oba.agenciesWithCoverage.list();
const agencies = agenciesResponse.data.list;

const routesPromises = agencies.map(async (agency) => {
const routesResponse = await oba.routesForAgency.list(agency.agencyId);
const routes = routesResponse.data.list;
const references = routesResponse.data.references;

const agencyReferenceMap = new Map(references.agencies.map((agency) => [agency.id, agency]));

routes.forEach((route) => {
route.agencyInfo = agencyReferenceMap.get(route.agencyId);
});

return routes;
});

const routes = await Promise.all(routesPromises);
return routes.flat();
} catch (error) {
console.error('Error fetching routes:', error);
return null;
}
}

async function preloadRoutesData() {
if (!routesCache) {
routesCache = await fetchRoutesData();
}
}

preloadRoutesData();

export async function handle({ event, resolve }) {
await preloadRoutesData();
return resolve(event);
}

export function getRoutesCache() {
return routesCache;
}
7 changes: 6 additions & 1 deletion src/locales/am.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
"placeholder": "ቆሻሻዎችን፣ መንገዶችን እና ቦታዎችን ይፈልጉ",
"results_for": "የፍለጋ ውጤቶች ለ",
"clear_results": "ውጤቶችን አጥፋ",
"search": "ፈልግ"
"search": "ፈልግ",
"click_here": "እዚህ ጠቅ ያድርጉ",
"for_a_list_of_available_routes": "ለመገኘት የሚገባቸው መንገዶች",
"search_for_routes": "መንገዶችን ይፈልጉ...",
"no_routes_found": "ምንም መንገዶች አልተገኙም",
"all_routes": "ሁሉም መንገዶች"
},
"header": {
"home": "ቤት",
Expand Down
7 changes: 6 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
"placeholder": "Search for stops, routes, and places",
"results_for": "Search results for",
"clear_results": "Clear results",
"search": "Search"
"search": "Search",
"click_here": "Click here",
"for_a_list_of_available_routes": "for a list of available routes",
"search_for_routes": "Search for routes...",
"no_routes_found": "No routes found",
"all_routes": "All Routes"
},
"header": {
"home": "Home",
Expand Down
Loading

0 comments on commit 4f50414

Please sign in to comment.