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

basic service alerts #158

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
572 changes: 537 additions & 35 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
"@fluentui/react-components": "^9.58.2",
"@fluentui/react-icons": "^2.0.274",
"@reduxjs/toolkit": "^2.5.1",
"@tanstack/react-query": "^5.64.2",
"date-fns": "^4.1.0",
"gtfs-realtime-bindings": "^1.1.1",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^8.0.2",
"idb": "^8.0.1",
Expand Down
43 changes: 24 additions & 19 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Link as LinkFluent, Title1 } from "@fluentui/react-components";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// eslint-disable-next-line no-unused-vars
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, Outlet } from "react-router-dom";

import "./App.css";
import { NavBar } from "./components/nav/NavBar";
import { NavBar } from "./components/nav/NavBar.js";

function App() {
const queryClient = new QueryClient();

const [width, setWidth] = useState(window.innerWidth);
const { t } = useTranslation();

Expand All @@ -21,24 +24,26 @@ function App() {
}, []);

return (
<div className="container">
<header className="nav-bar">
<Link
className="router-link"
to={"/"}
title={t("home.title.tooltip") ?? ""}
>
<LinkFluent>
<Title1 className="app-title text-xl font-bold">
{t("home.title.name")}
</Title1>
</LinkFluent>
</Link>
{width >= 800 && <NavBar width={width} />}
</header>
<Outlet />
{width < 800 && <NavBar width={width} />}
</div>
<QueryClientProvider client={queryClient}>
<div className="container">
<header className="nav-bar">
<Link
className="router-link"
to={"/"}
title={t("home.title.tooltip") ?? ""}
>
<LinkFluent>
<Title1 className="app-title text-xl font-bold">
{t("home.title.name")}
</Title1>
</LinkFluent>
</Link>
{width >= 800 && <NavBar width={width} />}
</header>
<Outlet />
{width < 800 && <NavBar width={width} />}
</div>
</QueryClientProvider>
);
}

Expand Down
27 changes: 27 additions & 0 deletions src/components/alerts/AlertUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { TtcBadge } from "../badges.js";

export const ParsedTtcAlertText = (
badge: { highlightAll?: boolean; line?: string },
feedText: string,
id: string
) => {
const lineNum = parseInt(`${badge.line}`);

const lineFilter = badge.line
? lineNum < 6
? `Line ${lineNum}`
: `${lineNum}`
: badge.highlightAll
? (feedText.match(/\d+/)?.[0] ?? "")
: "";

return badge.line || badge.highlightAll
? feedText
.split(lineFilter)
.flatMap((item) => [
item,
<TtcBadge lineNum={lineFilter} key={`${id}-${lineFilter}`} />,
])
.slice(0, -1)
: feedText;
};
40 changes: 40 additions & 0 deletions src/components/alerts/AlertsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useQuery } from "@tanstack/react-query";

import { gtfsAlerts, ttcAlerts } from "../fetch/queries.js";
import { ParsedTtcAlertText } from "./AlertUtils.js";
import { SkeetList } from "./SkeetList.js";

export default function TtcAlertList() {
const socialMediaQuery = useQuery(ttcAlerts);
const gtfsAlertsResp = useQuery(gtfsAlerts);

return (
<div className="alert-page">
<h1>Recent Service Alerts</h1>
<h2>Current alerts</h2>
{Array.isArray(gtfsAlertsResp.data?.entity) &&
gtfsAlertsResp.data.entity.map((item, index) => (
<p
key={item.alert.headerText.translation[0].text}
id={item.alert.headerText.translation[0].text}
>
{ParsedTtcAlertText(
{ highlightAll: true },
item.alert.descriptionText.translation[0].text,
item.alert.headerText.translation[0].text
)}
</p>
))}
<h2>Recent alerts ({socialMediaQuery.data?.feed.length})</h2>
<p>
Source:{" "}
<a href="https://bsky.app/profile/ttcalerts.bsky.social">
https://bsky.app/profile/ttcalerts.bsky.social
</a>
</p>
{socialMediaQuery.data && (
<SkeetList skeetList={socialMediaQuery.data?.feed} line={"all"} />
)}
</div>
);
}
31 changes: 31 additions & 0 deletions src/components/alerts/Skeet.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { formatDistanceStrict } from "date-fns";

import { ParsedTtcAlertText } from "./AlertUtils.js";

export const SkeetElement = ({
skeet,
badge,
}: {
skeet: {
post: {
cid: string;
record: { text: string; createdAt: string };
};
};
badge: { highlightAll?: boolean; line?: string };
}) => {
const cid = skeet.post.cid;
const feedText = skeet.post.record.text;

const parsedText = ParsedTtcAlertText(badge, feedText, cid);
return (
<li>
<p className="time">
{formatDistanceStrict(skeet.post.record.createdAt, new Date(), {
addSuffix: true,
})}
</p>
<span className="content">{parsedText}</span>
</li>
);
};
5 changes: 5 additions & 0 deletions src/components/alerts/SkeetList.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.skeet-list {
li {
list-style-type: none;
}
}
17 changes: 17 additions & 0 deletions src/components/alerts/SkeetList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { SkeetElement } from "./Skeet.js";
import style from "./SkeetList.module.css";

export const SkeetList = ({
skeetList,
line,
}: {
skeetList: any[];
line?: string;
}) => {
const badgeArg = line === "all" ? { highlightAll: true } : { line };
const dataArray = skeetList.map((skeet) => (
<SkeetElement key={skeet.post.cid} skeet={skeet} badge={badgeArg} />
));

return <ul className={style["skeet-list"]}>{dataArray}</ul>;
};
28 changes: 28 additions & 0 deletions src/components/alerts/TtcAlertList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useQuery } from "@tanstack/react-query";

import { ttcAlerts } from "../fetch/queries.js";
import { SkeetList } from "./SkeetList.js";

export function TtcAlertList({ lineNum }: { lineNum: number[] }) {
const bskyAlerts = useQuery(ttcAlerts);

const filteredBskyAlerts =
bskyAlerts.data?.feed.filter(
(skeet: { post: { record: { text: string } } }) =>
lineNum.some((line) => {
return line < 6
? skeet.post.record.text.match(`Line ${line}`)
: skeet.post.record.text.startsWith(`${line}`);
})
) ?? [];
return (
<>
{filteredBskyAlerts.length > 0 && (
<SkeetList
skeetList={filteredBskyAlerts}
line={lineNum.length > 1 ? "all" : `${lineNum[0]}`}
/>
)}
</>
);
}
32 changes: 21 additions & 11 deletions src/components/etaCard/EtaCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export function EtaCard(props: {
enabled?: string[];
direction?: string;
}) {
const uniqueLines = [...new Set(props.lines)];
const directionArray = props.direction?.split(", ") ?? [];
return (
<li
className={[
Expand All @@ -46,20 +48,26 @@ export function EtaCard(props: {
<>
<div
className={
props.lines.length > 6
uniqueLines.length > 6
? [style["badge-group"], style.overflow].join(" ")
: style["badge-group"]
}
>
{props.lines.map((line: string) => {
return <TtcBadge key={line} lineNum={line} />;
{uniqueLines.map((line: string) => {
return (
<TtcBadge key={`${props.id}-${line}`} lineNum={line} />
);
})}
</div>
<span className={style["multi-line"]}>
{props.direction && (
<DirectionBadge direction={props.direction} />
)}
{props.name}
{directionArray.length > 0 &&
directionArray.map((direction) => (
<DirectionBadge
direction={direction}
key={`${props.id}-${direction}`}
/>
))}
<span>{props.name}</span>
</span>
</>
}
Expand Down Expand Up @@ -98,7 +106,7 @@ export function EtaCard(props: {
<DialogTitle>Choose which bus(es) to show</DialogTitle>
<FavouriteEditor
id={props.id}
lines={props.lines}
lines={uniqueLines}
enabled={props.enabled}
onDelete={props.onDelete}
/>
Expand All @@ -115,14 +123,16 @@ function FavouriteEditor(props: {
enabled?: string[];
onDelete?: () => void;
}) {
const uniqueLines = [...new Set(props.lines)];

const { t } = useTranslation();

const dispatch = useAppDispatch();

const onChangeFunction = useCallback(
(line: string) => {
if (!props.enabled) {
const cutOffEnabled = [...props.lines];
const cutOffEnabled = [...uniqueLines];
const cutOffIndex = cutOffEnabled.indexOf(line);
cutOffEnabled.splice(cutOffIndex, 1);
dispatch(
Expand Down Expand Up @@ -161,13 +171,13 @@ function FavouriteEditor(props: {
}
}
},
[props.lines, props.enabled]
[uniqueLines, props.enabled]
);

return (
<DialogContent>
<div className={style["checkbox-list"]}>
{props.lines.map((line) => (
{uniqueLines.map((line) => (
<LineCheckbox
key={props.id + line}
id={props.id}
Expand Down
48 changes: 12 additions & 36 deletions src/components/fetch/FetchRouteList.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Card, Link as LinkFluent, Text } from "@fluentui/react-components";
import { UseQueryResult, useQuery } from "@tanstack/react-query";
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";

import { subwayLines } from "../../data/ttc.js";
import { RoutesJson } from "../../models/etaJson.js";
import { addRoutes } from "../../store/ttcRoutesDb.js";

Check failure on line 8 in src/components/fetch/FetchRouteList.tsx

View workflow job for this annotation

GitHub Actions / build

'addRoutes' is defined but never used
import { TtcBadge } from "../badges.js";
import RawDisplay from "../rawDisplay/RawDisplay.js";
import { FetchJSONWithCancelToken } from "./fetchUtils.js";
import { ttcLines } from "./queries.js";

const parseRouteTitle = (input: string) => {
const routeTitleRegex = /\d+-/;
Expand All @@ -17,39 +20,18 @@
};

export function RoutesInfo() {
const [routeJsonData, setRouteJsonData] = useState<RoutesJson>();
const [routesDb, setRoutesDb] = useState<{ tag: number; title: string }[]>(
[]
);
const lineData: UseQueryResult<RoutesJson["body"], Error> =
useQuery(ttcLines);

useEffect(() => {
const controller = new AbortController();

const fetchEtaData = async () => {
const { data, Error } = await FetchJSONWithCancelToken(
"https://webservices.umoiq.com/service/publicJSONFeed?command=routeList&a=ttc",
{ signal: controller.signal }
);

return { data, Error };
};

fetchEtaData().then(({ data, Error }) => {
if (Error || !data) {
return;
}

setRouteJsonData(data);
if (data.route.length > 0) {
setRoutesDb(data.route);
}
});

// when useEffect is called, the following clean-up fn will run first
return () => {
controller.abort();
};
}, []);
if (lineData.data?.route && (lineData.data?.route.length ?? 0) > 0) {
setRoutesDb(lineData.data.route);
// addRoutes(lineData.data.route);
}
}, [lineData]);

const routesCards = routesDb.map((routeItem) => {
return (
Expand Down Expand Up @@ -83,18 +65,12 @@
{subwayCards()}
{routesCards}
</ul>
{routeJsonData && <RawDisplay data={routeJsonData} />}
{lineData.data && <RawDisplay data={lineData} />}
</article>
);
}

function subwayCards() {
const subwayLines = [
{ line: 1, name: "Yonge-University" },
{ line: 2, name: "Bloor-Danforth" },
{ line: 4, name: "Sheppard" },
];

const result = subwayLines.map((subwayLine) => {
return (
<li key={subwayLine.line}>
Expand Down
Loading
Loading