diff --git a/assets/css/_circle_x_icon.scss b/assets/css/_circle_x_icon.scss
index ddad8cc7c4..4f529e978c 100644
--- a/assets/css/_circle_x_icon.scss
+++ b/assets/css/_circle_x_icon.scss
@@ -3,8 +3,6 @@
width: 0.75rem;
height: 0.75rem;
- color: $color-gray-600;
-
&__path {
fill: currentColor;
.c-circle-x-icon-container:hover & {
diff --git a/assets/css/_date_time_picker.scss b/assets/css/_date_time_picker.scss
new file mode 100644
index 0000000000..c36e64bf97
--- /dev/null
+++ b/assets/css/_date_time_picker.scss
@@ -0,0 +1,70 @@
+@use "./color/tokens_2024" as tokens;
+
+.flatpickr-calendar {
+ .flatpickr-current-month {
+ .flatpickr-monthDropdown-months {
+ // fix bug in flatpickr where year overflows
+ display: inline-block !important;
+ font-weight: 600;
+ font-size: 1.25rem;
+ }
+
+ input.cur-year {
+ color: tokens.$gray-700;
+ font-weight: 600;
+ font-size: 1.25rem;
+ }
+ }
+
+ .flatpickr-prev-month,
+ .flatpickr-next-month {
+ svg {
+ fill: tokens.$gray-900;
+ }
+
+ &:hover svg {
+ fill: tokens.$eggplant-500 !important;
+ }
+ }
+
+ .flatpickr-day {
+ color: tokens.$black;
+
+ &.today {
+ border: unset;
+ }
+
+ &:hover,
+ &.today:hover {
+ color: tokens.$black;
+ background-color: tokens.$gray-300;
+ border-color: tokens.$gray-300;
+ }
+
+ &.prevMonthDay,
+ &.nextMonthDay {
+ &:not(.flatpickr-disabled):not(.selected) {
+ color: tokens.$gray-500;
+ }
+ }
+
+ &.disabled {
+ color: tokens.$gray-300;
+ }
+
+ &.selected,
+ &.selected.prevMonthDay,
+ &.selected.nextMonthDay,
+ &.selected.today {
+ color: tokens.$white;
+ background-color: tokens.$eggplant-500;
+ border-color: tokens.$eggplant-500;
+
+ &:hover {
+ color: tokens.$white;
+ background-color: tokens.$eggplant-300;
+ border-color: tokens.$eggplant-300;
+ }
+ }
+ }
+}
diff --git a/assets/css/_filter_accordion.scss b/assets/css/_filter_accordion.scss
index d57d15acec..f81667405d 100644
--- a/assets/css/_filter_accordion.scss
+++ b/assets/css/_filter_accordion.scss
@@ -1,5 +1,10 @@
.c-filter-accordion {
- @extend .c-garage-filter;
+ @include filter-input;
+ padding: unset;
+
+ &__header {
+ @include filter-placeholder;
+ }
// Visible when collapsed
&__toggle-button {
diff --git a/assets/css/_garage_filter.scss b/assets/css/_garage_filter.scss
index aeb72e5e8a..723c24ed9a 100644
--- a/assets/css/_garage_filter.scss
+++ b/assets/css/_garage_filter.scss
@@ -1,19 +1,12 @@
-.c-garage-filter {
- @include font-small;
- font-weight: 400;
- color: $color-gray-600;
- background: $white;
- border: 1px solid $color-gray-400;
- border-radius: 6px;
+@use "./color/tokens_2024" as tokens;
- &:hover {
- border: 1px solid $color-eggplant-400;
- }
+.c-garage-filter {
+ @include filter-input;
+ padding: unset;
+}
- &:focus-within {
- border: 1px solid $color-eggplant-500;
- box-shadow: 0px 0px 0px 2px rgba(170, 78, 242, 0.17);
- }
+.c-garage-filter__header {
+ @include filter-placeholder;
}
.c-garage-filter__show-hide-button {
@@ -51,6 +44,8 @@
}
.c-garage-filter__garages {
+ @include font-small;
+ font-weight: 400;
margin: 0;
padding: 1rem;
display: flex;
diff --git a/assets/css/_route_filter.scss b/assets/css/_route_filter.scss
deleted file mode 100644
index 224d2b3f0d..0000000000
--- a/assets/css/_route_filter.scss
+++ /dev/null
@@ -1,15 +0,0 @@
-.c-route-filter__text {
- position: relative;
- display: flex;
- flex-direction: column;
-}
-
-.c-route-filter__input {
- @include filter-input;
-}
-
-.c-route-filter__clear {
- @include filter-clear;
- right: 1rem;
- top: 0.6875rem;
-}
diff --git a/assets/css/_search_form.scss b/assets/css/_search_form.scss
index 8a47fad552..442980a6ee 100644
--- a/assets/css/_search_form.scss
+++ b/assets/css/_search_form.scss
@@ -27,60 +27,6 @@
position: relative;
}
-.c-search-form__search-input-container {
- // Use bootstrap control as base for now
- @extend .form-control;
-
- // fill control defined valid area
- height: 100%;
- // Make space for `input-controls`
- padding-right: calc(2.25rem * 2);
- // Ignore bootstrap padding due to `input-controls`
- padding-block: 0;
-
- border-color: $color-gray-400;
-
- &:focus,
- &:focus-within,
- &:active {
- border: 1px solid $color-eggplant-500;
- box-shadow: 0px 0px 0px 2px #aa4ef22b;
- color: $color-gray-800;
- }
-}
-
-.c-search-form__input {
- // Align self within parent boundaries
- width: 100%;
- height: 100%;
-
- // Match font size from the Route Picker Search
- @include font-small;
-
- // Remove default displays
- border: none;
-}
-
-// #region Search Input Buttons
-.c-search-form__input-controls {
- @include filter-input-controls;
-}
-
-.c-search-form__clear {
- @include button-icon(0.875rem);
-
- // Reserve space around icon in flex container
- flex: 0;
-
- height: 100%;
- aspect-ratio: 1/1;
-}
-
-.c-search-form__submit {
- @include filter-submit;
-}
-// #endregion Search Input Buttons
-
// #region Autocomplete Control
// Hide the autocomplete control by default
.c-search-form__autocomplete-container {
@@ -96,7 +42,7 @@
// -- Visually join the bottom of the input bar with autocomplete by squaring
// off the bottom
- .c-search-form__search-input-container {
+ .c-search-form__input {
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
diff --git a/assets/css/_ui_kit.scss b/assets/css/_ui_kit.scss
index b82cc8d1d0..7f68c07b5c 100644
--- a/assets/css/_ui_kit.scss
+++ b/assets/css/_ui_kit.scss
@@ -379,35 +379,32 @@ $color-tooltip-background: #333333;
font-weight: 400;
}
-@mixin filter-input {
- @include font-small;
- line-height: 1;
- border: 1px solid $color-gray-400;
- border-radius: 6px;
- padding: 0.625rem 1rem;
- color: $color-gray-700;
- font-weight: 400;
+@mixin filter-placeholder {
+ color: new_tokens.$gray-700;
+}
- &::placeholder {
- opacity: 1;
- color: $color-gray-600;
- }
+@mixin filter-border {
+ border-color: new_tokens.$gray-400;
&:hover {
- border: 1px solid $color-eggplant-400;
+ border-color: new_tokens.$eggplant-500;
+ box-shadow: 0 0 0 4px rgba(new_tokens.$eggplant-500, 0.25);
}
- &:focus {
- border: 1px solid $color-eggplant-500;
- box-shadow: 0px 0px 0px 2px rgba(170, 78, 242, 0.17);
- color: $color-gray-800;
-
- &::placeholder {
- color: $color-gray-700;
- }
+ &::placeholder {
+ @include filter-placeholder;
}
}
+@mixin filter-input {
+ @extend .form-control;
+
+ padding-right: 64px;
+ text-overflow: ellipsis;
+
+ @include filter-border;
+}
+
@mixin filter-input-controls {
position: absolute;
// Stick to right side of box, do not intercept mouse events to the text input
@@ -420,57 +417,20 @@ $color-tooltip-background: #333333;
display: flex;
flex-flow: row nowrap;
justify-content: end;
-}
-
-@mixin filter-clear {
- @include button-transparent;
- @include button-icon(0.875rem);
- position: absolute;
- display: flex;
- width: 0.875rem;
- height: 0.875rem;
- color: $color-gray-600;
-
- &:hover {
- color: $color-gray-400;
- }
-
- &:focus-within {
- color: $color-gray-700;
- }
-}
-
-@mixin filter-submit {
- span > svg {
- fill: currentColor;
-
- display: block;
- height: 1rem;
- aspect-ratio: 1/1;
- }
-
- display: flex;
-
- justify-content: center;
- align-items: center;
- height: 100%;
- aspect-ratio: 1/1;
-
- color: $color-eggplant-700;
- border-radius: 0.375rem;
+ button {
+ @include button-icon(16px);
+ color: new_tokens.$gray-800;
+ min-width: 32px;
- &[disabled] {
- color: $color-gray-700;
- }
-
- &:focus {
- border: 1px solid $color-eggplant-500;
- box-shadow: 0px 0px 0px 2px rgba(170, 78, 242, 0.17);
- }
+ &:hover,
+ &:focus {
+ color: var(--bs-primary);
+ }
- &:hover svg {
- color: $color-eggplant-500;
+ span svg {
+ stroke: none;
+ }
}
}
@@ -520,11 +480,6 @@ select {
background-repeat: no-repeat, repeat;
background-position: right 0.7em top 50%, 0 0;
background-size: 0.65em auto, 100%;
-
- &:focus,
- &:hover {
- color: $color-secondary-dark;
- }
}
// #endregion
diff --git a/assets/css/app.scss b/assets/css/app.scss
index 97ec13c452..e80ad7e49e 100644
--- a/assets/css/app.scss
+++ b/assets/css/app.scss
@@ -41,6 +41,7 @@ $vpp-location-padding: 1rem;
@import "crowding";
@import "cutout_overlay";
@import "data_status_banner";
+@import "date_time_picker";
@import "detour_list_page";
@import "detours/detour_map";
@import "detours/detour_modal";
@@ -102,7 +103,6 @@ $vpp-location-padding: 1rem;
@import "properties_panel/tab_panels";
@import "properties_panel/vehicle_properties_panel";
@import "recent_searches";
-@import "route_filter";
@import "route_ladder";
@import "route_ladders";
@import "route_picker";
diff --git a/assets/css/bootstrap/_extra_bespoke.scss b/assets/css/bootstrap/_extra_bespoke.scss
index d04ccf64ff..4cef2ccaf2 100644
--- a/assets/css/bootstrap/_extra_bespoke.scss
+++ b/assets/css/bootstrap/_extra_bespoke.scss
@@ -12,3 +12,24 @@
--bs-btn-active-color: #{new_tokens.$lemon-800};
--bs-btn-active-border-color: #{new_tokens.$lemon-700};
}
+
+.input-group-filter {
+ position: relative;
+ display: flex;
+
+ input {
+ @include filter-input;
+
+ & + div {
+ @include filter-input-controls;
+ }
+ }
+}
+
+.select-filter {
+ @include filter-border;
+
+ &:has(option[value="all"]:checked) {
+ @include filter-placeholder;
+ }
+}
diff --git a/assets/css/detours/_detour_table.scss b/assets/css/detours/_detour_table.scss
index 0f1cace9c7..794b8e0741 100644
--- a/assets/css/detours/_detour_table.scss
+++ b/assets/css/detours/_detour_table.scss
@@ -21,21 +21,14 @@ $table-border-radius: 0.5rem;
.search-header {
th {
background: tokens.$eggplant-50;
- }
-
- &__select {
- width: 33%;
label {
@include filter-label;
}
- select {
- width: 80%;
-
- @include media-breakpoint-down(lg) {
- width: 100%;
- }
+ &.c-detours-table__col-sm {
+ width: 15%;
+ min-width: 150px;
}
}
}
@@ -110,68 +103,3 @@ $table-border-radius: 0.5rem;
.c-detours-table__route-info-direction {
color: tokens.$gray-600;
}
-
-.c-detour-list-filter__text {
- position: relative;
- display: flex;
- flex-direction: row;
- width: 80%;
- margin-top: 0.5rem;
-}
-
-.c-detour-list-filter__label {
- @include filter-label;
-}
-
-.c-detour-list-filter__input-container {
- height: 100%;
- padding-block: 0;
- width: 100%;
- border-color: $color-gray-400;
- border-radius: 6px;
-
- &:hover {
- border-color: tokens.$eggplant-400;
- outline: 0;
- box-shadow: 0 0 0 0.25rem rgba(109, 57, 172, 0.25);
- border-right: none;
- }
-
- &:focus,
- &:focus-within,
- &:active {
- border-color: tokens.$eggplant-400;
- outline: 0;
- box-shadow: 0 0 0 0.25rem rgba(109, 57, 172, 0.25);
- border-right: none;
- }
-}
-
-.c-detour-list-filter__input {
- @include filter-input;
- height: 2.375rem;
- box-sizing: border-box;
- border-radius: 6px;
- font-size: 1rem;
- width: 100%;
-
- &::placeholder {
- opacity: 1;
- color: $color-gray-600;
- }
-}
-
-.c-detour-list-filter__input-controls {
- @include filter-input-controls;
-}
-
-.c-detour-list-filter__clear {
- @include filter-clear;
- right: 2.5rem;
- top: 50%;
- transform: translateY(-50%);
-}
-
-.c-detour-list-filter__submit {
- @include filter-submit;
-}
diff --git a/assets/package-lock.json b/assets/package-lock.json
index d58c881f47..882373238d 100644
--- a/assets/package-lock.json
+++ b/assets/package-lock.json
@@ -18,6 +18,7 @@
"@xstate/react": "^4.1.1",
"bootstrap": "^5.3.2",
"core-js": "^3.28.0",
+ "flatpickr": "^4.6.13",
"identity-obj-proxy": "^3.0.0",
"leaflet": "^1.9.3",
"leaflet-defaulticon-compatibility": "^0.1.1",
@@ -12111,6 +12112,11 @@
"node": "^10.12.0 || >=12.0.0"
}
},
+ "node_modules/flatpickr": {
+ "version": "4.6.13",
+ "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.13.tgz",
+ "integrity": "sha512-97PMG/aywoYpB4IvbvUJi0RQi8vearvU0oov1WW3k0WZPBMrTQVqekSX5CjSG/M4Q3i6A/0FKXC7RyAoAUUSPw=="
+ },
"node_modules/flatted": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
diff --git a/assets/package.json b/assets/package.json
index fc09ae8490..e8d5a60fc0 100644
--- a/assets/package.json
+++ b/assets/package.json
@@ -35,6 +35,7 @@
"@xstate/react": "^4.1.1",
"bootstrap": "^5.3.2",
"core-js": "^3.28.0",
+ "flatpickr": "^4.6.13",
"identity-obj-proxy": "^3.0.0",
"leaflet": "^1.9.3",
"leaflet-defaulticon-compatibility": "^0.1.1",
diff --git a/assets/src/components/dateTimePicker.tsx b/assets/src/components/dateTimePicker.tsx
new file mode 100644
index 0000000000..8b5da11817
--- /dev/null
+++ b/assets/src/components/dateTimePicker.tsx
@@ -0,0 +1,71 @@
+import React, { useRef, useEffect } from "react"
+import flatpickr from "flatpickr"
+import { Options } from "flatpickr/dist/types/options"
+
+import "flatpickr/dist/flatpickr.css"
+import { Calendar } from "../helpers/bsIcons"
+import { CircleXIcon } from "./circleXIcon"
+
+interface Props {
+ value: Date[]
+ options: Options
+ className?: string
+}
+
+export const DateTimePicker = ({
+ value,
+ options,
+ className,
+ ...props
+}: Props) => {
+ const inputRef = useRef(null)
+ const fpRef = useRef(null)
+
+ useEffect(() => {
+ if (inputRef.current) {
+ fpRef.current = flatpickr(inputRef.current, {
+ altInput: true,
+ altFormat: "D, M j, Y",
+ wrap: true,
+ mode: "multiple",
+ ...options,
+ })
+ }
+
+ return () => {
+ if (fpRef.current) {
+ fpRef.current.destroy()
+ fpRef.current = null
+ }
+ }
+ // needed to prevent the flatpickr instance from being recreated on rerender
+ // as a react wrapper, we don't want to list out all their possible configuration options here
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ useEffect(() => {
+ if (fpRef.current) {
+ fpRef.current.setDate(value)
+ }
+ }, [value])
+
+ return (
+
+
+
+ {fpRef.current && fpRef.current.selectedDates.length > 0 && (
+ fpRef.current && fpRef.current.clear()}>
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ )
+}
diff --git a/assets/src/components/detourListPage.tsx b/assets/src/components/detourListPage.tsx
index 2cd4a925e3..4a9fbb4261 100644
--- a/assets/src/components/detourListPage.tsx
+++ b/assets/src/components/detourListPage.tsx
@@ -79,42 +79,52 @@ export const DetourListPage = () => {
)}
{activeDetours && draftDetours && pastDetours ? (
<>
-
+ }
/>
+ {/* options
+ 1. move title into DetourTable
+ 2. add Button here, onClick={clearDetour} ... requires value exposed ...
+ */}
{userInTestGroup(TestGroups.DetoursPilot) && (
<>
-
+ }
classNames={["mb-5", "u-hide-for-mobile"]}
/>
-
+ }
routeId={routeId}
setRouteId={setRouteId}
routes={routes}
diff --git a/assets/src/components/detours/activateDetourModal.tsx b/assets/src/components/detours/activateDetourModal.tsx
index eba4a2a009..883d93653a 100644
--- a/assets/src/components/detours/activateDetourModal.tsx
+++ b/assets/src/components/detours/activateDetourModal.tsx
@@ -1,6 +1,7 @@
import React, { PropsWithChildren } from "react"
import { Button, Form, Modal } from "react-bootstrap"
import { StepperBar } from "../stepperBar"
+import detourReasons from "../../data/detourReasons"
export const possibleDurations = [
"1 hour",
@@ -15,28 +16,6 @@ export const possibleDurations = [
"Until further notice",
]
-const possibleReasons = [
- "Accident",
- "Construction",
- "Demonstration",
- "Disabled bus",
- "Drawbridge being raised",
- "Electrical work",
- "Fire",
- "Hazmat condition",
- "Holiday",
- "Hurricane",
- "Maintenance",
- "Medical emergency",
- "Parade",
- "Police activity",
- "Snow",
- "Special event",
- "Tie replacement",
- "Traffic",
- "Weather",
-]
-
interface SurroundingModalProps extends PropsWithChildren {
onCancel: () => void
onNext?: () => void
@@ -149,7 +128,7 @@ const SelectingReason = ({
Step 2 of 3 - Select reason for detour
{
diff --git a/assets/src/components/detoursTable.tsx b/assets/src/components/detoursTable.tsx
index 85b8776fdc..0278c7be53 100644
--- a/assets/src/components/detoursTable.tsx
+++ b/assets/src/components/detoursTable.tsx
@@ -1,12 +1,14 @@
-import React, { useState, useEffect } from "react"
+import React, { useState, useEffect, useMemo } from "react"
import { Table, Form, Button } from "react-bootstrap"
import { XSquare } from "../helpers/bsIcons"
import { RoutePill } from "./routePill"
+import { DateTimePicker } from "./dateTimePicker"
import { useCurrentTime } from "../hooks/useCurrentTime"
import {
timeAgoLabel,
timeAgoLabelFromDate,
dateFromEpochSeconds,
+ isSameDay,
} from "../util/dateTime"
import { SimpleDetour } from "../models/detoursList"
import { EmptyDetourTableIcon } from "../helpers/skateIcons"
@@ -15,11 +17,13 @@ import { Route } from "../schedule"
import { CircleXIcon } from "./circleXIcon"
import { SearchIcon } from "../helpers/icon"
import { fullStoryEvent } from "../helpers/fullStory"
+import detourReasons from "../data/detourReasons"
interface DetoursTableProps {
data: SimpleDetour[]
onOpenDetour: (detourId: number) => void
status: DetourStatus
+ title: React.ReactNode
routes?: Route[] | null
routeId?: string
setRouteId?: (routeId: string) => void
@@ -59,6 +63,7 @@ export const DetoursTable = ({
data,
onOpenDetour,
status,
+ title,
routes,
routeId,
setRouteId = () => {},
@@ -66,6 +71,9 @@ export const DetoursTable = ({
}: DetoursTableProps) => {
const [filter, setFilter] = useState("")
const [debouncedFilter, setDebouncedFilter] = useState(filter)
+ const [dates, setDates] = useState([])
+ const [reason, setReason] = useState("all")
+ const hasFilters = routes && status === DetourStatus.Closed
useEffect(() => {
const handler = setTimeout(() => {
@@ -80,134 +88,187 @@ export const DetoursTable = ({
const resetInputs = () => {
setRouteId("all")
setFilter("")
+ setDates([])
+ setReason("all")
}
- const filteredData = data.filter((detour) =>
- detour.intersection.toLowerCase().includes(debouncedFilter.toLowerCase())
- )
+ const filteredData = useMemo(() => {
+ let result = data
+
+ if (debouncedFilter !== "") {
+ result = result.filter((detour) =>
+ detour.intersection
+ .toLowerCase()
+ .includes(debouncedFilter.toLowerCase())
+ )
+ }
+
+ if (dates.length > 0) {
+ result = result.filter((detour) => {
+ const updatedDate = dateFromEpochSeconds(detour.updatedAt)
+ return dates.some((date) => isSameDay(date, updatedDate))
+ })
+ }
+
+ if (reason !== "all") {
+ result = result.filter(
+ (detour) => reason.toLowerCase() === detour.reason?.toLowerCase()
+ )
+ }
+
+ return result
+ }, [data, debouncedFilter, dates, reason])
return (
-
-
- {routes && status === DetourStatus.Closed && (
-
-
- Route
- {
- setRouteId(changeEvent.target.value)
- }}
- >
-
- Please select route
-
- {routes?.map((route: Route) => (
-
- {route.name}
-
- ))}
-
-
-
-
-
+
+ {title}
+ {hasFilters && (
+
+
+ Clear
+
+ )}
+
+
+
+ {hasFilters && (
+
+
+ Route
+ {
+ setRouteId(changeEvent.target.value)
+ }}
>
+
+ Select route
+
+ {routes?.map((route: Route) => (
+
+ {route.name}
+
+ ))}
+
+
+
+
Starting intersection
-
-
-
- fullStoryEvent("Detour Intersection Filter Used", {})
- }
- onChange={(e) => setFilter(e.target.value)}
- className="c-detour-list-filter__input"
- />
-
-
+
+
+ fullStoryEvent("Detour Intersection Filter Used", {})
+ }
+ onChange={(e) => setFilter(e.target.value)}
+ />
+
{filter.length > 0 && (
- setFilter("")}
- title="Clear"
- >
-
+ setFilter("")} title="Clear">
+
+
+
)}
-
+
+
+
-
+
+
+ Reason
+ {
+ setReason(e.target.value)
+ }}
+ >
+
+ Select reason
+
+ {detourReasons.map((reason) => (
+
+ {reason}
+
+ ))}
+
+
+
+
+ Date
+
+
+
+
+ )}
+
+ Route and direction
+
+ Starting Intersection
-
-
-
- Clear
-
+ {hasReasonColumn(status) && (
+ Reason
+ )}
+
+ {timestampLabelFromStatus(status)}
+ {status === DetourStatus.Active && (
+ Est. Duration
+ )}
- )}
-
- Route and direction
- Starting Intersection
-
- {timestampLabelFromStatus(status)}
-
- {status === DetourStatus.Active && (
- Est. Duration
+
+
+ {filteredData.length ? (
+
+ ) : (
+
+
+
+
+
)}
- {hasReasonColumn(status) && (
- Reason
- )}
-
-
-
- {filteredData.length ? (
-
- ) : (
-
-
-
-
-
- )}
-
-
+
+
+ >
)
}
@@ -255,6 +316,11 @@ const PopulatedDetourRows = ({
{detour.intersection}
+ {hasReasonColumn(status) && (
+
+ {detour.reason}
+
+ )}
{status === DetourStatus.Active && detour.activatedAt ? (
<>
@@ -274,11 +340,6 @@ const PopulatedDetourRows = ({
{detour.estimatedDuration}
)}
- {hasReasonColumn(status) && (
-
- {detour.reason}
-
- )}
))}
>
diff --git a/assets/src/components/searchForm.tsx b/assets/src/components/searchForm.tsx
index f3b05d7f67..9a568e194d 100644
--- a/assets/src/components/searchForm.tsx
+++ b/assets/src/components/searchForm.tsx
@@ -299,7 +299,7 @@ export const Combobox = ({
}
}}
>
-
+
setAutocompleteEnabled(true)}
/>
-
-
{
- // Allow buttons to be pressed by space bar by preventing the parent
- // keydown handler
- if (e.key === " ") {
- e.stopPropagation()
- }
- }}
- >
-
{
- e.stopPropagation()
- onClear?.(e)
+ {
+ // Allow buttons to be pressed by space bar by preventing the parent
+ // keydown handler
+ if (e.key === " ") {
+ e.stopPropagation()
+ }
}}
>
-
-
- {comboboxType == "map_search" && (
-
-
-
- )}
- {comboboxType == "select" && (
{
+ e.stopPropagation()
+ onClear?.(e)
+ }}
>
-
+
+
+
- )}
+ {comboboxType == "map_search" && (
+
+
+
+
+
+ )}
+ {comboboxType == "select" && (
+
+
+
+ )}
+
+
(
)
+/**
+ * @returns https://icons.getbootstrap.com/icons/calendar3/
+ */
+export const Calendar = (props: SvgProps) => (
+
+
+
+
+)
+
/**
* @returns https://icons.getbootstrap.com/icons/card-checklist/
*/
diff --git a/assets/src/hooks/useRouteFilter.tsx b/assets/src/hooks/useRouteFilter.tsx
index 5a0d71ed44..52fd1f7b13 100644
--- a/assets/src/hooks/useRouteFilter.tsx
+++ b/assets/src/hooks/useRouteFilter.tsx
@@ -46,24 +46,20 @@ export const RouteFilter = ({
}
return (
-
-
-
handleTextInput(event)}
- onKeyDown={blurOnEnter}
- />
-
+
+
handleTextInput(event)}
+ onKeyDown={blurOnEnter}
+ />
+
{filterText.length > 0 ? (
-
-
+
+
+
+
) : null}
diff --git a/assets/src/util/dateTime.ts b/assets/src/util/dateTime.ts
index 3d44183335..dd3ee4e1ac 100644
--- a/assets/src/util/dateTime.ts
+++ b/assets/src/util/dateTime.ts
@@ -129,3 +129,6 @@ export const timeAgoLabelFromDate = (start: Date, end: Date) => {
const second = 1000
return timeAgoLabel(end.getTime() / second, start.getTime() / second)
}
+
+export const isSameDay = (first: Date, second: Date) =>
+ first.toDateString() === second.toDateString()
diff --git a/assets/tests/components/__snapshots__/app.test.tsx.snap b/assets/tests/components/__snapshots__/app.test.tsx.snap
index dd9a9e4730..3fe6dfaf66 100644
--- a/assets/tests/components/__snapshots__/app.test.tsx.snap
+++ b/assets/tests/components/__snapshots__/app.test.tsx.snap
@@ -319,18 +319,14 @@ exports[`App renders 1`] = `
class="c-route-picker u-hideable"
>
Snapshot renders the empty state 1`] = `
role="presentation"
>
Snapshot renders the empty state 1`] = `
type="text"
value=""
/>
-
-
Snapshot renders the null state 1`] = `
role="presentation"
>
Snapshot renders the null state 1`] = `
type="text"
value=""
/>
-
-
-
- Closed detours
-
-
-
+
- Dispatchers and supervisors
-
+
+
+
+
+ Clear
+
+
+
+ Reason
+
+
+
+ Select reason
+
+
+ Accident
+
+
+ Construction
+
+
+ Demonstration
+
+
+ Disabled bus
+
+
+ Drawbridge being raised
+
+
+ Electrical work
+
+
+ Fire
+
+
+ Hazmat condition
+
+
+ Holiday
+
+
+ Hurricane
+
+
+ Maintenance
+
+
+ Medical emergency
+
+
+ Parade
+
+
+ Police activity
+
+
+ Snow
+
+
+ Special event
+
+
+ Tie replacement
+
+
+ Traffic
+
+
+ Weather
+
+
+
+
+
- Starting intersection
+ Date
-
-
-
-
-
-
- Clear
-
-
- Last used
+ Reason
- Reason
+ Last used
@@ -712,54 +882,535 @@ exports[`DetourListPage renders detour list page for dispatchers 1`] = `
>
Street A1 & Avenue B1
+
Aug 28, 2024
-
-
-
+
+
+
+
+
+
+
-
- Active detours
-
+
+
+ January
+
+
+ February
+
+
+ March
+
+
+ April
+
+
+ May
+
+
+ June
+
+
+ July
+
+
+ August
+
+
+
+
+
+
+
+
+
+
-
+
+
+
- All Skate users
-
+
+
+
+
+
+ Sun
+
+
+ Mon
+
+
+ Tue
+
+
+ Wed
+
+
+ Thu
+
+
+ Fri
+
+
+ Sat
+
+
+
+
+
+
+
+
+
+ 28
+
+
+ 29
+
+
+ 30
+
+
+ 31
+
+
+ 1
+
+
+ 2
+
+
+ 3
+
+
+ 4
+
+
+ 5
+
+
+ 6
+
+
+ 7
+
+
+ 8
+
+
+ 9
+
+
+ 10
+
+
+ 11
+
+
+ 12
+
+
+ 13
+
+
+ 14
+
+
+ 15
+
+
+ 16
+
+
+ 17
+
+
+ 18
+
+
+ 19
+
+
+ 20
+
+
+ 21
+
+
+ 22
+
+
+ 23
+
+
+ 24
+
+
+ 25
+
+
+ 26
+
+
+ 27
+
+
+ 28
+
+
+ 29
+
+
+ 30
+
+
+ 31
+
+
+ 1
+
+
+ 2
+
+
+ 3
+
+
+ 4
+
+
+ 5
+
+
+ 6
+
+
+ 7
+
+
+
+
+
+
+
+
+
+
+
+
+ Active detours
+
+
+
+
- Last published
+ Reason
- Est. Duration
+ Last published
- Reason
+ Est. Duration
@@ -829,6 +1480,9 @@ exports[`DetourListPage renders limited detour list page for non-dispatchers 1`]
>
Street A3 & Avenue B3
+
@@ -841,9 +1495,6 @@ exports[`DetourListPage renders limited detour list page for non-dispatchers 1`]
>
2 hours
-
Street A4 & Avenue B4
+
@@ -890,9 +1544,6 @@ exports[`DetourListPage renders limited detour list page for non-dispatchers 1`]
>
2 hours
-
diff --git a/assets/tests/components/detours/__snapshots__/detoursListPage.openDetour.test.tsx.snap b/assets/tests/components/detours/__snapshots__/detoursListPage.openDetour.test.tsx.snap
index 3961e280ea..1e0743b547 100644
--- a/assets/tests/components/detours/__snapshots__/detoursListPage.openDetour.test.tsx.snap
+++ b/assets/tests/components/detours/__snapshots__/detoursListPage.openDetour.test.tsx.snap
@@ -37,31 +37,35 @@ exports[`Detours Page: Open a Detour renders detour details in an open drawer on
-
- Active detours
-
-
-
+
+ Active detours
+
+
+
+
- Last published
+ Reason
- Est. Duration
+ Last published
- Reason
+ Est. Duration
@@ -131,6 +135,9 @@ exports[`Detours Page: Open a Detour renders detour details in an open drawer on
>
Street A5 & Avenue B5
+
@@ -143,9 +150,6 @@ exports[`Detours Page: Open a Detour renders detour details in an open drawer on
>
2 hours
-
Street A6 & Avenue B6
+
@@ -192,38 +199,39 @@ exports[`Detours Page: Open a Detour renders detour details in an open drawer on
>
2 hours
-
-
- Draft detours
-
-
-
+
+ Draft detours
+
+
+
+
-
- Closed detours
-
-
-
+
- Dispatchers and supervisors
-
+
+
+
+
+ Clear
+
+
+
+ Reason
+
+
+
+ Select reason
+
+
+ Accident
+
+
+ Construction
+
+
+ Demonstration
+
+
+ Disabled bus
+
+
+ Drawbridge being raised
+
+
+ Electrical work
+
+
+ Fire
+
+
+ Hazmat condition
+
+
+ Holiday
+
+
+ Hurricane
+
+
+ Maintenance
+
+
+ Medical emergency
+
+
+ Parade
+
+
+ Police activity
+
+
+ Snow
+
+
+ Special event
+
+
+ Tie replacement
+
+
+ Traffic
+
+
+ Weather
+
+
+
+
+
- Starting intersection
+ Date
-
-
-
-
-
-
- Clear
-
-
- Last used
+ Reason
- Reason
+ Last used
@@ -490,61 +656,538 @@ exports[`Detours Page: Open a Detour renders detour details in an open drawer on
>
Street A3 & Avenue B3
+
Aug 28, 2024
-
-
-
-
+
+
+
+
+
+ January
+
+
+ February
+
+
+ March
+
+
+ April
+
+
+ May
+
+
+ June
+
+
+ July
+
+
+ August
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sun
+
+
+ Mon
+
+
+ Tue
+
+
+ Wed
+
+
+ Thu
+
+
+ Fri
+
+
+ Sat
+
+
+
+
+
+
+
+
+
+ 28
+
+
+ 29
+
+
+ 30
+
+
+ 31
+
+
+ 1
+
+
+ 2
+
+
+ 3
+
+
+ 4
+
+
+ 5
+
+
+ 6
+
+
+ 7
+
+
+ 8
+
+
+ 9
+
+
+ 10
+
+
+ 11
+
+
+ 12
+
+
+ 13
+
+
+ 14
+
+
+ 15
+
+
+ 16
+
+
+ 17
+
+
+ 18
+
+
+ 19
+
+
+ 20
+
+
+ 21
+
+
+ 22
+
+
+ 23
+
+
+ 24
+
+
+ 25
+
+
+ 26
+
+
+ 27
+
+
+ 28
+
+
+ 29
+
+
+ 30
+
+
+ 31
+
+
+ 1
+
+
+ 2
+
+
+ 3
+
+
+ 4
+
+
+ 5
+
+
+ 6
+
+
+ 7
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1102,31 +1745,35 @@ exports[`Detours Page: Open a Detour renders detour details modal to match mocke
-
- Active detours
-
-
-
+
+ Active detours
+
+
+
+
- Last published
+ Reason
- Est. Duration
+ Last published
- Reason
+ Est. Duration
@@ -1196,6 +1843,9 @@ exports[`Detours Page: Open a Detour renders detour details modal to match mocke
>
Street A3 & Avenue B3
+
@@ -1208,9 +1858,6 @@ exports[`Detours Page: Open a Detour renders detour details modal to match mocke
>
2 hours
-
Street A4 & Avenue B4
+
@@ -1257,38 +1907,39 @@ exports[`Detours Page: Open a Detour renders detour details modal to match mocke
>
2 hours
-
-
- Draft detours
-
-
-
+
+ Draft detours
+
+
+
+
-
- Closed detours
-
-
-
+
- Dispatchers and supervisors
-
+
+
+
+
+ Clear
+
+
+
+ Reason
+
+
+
+ Select reason
+
+
+ Accident
+
+
+ Construction
+
+
+ Demonstration
+
+
+ Disabled bus
+
+
+ Drawbridge being raised
+
+
+ Electrical work
+
+
+ Fire
+
+
+ Hazmat condition
+
+
+ Holiday
+
+
+ Hurricane
+
+
+ Maintenance
+
+
+ Medical emergency
+
+
+ Parade
+
+
+ Police activity
+
+
+ Snow
+
+
+ Special event
+
+
+ Tie replacement
+
+
+ Traffic
+
+
+ Weather
+
+
+
+
+
- Starting intersection
+ Date
-
-
-
-
-
-
- Clear
-
-
- Last used
+ Reason
- Reason
+ Last used
@@ -1555,19 +2364,496 @@ exports[`Detours Page: Open a Detour renders detour details modal to match mocke
>
Street A2 & Avenue B2
+
Aug 28, 2024
-
+
+
+
+
+
+
+
+
+
+
+
+
+ January
+
+
+ February
+
+
+ March
+
+
+ April
+
+
+ May
+
+
+ June
+
+
+ July
+
+
+ August
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sun
+
+
+ Mon
+
+
+ Tue
+
+
+ Wed
+
+
+ Thu
+
+
+ Fri
+
+
+ Sat
+
+
+
+
+
+
+
+
+
+ 28
+
+
+ 29
+
+
+ 30
+
+
+ 31
+
+
+ 1
+
+
+ 2
+
+
+ 3
+
+
+ 4
+
+
+ 5
+
+
+ 6
+
+
+ 7
+
+
+ 8
+
+
+ 9
+
+
+ 10
+
+
+ 11
+
+
+ 12
+
+
+ 13
+
+
+ 14
+
+
+ 15
+
+
+ 16
+
+
+ 17
+
+
+ 18
+
+
+ 19
+
+
+ 20
+
+
+ 21
+
+
+ 22
+
+
+ 23
+
+
+ 24
+
+
+ 25
+
+
+ 26
+
+
+ 27
+
+
+ 28
+
+
+ 29
+
+
+ 30
+
+
+ 31
+
+
+ 1
+
+
+ 2
+
+
+ 3
+
+
+ 4
+
+
+ 5
+
+
+ 6
+
+
+ 7
+
+
+
+
+
+