Skip to content

Commit

Permalink
Generalize setting up groups in dropdown (#306)
Browse files Browse the repository at this point in the history
<img width="260" alt="Screenshot 2025-01-11 at 11 00 25 AM"
src="https://github.com/user-attachments/assets/2502abcb-2c2d-45a9-bd1a-f8f9ac713c89"
/>
  • Loading branch information
Eric-Arellano authored Jan 11, 2025
1 parent f9756fd commit d84640d
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 63 deletions.
13 changes: 13 additions & 0 deletions packages/ct/src/js/dropdownGroups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { DropdownGroup } from "@prn-parking-lots/shared/src/js/city-ui/dropdownUtils";
import { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types";

export default function createDropdownGroups(
data: CityStatsCollection,
): DropdownGroup[] {
return [
{
label: "Group 1",
cities: Object.entries(data).map(([id, { name }]) => ({ id, name })),
},
];
}
2 changes: 2 additions & 0 deletions packages/ct/src/js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CITY_BOUNDARIES_GEOJSON,
PARKING_LOT_GEOJSON_MODULES,
} from "./data";
import createDropdownGroups from "./dropdownGroups";

export default async function initApp(): Promise<void> {
await bootstrapApp({
Expand All @@ -13,6 +14,7 @@ export default async function initApp(): Promise<void> {
boundaries: CITY_BOUNDARIES_GEOJSON,
parkingLots: PARKING_LOT_GEOJSON_MODULES,
},
dropdownGroups: createDropdownGroups(CITY_STATS_DATA),
initialCity: "hartford",
});
}
29 changes: 29 additions & 0 deletions packages/primary/src/js/dropdownGroups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {
DropdownGroup,
DropdownChoiceId,
} from "@prn-parking-lots/shared/src/js/city-ui/dropdownUtils";
import { CityStatsCollection } from "@prn-parking-lots/shared/src/js/model/types";

export default function createDropdownGroups(
data: CityStatsCollection,
): DropdownGroup[] {
const official: DropdownChoiceId[] = [];
const community: DropdownChoiceId[] = [];
Object.entries(data).forEach(([id, { name, contribution }]) => {
if (contribution) {
community.push({ id, name });
} else {
official.push({ id, name });
}
});
return [
{
label: "Official maps",
cities: official,
},
{
label: "Community maps",
cities: community,
},
];
}
2 changes: 2 additions & 0 deletions packages/primary/src/js/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CITY_BOUNDARIES_GEOJSON,
PARKING_LOT_GEOJSON_MODULES,
} from "./data";
import createDropdownGroups from "./dropdownGroups";

export default async function initApp(): Promise<void> {
await bootstrapApp({
Expand All @@ -13,6 +14,7 @@ export default async function initApp(): Promise<void> {
boundaries: CITY_BOUNDARIES_GEOJSON,
parkingLots: PARKING_LOT_GEOJSON_MODULES,
},
dropdownGroups: createDropdownGroups(CITY_STATS_DATA),
initialCity: "atlanta-ga",
});
}
21 changes: 0 additions & 21 deletions packages/primary/tests/communityMaps.test.ts

This file was deleted.

35 changes: 35 additions & 0 deletions packages/primary/tests/dropdownGroups.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { expect, test } from "@playwright/test";
import createDropdownGroups from "../src/js/dropdownGroups";

test("createDropdownGroups", () => {
const common = {
percentage: "",
cityType: "",
population: "",
urbanizedAreaPopulation: "",
parkingScore: null,
reforms: "",
url: "",
};
const input = {
"city1-ny": {
...common,
name: "City 1, NY",
},
"city2-ny": {
...common,
name: "City 2, NY",
contribution: "some-email@web.com",
},
};
expect(createDropdownGroups(input)).toEqual([
{
label: "Official maps",
cities: [{ id: "city1-ny", name: "City 1, NY" }],
},
{
label: "Community maps",
cities: [{ id: "city2-ny", name: "City 2, NY" }],
},
]);
});
4 changes: 3 additions & 1 deletion packages/shared/src/js/bootstrap.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import subscribeScorecard from "./city-ui/scorecard";
import initDropdown from "./city-ui/dropdown";
import type { DropdownGroup } from "./city-ui/dropdownUtils";

import initAbout from "./layout/about";
import initIcons from "./layout/fontAwesome";
Expand All @@ -23,6 +24,7 @@ import { initViewState } from "./state/ViewState";
interface Args {
data: DataSet;
initialCity: CityId;
dropdownGroups: DropdownGroup[];
}

export default async function bootstrapApp(args: Args): Promise<void> {
Expand All @@ -45,7 +47,7 @@ export default async function bootstrapApp(args: Args): Promise<void> {
args.initialCity,
);

initDropdown(args.data.stats, viewState);
initDropdown(args.dropdownGroups, viewState);
subscribeScorecard(viewState, cityEntries);
subscribeShareLink(viewState);
subscribeSnapToCity(viewState, map, cityEntries);
Expand Down
46 changes: 7 additions & 39 deletions packages/shared/src/js/city-ui/dropdown.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,36 @@
import ChoicesJS from "choices.js";

import { DropdownChoice, createChoice } from "./dropdownUtils";
import type { CityStatsCollection } from "../model/types";
import { DropdownGroup, convertToChoicesGroups } from "./dropdownUtils";
import { ViewStateObservable } from "../state/ViewState";

function createDropdown(cityStatsData: CityStatsCollection): ChoicesJS {
function createDropdown(groups: DropdownGroup[]): ChoicesJS {
const dropdown = new ChoicesJS("#city-dropdown", {
position: "bottom",
allowHTML: false,
itemSelectText: "",
searchEnabled: true,
searchResultLimit: 6,
searchFields: ["customProperties.city", "customProperties.context"],
// Disabling this option allows us to properly handle search groups.
// Disabling this option allows us to properly handle groups.
// We already sort entries in the JSON file.
shouldSort: false,
});

const officialCities: DropdownChoice[] = [];
const communityCities: DropdownChoice[] = [];
Object.entries(cityStatsData).forEach(([id, { name, contribution }]) => {
const entry = createChoice(id, name);
if (contribution) {
communityCities.push(entry);
} else {
officialCities.push(entry);
}
});

dropdown.setChoices([
{
value: "Official maps",
label: "Official maps",
disabled: false,
choices: officialCities,
},
]);

if (communityCities.length > 0) {
dropdown.setChoices([
{
value: "Community maps",
label: "Community maps",
disabled: false,
choices: communityCities,
},
]);
}

dropdown.setChoices(convertToChoicesGroups(groups));
return dropdown;
}

export default function initDropdown(
cityStatsData: CityStatsCollection,
groups: DropdownGroup[],
viewState: ViewStateObservable,
): void {
const dropdown = createDropdown(cityStatsData);
const dropdown = createDropdown(groups);

viewState.subscribe(
({ cityId }) => dropdown.setChoiceByValue(cityId),
"set dropdown to city",
);

// Bind user-changes in the dropdown to update the state in CitySelectionObservable.
// Bind user changes in the dropdown to update the view state.
// Note that `change` only triggers for user-driven changes, not programmatic updates.
const selectElement = dropdown.passedElement.element as HTMLSelectElement;
selectElement.addEventListener("change", () => {
Expand Down
27 changes: 26 additions & 1 deletion packages/shared/src/js/city-ui/dropdownUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Group as ChoicesJSGroup } from "choices.js";

import type { CityId } from "../model/types";

export interface DropdownChoice {
interface DropdownChoice {
value: string;
label: string;
customProperties: {
Expand All @@ -10,6 +12,16 @@ export interface DropdownChoice {
};
}

export interface DropdownChoiceId {
id: CityId;
name: string;
}

export interface DropdownGroup {
label: string;
cities: Array<DropdownChoiceId>;
}

export function createChoice(id: CityId, name: string): DropdownChoice {
const [city, context] = name.split(/,\s|\s-\s/);
return {
Expand All @@ -21,3 +33,16 @@ export function createChoice(id: CityId, name: string): DropdownChoice {
},
};
}

export function convertToChoicesGroups(
groups: DropdownGroup[],
): ChoicesJSGroup[] {
return groups
.filter(({ cities }) => cities.length > 0)
.map(({ label, cities }) => ({
label,
value: label,
disabled: false,
choices: cities.map(({ id, name }) => createChoice(id, name)),
}));
}
33 changes: 32 additions & 1 deletion packages/shared/tests/dropdownUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { expect, test } from "@playwright/test";
import { createChoice } from "../src/js/city-ui/dropdownUtils";
import {
createChoice,
convertToChoicesGroups,
} from "../src/js/city-ui/dropdownUtils";

test.describe("createChoice()", () => {
test("city and state", () => {
Expand All @@ -26,3 +29,31 @@ test.describe("createChoice()", () => {
});
});
});

test("convertToChoicesGroups()", () => {
const city1 = { id: "city1", name: "City 1" };
const city2 = { id: "city2", name: "City 2" };
const city3 = { id: "city2", name: "City 3" };
const input = [
{ label: "group 1", cities: [city1] },
{ label: "hidden", cities: [] },
{ label: "group 2", cities: [city2, city3] },
];
expect(convertToChoicesGroups(input)).toEqual([
{
value: "group 1",
label: "group 1",
disabled: false,
choices: [createChoice(city1.id, city1.name)],
},
{
value: "group 2",
label: "group 2",
disabled: false,
choices: [
createChoice(city2.id, city2.name),
createChoice(city3.id, city3.name),
],
},
]);
});

0 comments on commit d84640d

Please sign in to comment.