diff --git a/app/(aspects)/cycle/packing/page.mdx b/app/(aspects)/cycle/packing/page.mdx
index 76fb53ea..5325aa12 100644
--- a/app/(aspects)/cycle/packing/page.mdx
+++ b/app/(aspects)/cycle/packing/page.mdx
@@ -1,4 +1,4 @@
-import { ParksOrg, ParkReserveLink } from "@/components/park-reserve-link";
+import { Park, ParkReserveLink } from "@/components/park-reserve-link";
# 🏕️ Bike Packing
@@ -15,21 +15,18 @@ Resources: [BC Parks Camping][bc-parks] // [Parks Canada Camping][pc-parks] //
### [Parks Canada Sites][parks-canada]
[parks-canada]: https://web.archive.org/web/20240526195011/https://www.canadream.com/Website/media/Files/Parks-Canada-Discovery-Pass-Brochure-Map.pdf
-#### [K6: Fort Langley][langley-nhs]
+#### [K6: Fort Langley][langley-nhs]
[langley-nhs]: https://parks.canada.ca/lhn-nhs/bc/langley
- ~30 km cycling from King George
- ~50 km cycling from Waterfront via Central Valley Greenway
- ⛺ oTENTik available (May 15 to Sept 15)
-Still waiting for an opportunity to try this one. Reservation spots for the
-historic site campgrounds are hard to come by.
-
#### [K7: Gulf Islands National Park Reserve][gulf-np]
[gulf-np]:https://parks.canada.ca/pn-np/bc/gulf
-- ⛺ frontcountry campgrounds available (May 15 to Sept 30)
-- 🪵 backcountry campgrounds available all year round
+- ⛺ frontcountry campsites available (May 15 to Sept 30)
+- 🪵 backcountry campsites available all year round
Routes to Swartz Bay:
@@ -47,15 +44,15 @@ Three frontcountry campgrounds: SMONEĆTEN on Vancouver Island, Prior Centennial
on Pender Island, and Sidney Spit on Sidney Island. Backcountry campgrounds are
accessible year round but are not regularly maintained from Oct 1 to May 14.
-##### [SMONEĆTEN][smonecten]
+##### [SMONEĆTEN][smonecten]
[smonecten]: https://parks.canada.ca/pn-np/bc/gulf/activ/camping/campinglavantpays-frontcountrycamping#McDonald
The most bike-friendly campground is SMONEĆTEN, with access to the Lochside
Cycling Trail (from Swartz Bay to Downtown Victoria) and walk-in sites close to
-entrance in pleasant wooded area, with a communal campfire ring. From Swartz Bay
-to SMONEĆTEN via Swartz Bay Road it's about three more kms. The downside of this
-campsite is being next to a highway; depending on the campsite position it's
-possible to hear traffic noise.
+entrance in pleasant wooded area, and a communal campfire ring. From Swartz Bay
+to SMONEĆTEN via Swartz Bay Road it's about 3 km. The downside of this campsite
+is being next to a highway; depending on the campsite position it's possible to
+hear traffic noise.
##### Prior Centennial
@@ -63,6 +60,10 @@ possible to hear traffic noise.
Ferry from Tsawwassen to Mayne Island then another to Pender Island.
+##### Shingle Bay
+
+##### Narvaez Bay
+
#### K8: Fisgard Lighthouse
#### K9: Fort Rodd Hill
diff --git a/components/park-reserve-link/index.tsx b/components/park-reserve-link/index.tsx
index 55d0df1a..3752ac8f 100644
--- a/components/park-reserve-link/index.tsx
+++ b/components/park-reserve-link/index.tsx
@@ -1,17 +1,11 @@
+import {
+ getReservationUrl,
+ Park,
+ ParkReserveParams,
+} from "@/projects/outdoors/reservations";
import styles from "./styles.module.css";
-export enum ParksOrg {
- ParksCanada,
-}
-
-export interface ParkReserveLinkProps {
- org: ParksOrg;
- checkInDate?: Date;
- checkOutDate?: Date;
- searchTime?: Date;
-}
-
-export async function ParkReserveLink(props: ParkReserveLinkProps) {
+export async function ParkReserveLink(props: ParkReserveParams) {
const url = getReservationUrl(props);
return (
@@ -20,57 +14,4 @@ export async function ParkReserveLink(props: ParkReserveLinkProps) {
);
}
-export function getReservationUrl({
- org,
- checkInDate,
- checkOutDate,
- searchTime,
-}: ParkReserveLinkProps): string {
- const url = new URL(baseUrl(org));
- url.searchParams.set("resourceLocationId", "-2147483623");
- url.searchParams.set("mapId", "-2147483535");
- url.searchParams.set("searchTabGroupId", "2");
- url.searchParams.set("bookingCategoryId", "1");
-
- const [t1, t2] = todayAndTomorrow();
- url.searchParams.set(
- "startDate",
- (checkInDate ?? t1).toLocaleDateString("sv")
- );
- url.searchParams.set(
- "endDate",
- (checkOutDate ?? t2).toLocaleDateString("sv")
- );
-
- url.searchParams.set("nights", "1"); // TODO: compute
- url.searchParams.set("isReserving", "true");
- url.searchParams.set("partySize", "2"); // TODO: prop param
- url.searchParams.set(
- "searchTime",
- (searchTime ?? new Date()).toLocaleString("sv").replace(" ", "T")
- );
- url.searchParams.set("flexibleSearch", "[false,false,null,1]"); // TODO how to use this?
- url.searchParams.set("filterData", '{"-32756":"[[1],0,0,0]"}'); // TODO how to use this?
- return url.toString();
-}
-
-function baseUrl(org: ParksOrg): string {
- switch (org) {
- case ParksOrg.ParksCanada:
- return "https://reservation.pc.gc.ca/create-booking/results";
- default:
- throw new Error(`${org} not mapped to a reservation website`);
- }
-}
-
-/**
- * Returns today and tomorrow to be used in search params.
- * @returns a pair of strings in yyyy-mm-dd format
- */
-function todayAndTomorrow(): [Date, Date] {
- const offset = new Date().getTimezoneOffset();
- const today = new Date(new Date().getTime() - offset * 60 * 1000);
- const tomorrow = new Date(new Date().getTime() - offset * 60 * 1000);
- tomorrow.setDate(today.getDate() + 1);
- return [today, tomorrow];
-}
+export { Park };
diff --git a/components/park-reserve-link/reserve-link.test.ts b/components/park-reserve-link/reserve-link.test.ts
deleted file mode 100644
index 7946c813..00000000
--- a/components/park-reserve-link/reserve-link.test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { ParksOrg, getReservationUrl } from "@/components/park-reserve-link";
-
-describe(getReservationUrl.name, () => {
- const t0 = new Date(1704236645000); // 2024-01-02 3:04:05 PM (GMT-08:00)
- const t1 = new Date(1704323045000); // 1 day after t0
- const t2 = new Date(1704409445000); // 1 day after t1
-
- it("should work for Fort Langley National Historic Site", () => {
- const url = getReservationUrl({
- org: ParksOrg.ParksCanada,
- checkInDate: t1,
- checkOutDate: t2,
- searchTime: t0,
- });
-
- const expectedUrl = [
- "https://reservation.pc.gc.ca/create-booking/results",
- "?resourceLocationId=-2147483623&mapId=-2147483535",
- "&searchTabGroupId=2&bookingCategoryId=1",
- "&startDate=2024-01-03&endDate=2024-01-04&nights=1",
- "&isReserving=true&partySize=2",
- "&searchTime=2024-01-02T15%3A04%3A05",
- "&flexibleSearch=%5Bfalse%2Cfalse%2Cnull%2C1%5D",
- "&filterData=%7B%22-32756%22%3A%22%5B%5B1%5D%2C0%2C0%2C0%5D%22%7D",
- ].join("");
- expect(url).toBe(expectedUrl);
- });
-});
diff --git a/projects/outdoors/reservations.ts b/projects/outdoors/reservations.ts
new file mode 100644
index 00000000..268aa0ea
--- /dev/null
+++ b/projects/outdoors/reservations.ts
@@ -0,0 +1,140 @@
+export enum Park {
+ FortLangleyNHS,
+ SMONEĆTEN,
+}
+
+export enum Org {
+ ParksCanada,
+ BCParks,
+}
+
+export enum Group {
+ Frontcountry = 0,
+ Backcountry = 1,
+ Accommodations = 2,
+ DayUse = 3,
+}
+
+export enum Category {
+ Campsite = 0,
+ Accommodation = 1,
+ GroupCampsite = 2,
+ BackcountryCampsite = 5,
+ BackcountryZone = 7,
+}
+
+export enum EquipmentType {
+ TentOrVehicle = "-32768",
+ TentsOnly = "-32767",
+}
+
+export enum TentOrVehicleSubtype {
+ SmallTent = "-32768",
+ MediumTent = "-32767",
+ LargeTent = "-32766",
+ VanOrPickup = "-32765",
+}
+
+export interface TentOrVehicleSpec {
+ equipment: EquipmentType.TentOrVehicle;
+ subtype: TentOrVehicleSubtype[];
+}
+
+export type EquipmentSpec = TentOrVehicleSpec;
+
+export interface ParkInfo {
+ org: Org;
+ group: Group;
+ category: Category;
+ mapId: string;
+ resourceLocationId: string;
+ equipment?: EquipmentSpec;
+}
+
+function getParkInfo(park: Park): ParkInfo {
+ return PARKS[park];
+}
+
+export interface ParkReserveParams {
+ park: Park;
+ checkInDate?: Date;
+ checkOutDate?: Date;
+ searchTime?: Date;
+}
+
+export function getReservationUrl({
+ park,
+ checkInDate,
+ checkOutDate,
+ searchTime,
+}: ParkReserveParams): string {
+ const { org, group, category, mapId, resourceLocationId } = getParkInfo(park);
+ const url = new URL(baseUrl(org));
+ url.searchParams.set("searchTabGroupId", group.toString());
+ url.searchParams.set("bookingCategoryId", category.toString());
+ url.searchParams.set("mapId", mapId);
+ url.searchParams.set("resourceLocationId", resourceLocationId);
+
+ const [t1, t2] = todayAndTomorrow();
+ url.searchParams.set(
+ "startDate",
+ (checkInDate ?? t1).toLocaleDateString("sv")
+ );
+ url.searchParams.set(
+ "endDate",
+ (checkOutDate ?? t2).toLocaleDateString("sv")
+ );
+
+ url.searchParams.set("nights", "1"); // TODO: compute
+ url.searchParams.set("isReserving", "true");
+ url.searchParams.set("partySize", "2"); // TODO: prop param
+ url.searchParams.set(
+ "searchTime",
+ (searchTime ?? new Date()).toLocaleString("sv").replace(" ", "T")
+ );
+ url.searchParams.set("flexibleSearch", "[false,false,null,1]"); // TODO how to use this?
+ url.searchParams.set("filterData", '{"-32756":"[[1],0,0,0]"}'); // TODO how to use this?
+ return url.toString();
+}
+
+function baseUrl(org: Org): string {
+ switch (org) {
+ case Org.ParksCanada:
+ return "https://reservation.pc.gc.ca/create-booking/results";
+ default:
+ throw new Error(`${org} not mapped to a reservation website`);
+ }
+}
+
+const PARKS: Record = {
+ [Park.FortLangleyNHS]: {
+ org: Org.ParksCanada,
+ group: Group.Accommodations,
+ category: Category.Accommodation,
+ mapId: "-2147483535",
+ resourceLocationId: "-2147483623",
+ },
+ [Park.SMONEĆTEN]: {
+ org: Org.ParksCanada,
+ group: Group.Frontcountry,
+ category: Category.Campsite,
+ mapId: "-2147483477",
+ resourceLocationId: "-2147483601",
+ equipment: {
+ equipment: EquipmentType.TentOrVehicle,
+ subtype: [TentOrVehicleSubtype.SmallTent],
+ },
+ },
+};
+
+/**
+ * Returns today and tomorrow to be used in search params.
+ * @returns a pair of strings in yyyy-mm-dd format
+ */
+function todayAndTomorrow(): [Date, Date] {
+ const offset = new Date().getTimezoneOffset();
+ const today = new Date(new Date().getTime() - offset * 60 * 1000);
+ const tomorrow = new Date(new Date().getTime() - offset * 60 * 1000);
+ tomorrow.setDate(today.getDate() + 1);
+ return [today, tomorrow];
+}
diff --git a/projects/outdoors/reserve-link.test.ts b/projects/outdoors/reserve-link.test.ts
new file mode 100644
index 00000000..99c21689
--- /dev/null
+++ b/projects/outdoors/reserve-link.test.ts
@@ -0,0 +1,50 @@
+import { getReservationUrl, Park } from "./reservations";
+
+describe(getReservationUrl.name, () => {
+ const defaultArgs = {
+ searchTime: new Date(1704236645000), // 2024-01-02 3:04:05 PM (GMT-08:00)
+ checkInDate: new Date(1704323045000), // 1 day after t0
+ checkOutDate: new Date(1704409445000), // 1 day after t1
+ };
+ const defaultParams = [
+ "&startDate=2024-01-03&endDate=2024-01-04&nights=1",
+ "&isReserving=true&partySize=2",
+ "&searchTime=2024-01-02T15%3A04%3A05",
+ "&flexibleSearch=%5Bfalse%2Cfalse%2Cnull%2C1%5D",
+ ];
+
+ it("should work for Fort Langley National Historic Site", () => {
+ const url = getReservationUrl({
+ park: Park.FortLangleyNHS,
+ ...defaultArgs,
+ });
+ const expectedUrl = [
+ "https://reservation.pc.gc.ca/create-booking/results",
+ "?searchTabGroupId=2", // accommodation
+ "&bookingCategoryId=1", // accommodation
+ "&mapId=-2147483535",
+ "&resourceLocationId=-2147483623",
+ ...defaultParams,
+ "&filterData=%7B%22-32756%22%3A%22%5B%5B1%5D%2C0%2C0%2C0%5D%22%7D",
+ ].join("");
+ expect(url).toBe(expectedUrl);
+ });
+
+ it("should work for SMONEĆTEN", () => {
+ const url = getReservationUrl({
+ park: Park.SMONEĆTEN,
+ ...defaultArgs,
+ });
+ const expectedUrl = [
+ "https://reservation.pc.gc.ca/create-booking/results",
+ "?searchTabGroupId=0", // frontcountry
+ "&bookingCategoryId=0", // campsite
+ "&mapId=-2147483477",
+ "&resourceLocationId=-2147483601",
+ ...defaultParams,
+ "&filterData=%7B%7D&equipmentId=-32768",
+ "&subEquipmentId=-32768",
+ ].join("");
+ expect(url).toBe(expectedUrl);
+ });
+});