Skip to content

Commit 4220475

Browse files
Merge pull request #194 from OneBusAway/feat/service-alerts
Feat/service-alerts
2 parents b720e87 + fcf7b1a commit 4220475

17 files changed

+361
-1
lines changed

package-lock.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@sveltejs/adapter-node": "^5.2.9",
2323
"@sveltejs/kit": "^2.5.27",
2424
"@sveltejs/vite-plugin-svelte": "^4.0.0",
25+
"@tailwindcss/line-clamp": "^0.4.4",
2526
"@types/eslint": "^8.56.7",
2627
"@vitest/coverage-v8": "^2.1.8",
2728
"autoprefixer": "^10.4.19",

src/components/navigation/ModalPane.svelte

-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
44
import { faX } from '@fortawesome/free-solid-svg-icons';
55
import { keybinding } from '$lib/keybinding';
6-
76
/**
87
* @typedef {Object} Props
98
* @property {string} [title]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<script>
2+
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
3+
import { faCircleExclamation, faChevronRight } from '@fortawesome/free-solid-svg-icons';
4+
5+
let { alert = $bindable({}), openModal } = $props();
6+
</script>
7+
8+
<div
9+
class="flex cursor-pointer items-start gap-3 rounded-lg p-1 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
10+
role="button"
11+
tabindex="0"
12+
onclick={() => openModal(alert)}
13+
onkeydown={(e) => e.key === 'Enter'}
14+
>
15+
<div class="mt-2 flex-shrink">
16+
<FontAwesomeIcon
17+
icon={faCircleExclamation}
18+
class="text-green-500"
19+
style="width: 1.4rem; height: 1.4rem; fill: none; stroke: currentColor; stroke-width: 1.5;"
20+
/>
21+
</div>
22+
<div class="flex-1">
23+
<h4 class="line-clamp-3 font-medium text-gray-900 dark:text-white">{alert.summary.value}</h4>
24+
<p class="mt-1 line-clamp-3 text-sm text-gray-500 dark:text-gray-300">
25+
{alert?.description?.value}
26+
</p>
27+
</div>
28+
<div class="ml-2 flex-shrink-0">
29+
<FontAwesomeIcon icon={faChevronRight} class="h-5 w-5 text-gray-400" />
30+
</div>
31+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<script>
2+
import { modalOpen } from '$src/stores/modalOpen';
3+
import {
4+
faChevronLeft,
5+
faChevronRight as faChevronRightPagination
6+
} from '@fortawesome/free-solid-svg-icons';
7+
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
8+
import { Modal } from 'flowbite-svelte';
9+
import ServiceAlertItem from './ServiceAlertItem.svelte';
10+
import { t } from 'svelte-i18n';
11+
12+
let { serviceAlerts = $bindable([]) } = $props();
13+
14+
let modalAlert = $state(null);
15+
let isAlertsHidden = $state(false);
16+
let currentPage = $state(1);
17+
const alertsPerPage = 3;
18+
19+
let totalPages = $derived(Math.ceil(serviceAlerts.length / alertsPerPage));
20+
21+
let paginatedAlerts = $derived(
22+
serviceAlerts.slice((currentPage - 1) * alertsPerPage, currentPage * alertsPerPage)
23+
);
24+
25+
function openModal(alert) {
26+
modalAlert = alert;
27+
modalOpen.set(true);
28+
console.debug('modal opened');
29+
}
30+
31+
function closeModal() {
32+
modalOpen.set(false);
33+
console.debug('modal closed');
34+
}
35+
36+
function toggleAlerts() {
37+
if (isAlertsHidden) {
38+
isAlertsHidden = false;
39+
currentPage = 1;
40+
} else {
41+
isAlertsHidden = true;
42+
}
43+
}
44+
45+
function goToPage(page) {
46+
currentPage = Math.max(1, Math.min(page, totalPages));
47+
isAlertsHidden = false;
48+
}
49+
50+
function handleKeydown(event) {
51+
if (event.key === 'Escape') {
52+
event.stopPropagation();
53+
event.preventDefault();
54+
closeModal();
55+
}
56+
}
57+
</script>
58+
59+
{#if serviceAlerts.length > 0}
60+
<div class="relative flex flex-col gap-y-1 rounded-lg bg-white p-4 dark:bg-gray-800">
61+
<div class="mb-2 flex items-center justify-between">
62+
<h3 class="font-medium text-gray-700 dark:text-white">
63+
{$t('service_alerts.service_alerts')} ({serviceAlerts.length})
64+
</h3>
65+
<button
66+
class="text-sm font-medium text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-500"
67+
onclick={toggleAlerts}
68+
>
69+
{isAlertsHidden ? $t('service_alerts.show') : $t('service_alerts.hide')}
70+
</button>
71+
</div>
72+
73+
{#if !isAlertsHidden}
74+
<div class="space-y-2">
75+
{#each paginatedAlerts as alert}
76+
<ServiceAlertItem {alert} {openModal} />
77+
{/each}
78+
</div>
79+
80+
{#if totalPages > 1}
81+
<div class="mt-3 flex items-center justify-center gap-4">
82+
<button
83+
class="p-1 text-gray-500 hover:text-gray-700 disabled:opacity-50 dark:text-gray-400 dark:hover:text-gray-200"
84+
onclick={() => goToPage(currentPage - 1)}
85+
disabled={currentPage === 1}
86+
>
87+
<FontAwesomeIcon icon={faChevronLeft} class="h-4 w-4" />
88+
</button>
89+
<span class="text-sm text-gray-700 dark:text-gray-300">
90+
{$t('service_alerts.page')}
91+
{currentPage} of {totalPages}
92+
</span>
93+
<button
94+
class="p-1 text-gray-500 hover:text-gray-700 disabled:opacity-50 dark:text-gray-400 dark:hover:text-gray-200"
95+
onclick={() => goToPage(currentPage + 1)}
96+
disabled={currentPage === totalPages}
97+
>
98+
<FontAwesomeIcon icon={faChevronRightPagination} class="h-4 w-4" />
99+
</button>
100+
</div>
101+
{/if}
102+
{/if}
103+
</div>
104+
{/if}
105+
106+
{#if $modalOpen && modalAlert}
107+
<div class="center" onkeydown={handleKeydown} role="button" tabindex="0">
108+
<Modal
109+
outsideclose={true}
110+
title={modalAlert?.summary?.value}
111+
bind:open={$modalOpen}
112+
size="3xl"
113+
class="relative w-full max-w-3xl rounded-xl bg-white p-8 text-gray-900 shadow-2xl dark:bg-gray-800 dark:text-gray-100"
114+
>
115+
<p class="mt-3 text-base leading-relaxed text-gray-800 dark:text-gray-200">
116+
{modalAlert?.description?.value}
117+
</p>
118+
</Modal>
119+
</div>
120+
{/if}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export function filterActiveAlerts(situations) {
2+
const now = Date.now();
3+
return situations.filter((situation) =>
4+
situation.activeWindows.some((window) => {
5+
const from = normalizeTimestamp(window.from) || 0;
6+
// If no end date provided, default to Infinity.
7+
const to = window.to ? normalizeTimestamp(window.to) : Infinity;
8+
return now >= from && now <= to;
9+
})
10+
);
11+
}
12+
13+
/**
14+
* Normalizes a timestamp value.
15+
* If the difference between now and the timestamp is smaller when interpreted as milliseconds,
16+
* then it's assumed to be in milliseconds; otherwise, it's in seconds and converted to milliseconds.
17+
*
18+
* @param {number|null|undefined} time - The timestamp to normalize.
19+
* @returns {number} Normalized timestamp in milliseconds.
20+
*/
21+
export function normalizeTimestamp(time) {
22+
if (!time) return 0;
23+
const dtMilliseconds = new Date(time);
24+
const diffMilliseconds = Math.abs(Date.now() - dtMilliseconds.getTime());
25+
const dtSeconds = new Date(time * 1000);
26+
const diffSeconds = Math.abs(Date.now() - dtSeconds.getTime());
27+
return diffMilliseconds < diffSeconds ? time : time * 1000;
28+
}

src/components/stops/StopPane.svelte

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import Accordion from '$components/containers/SingleSelectAccordion.svelte';
66
import AccordionItem from '$components/containers/AccordionItem.svelte';
77
import SurveyModal from '$components/surveys/SurveyModal.svelte';
8+
import ServiceAlerts from '$components/service-alerts/ServiceAlerts.svelte';
89
import { onDestroy } from 'svelte';
910
import '$lib/i18n.js';
1011
import { isLoading, t } from 'svelte-i18n';
@@ -13,6 +14,7 @@
1314
import { getUserId } from '$lib/utils/user';
1415
import HeroQuestion from '$components/surveys/HeroQuestion.svelte';
1516
import analytics from '$lib/Analytics/PlausibleAnalytics';
17+
import { filterActiveAlerts } from '$components/service-alerts/serviceAlertsHelper';
1618
1719
/**
1820
* @typedef {Object} Props
@@ -31,6 +33,7 @@
3133
let arrivalsAndDepartures = $state();
3234
let loading = $state(false);
3335
let error = $state();
36+
let serviceAlerts = $state([]);
3437
3538
let interval = null;
3639
let currentStopSurvey = $state(null);
@@ -42,6 +45,8 @@
4245
if (response.ok) {
4346
arrivalsAndDeparturesResponse = await response.json();
4447
arrivalsAndDepartures = arrivalsAndDeparturesResponse.data.entry;
48+
let situations = arrivalsAndDeparturesResponse.data.references.situations || [];
49+
serviceAlerts = filterActiveAlerts(situations);
4550
} else {
4651
error = 'Unable to fetch arrival/departure data';
4752
}
@@ -172,6 +177,11 @@
172177
</div>
173178
</div>
174179
</div>
180+
181+
{#if serviceAlerts}
182+
<ServiceAlerts bind:serviceAlerts />
183+
{/if}
184+
175185
{#if showHeroQuestion && currentStopSurvey}
176186
<HeroQuestion {currentStopSurvey} {handleSkip} {handleNext} {handleHeroQuestionChange} />
177187
{/if}

src/locales/am.json

+8
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,13 @@
100100
"SW": "ደቡብ ምዕራብ",
101101
"W": "ምዕራብ",
102102
"NW": "ሰሜን ምዕራብ"
103+
},
104+
"service_alerts": {
105+
"more_info": "ተጨማሪ መረጃ",
106+
"close": "ዝጋ",
107+
"service_alerts": "አገልግሎት ማስጠንቀቂያዎች",
108+
"page": "ገፅ",
109+
"show": "አሳይ",
110+
"hide": "ደብቅ"
103111
}
104112
}

src/locales/ar.json

+8
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,13 @@
100100
"SW": "جنوب غرب",
101101
"W": "غرب",
102102
"NW": "شمال غرب"
103+
},
104+
"service_alerts": {
105+
"more_info": "مزيد من المعلومات",
106+
"close": "إغلاق",
107+
"service_alerts": "تنبيهات الخدمة",
108+
"page": "صفحة",
109+
"show": "عرض",
110+
"hide": "إخفاء"
103111
}
104112
}

src/locales/en.json

+8
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,13 @@
100100
"SW": "Southwest",
101101
"W": "West",
102102
"NW": "Northwest"
103+
},
104+
"service_alerts": {
105+
"more_info": "More info",
106+
"close": "Close",
107+
"service_alerts": "Service Alerts",
108+
"page": "Page",
109+
"show": "Show",
110+
"hide": "Hide"
103111
}
104112
}

src/locales/es.json

+8
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,13 @@
101101
"SW": "Suroeste",
102102
"W": "Oeste",
103103
"NW": "Noroeste"
104+
},
105+
"service_alerts": {
106+
"more_info": "Más información",
107+
"close": "Cerrar",
108+
"service_alerts": "Alertas de servicio",
109+
"page": "Página",
110+
"show": "Mostrar",
111+
"hide": "Ocultar"
104112
}
105113
}

src/locales/pl.json

+8
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,13 @@
100100
"SW": "Południowy zachód",
101101
"W": "Zachód",
102102
"NW": "Północny zachód"
103+
},
104+
"service_alerts": {
105+
"more_info": "Więcej informacji",
106+
"close": "Zamknij",
107+
"service_alerts": "Alerty serwisowe",
108+
"page": "Strona",
109+
"show": "Pokaż",
110+
"hide": "Ukryj"
103111
}
104112
}

src/locales/so.json

+8
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,13 @@
100100
"SW": "Koonfur Galbeed",
101101
"W": "Galbeed",
102102
"NW": "Waqooyi Galbeed"
103+
},
104+
"service_alerts": {
105+
"more_info": "Macluumaad dheeri ah",
106+
"close": "Xidho",
107+
"service_alerts": "Ogeysiisyada adeegga",
108+
"page": "Bogga",
109+
"show": "Muujin",
110+
"hide": "Qari"
103111
}
104112
}

src/locales/tl.json

+8
Original file line numberDiff line numberDiff line change
@@ -100,5 +100,13 @@
100100
"SW": "Timog-kanluran",
101101
"W": "Kanluran",
102102
"NW": "Hilagang-kanluran"
103+
},
104+
"service_alerts": {
105+
"more_info": "Karagdagang impormasyon",
106+
"close": "Isara",
107+
"service_alerts": "Mga abiso ng serbisyo",
108+
"page": "Pahina",
109+
"show": "Ipakita",
110+
"hide": "Itago"
103111
}
104112
}

src/locales/vi.json

+8
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,13 @@
9999
"SW": "Tây Nam",
100100
"W": "Tây",
101101
"NW": "Tây Bắc"
102+
},
103+
"service_alerts": {
104+
"more_info": "Thêm thông tin",
105+
"close": "Đóng",
106+
"service_alerts": "Thông báo dịch vụ",
107+
"page": "Trang",
108+
"show": "Hiển thị",
109+
"hide": "Ẩn"
102110
}
103111
}

src/stores/modalOpen.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { writable } from 'svelte/store';
2+
3+
export const modalOpen = writable(false);

0 commit comments

Comments
 (0)