Skip to content

Commit

Permalink
Rename CitySelectionState to ViewState (#290)
Browse files Browse the repository at this point in the history
We also move ViewState.ts and types.ts to packages/shared. The types are
currently over-specified in that the scorecard is not generic. This will
be worked upon in a follow up.
  • Loading branch information
Eric-Arellano authored Jan 10, 2025
1 parent bd5197a commit 4d65f88
Show file tree
Hide file tree
Showing 18 changed files with 86 additions and 71 deletions.
25 changes: 0 additions & 25 deletions packages/primary/src/js/CitySelectionState.ts

This file was deleted.

14 changes: 7 additions & 7 deletions packages/primary/src/js/ParkingLotLoader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { geoJSON, GeoJSON, Map as LeafletMap } from "leaflet";

import { CityId, ParkingLotGeoJSONModules } from "./types";
import { CitySelectionObservable } from "./CitySelectionState";
import type {
CityId,
ParkingLotGeoJSONModules,
} from "@prn-parking-lots/shared/src/js/types";
import { ViewStateObservable } from "@prn-parking-lots/shared/src/js/ViewState";
import { STYLES } from "./map";

function createParkingLotsLayer(map: LeafletMap): GeoJSON {
Expand Down Expand Up @@ -66,11 +69,8 @@ export default class ParkingLotLoader {
return loadPromise;
}

subscribe(observable: CitySelectionObservable): void {
observable.subscribe(
({ cityId }) => this.load(cityId),
"load parking lots",
);
subscribe(viewState: ViewStateObservable): void {
viewState.subscribe(({ cityId }) => this.load(cityId), "load parking lots");
}

private async loadCity(cityId: CityId): Promise<void> {
Expand Down
9 changes: 5 additions & 4 deletions packages/primary/src/js/citiesLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import type {
CityEntryCollection,
CityStatsCollection,
CityBoundaries,
} from "./types";
} from "@prn-parking-lots/shared/src/js/types";
import { ViewStateObservable } from "@prn-parking-lots/shared/src/js/ViewState";

import { STYLES } from "./map";
import { CitySelectionObservable } from "./CitySelectionState";

/**
* Load the cities from GeoJson and associate each city with its layer and scorecard entry.
Expand Down Expand Up @@ -38,7 +39,7 @@ export function createCitiesLayer(
}

export function setCityOnBoundaryClick(
observable: CitySelectionObservable,
viewState: ViewStateObservable,
map: Map,
cityBoundaries: GeoJSON,
): void {
Expand All @@ -47,6 +48,6 @@ export function setCityOnBoundaryClick(
// Only change cities if zoomed in enough.
if (currentZoom <= 7) return;
const cityId = e.sourceTarget.feature.properties.id;
observable.setValue({ cityId, shouldSnapMap: true });
viewState.setValue({ cityId, shouldSnapMap: true });
});
}
2 changes: 1 addition & 1 deletion packages/primary/src/js/cityId.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CityId } from "./types";
import type { CityId } from "@prn-parking-lots/shared/src/js/types";

/**
* Extract the city ID from the URL's `#`, if present.
Expand Down
2 changes: 1 addition & 1 deletion packages/primary/src/js/data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable import/no-relative-packages */

import { ParkingLotGeoJSONModules } from "./types";
import type { ParkingLotGeoJSONModules } from "@prn-parking-lots/shared/src/js/types";

import CITY_STATS_DATA from "../../../../data/city-stats.json" with { type: "json" };
// @ts-expect-error - move data to this package to fix declaration
Expand Down
4 changes: 2 additions & 2 deletions packages/primary/src/js/declarations.td.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
declare module "~/data/city-boundaries.geojson" {
const value: import("./types").CityBoundaries;
const value: import("@prn-parking-lots/shared/src/js/types").CityBoundaries;
export default value;
}

declare module "~/data/city-stats.json" {
const value: import("./types").CityStatsCollection;
const value: import("@prn-parking-lots/shared/src/js/types").CityStatsCollection;
export default value;
}

Expand Down
13 changes: 8 additions & 5 deletions packages/primary/src/js/dropdown.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Choices from "choices.js";

import type { CityStatsCollection, DropdownChoice } from "./types";
import { CitySelectionObservable } from "./CitySelectionState";
import type {
CityStatsCollection,
DropdownChoice,
} from "@prn-parking-lots/shared/src/js/types";
import { ViewStateObservable } from "@prn-parking-lots/shared/src/js/ViewState";

function createDropdown(cityStatsData: CityStatsCollection): Choices {
const dropdown = new Choices("#city-dropdown", {
Expand Down Expand Up @@ -60,11 +63,11 @@ function createDropdown(cityStatsData: CityStatsCollection): Choices {

export default function initDropdown(
cityStatsData: CityStatsCollection,
observable: CitySelectionObservable,
viewState: ViewStateObservable,
): void {
const dropdown = createDropdown(cityStatsData);

observable.subscribe(
viewState.subscribe(
({ cityId }) => dropdown.setChoiceByValue(cityId),
"set dropdown to city",
);
Expand All @@ -73,6 +76,6 @@ export default function initDropdown(
// Note that `change` only triggers for user-driven changes, not programmatic updates.
const selectElement = dropdown.passedElement.element as HTMLSelectElement;
selectElement.addEventListener("change", () => {
observable.setValue({ cityId: selectElement.value, shouldSnapMap: true });
viewState.setValue({ cityId: selectElement.value, shouldSnapMap: true });
});
}
20 changes: 10 additions & 10 deletions packages/primary/src/js/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import initIcons from "@prn-parking-lots/shared/src/js/fontAwesome";
import maybeDisableFullScreenIcon from "@prn-parking-lots/shared/src/js/iframe";
import { initViewState } from "@prn-parking-lots/shared/src/js/ViewState";

import { extractCityIdFromUrl } from "./cityId";
import initAbout from "./about";
Expand All @@ -10,7 +11,6 @@ import { createMap } from "./map";
import { setCityByMapPosition, subscribeSnapToCity } from "./mapPosition";
import { createCitiesLayer, setCityOnBoundaryClick } from "./citiesLayer";
import ParkingLotLoader from "./ParkingLotLoader";
import { initCitySelectionState } from "./CitySelectionState";
import {
CITY_STATS_DATA,
CITY_BOUNDARIES_GEOJSON,
Expand All @@ -34,22 +34,22 @@ export default async function initApp(): Promise<void> {
);

const initialCityId = extractCityIdFromUrl(window.location.href);
const cityState = initCitySelectionState(
const viewState = initViewState(
Object.keys(CITY_STATS_DATA),
initialCityId,
"atlanta-ga",
);

initDropdown(CITY_STATS_DATA, cityState);
subscribeScorecard(cityState, cityEntries);
subscribeShareLink(cityState);
subscribeSnapToCity(cityState, map, cityEntries);
parkingLotLoader.subscribe(cityState);
initDropdown(CITY_STATS_DATA, viewState);
subscribeScorecard(viewState, cityEntries);
subscribeShareLink(viewState);
subscribeSnapToCity(viewState, map, cityEntries);
parkingLotLoader.subscribe(viewState);

setCityOnBoundaryClick(cityState, map, cityBoundaries);
setCityByMapPosition(cityState, map, cityEntries, parkingLotLoader);
setCityOnBoundaryClick(viewState, map, cityBoundaries);
setCityByMapPosition(viewState, map, cityEntries, parkingLotLoader);

cityState.initialize();
viewState.initialize();

// There have been some issues on Safari with the map only rendering the top 20%
// on the first page load. This is meant to address that.
Expand Down
13 changes: 7 additions & 6 deletions packages/primary/src/js/mapPosition.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { ImageOverlay, Map } from "leaflet";

import { CityEntryCollection } from "./types";
import type { CityEntryCollection } from "@prn-parking-lots/shared/src/js/types";
import { ViewStateObservable } from "@prn-parking-lots/shared/src/js/ViewState";

import ParkingLotLoader from "./ParkingLotLoader";
import { CitySelectionObservable } from "./CitySelectionState";

/**
* Centers view to city, but translated down to account for the top UI elements.
Expand All @@ -19,11 +20,11 @@ function snapToCity(map: Map, layer: ImageOverlay): void {
}

export function subscribeSnapToCity(
observable: CitySelectionObservable,
viewState: ViewStateObservable,
map: Map,
cityEntries: CityEntryCollection,
): void {
observable.subscribe((state) => {
viewState.subscribe((state) => {
if (!state.shouldSnapMap) return;
snapToCity(map, cityEntries[state.cityId].layer);
}, "snap to city");
Expand All @@ -35,7 +36,7 @@ export function subscribeSnapToCity(
* Regardless of if the city is chosen, ensure its parking lots are loaded when in view.
*/
export function setCityByMapPosition(
observable: CitySelectionObservable,
viewState: ViewStateObservable,
map: Map,
cityEntries: CityEntryCollection,
parkingLotLoader: ParkingLotLoader,
Expand All @@ -58,7 +59,7 @@ export function setCityByMapPosition(
}
});
if (centralCity) {
observable.setValue({ cityId: centralCity, shouldSnapMap: false });
viewState.setValue({ cityId: centralCity, shouldSnapMap: false });
}
});
}
11 changes: 7 additions & 4 deletions packages/primary/src/js/scorecard.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import Observable from "@prn-parking-lots/shared/src/js/Observable";

import { CitySelectionObservable } from "./CitySelectionState";
import { CityEntryCollection, CityStats } from "./types";
import { ViewStateObservable } from "@prn-parking-lots/shared/src/js/ViewState";
import type {
CityEntryCollection,
CityStats,
} from "@prn-parking-lots/shared/src/js/types";

function generateScorecard(stats: CityStats): string {
let header = `
Expand Down Expand Up @@ -104,10 +107,10 @@ function initAccordion(): void {
}

export default function subscribeScorecard(
observable: CitySelectionObservable,
viewState: ViewStateObservable,
cityEntries: CityEntryCollection,
): void {
observable.subscribe(({ cityId }) => {
viewState.subscribe(({ cityId }) => {
const scorecardContainer = document.querySelector(".scorecard-container");
if (!scorecardContainer) return;
scorecardContainer.innerHTML = generateScorecard(cityEntries[cityId].stats);
Expand Down
7 changes: 4 additions & 3 deletions packages/primary/src/js/share.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* global document, navigator, window */

import { ViewStateObservable } from "@prn-parking-lots/shared/src/js/ViewState";
import { determineShareUrl } from "./cityId";
import { CitySelectionObservable } from "./CitySelectionState";

async function copyToClipboard(value: string): Promise<void> {
try {
Expand All @@ -25,9 +26,9 @@ function switchShareIcons(shareIcon: HTMLAnchorElement): void {
}

export default function subscribeShareLink(
observable: CitySelectionObservable,
viewState: ViewStateObservable,
): void {
observable.subscribe(({ cityId }) => {
viewState.subscribe(({ cityId }) => {
const shareIcon = document.querySelector<HTMLAnchorElement>(
".header-share-icon-container",
);
Expand Down
1 change: 1 addition & 0 deletions packages/scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
},
"dependencies": {
"@prn-parking-lots/primary": "workspace:*",
"@prn-parking-lots/shared": "workspace:*",
"ts-results": "^3.3.0"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/scripts/src/add-city.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from "fs/promises";

import results from "ts-results";

import { CityId } from "@prn-parking-lots/primary/src/js/types.ts";
import type { CityId } from "@prn-parking-lots/shared/src/js/types.ts";
import { determineArgs, updateCoordinates, updateParkingLots } from "./base.ts";

const addScoreCard = async (
Expand Down
2 changes: 1 addition & 1 deletion packages/scripts/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from "geojson";

import { parseCityIdFromJson } from "@prn-parking-lots/primary/src/js/cityId.ts";
import { CityId } from "../../primary/src/js/types";
import type { CityId } from "@prn-parking-lots/shared/src/js/types";

const determineArgs = (
scriptCommand: string,
Expand Down
3 changes: 2 additions & 1 deletion packages/scripts/tests/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import fs from "fs/promises";
import { expect, test } from "@playwright/test";
import { Feature, Polygon, FeatureCollection } from "geojson";

import type { CityId } from "@prn-parking-lots/shared/src/js/types";

import {
determineArgs,
updateCoordinates,
updateParkingLots,
} from "../src/base";
import { CityId } from "../../primary/src/js/types";

test.describe("determineArgs()", () => {
test("returns the city name and ID", () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/shared/src/js/ViewState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Observable from "./Observable";
import type { CityId } from "./types";

type ViewState = {
cityId: CityId;
shouldSnapMap: boolean;
};

export type ViewStateObservable = Observable<ViewState>;

export function initViewState(
cityIds: CityId[],
initialCityId: CityId | null,
fallBackCityId: CityId,
): ViewStateObservable {
const startingCity =
initialCityId && cityIds.includes(initialCityId)
? initialCityId
: fallBackCityId;
return new Observable<ViewState>("city state", {
cityId: startingCity,
shouldSnapMap: true,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
GeoJsonProperties,
} from "geojson";

/// The slugified ID, e.g. `st.-louis-mo` or `hartford`.
/// (The state code is missing for state-specific maps like CT.)
export type CityId = string; // e.g. `st.-louis-mo`

export interface CityStats {
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4d65f88

Please sign in to comment.