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() + }) +})