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