From 3e0691f80bb1563dccd2be9fbad07b19bcf9d776 Mon Sep 17 00:00:00 2001 From: eelkus01 <130937420+eelkus01@users.noreply.github.com> Date: Tue, 18 Jun 2024 14:53:38 -0400 Subject: [PATCH] calling api to get district num based on address --- client/src/pages/ballotInfo/districtForm.tsx | 123 ++++++++++++++++++ client/src/pages/ballotInfo/index.tsx | 12 +- client/src/pages/voterInfo/index.tsx | 12 +- server/app/index.ts | 49 ++++++- .../content-types/candidate/schema.json | 34 +++++ .../api/candidate/controllers/candidate.js | 9 ++ strapi/src/api/candidate/routes/candidate.js | 9 ++ .../src/api/candidate/services/candidate.js | 9 ++ strapi/types/generated/contentTypes.d.ts | 114 +++++++++++----- 9 files changed, 324 insertions(+), 47 deletions(-) create mode 100644 client/src/pages/ballotInfo/districtForm.tsx create mode 100644 strapi/src/api/candidate/content-types/candidate/schema.json create mode 100644 strapi/src/api/candidate/controllers/candidate.js create mode 100644 strapi/src/api/candidate/routes/candidate.js create mode 100644 strapi/src/api/candidate/services/candidate.js diff --git a/client/src/pages/ballotInfo/districtForm.tsx b/client/src/pages/ballotInfo/districtForm.tsx new file mode 100644 index 000000000..97a4b4aea --- /dev/null +++ b/client/src/pages/ballotInfo/districtForm.tsx @@ -0,0 +1,123 @@ +/* Form asking for user address and getting council district from Google Civic + * API. Note: API key is in .env file +*/ + +import React, { useState } from 'react'; +import axios from 'axios'; +import { Button, Grid, TextField } from '@mui/material'; + + +// Set base URL for Axios +// const api = axios.create({ +// baseURL: 'https://pitne-voter-app-express-production.up.railway.app/', // Point this to server URL +// }); +const api = axios.create({ + baseURL: 'http://localhost:3001', // Point this to server URL +}); + + +const DistrictForm: React.FC = () => { + // Functions and variables to set district data + const [street, setStreet] = useState(''); + const [city, setCity] = useState(''); + const [state, setState] = useState(''); + const [zip, setZip] = useState(''); + const [districtNum, setDistrictNum] = useState(null); + + // Call API when address is submitted + const handleSubmit = async (event: React.FormEvent) => { + + // Reset past data + event.preventDefault(); + setDistrictNum(null); + + // Set address + const address = `${street} ${city}, ${state} ${zip}`; + + // Call API + try { + const response = await api.get('/api/district', { + params: { address }, + }); + + const data = response.data; + + // Set district number or error if no district number + if (data) { + console.log(data); + setDistrictNum(data); + } else { + console.log("ERROR FETCHING DISTRICT - ensure address is within Boston bounds") + } + } catch { + console.log("ERROR FETCHING DISTRICT - ensure address is within Boston bounds"); + } + }; + + + // Address form + return ( +
+ + {/* Address form */} +
+ + + setStreet(e.target.value)} + required + sx={{ mb: 2 }} + /> + + + setCity(e.target.value)} + required + sx={{ mb: 2 }} + /> + + + setState(e.target.value)} + required + sx={{ mb: 2 }} + /> + + + setZip(e.target.value)} + required + sx={{ mb: 2 }} + /> + + +
+ +
+
+ + {/* NOTE: REMOVE BELOW PRINT, JUST FOR CHECKING WHILE BALLOT INFO IS IN PROGRESS */} +

District Num: {districtNum}

+
+ ); +}; + +export default DistrictForm; \ No newline at end of file diff --git a/client/src/pages/ballotInfo/index.tsx b/client/src/pages/ballotInfo/index.tsx index b517c443f..09e86a227 100644 --- a/client/src/pages/ballotInfo/index.tsx +++ b/client/src/pages/ballotInfo/index.tsx @@ -1,13 +1,12 @@ import ButtonFill from "@/components/button/ButtonFill" -import { Box, TextField } from "@mui/material"; import Checkbox from '@mui/material/Checkbox'; import * as React from 'react'; import DropDown from './dropDown'; -import HelpIcon from '@mui/icons-material/Help'; import BoxAddress from "../../components/button/boxAddress"; import NavBar from "@/components/nav/NavBar"; import BallotInitDropDown from "./ballotInitDropDown"; import ButtonFillEx from "@/components/button/ButtonFillEx"; +import DistrictForm from "./districtForm"; @@ -29,12 +28,13 @@ export default function BallotInfo() {

Explore the elections, candidates, and crucial issues personalized to your community.

+ {/* Address form */} -
- - +
+
+ {/* Election checkbox card */}
@@ -69,6 +69,7 @@ export default function BallotInfo() {
+ {/* What's on the Ballot dropdown */}

What's on the Ballot?

@@ -82,6 +83,7 @@ export default function BallotInfo() {
+ {/* Footer */}

You may be wondering ...

diff --git a/client/src/pages/voterInfo/index.tsx b/client/src/pages/voterInfo/index.tsx index 710342adc..c430fde40 100644 --- a/client/src/pages/voterInfo/index.tsx +++ b/client/src/pages/voterInfo/index.tsx @@ -25,12 +25,12 @@ export default function VoterInfo() { {/* County (fixed for all Boston voters) */}
-

Basic Voter Info

+ backgroundImage: 'url(/Star1.png)', + backgroundPosition: 'right center', + backgroundSize: 'contain', + backgroundRepeat: 'no-repeat', + }}> +

Basic Voter Info

diff --git a/server/app/index.ts b/server/app/index.ts index 07a8638e4..c923c0f5c 100644 --- a/server/app/index.ts +++ b/server/app/index.ts @@ -8,11 +8,13 @@ import cors from 'cors'; dotenv.config(); // Load environment variables const app = express(); -const port = process.env.PORT || 3001 -// process.env.PORT || 3001 +// const port = process.env.PORT || 3001; +const port = 3001; app.use(cors()); // Needed to send data back to frontend + +// API call for polling location app.get('/api/lookup', async (req: Request, res: Response) => { const { address } = req.query; @@ -37,6 +39,49 @@ app.get('/api/lookup', async (req: Request, res: Response) => { } }); + +/* API call for district number (1-9) + * NOTE: Google Civic API is turning down the Representatives API in April 2025, + * so the below API call will need to be updated to their new system after +*/ +app.get('/api/district', async (req: Request, res: Response) => { + const { address } = req.query; + + // No address check (should be unnecessary b/c form validation) + if (!address) { + return res.status(400).json({ error: 'Address is required' }); + } + + try { + // Get data from API + const response = await axios.get('https://civicinfo.googleapis.com/civicinfo/v2/representatives', { + params: { + address, + key: process.env.GOOGLE_CIVIC_API_KEY, + }, + }); + + const divisions = response.data.divisions; + + // Extract the council district number + const councilDistrictKey = Object.keys(divisions).find(key => key.includes('council_district')); + let councilDistrictNumber = null; + + if (councilDistrictKey) { + const match = councilDistrictKey.match(/council_district:(\d+)/); + if (match && match[1]) { + councilDistrictNumber = match[1]; + console.log(`Council District Number: ${councilDistrictNumber}`); + } + } + + // Send data back to frontend + res.status(200).json(councilDistrictNumber); + } catch (error) { + res.status(500).json({ error: 'Error fetching district' }); + } +}); + app.get('', (req: Request, res: Response) => { res.send('Hello from the Voter Info API!'); }); diff --git a/strapi/src/api/candidate/content-types/candidate/schema.json b/strapi/src/api/candidate/content-types/candidate/schema.json new file mode 100644 index 000000000..f182ee42a --- /dev/null +++ b/strapi/src/api/candidate/content-types/candidate/schema.json @@ -0,0 +1,34 @@ +{ + "kind": "collectionType", + "collectionName": "candidates", + "info": { + "singularName": "candidate", + "pluralName": "candidates", + "displayName": "Candidates", + "description": "" + }, + "options": { + "draftAndPublish": true + }, + "pluginOptions": {}, + "attributes": { + "District": { + "type": "enumeration", + "enum": [ + "District 1", + "District 2", + "District 3", + "District 4", + "District 5", + "District 6", + "District 7", + "District 8", + "District 9" + ], + "required": true + }, + "Name": { + "type": "string" + } + } +} diff --git a/strapi/src/api/candidate/controllers/candidate.js b/strapi/src/api/candidate/controllers/candidate.js new file mode 100644 index 000000000..17ccebe1d --- /dev/null +++ b/strapi/src/api/candidate/controllers/candidate.js @@ -0,0 +1,9 @@ +'use strict'; + +/** + * candidate controller + */ + +const { createCoreController } = require('@strapi/strapi').factories; + +module.exports = createCoreController('api::candidate.candidate'); diff --git a/strapi/src/api/candidate/routes/candidate.js b/strapi/src/api/candidate/routes/candidate.js new file mode 100644 index 000000000..03c5f7f93 --- /dev/null +++ b/strapi/src/api/candidate/routes/candidate.js @@ -0,0 +1,9 @@ +'use strict'; + +/** + * candidate router + */ + +const { createCoreRouter } = require('@strapi/strapi').factories; + +module.exports = createCoreRouter('api::candidate.candidate'); diff --git a/strapi/src/api/candidate/services/candidate.js b/strapi/src/api/candidate/services/candidate.js new file mode 100644 index 000000000..b1874ccf2 --- /dev/null +++ b/strapi/src/api/candidate/services/candidate.js @@ -0,0 +1,9 @@ +'use strict'; + +/** + * candidate service + */ + +const { createCoreService } = require('@strapi/strapi').factories; + +module.exports = createCoreService('api::candidate.candidate'); diff --git a/strapi/types/generated/contentTypes.d.ts b/strapi/types/generated/contentTypes.d.ts index 289ee3ef6..e1adb5565 100644 --- a/strapi/types/generated/contentTypes.d.ts +++ b/strapi/types/generated/contentTypes.d.ts @@ -362,39 +362,6 @@ export interface AdminTransferTokenPermission extends Schema.CollectionType { }; } -export interface ApiBostonMunicipalElectionDateBostonMunicipalElectionDate - extends Schema.CollectionType { - collectionName: 'boston_municipal_election_dates'; - info: { - singularName: 'boston-municipal-election-date'; - pluralName: 'boston-municipal-election-dates'; - displayName: 'Boston Municipal Election Dates'; - description: ''; - }; - options: { - draftAndPublish: true; - }; - attributes: { - ElectionName: Attribute.String; - ElectionDate: Attribute.Date; - createdAt: Attribute.DateTime; - updatedAt: Attribute.DateTime; - publishedAt: Attribute.DateTime; - createdBy: Attribute.Relation< - 'api::boston-municipal-election-date.boston-municipal-election-date', - 'oneToOne', - 'admin::user' - > & - Attribute.Private; - updatedBy: Attribute.Relation< - 'api::boston-municipal-election-date.boston-municipal-election-date', - 'oneToOne', - 'admin::user' - > & - Attribute.Private; - }; -} - export interface PluginUploadFile extends Schema.CollectionType { collectionName: 'files'; info: { @@ -821,6 +788,84 @@ export interface PluginUsersPermissionsUser extends Schema.CollectionType { }; } +export interface ApiBostonMunicipalElectionDateBostonMunicipalElectionDate + extends Schema.CollectionType { + collectionName: 'boston_municipal_election_dates'; + info: { + singularName: 'boston-municipal-election-date'; + pluralName: 'boston-municipal-election-dates'; + displayName: 'Boston Municipal Election Dates'; + description: ''; + }; + options: { + draftAndPublish: true; + }; + attributes: { + ElectionName: Attribute.String; + ElectionDate: Attribute.Date; + createdAt: Attribute.DateTime; + updatedAt: Attribute.DateTime; + publishedAt: Attribute.DateTime; + createdBy: Attribute.Relation< + 'api::boston-municipal-election-date.boston-municipal-election-date', + 'oneToOne', + 'admin::user' + > & + Attribute.Private; + updatedBy: Attribute.Relation< + 'api::boston-municipal-election-date.boston-municipal-election-date', + 'oneToOne', + 'admin::user' + > & + Attribute.Private; + }; +} + +export interface ApiCandidateCandidate extends Schema.CollectionType { + collectionName: 'candidates'; + info: { + singularName: 'candidate'; + pluralName: 'candidates'; + displayName: 'Candidates'; + description: ''; + }; + options: { + draftAndPublish: true; + }; + attributes: { + District: Attribute.Enumeration< + [ + 'District 1', + 'District 2', + 'District 3', + 'District 4', + 'District 5', + 'District 6', + 'District 7', + 'District 8', + 'District 9' + ] + > & + Attribute.Required; + Name: Attribute.String; + createdAt: Attribute.DateTime; + updatedAt: Attribute.DateTime; + publishedAt: Attribute.DateTime; + createdBy: Attribute.Relation< + 'api::candidate.candidate', + 'oneToOne', + 'admin::user' + > & + Attribute.Private; + updatedBy: Attribute.Relation< + 'api::candidate.candidate', + 'oneToOne', + 'admin::user' + > & + Attribute.Private; + }; +} + declare module '@strapi/types' { export module Shared { export interface ContentTypes { @@ -831,7 +876,6 @@ declare module '@strapi/types' { 'admin::api-token-permission': AdminApiTokenPermission; 'admin::transfer-token': AdminTransferToken; 'admin::transfer-token-permission': AdminTransferTokenPermission; - 'api::boston-municipal-election-date.boston-municipal-election-date': ApiBostonMunicipalElectionDateBostonMunicipalElectionDate; 'plugin::upload.file': PluginUploadFile; 'plugin::upload.folder': PluginUploadFolder; 'plugin::content-releases.release': PluginContentReleasesRelease; @@ -840,6 +884,8 @@ declare module '@strapi/types' { 'plugin::users-permissions.permission': PluginUsersPermissionsPermission; 'plugin::users-permissions.role': PluginUsersPermissionsRole; 'plugin::users-permissions.user': PluginUsersPermissionsUser; + 'api::boston-municipal-election-date.boston-municipal-election-date': ApiBostonMunicipalElectionDateBostonMunicipalElectionDate; + 'api::candidate.candidate': ApiCandidateCandidate; } } }