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

Feat/view-all-routes #70

Merged
merged 19 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fd27d95
feat: Add API endpoint to fetch all routes
Ahmedhossamdev Oct 28, 2024
ad68ad1
feat: Add LoadingSpinner component
Ahmedhossamdev Oct 28, 2024
d55768d
feat: Add RouteItem component
Ahmedhossamdev Oct 28, 2024
3594766
feat: Add routesStore and dataFetched for caching and store the route…
Ahmedhossamdev Oct 28, 2024
2d1aa2e
feat: Add ViewAllRoutesModal component and functionality
Ahmedhossamdev Oct 28, 2024
794bcf0
refactor: Update LoadingSpinner component with new SVG path
Ahmedhossamdev Oct 28, 2024
d2d0e81
feat: Add new translations
Ahmedhossamdev Oct 28, 2024
c8277f2
refactor: Update SearchPane component
Ahmedhossamdev Oct 28, 2024
3caf2a7
refactor: Update StopPane component with LoadingSpinner
Ahmedhossamdev Oct 28, 2024
cd18aa8
feat: Add ViewAllRoutesModal component and functionality
Ahmedhossamdev Oct 28, 2024
edc270c
refactor: Add src path
Ahmedhossamdev Oct 29, 2024
3d7ab95
refactor: Add routes data preloading
Ahmedhossamdev Oct 29, 2024
ed38e70
refactor: Update routes GET endpoint to use the preload routes
Ahmedhossamdev Oct 29, 2024
a0911e1
refactor: Remove unused routesStore and dataFetched variables
Ahmedhossamdev Oct 29, 2024
6d31c35
refactor: Add "all_routes" translation to language files
Ahmedhossamdev Oct 29, 2024
30d568a
refactor: route fetching logic
Ahmedhossamdev Oct 29, 2024
d83433f
refactor: display color logic
Ahmedhossamdev Oct 29, 2024
fb58f6f
chore: linting and formatting
Ahmedhossamdev Oct 29, 2024
7622e76
remove: unused getTextColor function from RouteItem.svelte
Ahmedhossamdev Oct 29, 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
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 />
Copy link
Member

Choose a reason for hiding this comment

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

👍

Copy link
Member Author

Choose a reason for hiding this comment

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

Hi @aaronbrethorst

The search bar is working but before I PR, I also added search by rotueId, I think I will remove it from the search filter so we can search by short and long name and description only

{/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) {
Copy link
Member

Choose a reason for hiding this comment

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

🔥 🔥 🔥

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
Loading