+
+ )}
+
+ );
+}
diff --git a/package-lock.json b/package-lock.json
index 97a33eb..f3f0ffa 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,11 @@
"name": "glenzz",
"version": "0.1.0",
"dependencies": {
+ "@fortawesome/fontawesome-svg-core": "^6.7.2",
+ "@fortawesome/free-brands-svg-icons": "^6.7.2",
+ "@fortawesome/free-regular-svg-icons": "^6.7.2",
+ "@fortawesome/free-solid-svg-icons": "^6.7.2",
+ "@fortawesome/react-fontawesome": "^0.2.2",
"lucide-react": "^0.473.0",
"mapbox": "^1.0.0-beta10",
"mapbox-gl": "^3.9.3",
@@ -15,11 +20,13 @@
"motion": "^11.18.1",
"next": "15.1.5",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "zod": "^3.24.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
+ "@types/mapbox-gl": "^3.4.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -309,6 +316,76 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@fortawesome/fontawesome-common-types": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.2.tgz",
+ "integrity": "sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/fontawesome-svg-core": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
+ "integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
+ "license": "MIT",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.7.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-brands-svg-icons": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
+ "integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.7.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-regular-svg-icons": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
+ "integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.7.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/free-solid-svg-icons": {
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
+ "integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
+ "license": "(CC-BY-4.0 AND MIT)",
+ "dependencies": {
+ "@fortawesome/fontawesome-common-types": "6.7.2"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@fortawesome/react-fontawesome": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
+ "integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "@fortawesome/fontawesome-svg-core": "~1 || ~6",
+ "react": ">=16.3"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -1165,6 +1242,16 @@
"@types/pbf": "*"
}
},
+ "node_modules/@types/mapbox-gl": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
+ "integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "20.17.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.14.tgz",
@@ -4014,7 +4101,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -4190,7 +4276,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -4525,7 +4610,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -5112,7 +5196,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -5188,7 +5271,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/read-cache": {
@@ -6540,6 +6622,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zod": {
+ "version": "3.24.1",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
+ "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/package.json b/package.json
index 0c26238..bdc59db 100644
--- a/package.json
+++ b/package.json
@@ -11,6 +11,11 @@
"format": "prettier --write ."
},
"dependencies": {
+ "@fortawesome/fontawesome-svg-core": "^6.7.2",
+ "@fortawesome/free-brands-svg-icons": "^6.7.2",
+ "@fortawesome/free-regular-svg-icons": "^6.7.2",
+ "@fortawesome/free-solid-svg-icons": "^6.7.2",
+ "@fortawesome/react-fontawesome": "^0.2.2",
"lucide-react": "^0.473.0",
"mapbox": "^1.0.0-beta10",
"mapbox-gl": "^3.9.3",
@@ -18,11 +23,13 @@
"motion": "^11.18.1",
"next": "15.1.5",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "zod": "^3.24.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@ianvs/prettier-plugin-sort-imports": "^4.4.1",
+ "@types/mapbox-gl": "^3.4.1",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
diff --git a/types/evChargingStations.ts b/types/evChargingStations.ts
new file mode 100644
index 0000000..f69ad77
--- /dev/null
+++ b/types/evChargingStations.ts
@@ -0,0 +1,158 @@
+import { z } from 'zod';
+
+export const EVChargingStationSchema = z
+ .object({
+ /**
+ * Acronym/Initialism of City agency or entity the station is located at
+ */
+ agency: z.string(),
+
+ /**
+ * Street number of station's address.
+ */
+ station_name: z.string(),
+
+ /**
+ * The type of charger at the station
+ */
+ type_of_charger: z.enum([
+ 'DOT Municipal Level 2 Charger',
+ 'DOT Municipal Level 3 Charger',
+ 'EV Solar Arc Charger',
+ 'EV Solar Canopy Charger',
+ 'L2 DOT Flo Curbside Charger',
+ 'Level 2 Charger',
+ 'Level 3 Fast Charger',
+ 'Mobile Charger',
+ ]),
+
+ /**
+ * The # of ports at the station
+ */
+ no_of_ports: z.coerce.number(),
+
+ /**
+ * The street address of the station
+ */
+ street: z.string().optional(),
+
+ /**
+ * The city of the station's location.
+ */
+ city: z.string().optional(),
+
+ /**
+ * ZIP code of the station's location.
+ */
+ postcode: z.coerce.number().optional(),
+
+ /**
+ * New York City's boroughs are five county-level administrative divisions,
+ * with each one also being a state county. (Manhattan - New York County;
+ * Bronx - Bronx County; Brooklyn - Kings County; Queens - Queens County;
+ * Staten Island - Richmond County). NYC's boroughs have existed since the
+ * consolidation of the city in 1898.
+ */
+ borough: z
+ .enum([
+ 'Bronx',
+ 'Brooklyn',
+ 'Manhattan',
+ 'Queens',
+ 'Staten Island',
+ 'Upstate',
+ ])
+ .optional(),
+
+ /**
+ * Is the charging station open to the general public?
+ */
+ public_charger: z
+ .enum([
+ '*adapter required for use',
+ 'City Hall Charging',
+ 'NYC DC Fast Public Charging',
+ 'Solar Carport Charging',
+ 'Yes',
+ ])
+ .optional(),
+
+ /**
+ * Is there a fee to use the charger at the station for City drivers?
+ */
+ fee_for_city_drivers: z.string().optional(),
+
+ /**
+ * Latitude of station's address.
+ */
+ latitude: z.coerce.number().optional(),
+
+ /**
+ * Longitude of station's address.
+ */
+ longitude: z.coerce.number().optional(),
+
+ /**
+ * Community Districts are geographic areas created under the City Charter
+ * that are used for the delivery of municipal services, to facilitate
+ * community engagement, and as a common unit of analysis. The City is
+ * divided into 59 community districts. Each Community District has a
+ * Community Board.
+ */
+ community_district: z.coerce.number().optional(),
+
+ /**
+ * The City Council is the lawmaking body of NYC, on equal footing with
+ * the mayor in terms of governing power. Besides legislating, the Council
+ * has sole approval power over the city budget, and is the final
+ * decision-maker in land use matters. There are 51 City Council members
+ * in total, each representing a unique geographical area, called a
+ * Council District.
+ */
+ council_district: z.coerce.number().optional(),
+
+ /**
+ * Geographic area defined by the U.S. Census Bureau for the various
+ * decennial censuses. Census tracts for a particular census year are
+ * numbered uniquely within borough. Please note that as part of the
+ * geocoding process, leading and trailing zeros are dropped.
+ */
+ census_tract: z.coerce.number().optional(),
+
+ /**
+ * A unique 7-digit number assigned to every known building by the
+ * Department of City Planning (DCP), the first digit of which is the
+ * borough code. BINs allow city agencies to process and match
+ * building-related data easily and in a consistent manner.
+ */
+ bin: z.coerce.number().optional(),
+
+ /**
+ * A combination of three numeric codes -- a 1-digit borough number, a
+ * block number (up to 5 digits) and a lot number (up to 4 digits) --
+ * designated and modified by the Department of Finance (DOF). BBLs are
+ * used by various city agencies to identify real estate for taxes,
+ * zoning, construction, and other purposes.
+ */
+ bbl: z.coerce.number().optional(),
+
+ /**
+ * NTAs are small area boundaries, created by the Department of City
+ * Planning (DCP) to aggregate population projections in a small area.
+ * Each NTA approximates a minimum population of 15,000. While NTAs were
+ * initially created to support PlaNYC, the thirty-year (2000-2030)
+ * sustainability plan for NYC, NTAs are now also being used to present
+ * data from the Decennial Census and American Community Survey. NTA
+ * boundaries and their associated names do not definitively represent
+ * neighborhood boundaries.
+ */
+ nta: z.string().optional(),
+
+ // Unknown Keys - keys that are not in the docs but are in the data
+ public_charger_: z.string().optional(),
+ })
+ .strict();
+
+export type EVChargingStation = z.infer;
+
+export const EVChargingStationsSchema = z.array(EVChargingStationSchema);
diff --git a/types/outages.ts b/types/outages.ts
new file mode 100644
index 0000000..739b805
--- /dev/null
+++ b/types/outages.ts
@@ -0,0 +1,248 @@
+import { z } from 'zod';
+
+export const OutageSchema = z
+ .object({
+ /**
+ * Unique identifier of a Service Request (SR) in the open data set
+ */
+ unique_key: z.string(),
+
+ /**
+ * Date SR was created
+ */
+ created_date: z.coerce.date(),
+
+ /**
+ * Date SR was closed by responding agency
+ */
+ closed_date: z.coerce.date().optional(),
+
+ /**
+ * Acronym of responding City Government Agency
+ */
+ agency: z.enum(['HPD']),
+
+ /**
+ * Full Agency name of responding City Government Agency
+ */
+ agency_name: z.enum([
+ 'Department of Housing Preservation and Development',
+ 'Division of Alternative Management',
+ ]),
+
+ /**
+ * This is the first level of a hierarchy identifying the topic of the
+ * incident or condition. Complaint Type may have a corresponding
+ * Descriptor (below) or may stand alone.
+ */
+ complaint_type: z.enum(['ELECTRIC', 'Electric']),
+
+ /**
+ * This is associated to the Complaint Type, and provides further detail on
+ * the incident or condition. Descriptor values are dependent on the
+ * Complaint Type, and are not always required in SR.
+ */
+ descriptor: z.enum(['POWER OUTAGE', 'Power Outage']),
+
+ /**
+ * Describes the type of location used in the address information
+ */
+ location_type: z.enum([
+ 'Apartment',
+ 'Building-Wide',
+ 'RESIDENTIAL BUILDING',
+ ]),
+
+ /**
+ * Incident location zip code, provided by geo validation.
+ */
+ incident_zip: z.coerce.number().optional(),
+
+ /**
+ * House number of incident address provided by submitter.
+ */
+ incident_address: z.string(),
+
+ /**
+ * Street name of incident address provided by the submitter
+ */
+ street_name: z.string(),
+
+ /**
+ * First Cross street based on the geo validated incident location
+ */
+ cross_street_1: z.string().optional(),
+
+ /**
+ * Second Cross Street based on the geo validated incident location
+ */
+ cross_street_2: z.string().optional(),
+
+ /**
+ * First intersecting street based on geo validated incident location
+ */
+ intersection_street_1: z.string().optional(),
+
+ /**
+ * Second intersecting street based on geo validated incident location
+ */
+ intersection_street_2: z.string().optional(),
+
+ /**
+ * Type of incident location information available.
+ */
+ address_type: z.enum(['ADDRESS']).optional(),
+
+ /**
+ * City of the incident location provided by geovalidation.
+ */
+ city: z.string().optional(),
+
+ /**
+ * If the incident location is identified as a Landmark the name of the
+ * landmark will display here
+ */
+ landmark: z.string().optional(),
+
+ /**
+ * If available, this field describes the type of city facility associated
+ * to the SR
+ */
+ facility_type: z.string().optional(),
+
+ /**
+ * Status of SR submitted
+ */
+ status: z.enum(['Closed', 'In Progress', 'Open']),
+
+ /**
+ * Date when responding agency is expected to update the SR. This is based
+ * on the Complaint Type and internal Service Level Agreements (SLAs).
+ */
+ due_date: z.coerce.date().optional(),
+
+ /**
+ * Date when responding agency last updated the SR.
+ */
+ resolution_action_updated_date: z.coerce.date().optional(),
+
+ /**
+ * Provided by geovalidation.
+ */
+ community_board: z.string(),
+
+ /**
+ * Provided by the submitter and confirmed by geovalidation.
+ */
+ borough: z.enum([
+ 'BRONX',
+ 'BROOKLYN',
+ 'MANHATTAN',
+ 'QUEENS',
+ 'STATEN ISLAND',
+ 'Unspecified',
+ ]),
+
+ /**
+ * Geo validated, X coordinate of the incident location.
+ */
+ x_coordinate_state_plane: z.coerce.number().optional(),
+
+ /**
+ * Geo validated, Y coordinate of the incident location.
+ */
+ y_coordinate_state_plane: z.coerce.number().optional(),
+
+ /**
+ * If the incident location is a Parks Dept facility, the Name of the
+ * facility will appear here
+ */
+ park_facility_name: z.string(),
+
+ /**
+ * The borough of incident if it is a Parks Dept facility
+ */
+ park_borough: z.enum([
+ 'BRONX',
+ 'BROOKLYN',
+ 'MANHATTAN',
+ 'QUEENS',
+ 'STATEN ISLAND',
+ 'Unspecified',
+ ]),
+
+ /**
+ * If the incident is a taxi, this field describes the type of TLC
+ * vehicle.
+ */
+ vehicle_type: z.string().optional(),
+
+ /**
+ * If the incident is identified as a taxi, this field will display the
+ * borough of the taxi company.
+ */
+ taxi_company_borough: z.string().optional(),
+
+ /**
+ * If the incident is identified as a taxi, this field displays the taxi
+ * pick up location
+ */
+ taxi_pick_up_location: z.string().optional(),
+
+ /**
+ * If the incident is identified as a Bridge/Highway, the name will be
+ * displayed here.
+ */
+ bridge_highway_name: z.string().optional(),
+
+ /**
+ * If the incident is identified as a Bridge/Highway, the direction where
+ * the issue took place would be displayed here.
+ */
+ bridge_highway_direction: z.string().optional(),
+
+ /**
+ * If the incident location was Bridge/Highway this column differentiates
+ * if the issue was on the Road or the Ramp.
+ */
+ road_ramp: z.string().optional(),
+
+ /**
+ * Additional information on the section of the Bridge/Highway were the
+ * incident took place.
+ */
+ bridge_highway_segment: z.string().optional(),
+
+ /**
+ * Geo based Lat of the incident location
+ */
+ latitude: z.coerce.number().optional(),
+
+ /**
+ * Geo based Long of the incident location
+ */
+ longitude: z.coerce.number().optional(),
+
+ /**
+ * Combination of the geo based lat & long of the incident location
+ */
+ location: z
+ .object({
+ coordinates: z.tuple([z.number(), z.number()]),
+ type: z.literal('Point'),
+ })
+ .strict()
+ .optional(),
+
+ // Unknown Keys - keys that are not in the docs but are in the data
+ ':@computed_region_sbqj_enih': z.coerce.number().optional(),
+ ':@computed_region_efsh_h5xi': z.coerce.number().optional(),
+ ':@computed_region_92fq_4b7q': z.coerce.number().optional(),
+ ':@computed_region_yeji_bk3q': z.coerce.number().optional(),
+ ':@computed_region_f5dn_yrer': z.coerce.number().optional(),
+ })
+ .strict();
+
+export type Outage = z.infer;
+
+export const OutagesSchema = z.array(OutageSchema);
diff --git a/types/projects.ts b/types/projects.ts
new file mode 100644
index 0000000..601261f
--- /dev/null
+++ b/types/projects.ts
@@ -0,0 +1,357 @@
+import { z } from 'zod';
+
+export const ProjectSchema = z
+ .object({
+ /** The date the dataset was refreshed */
+ data_through_date: z.coerce.date(),
+
+ /**
+ * Either Maintenance, Non-Tier 1, Tier 1, Tier 2, Tier 4, OREC, or N/A.
+ * Maintenance represents existing baseline resources. Non-Tier 1 are
+ * facilities with a commercial operation date before 1/1/2015, and Tier 1
+ * are facilities with a commercial operation date after 1/1/2015. Tier 2
+ * facilities are from wind and non-state-owned run-of-river hydroelectric
+ * generating facilities located within New York State that entered
+ * commercial operation prior to January 1, 2015. OREC represents projects
+ * in the offshore wind program. Tier 4 represents renewable energy and
+ * transmission projects.
+ */
+ eligibility: z
+ .enum([
+ 'Maintenance',
+ 'Non-Tier 1',
+ 'OREC',
+ 'Tier 1',
+ 'Tier 2',
+ 'Tier 4',
+ 'N/A',
+ ])
+ .optional(),
+
+ /**
+ * Name of project. Project names with an asterisk have the same project
+ * name as projects awarded under different solicitations
+ */
+ project_name: z.string(),
+
+ /**
+ * Request for Proposal (RFP) number, or Maintenance Agreement number under
+ * Renewable Portfolio Standard (RPS) or Tier 2 Maintenance under the
+ * Renewable Energy Standard (RES)
+ */
+ solicitation_name: z.string(),
+
+ /**
+ * Certain solicitations provided for an optional or mandatory adjustment to
+ * the Index REC Strike Price or Fixed REC Price based on changes in
+ * certain commodity price indices as described in each RFP. Proposers
+ * awarded under these solicitations whose proposals were subject to this
+ * adjustment may have their Index REC Strike Price or Fixed REC Price
+ * adjusted consistent with the methodology described in each applicable RFP.
+ */
+ inflation_adjustment: z.enum(['Yes']).optional(),
+
+ /**
+ * The Fixed REC bid price of Tier 1 and Non-Tier 1 awarded projects in US
+ * dollars. Blank cells represent data that was not required or is not
+ * currently available.
+ */
+ fixed_rec_price: z.coerce.number().optional(),
+
+ /**
+ * The Index OREC bid price or the Index REC Conversion Offer Price of
+ * Tier 1 or Off-shore wind awarded projects in US dollars. Blank cells
+ * represent data that was not required or is not currently available.
+ */
+ index_orec_strike_price: z.coerce.number().optional(),
+
+ /**
+ * Indicates if a Non-Tier 1 or Tier 1 Project claimed Incremental Economic
+ * Benefits as part of its Bid Proposal, and these claims were accepted by
+ * NYSERDA as part of its award. Blank cells represent data that was not
+ * required or is not currently available. For more information, please
+ * refer to each specific RFP's Section on Incremental Economic Benefits to
+ * New York State.
+ */
+ incremental_economic_benefits_claimed: z.enum(['Yes']).optional(),
+
+ /**
+ * For Non-Tier 1 or Tier I Projects, indicates the verification status of
+ * the Incremental Economic Benefits included in the projects Agreement.
+ * Incremental Economic Benefits are verified following three years of
+ * operation and must meet a threshold of 85% of claim accepted by NYSERDA.
+ * Yes - indicates verification complete and project met threshold. No –
+ * verification complete and project did not meet threshold. Blank cells
+ * represent – verification not due yet/ not yet complete; no economic
+ * benefit claim; or project cancelled/terminated.
+ */
+ project_met_economic_benefits_threshold: z.enum(['Yes']).optional(),
+
+ /**
+ * The type of renewable energy technology for the project currently under
+ * contract or installed; either Land Based Wind, Offshore Wind,
+ * Hydroelectric, Biomass, Biogas-LFG, Biogas-ADG, Fuel Cell, Solar,
+ * Geothermal, Maintenance Biomass, or Maintenance Hydroelectric.
+ */
+ renewable_technology: z
+ .enum([
+ 'Biogas - ADG',
+ 'Biogas - LFG',
+ 'Biomass',
+ 'Fuel Cell',
+ 'Hydroelectric',
+ 'Land Based Wind',
+ 'Land-based Wind',
+ 'Maintenance Biomass',
+ 'Maintenance Hydroelectric',
+ 'Offshore Wind',
+ 'Solar',
+ 'Wind/Solar',
+ ])
+ .optional(),
+
+ /**
+ * Indicates the type of eligible generation, either Existing or New
+ * Generation. Blank cells represent data that was not required or is not
+ * currently available.
+ */
+ generation_type: z.enum(['Existing', 'New']).optional(),
+
+ /**
+ * Vintage Generation Facility (VGS). VGFs are projects that are either
+ * upgraded, returned to service, relocated, or repowered, and must meet
+ * the specified requirements
+ */
+ type_of_existing: z
+ .enum(['Repower', 'Return to Service', 'Upgrade'])
+ .optional(),
+
+ /**
+ * Name of the Seller in NYSERDA's agreement. A counterparty is the other
+ * party to a NYSERDA agreement. Blank cells represent data that was not
+ * required or is not currently available.
+ */
+ counterparty: z.string().optional(),
+
+ /**
+ * Name of Developer responsible for developing the project. Blank cells
+ * represent data that was not required or is not currently available.
+ */
+ developer_name: z.string().optional(),
+
+ /**
+ * Rating of the electric power to be delivered from an energy storage
+ * project in alternating current (AC) power. Blank cells represent data
+ * that was not required or is not currently available.
+ */
+ energy_storage_power_capacity_mwac: z.coerce.number().optional(),
+
+ /**
+ * Usable installed energy storage capacity measured in alternating current
+ * (AC) power. It is equal to the total capacity measured during a complete
+ * discharge from a 100% usable state of charge, performed in accordance
+ * with the storage manufacturer's specifications, on the commercial
+ * operation date. Blank cells represent data that was not required or is
+ * not currently available. This category does not include pumped storage.
+ */
+ energy_storage_energy_capacity_mwh: z.coerce.number().optional(),
+
+ /**
+ * Indicates the New York Control Area Load Zone into which the Bid
+ * Facility will interconnect or deliver. Blank cells represent data that
+ * was not required or is not currently available.
+ */
+ nyiso_zone: z.string().optional(),
+
+ /**
+ * Indicates the NYISO Generator Point Identifier. A Point Identifier is a
+ * resource-specific numerical identifier used by the NYISO's software
+ * systems to identify Generators and other Suppliers and only applies to
+ * Operating Projects. Blank cells represent data that was not required or
+ * is not currently available.
+ */
+ ptid: z.string().optional(),
+
+ /**
+ * For the NYISO Interconnection Queue, indicates the number on the list of
+ * transmission and generation projects seeking to join the grid.
+ * Additional information is located at:
+ * https://www.nyiso.com/interconnections. Tier 4 projects include both
+ * Resources and Transmission numbers.
+ */
+ interconnection_queue_number: z.string().optional(),
+
+ /**
+ * The applicable federal, state, or local permitting requirements that the
+ * Bid Facility is subject to. Blank cells represent data that was not
+ * required or is not currently available.
+ */
+ permit_process: z.string().optional(),
+
+ /**
+ * For Offshore Wind and Tier 4 projects, the Federal Permitting Mechanism
+ * related to Environmental Review and Permitting. For Tier 1 projects, the
+ * Name of Article 10 or Article VIII Case Number. Blank cells represent
+ * data that were not required, applicable or are not currently available
+ */
+ regulatory_permitting: z
+ .object({
+ url: z.string(),
+ })
+ .strict()
+ .optional(),
+
+ /** Name of Article VII pertaining to Offshore Wind Project or Tier 4 Project. */
+ article_vii: z
+ .object({
+ url: z.string(),
+ })
+ .strict()
+ .optional(),
+
+ /**
+ * ZIP code of project. Blank cells represent data that was not required or
+ * is not currently available.
+ */
+ zip_code: z.string().optional(),
+
+ /**
+ * Name of US county or Canadian Province for project. Blank cells
+ * represent data that was not required or is not currently available.
+ */
+ county_province: z.string().optional(),
+
+ /**
+ * Name of US state or Canadian Province for project. Blank cells represent
+ * data that was not required or is not currently available.
+ */
+ state_province: z.string().optional(),
+
+ /**
+ * Regional Economic Development Council of project. In 2011, 10 Regional
+ * Councils were established and charged with developing long-term
+ * strategic plans for economic growth in their regions. Blank cells
+ * indicate the project is not located in New York State.
+ */
+ redc: z.string().optional(),
+
+ /**
+ * The phase/status of which the project is in as of the Data Through
+ * Date; either Cancelled, Completed, Under Development, or Operational. A
+ * status of Cancelled means that NYSERDA's award or contract with the
+ * counterparty was cancelled or terminated. A status of Completed means
+ * the project has fulfilled their contractual obligation to NYSERDA. A
+ * status of Under Development means that the project has not yet entered
+ * operation. A status of Operational means that the project has entered
+ * operation.
+ */
+ project_status: z.enum([
+ 'Cancelled',
+ 'Completed',
+ 'Under Development',
+ 'Operational',
+ ]),
+
+ /**
+ * Date declared as the Commercial Operation date of facility by NYISO or
+ * local utility. Blank cells represent data that was not required or is
+ * not currently available.
+ */
+ year_of_commercial_operation: z.string().optional(),
+
+ /**
+ * Date NYSERDA's payments started or are expected to start. For projects
+ * Under Development, the dates are reflective of the current status of
+ * NYSERDA agreements and are subject to change based on project
+ * development progress. Blank cells represent data that was not required
+ * or is not currently available.
+ */
+ year_of_delivery_start_date: z.string().optional(),
+
+ /** Number of years of performance under the NYSERDA Agreement. */
+ contract_duration: z.coerce.number().optional(),
+
+ /**
+ * For a new facility, the New Renewable Capacity equals the nameplate
+ * capacity of the installed equipment. For a repowered facility, End of
+ * Useful Life Bid Capacity is reported. For an upgraded facility, only the
+ * renewable capacity resulting from the upgrade of a facility is provided.
+ * For a single facility that was awarded multiple contracts, each for a
+ * percentage of the facility's output under multiple RFPs, the total New
+ * Renewable Capacity for the facility is listed once to avoid
+ * over-counting. Therefore, the same facility appearing in multiple
+ * instances may show a New Renewable Capacity of zero. Blank cells
+ * represent projects with expired contracts with NYSERDA.
+ */
+ new_renewable_capacity_mw: z.coerce.number().optional(),
+
+ /**
+ * The NYSERDA-contracted portion of the New Renewable Capacity in
+ * Megawatts. Blank cells represent data that were not applicable,
+ * required or are not currently available.
+ */
+ bid_capacity_mw: z.coerce.number().optional(),
+
+ /**
+ * The annual production of the project in Megawatt hours that NYSERDA and
+ * the Counterparty agree on constitutes performance in an Agreement. The
+ * Bid Quantity may not constitute the entire output of a project. Blank
+ * cells represent data that was not required or is not currently
+ * available.
+ */
+ bid_quantity_mwh: z.coerce.number().optional(),
+
+ /**
+ * The maximum annual contractual obligation of the project in Megawatt
+ * hours. Blank cells represent data that was not required or is not
+ * currently available.
+ */
+ max_annual_contract_quantity: z.coerce.number().optional(),
+
+ /**
+ * An amount of electrical energy (in MWh), such that the estimated
+ * probability in any given year that generation from the Selected Project
+ * delivered to the Delivery Point would exceed that amount is 10 percent.
+ * Applicable to Offshore Wind projects only. Blank cells represent data
+ * that was not required or is not currently available.
+ */
+ p10_annual_orec_exceedance: z.coerce.number().optional(),
+
+ /**
+ * A NYSERDA calculation based on a 10.5 meter/second average Hub Height
+ * (m/s avgHH) wind speed. Weibull parameters of A = 11.3 and K = 2.2 were
+ * used to create an annual wind frequency distribution table. The
+ * resulting frequency distribution was then applied in the NREL 15 MW
+ * turbine power curve assuming 22% losses. Blank cells represent data
+ * there were not required or is not currently available.
+ */
+ p50_generation_calculated_by_nyserda_mwh_yr: z.coerce.number().optional(),
+
+ /**
+ * Capacity of a project's new HVDC transmission line. Applicable to Tier 4
+ * projects only. Blank cells represent data that was not required or is
+ * not currently available.
+ */
+ transmission_capacity_hvdc: z.coerce.number().optional(),
+
+ /**
+ * Open Data/Socrata-generated geocoding information based on supplied
+ * address components.
+ */
+ georeference: z
+ .object({
+ coordinates: z.tuple([z.number(), z.number()]),
+ type: z.literal('Point'),
+ })
+ .strict()
+ .optional(),
+
+ // Unknown Keys - keys that are not in the docs but are in the data
+ transmission_capacity_hvdc_: z.coerce.number().optional(),
+ p50_generation_calculated_by_nyserda_mwh_yr_: z.coerce.number().optional(),
+ })
+ .strict();
+
+export type Project = z.infer;
+
+export const ProjectsSchema = z.array(ProjectSchema);
diff --git a/types/ratSightings.ts b/types/ratSightings.ts
new file mode 100644
index 0000000..14dcc29
--- /dev/null
+++ b/types/ratSightings.ts
@@ -0,0 +1,270 @@
+import { z } from 'zod';
+
+export const RatSightingSchema = z
+ .object({
+ /**
+ * Unique identifier of a Service Request (SR) in the open data set
+ */
+ unique_key: z.coerce.number(),
+
+ /**
+ * Date SR was created
+ */
+ created_date: z.coerce.date(),
+
+ /**
+ * Date SR was closed by responding agency
+ */
+ closed_date: z.coerce.date().optional(),
+
+ /**
+ * Acronym of responding City Government Agency
+ */
+ agency: z.enum(['DOHMH']),
+
+ /**
+ * Full Agency name of responding City Government Agency
+ */
+ agency_name: z.enum(['Department of Health and Mental Hygiene']),
+
+ /**
+ * This is the first level of a hierarchy identifying the topic of the
+ * incident or condition. Complaint Type may have a corresponding
+ * Descriptor (below) or may stand alone.
+ */
+ complaint_type: z.enum(['Rodent']),
+
+ /**
+ * This is associated to the Complaint Type, and provides further detail
+ * on the incident or condition. Descriptor values are dependent on the
+ * Complaint Type, and are not always required in SR.
+ */
+ descriptor: z.enum(['Rat Sighting']),
+
+ /**
+ * Describes the type of location used in the address information
+ */
+ location_type: z.string().optional(),
+
+ /**
+ * Incident location zip code, provided by geo validation.
+ */
+ //? Handle extremely rare edge case, where the string 'N/A' is returned
+ incident_zip: z.preprocess(
+ (val) => (val === 'N/A' ? undefined : val),
+ z.coerce.number().optional()
+ ),
+
+ /**
+ * House number of incident address provided by submitter.
+ */
+ incident_address: z.string().optional(),
+
+ /**
+ * Street name of incident address provided by the submitter
+ */
+ street_name: z.string().optional(),
+
+ /**
+ * First Cross street based on the geo validated incident location
+ */
+ cross_street_1: z.string().optional(),
+
+ /**
+ * Second Cross Street based on the geo validated incident location
+ */
+ cross_street_2: z.string().optional(),
+
+ /**
+ * First intersecting street based on geo validated incident location
+ */
+ intersection_street_1: z.string().optional(),
+
+ /**
+ * Second intersecting street based on geo validated incident location
+ */
+ intersection_street_2: z.string().optional(),
+
+ /**
+ * Type of incident location information available.
+ */
+ address_type: z
+ .enum([
+ 'ADDRESS',
+ 'BLOCKFACE',
+ 'INTERSECTION',
+ 'LATLONG',
+ 'PLACENAME',
+ 'UNRECOGNIZED',
+ ])
+ .optional(),
+
+ /**
+ * City of the incident location provided by geovalidation.
+ */
+ city: z.string().optional(),
+
+ /**
+ * If the incident location is identified as a Landmark the name of the
+ * landmark will display here
+ */
+ landmark: z.string().optional(),
+
+ /**
+ * If available, this field describes the type of city facility
+ * associated to the SR
+ */
+ facility_type: z.string().optional(),
+
+ /**
+ * Status of SR submitted
+ */
+ status: z.enum([
+ 'Assigned',
+ 'Closed',
+ 'Draft',
+ 'In Progress',
+ 'Open',
+ 'Pending',
+ 'Unspecified',
+ ]),
+
+ /**
+ * Date when responding agency is expected to update the SR. This is
+ * based on the Complaint Type and internal Service Level Agreements
+ * (SLAs).
+ */
+ due_date: z.coerce.date().optional(),
+
+ /**
+ * Date when responding agency last updated the SR.
+ */
+ resolution_action_updated_date: z.coerce.date().optional(),
+
+ /**
+ * Provided by geovalidation.
+ */
+ community_board: z.string().optional(),
+
+ /**
+ * Provided by the submitter and confirmed by geovalidation.
+ */
+ borough: z
+ .enum([
+ 'BRONX',
+ 'BROOKLYN',
+ 'MANHATTAN',
+ 'QUEENS',
+ 'STATEN ISLAND',
+ 'Unspecified',
+ ])
+ .optional(),
+
+ /**
+ * Geo validated, X coordinate of the incident location.
+ */
+ x_coordinate_state_plane: z.coerce.number().optional(),
+
+ /**
+ * Geo validated, Y coordinate of the incident location.
+ */
+ y_coordinate_state_plane: z.coerce.number().optional(),
+
+ /**
+ * If the incident location is a Parks Dept facility, the Name of the
+ * facility will appear here
+ */
+ park_facility_name: z.string(),
+
+ /**
+ * The borough of incident if it is a Parks Dept facility
+ */
+ park_borough: z
+ .enum([
+ 'BRONX',
+ 'BROOKLYN',
+ 'MANHATTAN',
+ 'QUEENS',
+ 'STATEN ISLAND',
+ 'Unspecified',
+ '',
+ ])
+ .optional(),
+
+ /**
+ * If the incident is a taxi, this field describes the type of TLC
+ * vehicle.
+ */
+ vehicle_type: z.string().optional(),
+
+ /**
+ * If the incident is identified as a taxi, this field will display the
+ * borough of the taxi company.
+ */
+ taxi_company_borough: z.string().optional(),
+
+ /**
+ * If the incident is identified as a taxi, this field displays the taxi
+ * pick up location
+ */
+ taxi_pick_up_location: z.string().optional(),
+
+ /**
+ * If the incident is identified as a Bridge/Highway, the name will be
+ * displayed here.
+ */
+ bridge_highway_name: z.string().optional(),
+
+ /**
+ * If the incident is identified as a Bridge/Highway, the direction where
+ * the issue took place would be displayed here.
+ */
+ bridge_highway_direction: z.string().optional(),
+
+ /**
+ * If the incident location was Bridge/Highway this column differentiates
+ * if the issue was on the Road or the Ramp.
+ */
+ road_ramp: z.string().optional(),
+
+ /**
+ * Additional information on the section of the Bridge/Highway were the
+ * incident took place.
+ */
+ bridge_highway_segment: z.string().optional(),
+
+ /**
+ * Geo based Lat of the incident location
+ */
+ latitude: z.coerce.number().optional(),
+
+ /**
+ * Geo based Long of the incident location
+ */
+ longitude: z.coerce.number().optional(),
+
+ /**
+ * Combination of the geo based lat & long of the incident location
+ */
+ location: z
+ .object({
+ coordinates: z.tuple([z.number(), z.number()]),
+ type: z.literal('Point'),
+ })
+ .strict()
+ .optional(),
+
+ // Unknown Keys - keys that are not in the docs but are in the data
+ x_coordinate_state_plane_: z.coerce.number().optional(),
+ y_coordinate_state_plane_: z.coerce.number().optional(),
+ ':@computed_region_efsh_h5xi': z.coerce.number().optional(),
+ ':@computed_region_f5dn_yrer': z.coerce.number().optional(),
+ ':@computed_region_yeji_bk3q': z.coerce.number().optional(),
+ ':@computed_region_92fq_4b7q': z.coerce.number().optional(),
+ ':@computed_region_sbqj_enih': z.coerce.number().optional(),
+ })
+ .strict();
+
+export type RatSighting = z.infer;
+
+export const RatSightingsSchema = z.array(RatSightingSchema);