diff --git a/assets/css/nav/_top_nav.scss b/assets/css/nav/_top_nav.scss
index ac2bf97fb..f5737ea6b 100644
--- a/assets/css/nav/_top_nav.scss
+++ b/assets/css/nav/_top_nav.scss
@@ -5,7 +5,7 @@
display: flex;
justify-content: space-between;
align-items: center;
- padding: 0.75rem 1.5rem;
+ padding: 0;
background-color: $color-gray-50;
@@ -14,7 +14,6 @@
.c-top-nav__logo {
width: 2.5rem;
- height: 1.5rem;
svg {
width: 100%;
@@ -23,14 +22,26 @@
}
.c-top-nav__logo-icon {
+ &:not(.c-top-nav__logo-halloween-icon) {
+ margin-left: 1.5rem;
+ }
svg {
fill: $color-eggplant-900;
}
}
+.c-top-nav__logo-halloween-icon {
+ background-color: #f4b347;
+ padding: 0 0.875rem;
+ @include media-breakpoint-up(lg) {
+ margin-left: 1.5rem;
+ }
+}
+
.c-top-nav__right-items {
display: flex;
align-items: center;
+ padding-right: 1.5rem;
:not(:last-child) {
padding-right: 1.5rem;
diff --git a/assets/src/components/nav/topNav.tsx b/assets/src/components/nav/topNav.tsx
index b145a6c92..f4fa7e87b 100644
--- a/assets/src/components/nav/topNav.tsx
+++ b/assets/src/components/nav/topNav.tsx
@@ -8,6 +8,7 @@ import { LoggedInAs } from "../loggedInAs"
import getEmailAddress from "../../userEmailAddress"
import { CircleButton } from "../circleButton"
import { UserAvatar } from "../userAvatar"
+import { todayIsHalloween } from "../../helpers/date"
const TopNav = (): JSX.Element => {
const email = getEmailAddress()
@@ -17,7 +18,11 @@ const TopNav = (): JSX.Element => {
return (
-
+ {todayIsHalloween() ? (
+
+ ) : (
+
+ )}
-
@@ -69,4 +74,20 @@ const TopNav = (): JSX.Element => {
)
}
+export const HalloweenIcon = (props: BsIcon.SvgProps) => (
+
+)
+
export default TopNav
diff --git a/assets/src/components/vehicleIcon.tsx b/assets/src/components/vehicleIcon.tsx
index 913715b09..58c566c75 100644
--- a/assets/src/components/vehicleIcon.tsx
+++ b/assets/src/components/vehicleIcon.tsx
@@ -2,7 +2,11 @@ import React, { ReactElement } from "react"
import Tippy from "@tippyjs/react"
import "tippy.js/dist/tippy.css"
import { joinClasses } from "../helpers/dom"
-import { DrawnStatus, statusClasses } from "../models/vehicleStatus"
+import {
+ DrawnStatus,
+ OnTimeStatus,
+ statusClasses,
+} from "../models/vehicleStatus"
import { AlertIconStyle, IconAlertCircleSvgNode } from "./iconAlertCircle"
import { runIdToLabel } from "../helpers/vehicleLabel"
import { isGhost } from "../models/vehicle"
@@ -10,6 +14,7 @@ import { Ghost, RunId, Vehicle, VehicleInScheduledService } from "../realtime"
import { BlockId, ViaVariant } from "../schedule.d"
import { scheduleAdherenceLabelString } from "./propertiesPanel/header"
import { UserSettings } from "../userSettings"
+import { todayIsHalloween } from "../helpers/date"
import {
directionOnLadder,
getLadderDirectionForRoute,
@@ -279,6 +284,8 @@ export const VehicleIconSvgNode = React.memo(
) : null}
{status === "ghost" ? (
+ ) : isBat(status) ? (
+
) : (
)}
@@ -320,6 +327,32 @@ const Triangle = React.memo(
}
)
+const isBat = (
+ status: OnTimeStatus | "off-course" | "ghost" | "plain" | "logged-out"
+) => status === "off-course" && todayIsHalloween()
+
+const Bat = ({ size }: { size: Size }) => {
+ const scale = scaleBatForSize(size)
+ return (
+
+ )
+}
+
+const scaleBatForSize = (size: Size): number => {
+ switch (size) {
+ case Size.Small:
+ return 0.38
+ case Size.Medium:
+ return 0.5
+ case Size.Large:
+ return 1
+ }
+}
+
const GhostIcon = React.memo(
({ size, variant }: { size: Size; variant?: string }) => {
// No orientation argument, because the ghost icon is always right side up.
@@ -437,18 +470,23 @@ const Variant = React.memo(
status: DrawnStatus
}) => {
const scale = scaleForSize(size)
+ const bat = isBat(status)
+ const isSideways =
+ orientation === Orientation.Left || orientation === Orientation.Right
// space between the triangle base and the variant letter
let margin = 0
switch (size) {
case Size.Small:
- margin = status === "ghost" ? 4 : 2
+ margin = status === "ghost" || bat ? 4 : 2
+ if (bat && isSideways) margin++
break
case Size.Medium:
- margin = status === "ghost" ? 8 : 4
+ margin = status === "ghost" || bat ? 8 : 4
break
case Size.Large:
- margin = status === "ghost" ? 12 : 6
+ margin = status === "ghost" || bat ? 12 : 6
+ if (bat && isSideways) margin += 2
break
}
diff --git a/assets/src/helpers/date.ts b/assets/src/helpers/date.ts
new file mode 100644
index 000000000..9118a303e
--- /dev/null
+++ b/assets/src/helpers/date.ts
@@ -0,0 +1,2 @@
+export const todayIsHalloween = (today: Date = new Date()): boolean =>
+ today.getMonth() === 9 && today.getDate() === 31
diff --git a/assets/tests/components/app.test.tsx b/assets/tests/components/app.test.tsx
index 42f33a9d2..9635dde35 100644
--- a/assets/tests/components/app.test.tsx
+++ b/assets/tests/components/app.test.tsx
@@ -27,6 +27,11 @@ import { viewFactory } from "../factories/pagePanelStateFactory"
import userEvent from "@testing-library/user-event"
import { mockUsePanelState } from "../testHelpers/usePanelStateMocks"
+// Avoid Halloween
+jest
+ .useFakeTimers({ doNotFake: ["setTimeout"] })
+ .setSystemTime(new Date("2018-08-15T17:41:21.000Z"))
+
jest.mock("../../src/hooks/useDataStatus", () => ({
__esModule: true,
default: jest.fn(() => "good"),
diff --git a/assets/tests/components/appStateWrapper.test.tsx b/assets/tests/components/appStateWrapper.test.tsx
index 8138e03a8..f0e3004b2 100644
--- a/assets/tests/components/appStateWrapper.test.tsx
+++ b/assets/tests/components/appStateWrapper.test.tsx
@@ -3,6 +3,11 @@ import React from "react"
import { render } from "@testing-library/react"
import AppStateWrapper from "../../src/components/appStateWrapper"
+// Avoid Halloween
+jest
+ .useFakeTimers({ doNotFake: ["setTimeout"] })
+ .setSystemTime(new Date("2024-08-29T20:00:00"))
+
jest.mock("userTestGroups", () => ({
__esModule: true,
default: jest.fn(() => []),
diff --git a/assets/tests/components/ladder.test.tsx b/assets/tests/components/ladder.test.tsx
index 343969c63..396e8b036 100644
--- a/assets/tests/components/ladder.test.tsx
+++ b/assets/tests/components/ladder.test.tsx
@@ -19,6 +19,11 @@ import { render } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { mockUsePanelState } from "../testHelpers/usePanelStateMocks"
+// Avoid Halloween
+jest
+ .useFakeTimers({ doNotFake: ["setTimeout"] })
+ .setSystemTime(new Date("2024-08-29T20:00:00"))
+
jest.mock("../../src/hooks/useVehicles", () => ({
__esModule: true,
default: () => ({}),
diff --git a/assets/tests/components/propertiesPanel/header.test.tsx b/assets/tests/components/propertiesPanel/header.test.tsx
index 11420ce2f..af0f7113d 100644
--- a/assets/tests/components/propertiesPanel/header.test.tsx
+++ b/assets/tests/components/propertiesPanel/header.test.tsx
@@ -24,6 +24,11 @@ import routeFactory from "../../factories/route"
import routeTabFactory from "../../factories/routeTab"
import userEvent from "@testing-library/user-event"
+// Avoid Halloween
+jest
+ .useFakeTimers({ doNotFake: ["setTimeout"] })
+ .setSystemTime(new Date("2024-08-29T20:00:00"))
+
jest.spyOn(Date, "now").mockImplementation(() => 234000)
const setTabMode = jest.fn()
diff --git a/assets/tests/components/propertiesPanel/vehiclePropertiesPanel.test.tsx b/assets/tests/components/propertiesPanel/vehiclePropertiesPanel.test.tsx
index 5a8665604..5c3dca413 100644
--- a/assets/tests/components/propertiesPanel/vehiclePropertiesPanel.test.tsx
+++ b/assets/tests/components/propertiesPanel/vehiclePropertiesPanel.test.tsx
@@ -16,7 +16,6 @@ import {
VehicleInScheduledService,
} from "../../../src/realtime"
import { Route } from "../../../src/schedule"
-import * as dateTime from "../../../src/util/dateTime"
import { vehicleFactory, invalidVehicleFactory } from "../../factories/vehicle"
import { render, screen } from "@testing-library/react"
import "@testing-library/jest-dom/jest-globals"
@@ -32,9 +31,10 @@ import { closeButton } from "../../testHelpers/selectors/components/closeButton"
import { MemoryRouter } from "react-router-dom"
import { loading } from "../../../src/util/fetchResult"
+// Avoid Halloween for off-course vehicles
jest
- .spyOn(dateTime, "now")
- .mockImplementation(() => new Date("2018-08-15T17:41:21.000Z"))
+ .useFakeTimers({ doNotFake: ["setTimeout"] })
+ .setSystemTime(new Date("2018-08-15T17:41:21.000Z"))
jest.spyOn(Date, "now").mockImplementation(() => 234000)
diff --git a/assets/tests/components/vehicleIcon.test.tsx b/assets/tests/components/vehicleIcon.test.tsx
index 83a8d42a5..fdaa930b5 100644
--- a/assets/tests/components/vehicleIcon.test.tsx
+++ b/assets/tests/components/vehicleIcon.test.tsx
@@ -1,4 +1,4 @@
-import { test, expect } from "@jest/globals"
+import { test, expect, jest } from "@jest/globals"
import React from "react"
import renderer from "react-test-renderer"
import VehicleIcon, {
@@ -9,6 +9,11 @@ import VehicleIcon, {
import { AlertIconStyle } from "../../src/components/iconAlertCircle"
import { defaultUserSettings } from "../../src/userSettings"
+// Avoid Halloween
+jest
+ .useFakeTimers({ doNotFake: ["setTimeout"] })
+ .setSystemTime(new Date("2024-08-29T20:00:00"))
+
test("renders in all directions and sizes", () => {
const tree = renderer
.create(
diff --git a/assets/tests/helpers/date.test.ts b/assets/tests/helpers/date.test.ts
new file mode 100644
index 000000000..7663016bc
--- /dev/null
+++ b/assets/tests/helpers/date.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, test } from "@jest/globals"
+import { todayIsHalloween } from "../../src/helpers/date"
+
+describe("todayIsHalloween", () => {
+ test("returns true on Halloween", () => {
+ const halloweenDate = new Date("October 31, 2021 12:00:00")
+
+ expect(todayIsHalloween(halloweenDate)).toBeTruthy()
+ })
+
+ test("returns false on other days", () => {
+ const nonHalloweenDate = new Date("October 30, 2021 12:00:00")
+
+ expect(todayIsHalloween(nonHalloweenDate)).toBeFalsy()
+ })
+})