diff --git a/.DS_Store b/.DS_Store index ca2b787..05842dc 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.flake8 b/.flake8 index 0eaba0b..e4a0f4b 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,3 @@ [flake8] -exclude = ghi,docs,journals,sample_service/migrations +exclude = ghi,docs,journals,**/migrations/**, +ignore=E402,W503 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8b0e101..b23dac6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ new_keys.py __pycache__/ *.py[cod] *$py.class - +.DS_Store # C extensions *.so diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 00ec037..b5bccd1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,38 +3,39 @@ stages: - build - deploy -# Run flake8 to check for code quality lint-test-job: - image: python:3-bullseye + image: "python:3-bullseye" stage: test script: - pip install flake8 - - flake8 sample_service + - flake8 letters_service + - flake8 users_service -# Run the unit tests -# api-unit-test-job: -# image: python:3-bullseye -# stage: test -# needs: -# - lint-test-job -# script: -# - cd sample_service -# - pip install -r requirements.txt -# - python -m pytest +user-service-test-job: + image: "python:3-bullseye" + stage: test + script: + - cd users_service + - pip install -r requirements.txt + - python -m pytest + +letters-service-test-job: + image: "python:3-bullseye" + stage: test + script: + - cd letters_service + - pip install -r requirements.txt + - python -m pytest -# Build the React/JavaScript front-end build-front-end-job: stage: build - image: node:lts-bullseye + image: "node:lts-bullseye" needs: - lint-test-job variables: - # If either of these variables is defined in the GitLab - # CI/CD variables, that value will override the value here. - # You need to substitute in your real values for - # GROUP_NAME, PROJECT_NAME, & WEBSERVICE_NAME below. - PUBLIC_URL: https://GROUP_NAME.gitlab.io/PROJECT_NAME/ - REACT_APP_SAMPLE_SERVICE_API_HOST: https://WEBSERVICE_NAME.onrender.com + PUBLIC_URL: "https://directrep.gitlab.io/direct-rep" + REACT_APP_LETTERS_API_HOST: "https://letters-service-u1pb.onrender.com" + REACT_APP_USERS_API_HOST: "https://users-service-57wg.onrender.com" script: - cd ghi - npm install @@ -43,8 +44,6 @@ build-front-end-job: artifacts: paths: - ghi/build/ - -# Deploy the React/JavaScript front-end to GitLab pages pages: stage: deploy rules: diff --git a/.vscode/settings.json b/.vscode/settings.json index 8a61af3..12901f9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,3 @@ { - "editor.formatOnPaste": true, - "python.formatting.provider": "black", - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "files.autoSave": "afterDelay", - "cSpell.words": [ - "Excalidraw", - "labeltext", - "Mittal", - "Signup", - "wireframes", - "zipcode" - ] + "python.analysis.typeCheckingMode": "basic" } diff --git a/README.md b/README.md index fdde056..cee7486 100644 --- a/README.md +++ b/README.md @@ -1,147 +1,211 @@ -# Module3 Project Gamma - -## Getting started - -You have a project repository, now what? The next section -lists all of the deliverables that are due at the end of the -week. Below is some guidance for getting started on the -tasks for this week. - -## Install Extensions - -* Prettier: -* Black Formatter: -## Deliverables +# **DirectRep** -* [ ] Wire-frame diagrams -* [ ] API documentation -* [ ] Project is deployed to Render.com/GitLab-pages -* [ ] GitLab issue board is setup and in use -* [ ] Journals -## Project layout +⚡️ Powered by: [Google Civics API](https://developers.google.com/civic-information) and [OpenAI GPT3](https://beta.openai.com/docs/models/gpt-3) ⚡️ -The layout of the project is just like all of the projects -you did with `docker-compose` in module #2. You will create -a directory in the root of the repository for each service -that you add to your project just like those previous -projects were setup. - -### Directories - -Several directories have been added to your project. The -directories `docs` and `journals` are places for you and -your team-mates to, respectively, put any documentation -about your project that you create and to put your -project-journal entries. See the _README.md_ file in each -directory for more info. - -The other directories, `ghi` and `sample_service`, are -sample services, that you can start building off of or use -as a reference point. - -Inside of `ghi` is a minimal React app that has an "under -construction" page. It is setup similarly to all of the -other React projects that you have worked on. - -Inside of `sample_service` is a minimal FastAPI application. -"Where are all the files?" you might ask? Well, the -`main.py` file is the whole thing, and go take look inside -of it... There's not even much in there..., hmm? That is -FastAPI, we'll learn more about it in the coming days. Can -you figure out what this little web-application does even -though you haven't learned about FastAPI yet? - -Also in `sample_service` is a directory for your migrations. -If you choose to use PostgreSQL, then you'll want to use -migrations to control your database. Unlike Django, where -migrations were automatically created for you, you'll write -yours by hand using DDL. Don't worry about not knowing what -DDL means; we have you covered. There's a sample migration -in there that creates two tables so you can see what they -look like. - -The sample Dockerfile and Dockerfile.dev run your migrations -for you automatically. - -### Other files - -The following project files have been created as a minimal -starting point. Please follow the guidance for each one for -a most successful project. - -* `docker-compose.yaml`: there isn't much in here, just a - **really** simple UI and FastAPI service. Add services - (like a database) to this file as you did with previous - projects in module #2. -* `.gitlab-ci.yml`: This is your "ci/cd" file where you will - configure automated unit tests, code quality checks, and - the building and deployment of your production system. - Currently, all it does is deploy an "under construction" - page to your production UI on GitLab and a sample backend - to Render.com. We will learn much more about this file. -* `.gitignore`: This is a file that prevents unwanted files - from getting added to your repository, files like - `pyc` files, `__pycache__`, etc. We've set it up so that - it has a good default configuration for Python projects. - -## How to complete the initial deploy - -There will be further guidance on completing the initial -deployment, but it just consists of these steps: - -### Setup GitLab repo/project - -* make sure this project is in a group. If it isn't, stop - now and move it to a GitLab group -* remove the fork relationship: In GitLab go to: - - Settings -> General -> Advanced -> Remove fork relationship - -* add these GitLab CI/CD variables: - * PUBLIC_URL : this is your gitlab pages URL - * SAMPLE_SERVICE_API_HOST: enter "blank" for now - -#### Your GitLab pages URL - -You can't find this in GitLab until after you've done a deploy -but you can figure it out yourself from your GitLab project URL. - -If this is your project URL - -https://gitlab.com/GROUP_NAME/PROJECT_NAME - -then your GitLab pages URL will be - -https://GROUP_NAME.gitlab.io/PROJECT_NAME - -### Create render.com account and application - -* create account on render.com -* one person create a group and invite all other members -* create a new "Web Service" - * authenticate with GitLab and choose your project - * Enter fields: - * Name: name of your service - * Root Directory: the directory of your service in your git repo. - For this example use "sample_service". - * Environment: Docker - * Plan Type: Free - * click the "Create Web Service" button to create it - * the build will succeed and it will look like the server is running, - most likely, in 6-10 minutes, it will fail. - * click "Manual Deploy" -> "Deploy latest commit" and the service - should deploy successfully. - -### Update GitLab CI/CD variables - -Copy the service URL for your new render.com service and then paste -that into the value for the SAMPLE_SERVICE_API_HOST CI/CD variable -in GitLab. - -### Deploy it - -Merge a change into main to kick off the initial deploy. Once the build pipeline -finishes you should be able to see an "under construction" page on your GitLab -pages site. +DirectRep is an app that allows US Citizens to write letters to their elected representatives using Artifical Intelligence to generate the body text given a specific "issue" and "stance" on that issue. The user's representatives are gathered using [Google Civic Information API](https://developers.google.com/civic-information) and are determined by the Zip Code the user provides on Sign-Up. + +This service lowers the barrier-to-entry for citizens to have their voices heard by their elected representatives. Regardless of your literacy, your English proficiency, your ability to articulate your thoughts and opinions, having your voice heard is now easier for ***everyone*** to access. + +

+**\*\*Disclaimers\*\*** +There may be data missing from Google Civics API which may lead to certain representatives not being shown in the results. Always double check and verify your representatives' data. + + +
+ + +## 👾**Developers**👾 +### **[Kim Geraghty](https://gitlab.com/kim.geraghty.kg)** -- FullStack Developer +### **[Aaryan Mittal](https://gitlab.com/aaryanmittal154)** -- FullStack Developer +### **[Vincent Phung](https://gitlab.com/vincent.h.phung)** -- FullStack Developer +### **[John Gardner](https://gitlab.com/John.Gardner)** -- FullStack Developer + + +

+ + +## 🗺️**Diagrams of Architecture**🗺️ + +--- +### **DirectRep Wireframe Overview** +![DirectRep wireframe overview](https://i.imgur.com/soj2m9q.png) +### **Users-Service Microservice** +![users-service microservice architecture diagram](https://i.imgur.com/2rLvTBS.png) +### **Users-Service Endpoints** +![users-service microservice endpoints diagram](https://i.imgur.com/kRd2CNz.png) +### **Letters-Service Microservice** +![letters-service microservice architecture diagram](https://i.imgur.com/hP3HoZe.jpg) +### **Letters-Service Endpoints** +![letters-service microservice endpoints diagram](https://i.imgur.com/0JKQQVk.png) +


+ +## 👥**Users-Service Microservice**👥 -- `:8080` + +

+ +### **Endpoints** + + +| **Page** | **Request Type** | **Path** | **Request Shape JSON** | **Response Shape (JSON) | +| :-----------------: | :--------------: | :-------------------------------: | --------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Signup Page | `POST` | localhost:8080/api/accounts | {
;"full_name": `"string"`,
"email": `"string"`,
"zipcode": `"string"`,
"password": `"string"`
} | {
"access_token": `"string"`,
"token_type": `"Bearer"`,
"account":
{
"id": `0`,
"full_name": `"string"`,
"email": `"string"`,
"zipcode": `"string"`
}
} | +| Login Page | `POST` | localhost:8080/token | {
"username": `"string"`,
"password": `"string"`
} | {
"access_token": `"string"`,
"token_type": `"Bearer"`
} | +| Get Token | `GET` | localhost:8080/token | N/A | {
"access_token": `"string"`,
"token_type": `"Bearer"`,
"account":
{
"id": `0`,
"full_name": `"string"`,
"email": `"string"`,
"zipcode": `"string"`
}
} | +| Update Profile Page | `PUT` | localhost:8080/api/accounts/`$id` | {
"full_name": `"string"`,
"email": `"string"`,
"zipcode": `"string"`,
"password": `"string"`
} | {
"access_token": `"string"`,
"token_type": `"Bearer"`,
"account":
{
"id": `0`,
"full_name": `"string"`,
"email": `"string"`,
"zipcode": `"string"`
}
} | +| Logout Page | `DELETE` | localhost:8080/token | N/A | {
`true`
} | + +
+ +### **Database Models** +*(values in parenthesis are max values. e.g. VARCHAR(255) is a variable character with a max_length of 255 characters)* +


+#### **'user' database table** +| **Field** | **FieldType** | **\*\*options** | +| :-------------: | :------------------: | :-------------: | +| id | `SERIAL PRIMARY KEY` | `NOT NULL` | +| full_name | `VARCHAR(255)` | `NOT NULL` | +| email | `VARCHAR(255)` | `NOT NULL` | +| zipcode | `VARCHAR(5)` | `NOT NULL` | +| hashed_password | `VARCHAR(255)` | `NOT NULL` | + + +


+ +## 📨**Letters Microservice**📨 -- `:8090` + +### **Endpoints** + +

+**Letters-Service Endpoints** +| **Page** | **Request Type** | **Path** | **Request Shape (JSON)** | **Response Shape (JSON)** | +| :----------------------: | :--------------: | :-----------------------------------: | :------------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Create Letter | POST | localhost:8090/api/letters | {
"topic": `"string"`,
"stance": `"boolean"`
} | {
"id": `0`,
"topic": `"string"`
"stance": `true`,
"content": `"string"`,
"user_id": `0`
} | +| Get ALL Letters for User | GET | localhost:8090/api/letters | N/A | {
"id": `0`,
"created": `"2022-12-12T04:00:12.644Z"`,
"topic": `"string"`,
"stance": `true`,
"content": `"string"`,
"user_id": `0`
} | +| Get Single Letter | GET | localhost:8090/letters/`${letter_id}` | N/A | {
"id": `0`,
"created": `"2022-12-12T04:00:12.644Z"`,
"topic": `"string"`,
"stance": `true`,
"content": `"string"`,
"user_id": `0`
} | +| Edit Letter Body | PUT | localhost:8090/letters/`${letter_id}` | {
"letter_id": `0`,
"content": `"string"`
} | {
"content": `"string"`
} | +| Delete Letter | DELETE | localhost:8090/letters/`${letter_id}` | N/A | {
`true`
} | + + +

+**Issues Endpoints** + +| **Page** | **Request Type** | **Path** | **Request Shape (JSON)** | **Response Shape (JSON)** | +| :------------: | :--------------: | :-----------------------: | :----------------------: | :------------------------ | +| Get ALL issues | GET | localhost:8090/api/issues | N/A | {
`"string"`
} | + +

+**Reps Endpoints** + +| **Page** | **Request Type** | **Path** | **Request Shape (JSON)** | **Response Shape (JSON)** | +| :----------------------------------------------: | :--------------: | :-------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Get Reps from API | GET | localhost:8090/civics | N/A | {
"office": `"string"`,
"level": `"string"`,
"name": `"string"`,
"party": `"string"`,
"address": `"string"`
"address": `{}`,
"email": `"string"`
} | +| Get All Reps | GET | localhost:8090/api/reps | N/A | [
{
"office": `"string"`,
"level": `"string"`,
"name": `"string"`,
"party": `"string"`,
"address": `"string"`,
"address": `{}`,
"email": `"string"`
}
] | +| Select Rep | POST | localhost:8090/api/reps | {
"office": `"string"`,
"level": `"string"`,
"name": `"string"`,
"party": `"string"`,
"address": `"string"`,
"address": `{}`,
"email": `"string"`
} | {
"rep_id" `0`,
"office": `"string"`,
"level": `"string"`,
"name": `"string"`,
"party": `"string"`,
"address": `"string"`,
"address": `{}`,
"email": `"string"`,
"letter_id": `0`
} | +| Get SINGLE Rep Selection | GET | localhost:8090/reps/${rep_id} | N/A | {
"office": `"string"`,
"level": `"string"`,
"name": `"string"`,
"party": `"string"`,
"address": `"string"`,
"address": `{}`,
"email": `"string"`
} | +| Get ALL Rep Selections for SPECIFIC letter | GET | localhost:8090/reps/letter/${letter_id} | N/A | [
{
"office": `"string"`,
"level": `"string"`,
"name": `"string"`,
"party": `"string"`,
"address": `"string"`,
"address": `{}`,
"email": `"string"`
}
] | +| Delete SINGLE Rep Selection from SPECIFIC letter | DELETE | localhost:8090/reps/letter/${letter_id} | N/A | {
`true`
} | + + + +


+ + +### **Database Models** +*(values in parenthesis are max values. e.g. VARCHAR(255) is a variable character with a max_length of 255 characters)* + +


+#### **'letter' database table** +| **Field** | **FieldType** | **\*\*options** | +| :-------: | :------------------: | :------------------------: | +| id | `SERIAL PRIMARY KEY` | `NOT NULL` | +| created | `TIMESTAMP` | `DEFAULT CURRENT_TIMESTAMP | +| topic | `VARCHAR(1000)` | `NOT NULL` | +| stance | `BOOLEAN` | `NOT NULL` | +| user_id | `INTEGER` | `NOT NULL` | + + +


+ +#### **'issue' database table** +| **Field** | **FieldType** | **\*\*options** | +| :----------: | :------------------: | :-------------: | +| id | `SERIAL PRIMARY KEY` | `NOT NULL` | +| user_issue | `VARCHAR(1000)` | `NOT NULL` | +| openai_issue | `VARCHAR(1000)` | `NOT NULL` | + + +


+ +#### **'rep' database table** +| **Field | **FieldType** | **\*\*options** | +| :-------: | :------------------: | :--------------------------------------------------------------------------: | +| rep_id | `SERIAL PRIMARY KEY` | `NOT NULL` | +| office | `VARCHAR(255)` | `NOT NULL` | +| level | `VARCHAR(255)` | `NOT NULL` | +| name | `VARCHAR(255)` | `NOT NULL` | +| address | `VARCHAR(1000)` | `NOT NULL` | +| email | `VARCHAR(255)` | `NOT NULL` | +| letter_id | `INT` | `FOREIGN KEY (letter_id)`
`REFERENCES (letter_id)`
`ON DELETE CASCADE` | + + +


+ + + + +**Key Features** +--- + +### **Location Dependent Representative Results** + +> Your list of available representatives is pulled from Google Civics API using the Zip Code provided on Sign-Up + +### **AI Generated Text Response** + +> When creating a letter, your "issue" and "stance" are sent to a 3rd party API that then uses a large-language model to generate the letter body the users desires. + +### **User Customization** + +> When the AI returns your letter body, the user can change, add, or remove any part of the letter, making it fully customizable and ensuring the voice accurately represents the user. + +### **Account Dependent Information** + +> After creating an account and creating letters, the letters you see in your Dashboard are only the letters you created + +### **Account Dependent Information** + +> After creating an account and creating letters, the letters you see in your Dashboard are only the letters you created + +
+ +## **Getting the App Started** +--- + +1. Git clone into your local repository + `git clone «repo»` +2. cd into it + `cd direct-rep` +3. Create a new file in the root directory called `.env` (This is where you'll store your Google Civics API Key and you OpenAI API Key) +4. Create a volume and name it beta-data + `docker volume create postgres-data` +5. Build the image and run the container + `docker compose up --build` +6. Open browser to http://localhost:3000 to make sure it’s running +7. Once it’s up and running, you can begin with creating an account and confirming that account. Once you confirm the account, you can create a new letter. + + +


+## **FAQ** +### **Who are your customers?** +> People residing in the United States of America. +### **What needs/desires does your application satisfy for your customers?** +> Making contacting your elected officials more accessible +### What features/functionality do you plan to have in the application that helps your customers? +> - List of representatives with their details. +> - List of issues. +> - Open AI to draft an automated AI generated letter. +> - Login / logout feature. +> - Email template sent to user’s email with letter draft and rep’s emails list. +> - Keep track of past letter templates / requests \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 325cc36..2cd2403 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -14,38 +14,34 @@ services: - POSTGRES_PASSWORD=password ports: - 15432:5432 - fastapi: + users_service: build: - context: sample_service - dockerfile: Dockerfile.dev - ports: - - 8000:8000 - volumes: - - ./sample_service:/app - users-service: - build: - context: ./users-service + context: ./users_service dockerfile: ./Dockerfile.dev ports: - 8080:8000 volumes: - - ./users-service:/app + - ./users_service:/app environment: - SIGNING_KEY: 7ea8f8c1d636b6f92b59a46326d9f59da1e09f0c986fbaeacaad64fed817ce6d - DATABASE_URL: postgresql://users:password@postgres/users + SIGNING_KEY: ${SIGNING_KEY} + USERS_DATABASE_URL: ${USERS_DATABASE_URL} WAIT_HOSTS: postgres:5432 - letters-service: + letters_service: build: - context: ./letters-service + context: ./letters_service dockerfile: ./Dockerfile.dev ports: - 8090:8000 volumes: - - ./letters-service:/app + - ./letters_service:/app environment: - SIGNING_KEY: 7ea8f8c1d636b6f92b59a46326d9f59da1e09f0c986fbaeacaad64fed817ce6d - DATABASE_URL: postgresql://letters:password@postgres/letters + SIGNING_KEY: ${SIGNING_KEY} + LETTERS_DATABASE_URL: ${LETTERS_DATABASE_URL} WAIT_HOSTS: postgres:5432 + OPENAI_API_KEY: ${OPENAI_API_KEY} + GOOGLE_CIVICS_KEY: ${GOOGLE_CIVICS_KEY} + OPENAI_URL: ${OPENAI_URL} + GOOGLE_CIVICS_URL: ${GOOGLE_CIVICS_URL} ghi: image: node:lts-bullseye command: /bin/bash run.sh diff --git a/docs/.vscode/settings.json b/docs/.vscode/settings.json new file mode 100644 index 0000000..3ce09df --- /dev/null +++ b/docs/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "editor.formatOnPaste": true, + "python.formatting.provider": "black", + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "files.autoSave": "off", + "cSpell.words": [ + "Excalidraw", + "labeltext", + "Mittal", + "Signup", + "wireframes", + "zipcode" + ] +} diff --git a/docs/README.MD b/docs/README.MD index ffaa43f..c1cf080 100644 --- a/docs/README.MD +++ b/docs/README.MD @@ -1,38 +1,53 @@ # Direct Rep + ## Who are your customers? + People residing in the United States of America. ## What needs/desires does your application satisfy for your customers? + Making contacting your elected officials more accessible ## What features/functionality do you plan to have in the application that helps your customers? + - List of representatives with their details. - List of issues. - Open AI to draft an automated AI generated letter. -- Login / logout feature (Gmail API). +- Login / logout feature. - Email template sent to user’s email with letter draft and rep’s emails list. - Keep track of past letter templates / requests ## Design - [API Design](api-design.md) - ![API Design](wireframes/wo.png) - ![API Design](wireframes/direct-rep-wireframe.png) - ![API Design](wireframes/user-service-endpoints.png) - ![API Design](wireframes/letter-service-endpoints.png) + +### Users endpoints + +[API Design Users](api-design-users.md) + +### Letters endpoints + +[API Design Letters](api-design-letters.md) + +### Wireframe and endpoints diagrams + +![API Design](wireframes/direct-rep-wireframe-overview.png) +![API Design](wireframes/users-microservice-wireframe.png) +![API Design](wireframes/letters-microservice-wireframe.png) +![API Design](wireframes/users-microservice-endpoints.png) +![API Design](wireframes/letters-microservice-endpoints.png) ## Stretch Goals + - Info about upcoming elections - “Am I registered?” function to check user voter registration - “When’s the next election?” function - Direct Rep uses your location and the Google Civic Information API to gather information about your representatives. - - ## The Team - - Aaryan Mittal- Developer 💻 - - John Gardner - Developer 💻 +- Aaryan Mittal- Developer 💻 + +- John Gardner - Developer 💻 - - Kim Geraghty - Developer 💻 +- Kim Geraghty - Developer 💻 - - Vincent Phung - Developer 💻 +- Vincent Phung - Developer 💻 diff --git a/docs/api-design-letters.md b/docs/api-design-letters.md new file mode 100644 index 0000000..eabd94c --- /dev/null +++ b/docs/api-design-letters.md @@ -0,0 +1,309 @@ +## localhost:8090: letters_service: + +### Get All Letters + +- Endpoint path: /api/letters +- Endpoint method: GET method + +- Headers: + + - Authorization: Bearer token + +- Response: List of all letters + +- Response shape (JSON): + ```json + [ + { + "id": 0, + "created": "2022-12-12T03:56:37.142Z", + "topic": "string", + "stance": true, + "content": "string", + "user_id": 0 + } + ] + ``` + +### Create Letter + +- Endpoint path: /api/letters +- Endpoint method: POST method +- Query parameters: + + - topic: define letter's topic + - stance: define letter's stance + +- Headers: + + - Authorization: Bearer token + +- Request shape (JSON): + + ```json + { "topic": "string", "stance": "boolean" } + ``` + +- Response: Letter created with third party Open AI API +- Response shape (JSON): + ```json + { + "id": 0, + "topic": "string", + "stance": true, + "content": "string", + "user_id": 0 + } + ``` + +### Get One Letter + +- Endpoint path: /letters/{letter_id} +- Endpoint method: GET method +- Query parameters: + + - letter_id: pick one letter + +- Headers: + + - Authorization: Bearer token + +- Response: One letter +- Response shape (JSON): + ```json + { + "id": 0, + "created": "2022-12-12T04:00:12.644Z", + "topic": "string", + "stance": true, + "content": "string", + "user_id": 0 + } + ``` + +### Edit Letter Body + +- Endpoint path: /letters/{letter_id} +- Endpoint method: PUT method +- Query parameters: + + - letter_id: pick one letter + - content: content to edit + +- Headers: + + - Authorization: Bearer token + +- Request shape (JSON): + + ```json + { + "letter_id": "integer", + "content": "string" + } + ``` + +- Response: Edited letter content +- Response shape (JSON): + ```json + { + "content": "string" + } + ``` + +### Delete Letter + +- Endpoint path: /letters/{letter_id} +- Endpoint method: DELETE method +- Query parameters: + + - letter_id: pick one letter + +- Headers: + + - Authorization: Bearer token + +- Response: Confirmation of deletion +- Response shape (JSON): + ```json + true + ``` + +### Get All Issues + +- Endpoint path: /api/issues +- Endpoint method:GET method +- Headers: + + - Authorization: Bearer token + +- Response: List of issues +- Response shape (JSON): + ```json + "string" + ``` + +### Get Reps From Api + +- Endpoint path: /civics +- Endpoint method: GET method +- Query parameters: + + - zipcode: to send to Google Civics Third Party API + +- Headers: + + - Authorization: Bearer token + +- Response: List of reps for that zipcode from google civics +- Response shape (JSON): + ```json + [ + { + "office": "string", + "level": "string", + "name": "string", + "party": "string", + "address": {}, + "email": "string" + } + ] + ``` + +### Get All Reps Selection + +- Endpoint path: /api/reps +- Endpoint method: GET method +- Headers: + + - Authorization: Bearer token + +- Response: List of reps selected by user +- Response shape (JSON): + ```json + [ + { + "rep_id": 0, + "office": "string", + "level": "string", + "name": "string", + "party": "string", + "address": "string", + "email": "string", + "letter_id": 0 + } + ] + ``` + +### Select Rep + +- Endpoint path: /api/reps +- Endpoint method: «HTTP method» + +- Headers: + + - Authorization: Bearer token + +- Request shape (JSON): + + ```json + { + "office": "string", + "level": "string", + "name": "string", + "party": "string", + "address": "string", + "email": "string", + "letter_id": 0 + } + ``` + +- Response: Rep selected by user +- Response shape (JSON): + ```json + { + "rep_id": 0, + "office": "string", + "level": "string", + "name": "string", + "party": "string", + "address": "string", + "email": "string", + "letter_id": 0 + } + ``` + +### Get One Rep Selection + +- Endpoint path: /reps/{rep_id} +- Endpoint method: GET method +- Query parameters: + + - rep_id: pick one rep + +- Headers: + + - Authorization: Bearer token + +- Response: One rep selected by user +- Response shape (JSON): + ```json + { + "rep_id": 0, + "office": "string", + "level": "string", + "name": "string", + "party": "string", + "address": "string", + "email": "string", + "letter_id": 0 + } + ``` + +### Get Reps Per Letter + +- Endpoint path: /reps/letter/{letter_id} +- Endpoint method: GET method +- Query parameters: + + - letter_id: get list of reps selected for one letter + +- Headers: + + - Authorization: Bearer token + +- Response: List of reps selected for that letter +- Response shape (JSON): + ```json + [ + { + "rep_id": 0, + "office": "string", + "level": "string", + "name": "string", + "party": "string", + "address": "string", + "email": "string", + "letter_id": 0 + } + ] + ``` + +### Delete Reps From Letter + +- Endpoint path: /reps/letters/{letter_id} +- Endpoint method: DELETE method +- Query parameters: + + - letter_id: to select letter + - rep_id: to select rep + +- Headers: + + - Authorization: Bearer token + +- Response: Confirmation of deletion +- Response shape (JSON): + ```json + true + ``` diff --git a/docs/api-design-users.md b/docs/api-design-users.md new file mode 100644 index 0000000..85e2f84 --- /dev/null +++ b/docs/api-design-users.md @@ -0,0 +1,126 @@ +## localhost:8080: users_service: + +### Create Account + +- Endpoint path: /api/accounts +- Endpoint method: POST method + +- Request shape (JSON): + +```json +{ + "full_name": "string", + "email": "string", + "zipcode": "string", + "password": "string" +} +``` + +- Response: Account information + of response» +- Response shape (JSON): + +```json +{ + "access_token": "string", + "token_type": "Bearer", + "account": { + "id": 0, + "full_name": "string", + "email": "string", + "zipcode": "string" + } +} +``` + +### Edit Account + +- Endpoint path: /api/accounts/{id} +- Endpoint method: PUT method +- Query parameters: + + - id: to select the account + +- Request shape (JSON): + + ```json + { + "full_name": "string", + "email": "string", + "zipcode": "string", + "password": "string" + } + ``` + +- Response: Account information +- Response shape (JSON): + ```json + { + "access_token": "string", + "token_type": "Bearer", + "account": { + "id": 0, + "full_name": "string", + "email": "string", + "zipcode": "string" + } + } + ``` + +### Get Token + +- Endpoint path: /token +- Endpoint method: GET method + +- Response: Account token +- Response shape (JSON): + ```json + { + "access_token": "string", + "token_type": "Bearer", + "account": { + "id": 0, + "full_name": "string", + "email": "string", + "zipcode": "string" + } + } + ``` + +### Login + +- Endpoint path: /token +- Endpoint method: POST method +- Query parameters: + + - username: to login + - password: to login + +- Request shape (JSON): + + ```json + { + "username": "string", + "password": "string" + } + ``` + +- Response: Login token +- Response shape (JSON): + ```json + { + "access_token": "string", + "token_type": "Bearer" + } + ``` + +### Logout + +- Endpoint path: /token +- Endpoint method: DELETE method + +- Response: Logout +- Response shape (JSON): + ```json + true + ``` diff --git a/docs/api-design.md b/docs/api-design.md deleted file mode 100644 index 011d6a7..0000000 --- a/docs/api-design.md +++ /dev/null @@ -1,324 +0,0 @@ -## Log in - -- Endpoint path: localhost:8000/api/login/ -- Endpoint method: POST - -- Request shape (form): - - - email: string - - password: string - -- Response: Account information and a token -- Response shape (JSON): - ```json - { - "account": { - «key»: type», - }, - "token": string, - "email": string - } - ``` - -# Signup Page - -- Endpoint path: localhost:8000/api/accounts/ -- Endpoint method: POST - -- Request shape (form): - - - email: string - - password: string - - state: dropdown - - county: dropdown - - city: dropdown - -- Response: Account information and a token -- Response shape (JSON): - ```json - { - "account": { - «key»: type», - }, - "token": string - } - ``` - -## Log out (NavBar Button) - -- Endpoint path: localhost:8000/api/login/ -- Endpoint method: DELETE - -- Headers: - - - Authorization: Bearer token - -- Response: Always true -- Response shape (JSON): - ```json - true - ``` - -# Update Profile Page - -- Endpoint path: localhost:8000/api/accounts/$id/ -- Endpoint method: PUT - -- Headers: - - - Authorization: Bearer token - -- Request shape (form): - - - email: string - - password: string - - state: dropdown - - county: dropdown - - city: dropdown - -- Response: Account Information -- Response shape (JSON): - ```json - { - "account": { - «key»: type», - }, - "token": string, - "email": string, - "state": string, - "county": string, - "city": string, - } - ``` - -# Dashboard Page (GET.all) - -### «Human-readable of the endpoint» - -- Endpoint path: localhost:8000/api/dashboard/ -- Endpoint method: GET -- Query parameters: - - - account: filter letters to show - -- Headers: - - - Authorization: Bearer token - -- Response: list of users past letters -- Response shape (JSON): - ```json - { - letters: [ - "id": integer, - ] - } - ``` - -# Create Letter Page - -## (GET issues list) - -- Endpoint path: our database ex. localhost:8000/api/issues -- Endpoint method: GET -- Query parameters: - -- Headers: - - - Authorization: Bearer token - -- Response: 3 dropdowns with users reps -- Response shape (JSON): - - ```json - "issues": [ - ... - ] - - ``` - -## (POST ai letter query) - -### client makes request to our backend endpoint, which makes request to open AI - -### TBD - -- Endpoint path: https://api.openai.com/v1/completions -- Endpoint method: POST -- Query parameters: - - - issue: dropdown - - stance: for/against - - - { - "model": "text-davinci-002", - "prompt": "Say this is a test", - "max_tokens": 6, - "temperature": 0, - "top_p": 1, - "n": 1, - "stream": false, - "logprobs": null, - "stop": "\n" - } - -- Headers: - - - Authorization: Bearer token - -- Request shape (JSON): - -```json - -``` - -- Response:AI written letter -- Response shape (JSON): - `json { "id": "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7", "object": "text_completion", "created": 1589478378, "model": "text-davinci-002", "choices": [ { "text": "\n\nThis is a test", "index": 0, "logprobs": null, "finish_reason": "length" } ], "usage": { "prompt_tokens": 5, "completion_tokens": 6, "total_tokens": 11 } } ` - -## POST create instance of letter in our database - -- Endpoint path: localhost:8000/api/letters -- Endpoint method: POST -- Query parameters: - - - «name»: «purpose» - -- Headers: - - - Authorization: Bearer token - -- Request shape (JSON): - - ```json - «JSON-looking thing that has the - keys and types in it» - ``` - -- Response: «Human-readable description - of response» -- Response shape (JSON): - ```json - «JSON-looking thing that has the - keys and types in it» - ``` - -# Letter Edit Page (PUT) - -## PUT update / edit instance of letter in our database - -- Endpoint path: localhost:8000/api/letters/pk -- Endpoint method: PUT -- Query parameters: - - - «name»: «purpose» - -- Headers: - - - Authorization: Bearer token - -- Request shape (JSON): - - ```json - «JSON-looking thing that has the - keys and types in it» - ``` - -- Response: «Human-readable description - of response» -- Response shape (JSON): - ```json - «JSON-looking thing that has the - keys and types in it» - ``` - -# Select Reps Page - -## (GET reps) - -### send through backend request to hide api key - -- 3rd party endpoint path: Google civics API -- Local endpoint path:localhost:8000/api/reps -- Endpoint method: POST -- Query parameters: - - - state: user.state - - county: user.county - - city: user.city - -- Headers: - - - Authorization: Bearer token - -- Request shape (JSON): - - ```json - TBD - ``` - -- Response: List of reps - -- Response shape (JSON): - - ````json - {} - - ``` - ```` - -## PUT (update letter with reps) - -- Local endpoint path:localhost:8000/api/letter/$pk -- Endpoint method: PUT -- Query parameters: - - - reps info - -- Headers: - - - Authorization: Bearer token - -- Request shape (JSON): - - ```json - TBD - ``` - -- Response: Updated letter with reps info - -- Response shape (JSON): - - ````json - {} - - ``` - ```` - -# Review Page (GET.pk) - -## (GET letter) - -- Local endpoint path:localhost:8000/api/review -- Endpoint method: GET -- Query parameters: - -- Headers: - - - Authorization: Bearer token - -- Request shape (JSON): - - ```json - TBD - ``` - -- Response: List of reps - -- Response shape (JSON): - - ````json - {} - - ``` - ```` diff --git a/docs/wireframes/direct-rep-wireframe-overview.png b/docs/wireframes/direct-rep-wireframe-overview.png new file mode 100644 index 0000000..81c1278 Binary files /dev/null and b/docs/wireframes/direct-rep-wireframe-overview.png differ diff --git a/docs/wireframes/direct-rep-wireframe.png b/docs/wireframes/direct-rep-wireframe.png deleted file mode 100644 index dd4d453..0000000 Binary files a/docs/wireframes/direct-rep-wireframe.png and /dev/null differ diff --git a/docs/wireframes/letter-service-endpoints.png b/docs/wireframes/letter-service-endpoints.png deleted file mode 100644 index bec6806..0000000 Binary files a/docs/wireframes/letter-service-endpoints.png and /dev/null differ diff --git a/docs/wireframes/letters-microservice-endpoints.png b/docs/wireframes/letters-microservice-endpoints.png new file mode 100644 index 0000000..4dba6e5 Binary files /dev/null and b/docs/wireframes/letters-microservice-endpoints.png differ diff --git a/docs/wireframes/letters-microservice-wireframe.png b/docs/wireframes/letters-microservice-wireframe.png new file mode 100644 index 0000000..f99a09b Binary files /dev/null and b/docs/wireframes/letters-microservice-wireframe.png differ diff --git a/docs/wireframes/user-service-endpoints.png b/docs/wireframes/user-service-endpoints.png deleted file mode 100644 index 678b5c9..0000000 Binary files a/docs/wireframes/user-service-endpoints.png and /dev/null differ diff --git a/docs/wireframes/users-microservice-endpoints.png b/docs/wireframes/users-microservice-endpoints.png new file mode 100644 index 0000000..12e7e1b Binary files /dev/null and b/docs/wireframes/users-microservice-endpoints.png differ diff --git a/docs/wireframes/users-microservice-wireframe.png b/docs/wireframes/users-microservice-wireframe.png new file mode 100644 index 0000000..a75e17b Binary files /dev/null and b/docs/wireframes/users-microservice-wireframe.png differ diff --git a/docs/wireframes/wo.png b/docs/wireframes/wo.png deleted file mode 100644 index 4650c50..0000000 Binary files a/docs/wireframes/wo.png and /dev/null differ diff --git a/ghi/package-lock.json b/ghi/package-lock.json index 17db879..fe7b1a8 100644 --- a/ghi/package-lock.json +++ b/ghi/package-lock.json @@ -16,6 +16,8 @@ "react": "^18.2.0", "react-bootstrap": "^2.6.0", "react-dom": "^18.2.0", + "react-loader-spinner": "^5.3.4", + "react-promise-tracker": "^2.1.1", "react-redux": "^8.0.5", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", @@ -1993,6 +1995,29 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "dependencies": { + "@emotion/memoize": "^0.8.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "node_modules/@eslint/eslintrc": { "version": "1.3.2", "license": "MIT", @@ -4879,6 +4904,26 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + }, + "peerDependencies": { + "styled-components": ">= 2" + } + }, + "node_modules/babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "node_modules/babel-plugin-transform-react-remove-prop-types": { "version": "0.4.24", "license": "MIT" @@ -5162,6 +5207,14 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-api": { "version": "3.0.0", "license": "MIT", @@ -5567,6 +5620,14 @@ "postcss": "^8.4" } }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "6.3.1", "license": "ISC", @@ -5736,6 +5797,16 @@ "version": "0.1.1", "license": "MIT" }, + "node_modules/css-to-react-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", + "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/css-tree": { "version": "1.0.0-alpha.37", "license": "MIT", @@ -13012,6 +13083,33 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-loader-spinner": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.3.4.tgz", + "integrity": "sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==", + "dependencies": { + "react-is": "^18.2.0", + "styled-components": "^5.3.5", + "styled-tools": "^1.7.2" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-loader-spinner/node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + }, + "node_modules/react-promise-tracker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-promise-tracker/-/react-promise-tracker-2.1.1.tgz", + "integrity": "sha512-NM7FrJ4y5aRN3FIeVtmeF8leIDQIdYni9pleHTvuX6Kz5zsIHMYSNERmvzTxvPuzei0iNiNiudsXySP63TXA8A==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-redux": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", @@ -13833,6 +13931,11 @@ "version": "1.2.0", "license": "ISC" }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "node_modules/shebang-command": { "version": "2.0.0", "license": "MIT", @@ -14182,6 +14285,41 @@ "webpack": "^5.0.0" } }, + "node_modules/styled-components": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz", + "integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==", + "hasInstallScript": true, + "dependencies": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/styled-components" + }, + "peerDependencies": { + "react": ">= 16.8.0", + "react-dom": ">= 16.8.0", + "react-is": ">= 16.8.0" + } + }, + "node_modules/styled-tools": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz", + "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg==" + }, "node_modules/stylehacks": { "version": "5.1.0", "license": "MIT", @@ -16790,6 +16928,29 @@ "version": "2.0.2", "requires": {} }, + "@emotion/is-prop-valid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", + "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "requires": { + "@emotion/memoize": "^0.8.0" + } + }, + "@emotion/memoize": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", + "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + }, + "@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "@eslint/eslintrc": { "version": "1.3.2", "requires": { @@ -18573,6 +18734,23 @@ "@babel/helper-define-polyfill-provider": "^0.3.3" } }, + "babel-plugin-styled-components": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.7.tgz", + "integrity": "sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.0", + "@babel/helper-module-imports": "^7.16.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "lodash": "^4.17.11", + "picomatch": "^2.3.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==" + }, "babel-plugin-transform-react-remove-prop-types": { "version": "0.4.24" }, @@ -18757,6 +18935,11 @@ "camelcase-css": { "version": "2.0.1" }, + "camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==" + }, "caniuse-api": { "version": "3.0.0", "requires": { @@ -19001,6 +19184,11 @@ "postcss-selector-parser": "^6.0.9" } }, + "css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==" + }, "css-declaration-sorter": { "version": "6.3.1", "requires": {} @@ -19084,6 +19272,16 @@ "css-select-base-adapter": { "version": "0.1.1" }, + "css-to-react-native": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.0.0.tgz", + "integrity": "sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==", + "requires": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "css-tree": { "version": "1.0.0-alpha.37", "requires": { @@ -23311,6 +23509,29 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-loader-spinner": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/react-loader-spinner/-/react-loader-spinner-5.3.4.tgz", + "integrity": "sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==", + "requires": { + "react-is": "^18.2.0", + "styled-components": "^5.3.5", + "styled-tools": "^1.7.2" + }, + "dependencies": { + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "react-promise-tracker": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/react-promise-tracker/-/react-promise-tracker-2.1.1.tgz", + "integrity": "sha512-NM7FrJ4y5aRN3FIeVtmeF8leIDQIdYni9pleHTvuX6Kz5zsIHMYSNERmvzTxvPuzei0iNiNiudsXySP63TXA8A==", + "requires": {} + }, "react-redux": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.0.5.tgz", @@ -23815,6 +24036,11 @@ "setprototypeof": { "version": "1.2.0" }, + "shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, "shebang-command": { "version": "2.0.0", "requires": { @@ -24027,6 +24253,28 @@ "version": "3.3.1", "requires": {} }, + "styled-components": { + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz", + "integrity": "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@babel/traverse": "^7.4.5", + "@emotion/is-prop-valid": "^1.1.0", + "@emotion/stylis": "^0.8.4", + "@emotion/unitless": "^0.7.4", + "babel-plugin-styled-components": ">= 1.12.0", + "css-to-react-native": "^3.0.0", + "hoist-non-react-statics": "^3.0.0", + "shallowequal": "^1.1.0", + "supports-color": "^5.5.0" + } + }, + "styled-tools": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/styled-tools/-/styled-tools-1.7.2.tgz", + "integrity": "sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg==" + }, "stylehacks": { "version": "5.1.0", "requires": { diff --git a/ghi/package.json b/ghi/package.json index e39aecc..2a2509c 100644 --- a/ghi/package.json +++ b/ghi/package.json @@ -11,6 +11,8 @@ "react": "^18.2.0", "react-bootstrap": "^2.6.0", "react-dom": "^18.2.0", + "react-loader-spinner": "^5.3.4", + "react-promise-tracker": "^2.1.1", "react-redux": "^8.0.5", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", diff --git a/ghi/public/index.html b/ghi/public/index.html index 922efc6..1f43ae8 100644 --- a/ghi/public/index.html +++ b/ghi/public/index.html @@ -25,7 +25,7 @@ Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Direct Rep diff --git a/ghi/src/AccountForm.js b/ghi/src/AccountForm.js index 5499265..61754e0 100644 --- a/ghi/src/AccountForm.js +++ b/ghi/src/AccountForm.js @@ -1,6 +1,7 @@ import { useState } from "react"; import React from "react"; import { useCreateUserMutation } from "./store/usersApi"; +import { useNavigate } from "react-router-dom"; function InputLabel(props) { const { id, placeholder, labelText, value, onChange, type } = props; @@ -23,16 +24,18 @@ function InputLabel(props) { ); } -function AccountForm(props) { +function AccountForm() { const [full_name, setName] = useState(""); const [email, setEmail] = useState(""); const [zipcode, setZipcode] = useState(""); const [password, setPassword] = useState(""); - const [createUser, result] = useCreateUserMutation(); + const [createUser] = useCreateUserMutation(); + const navigate = useNavigate(); async function handleSubmit(e) { e.preventDefault(); createUser({ full_name, email, zipcode, password }); + navigate("/newlogin"); } return ( @@ -42,8 +45,8 @@ function AccountForm(props) {
setName(e.target.value)} type="text" @@ -51,7 +54,7 @@ function AccountForm(props) { setEmail(e.target.value)} type="email" @@ -59,7 +62,7 @@ function AccountForm(props) { setZipcode(e.target.value)} type="integer" diff --git a/ghi/src/App.js b/ghi/src/App.js index 9fd138d..8d09136 100644 --- a/ghi/src/App.js +++ b/ghi/src/App.js @@ -1,60 +1,51 @@ -import { useEffect, useState } from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; -// import Construct from "./Construct.js"; -import ErrorNotification from "./ErrorNotification"; +import { AuthProvider, useToken } from "./TokenTest.js"; import AccountForm from "./AccountForm"; import RepForm from "./RepForm"; import "./App.css"; -import Navigation from "./Nav"; import Dashboard from "./Dashboard"; import LetterForm from "./CreateLetter"; import EditLetter from "./EditLetter"; import ReviewForm from "./Review"; import LandingPage from "./Landing"; import EditAccount from "./EditAccount"; +import LoginForm from "./LoginTest"; +import LogoutButton from "./Logout.js"; +import NewLoginForm from "./NewLogin"; +import DetailView from "./Detailview"; +import Navb from "./Nav"; -function App() { - const [launch_info, setLaunchInfo] = useState([]); - const [error, setError] = useState(null); - - useEffect(() => { - async function getData() { - let url = `${process.env.REACT_APP_SAMPLE_SERVICE_API_HOST}/api/launch-details`; - console.log("fastapi url: ", url); - let response = await fetch(url); - console.log("------- hello? -------"); - let data = await response.json(); +function GetToken() { + // Get token from JWT cookie (if already logged in) + useToken(); + return null; +} - if (response.ok) { - console.log("got launch data!"); - setLaunchInfo(data.launch_details); - } else { - console.log("drat! something happened"); - setError(data.message); - } - } - getData(); - }, []); +function App() { + const domain = /https:\/\/[^/]+/; + const basename = process.env.PUBLIC_URL.replace(domain, ""); return ( -
- - + + + + } /> + } /> } /> + } /> } /> } /> - } /> } /> } /> } /> } /> + } /> + } /> - - - {/* */} -
+ + ); } diff --git a/ghi/src/App.test.js b/ghi/src/App.test.js index 1f03afe..9382b9a 100644 --- a/ghi/src/App.test.js +++ b/ghi/src/App.test.js @@ -1,7 +1,7 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; +import { render, screen } from "@testing-library/react"; +import App from "./App"; -test('renders learn react link', () => { +test("renders learn react link", () => { render(); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); diff --git a/ghi/src/Construct.js b/ghi/src/Construct.js index 1d151ad..a1c236b 100644 --- a/ghi/src/Construct.js +++ b/ghi/src/Construct.js @@ -1,18 +1,21 @@ function Construct(props) { + const pad2 = (num) => String(num).padStart(2, "0"); - const pad2 = num => String(num).padStart(2, '0'); - - return ( -
-
-

Under construction

-

Coming on (or before)

-

{props.info.year}-{pad2(props.info.month)}-{pad2(props.info.day)}

-

by or WELL BEFORE {pad2(props.info.hour)}:{pad2(props.info.min)}

- -
-
- ) + return ( +
+
+

Under construction

+

Coming on (or before)

+

+ {props.info.year}-{pad2(props.info.month)}-{pad2(props.info.day)} +

+

+ by or WELL BEFORE {pad2(props.info.hour)}: + {pad2(props.info.min)} +

+
+
+ ); } -export default Construct; \ No newline at end of file +export default Construct; diff --git a/ghi/src/CreateLetter.js b/ghi/src/CreateLetter.js index 93ea112..c910f06 100644 --- a/ghi/src/CreateLetter.js +++ b/ghi/src/CreateLetter.js @@ -1,35 +1,52 @@ import { useState, useEffect } from "react"; import React from "react"; import Form from "react-bootstrap/Form"; -import { useCreateLetterMutation } from "./store/lettersApi"; +import { useAuthContext } from "./TokenTest.js"; +import { useNavigate } from "react-router-dom"; +import { trackPromise } from "react-promise-tracker"; function LetterForm() { + const { token } = useAuthContext(); const [issues, setIssues] = useState([]); const [topic, setTopic] = useState(""); - const [stance, setStance] = useState(true); - // const [content, setContent] = useState(""); - const [createLetter, result] = useCreateLetterMutation(); + const [stance, setStance] = useState(); + const navigate = useNavigate(); - // to collect the issues list from the database - async function fetchIssues() { - const url = `http://localhost:8090/api/issues`; - const response = await fetch(url); + // to collect and load the issues list from the database + useEffect(() => { + async function fetchIssues() { + const url = `${process.env.REACT_APP_LETTERS_API_HOST}/api/issues`; + const response = await fetch(url, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.ok) { + const data = await response.json(); + setIssues(data); + } + } + fetchIssues(); + }, [token]); + + // to create a letter: + async function postLetter(topic, stance) { + const url = `${process.env.REACT_APP_LETTERS_API_HOST}/api/letters?topic=${topic}&stance=${stance}`; + const fetchConfig = { + method: "post", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }; + const response = await fetch(url, fetchConfig); if (response.ok) { + // eslint-disable-next-line no-unused-vars const data = await response.json(); - setIssues(data); } } - // to load issues with the first page render - useEffect(() => { - fetchIssues(); - }, []); - - console.log("ISSUES LIST", issues); - async function handleSubmit(e) { e.preventDefault(); - createLetter({ topic, stance }); + trackPromise(postLetter(topic, stance).then(() => navigate("/eletter"))); } return ( @@ -46,7 +63,7 @@ function LetterForm() { name="issues" className="form-select" > - + {issues.map((issue) => { return ( + diff --git a/ghi/src/Dashboard.js b/ghi/src/Dashboard.js index 2c7abb1..5a9b18e 100644 --- a/ghi/src/Dashboard.js +++ b/ghi/src/Dashboard.js @@ -1,184 +1,103 @@ -import Card from "react-bootstrap/Card"; import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; +import { useAuthContext } from "./TokenTest.js"; function Dashboard() { + const { token } = useAuthContext(); const [letters, setLetters] = useState([]); - const [oneLetter, setOneLetter] = useState([""]); - const [oneId, setId] = useState("Letter id"); - const [oneContent, setContent] = useState("Letter content"); - const [oneStance, setStance] = useState("Letter stance"); - const [oneTopic, setTopic] = useState("Letter topic"); - const [oneDate, setDate] = useState("Date"); - const [repSelection, setSelection] = useState([]); + const [user, setUser] = useState(); + + // to get the user's id + useEffect(() => { + async function getUserId() { + const url = `${process.env.REACT_APP_USERS_API_HOST}/token`; + const response = await fetch(url, { + credentials: "include", + }); + if (response.ok) { + const data = await response.json(); + setUser(data.account.id); + } + } + getUserId(); + }, [token, user]); useEffect(() => { (async () => { - const response = await fetch("http://localhost:8090/api/letters"); + const response = await fetch( + `${process.env.REACT_APP_LETTERS_API_HOST}/api/letters`, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); const content = await response.json(); - setLetters(content); - console.log("LETTER CONTENT", content); + const userContent = content.filter((c) => c["user_id"] === user); + setLetters(userContent); })(); - }, []); + }, [token, user]); const del = async (id) => { - if (window.confirm("Are you sure: This Letter will be Deleted")) { - await fetch(`http://localhost:8090/letters/${id}`, { + if (window.confirm("Are you sure? This letter will be deleted")) { + await fetch(`${process.env.REACT_APP_LETTERS_API_HOST}/letters/${id}`, { + headers: { Authorization: `Bearer ${token}` }, method: "DELETE", }); setLetters(letters.filter((p) => p.id !== id)); } }; - async function seeLetter(id) { - const response = await fetch(`http://localhost:8090/letters/${id}`); - const content = await response.json(); - setOneLetter(content); - setId(content["id"]); - setContent(content["content"]); - setStance(content["stance"]); - setTopic(content["topic"]); - setDate(content["created"]); - console.log("LETTER ONE CONTENT", content); - - // to show selected reps - async function seeReps() { - const urlReps = `http://localhost:8090/reps/letter/${id}`; - const response = await fetch(urlReps); - if (response.ok) { - const data = await response.json(); - console.log("\n \n DATA", data); - setSelection(data); - } - } - seeReps(); - } - - console.log("ONE LETTER", oneLetter); - console.log("ONE LETTER STANCE", oneStance); - - // const edit = async (id) => { - // if (window.confirm("Are you sure: This Letter will be Deleted")) { - // await fetch(`http://localhost:8090/letters/${id}`, { - // method: "PUT", - // body: JSON.stringify(), - // headers: { - // "Content-Type": "application/json", - // }, - // }); - // setLetter(letter.filter((p) => p.id !== id)); - // } - // }; - return (

Past letters

- {/*
*/} - - - - - - - - - - - - {letters.map((letter) => { - return ( - - - - - - - - ); - })} - -
Date createdStanceIssueDeleteSee details
{letter.created} - {letter.stance ? "in favor of" : "in opposition to"} - {letter.topic} - del(letter.id)} - > - Delete - - - seeLetter(letter.id)} - > - See details - -
-
-
-
-
-

Detail letter view

-
- - Date created: {oneDate} - - - Write a letter{" "} - {oneStance ? "in favor of" : "in opposition to"} {oneTopic} - - {oneContent} - {/* - - */} - - - - Reps selected for this letter - - - - - - - - - - - {repSelection.map((rep, i, j) => { - return ( - - - - - - ); - })} - -
Rep nameRep officeRep address
- {rep.name} - - {rep.office} - - {rep.address} -
-
-
- -
-
-
-
-
+
+ + + + + + + + + + + + {letters.map((letter) => { + return ( + + + + + + + + ); + })} + +
Date createdStanceIssueDeleteSee details
{new Date(letter.created).toLocaleDateString()} + {letter.stance ? "in favor of" : "in opposition to"} + {letter.topic} + del(letter.id)} + > + Delete + + + + See details + +
- - + +
diff --git a/ghi/src/Detailview.js b/ghi/src/Detailview.js new file mode 100644 index 0000000..8db9271 --- /dev/null +++ b/ghi/src/Detailview.js @@ -0,0 +1,147 @@ +import { useParams } from "react-router-dom"; +import { useAuthContext } from "./TokenTest.js"; +import { useEffect, useState } from "react"; +import Card from "react-bootstrap/Card"; +import { Link } from "react-router-dom"; +import copy from "copy-to-clipboard"; + +function DetailView() { + const params = useParams(); + const { token } = useAuthContext(); + const [, setOneLetter] = useState([""]); + const [, setId] = useState(); + const [oneContent, setContent] = useState(); + const [oneStance, setStance] = useState(); + const [oneTopic, setTopic] = useState(); + const [oneDate, setDate] = useState(); + const [repSelection, setSelection] = useState([]); + const options = { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }; + const s = params.id; + useEffect(() => { + (async () => { + const response = await fetch( + `${process.env.REACT_APP_LETTERS_API_HOST}/letters/${s}`, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + const content = await response.json(); + setOneLetter(content); + setId(content["id"]); + setContent(content["content"]); + setStance(content["stance"]); + setTopic(content["topic"]); + setDate(content["created"]); + })(); + async function seeReps() { + const urlReps = `${process.env.REACT_APP_LETTERS_API_HOST}/reps/letter/${s}`; + const response = await fetch(urlReps, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.ok) { + const data = await response.json(); + setSelection(data); + } + } + seeReps(); + }, [token, s]); + const copyToClipboard = () => { + copy(oneContent); + alert(`Your letter has been copied:${oneContent}`); + }; + + const ButtonMailto = ({ mailto, text }) => { + return ( + { + window.location.href = mailto; + e.preventDefault(); + }} + > + {text} + + ); +}; + + return ( +
+
+
+

Detail letter view

+
+
+ + + Date created: {""}{" "} + {oneDate + ? new Date(oneDate).toLocaleDateString(undefined, options) + : ""}{" "} + + + + {oneTopic != null + ? oneStance + ? `Write a letter in favor of ${oneTopic}` + : `Write a letter in opposition to ${oneTopic}` + : ""} + + {oneContent} + + + +
+
+

Reps selected for this letter

+ + + + + + + + + + + + {repSelection.map((rep) => { + return ( + + + + + + + + ); + })} + +
NameOfficeAddressEmailSend email
{rep.name}{rep.office}{rep.address}{rep.email} + { + rep.email === "N/A"? "N/A" : + + } +
+ + + + +
+
+
+ ); +} + +export default DetailView; diff --git a/ghi/src/EditAccount.js b/ghi/src/EditAccount.js index a0664c2..985b08c 100644 --- a/ghi/src/EditAccount.js +++ b/ghi/src/EditAccount.js @@ -1,6 +1,7 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import React from "react"; -import { useCreateUserMutation } from "./store/usersApi"; +import { useEditUserMutation } from "./store/usersApi"; +import { useNavigate } from "react-router-dom"; function InputLabel(props) { const { id, placeholder, labelText, value, onChange, type } = props; @@ -23,19 +24,38 @@ function InputLabel(props) { ); } -// NEED TO UPDATE THIS PAGE FROM CREATE ACCOUNT TO EDIT ACCOUNT -// CURRENTLY THIS IS JUST A COPY OF THE CREATE ACCOUNT PAGE - -function EditAccount(props) { +function EditAccount() { const [full_name, setName] = useState(""); const [email, setEmail] = useState(""); const [zipcode, setZipcode] = useState(""); const [password, setPassword] = useState(""); - const [createUser, result] = useCreateUserMutation(); + const [userId, setId] = useState(""); + const navigate = useNavigate(); + const [editUser] = useEditUserMutation(); + + // to get current account info + useEffect(() => { + async function getUserData() { + const url = `${process.env.REACT_APP_USERS_API_HOST}/token`; + const response = await fetch(url, { + credentials: "include", + }); + if (response.ok) { + const data = await response.json(); + setName(data.account.full_name); + setEmail(data.account.email); + setZipcode(data.account.zipcode); + setId(data.account.id); + } + } + getUserData(); + }, []); async function handleSubmit(e) { e.preventDefault(); - createUser({ full_name, email, zipcode, password }); + editUser({ full_name, email, zipcode, password, userId }).then(() => + navigate("/dashboard") + ); } return ( @@ -76,7 +96,7 @@ function EditAccount(props) { type="password" />
diff --git a/ghi/src/EditLetter.js b/ghi/src/EditLetter.js index 41df855..86be789 100644 --- a/ghi/src/EditLetter.js +++ b/ghi/src/EditLetter.js @@ -1,3 +1,160 @@ -export const EditLetter = () => {}; +import Card from "react-bootstrap/Card"; +import { useState, useEffect } from "react"; +import React from "react"; +import { useAuthContext } from "./TokenTest.js"; +import { useNavigate } from "react-router-dom"; +import { trackPromise } from "react-promise-tracker"; + +export const EditLetter = () => { + const { token } = useAuthContext(); + const [user, setUser] = useState(); + const [, setOneLetter] = useState([""]); + const [oneId, setId] = useState(); + const [oneContent, setContent] = useState(); + const [oneStance, setStance] = useState(); + const [oneTopic, setTopic] = useState(); + const [oneDate, setDate] = useState(); + const navigate = useNavigate(); + + // to get the user's id + useEffect(() => { + async function getUserId() { + const url = `${process.env.REACT_APP_USERS_API_HOST}/token`; + const response = await fetch(url, { + credentials: "include", + }); + if (response.ok) { + const data = await response.json(); + setUser(data.account.id); + } + } + getUserId(); + }, [token, user]); + + // to get the id of the most recent letter created: + useEffect(() => { + async function fetchLetterId() { + const urlLetter = `${process.env.REACT_APP_LETTERS_API_HOST}/api/letters`; + const response = await fetch(urlLetter, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.ok) { + const content = await response.json(); + const data = content.filter((c) => c["user_id"] === user); + for (let i = 0; i < data.length; i++) { + if (i === data.length - 1) { + const lastId = data[i].id; + setId(lastId); + } + } + } + } + fetchLetterId(); + }, [token, user]); + + // to get the content of the letter + useEffect(() => { + if (oneId != null) { + async function showLetter(oneId) { + const response = await fetch( + `${process.env.REACT_APP_LETTERS_API_HOST}/letters/${oneId}`, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + const content = await response.json(); + setOneLetter(content); + setId(content["id"]); + setContent(content["content"]); + setStance(content["stance"]); + setTopic(content["topic"]); + setDate(content["created"]); + } + showLetter(oneId); + } + }, [oneId, token]); + + // to edit a letter: + async function putLetter(oneId, oneContent) { + const url = `${process.env.REACT_APP_LETTERS_API_HOST}/letters/${oneId}?content=${oneContent}`; + const fetchConfig = { + method: "put", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }; + const response = await fetch(url, fetchConfig); + if (response.ok) { + // eslint-disable-next-line no-unused-vars + const data = await response.json(); + } + } + + async function handleSubmit(e) { + e.preventDefault(); + trackPromise( + putLetter(oneId, oneContent).then(() => navigate("/selectreps")) + ); + } + + const options = { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }; + return ( +
+
+
+
+

Edit letter

+
+
+ + + Date created: {""}{" "} + {oneDate + ? new Date(oneDate).toLocaleDateString(undefined, options) + : ""}{" "} + + + + Write a letter{" "} + {oneStance ? "in favor of" : "in opposition to"} {oneTopic} + + + + + + + + +
+ +
+
+
+ ); +}; export default EditLetter; diff --git a/ghi/src/ErrorNotification.js b/ghi/src/ErrorNotification.js index 725883e..b8f5d5c 100644 --- a/ghi/src/ErrorNotification.js +++ b/ghi/src/ErrorNotification.js @@ -1,13 +1,9 @@ function ErrorNotification(props) { - if (!props.error) { - return null; - } + if (!props.error) { + return null; + } - return ( -
- {props.error} -
- ); + return
{props.error}
; } export default ErrorNotification; diff --git a/ghi/src/Landing.js b/ghi/src/Landing.js index b507780..42a6380 100644 --- a/ghi/src/Landing.js +++ b/ghi/src/Landing.js @@ -1,29 +1,26 @@ import React from "react"; import { Link } from "react-router-dom"; - function LandingPage() { return ( -
-
-
-
-

Welcome to Direct Rep

-
-
- - - -
-
- - - -
-
+
+
+ +

Direct Rep

+

+ Get in touch with your representatives and let AI help you write a + letter about important political issues. +

+
+ + + + + +
); diff --git a/ghi/src/LoginTest.js b/ghi/src/LoginTest.js new file mode 100644 index 0000000..cb13f18 --- /dev/null +++ b/ghi/src/LoginTest.js @@ -0,0 +1,61 @@ +import React from "react"; +import { useState } from "react"; +import { useToken } from "./TokenTest.js"; +import { useNavigate } from "react-router-dom"; + +function LoginForm() { + const [, login] = useToken(); + const [username, setUser] = useState(""); + const [password, setPassword] = useState(""); + const navigate = useNavigate(); + + async function clickForm(e) { + e.preventDefault(); + login(username, password).then(() => navigate("/dashboard")); + } + + return ( +
+
+
+

Login

+
+
+ + setUser(e.target.value)} + type="email" + name="username" + value={username} + className="form-control" + required + placeholder="email@email.com" + /> +
+
+ + setPassword(e.target.value)} + type="password" + name="password" + value={password} + className="form-control" + required + placeholder="password" + /> +
+ +
+
+
+
+ ); +} + +export default LoginForm; diff --git a/ghi/src/Logout.js b/ghi/src/Logout.js new file mode 100644 index 0000000..2c6423a --- /dev/null +++ b/ghi/src/Logout.js @@ -0,0 +1,16 @@ +import { useEffect } from "react"; +import { useToken } from "./TokenTest.js"; + +function LogoutButton() { + const [, , logout] = useToken(); + + useEffect(() => { + async function clickLogout() { + logout(); + } + + clickLogout(); + }, [logout]); +} + +export default LogoutButton; diff --git a/ghi/src/Nav.js b/ghi/src/Nav.js index b68a1dd..933c11a 100644 --- a/ghi/src/Nav.js +++ b/ghi/src/Nav.js @@ -1,40 +1,55 @@ import Container from "react-bootstrap/Container"; import Nav from "react-bootstrap/Nav"; import Navbar from "react-bootstrap/Navbar"; -// import NavDropdown from "react-bootstrap/NavDropdown"; +import { Fragment } from "react"; import { NavLink } from "react-router-dom"; +import { useAuthContext } from "./TokenTest.js"; function Navb() { + const { token } = useAuthContext(); + return ( - - DirectREP - + DirectREP - + {token ? ( + + ) : ( + + )} diff --git a/ghi/src/NewLogin.js b/ghi/src/NewLogin.js new file mode 100644 index 0000000..99126a6 --- /dev/null +++ b/ghi/src/NewLogin.js @@ -0,0 +1,62 @@ +import React from "react"; +import { useState } from "react"; +import { useToken } from "./TokenTest.js"; +import { useNavigate } from "react-router-dom"; + +function NewLoginForm() { + const [, login] = useToken(); + const [username, setUser] = useState(""); + const [password, setPassword] = useState(""); + const navigate = useNavigate(); + + async function clickForm(e) { + e.preventDefault(); + login(username, password).then(() => navigate("/dashboard")); + } + + return ( +
+
+
+

Confirm Account

+ {/*
*/} + +
+ + setUser(e.target.value)} + type="email" + name="username" + value={username} + className="form-control" + required + placeholder="email@email.com" + /> +
+
+ + setPassword(e.target.value)} + type="password" + name="password" + value={password} + className="form-control" + required + placeholder="password" + /> +
+ +
+
+
+
+ ); +} + +export default NewLoginForm; diff --git a/ghi/src/RepForm.js b/ghi/src/RepForm.js index c98e63a..ffca59c 100644 --- a/ghi/src/RepForm.js +++ b/ghi/src/RepForm.js @@ -1,55 +1,92 @@ import { useState, useEffect } from "react"; import React from "react"; -import { useCreateRepMutation } from "./store/repsApi"; -import { Link } from "react-router-dom"; +import { useAuthContext } from "./TokenTest.js"; +import { useNavigate } from "react-router-dom"; function RepForm() { - const [name, setName] = useState("name"); - const [office, setOffice] = useState("office"); - const [level, setLevel] = useState("level"); - const [party, setParty] = useState("party"); - const [address, setAddress] = useState("address"); - const [letter_id, setLetterId] = useState("letter id"); + const { token } = useAuthContext(); + const [user, setUser] = useState(); + const [name, setName] = useState(); + const [office, setOffice] = useState(); + const [level, setLevel] = useState(); + const [party, setParty] = useState(); + const [address, setAddress] = useState(); + const [letter_id, setLetterId] = useState(); const [reps_list, setList] = useState([]); const [selection, setSelection] = useState([]); - const [createRep, result] = useCreateRepMutation(); + const [zip, setZip] = useState(); + const [email, setEmail] = useState(); + const navigate = useNavigate(); + + // to get the user's id + useEffect(() => { + async function getUserId() { + const url = `${process.env.REACT_APP_USERS_API_HOST}/token`; + const response = await fetch(url, { + credentials: "include", + }); + if (response.ok) { + const data = await response.json(); + setUser(data.account.id); + } + } + getUserId(); + }, [token, user]); // to get the id of the most recent letter created: useEffect(() => { async function fetchLetterId() { - const urlLetter = `http://localhost:8090/api/letters`; - const response = await fetch(urlLetter); + const urlLetter = `${process.env.REACT_APP_LETTERS_API_HOST}/api/letters`; + const response = await fetch(urlLetter, { + headers: { Authorization: `Bearer ${token}` }, + }); if (response.ok) { - const data = await response.json(); - // console.log("LETTER DATA", data); + const content = await response.json(); + const data = content.filter((c) => c["user_id"] === user); for (let i = 0; i < data.length; i++) { if (i === data.length - 1) { const lastId = data[i].id; - // console.log("LAST", lastId); setLetterId(lastId); } } } } fetchLetterId(); - }, []); + }, [token, user]); - // to select the reps + // to get the zipcode useEffect(() => { - async function fetchReps(zipcode) { - const urlCivics = `http://localhost:8090/civics?zipcode=${zipcode}`; - const response = await fetch(urlCivics); + async function getZipFromUser() { + const url = `${process.env.REACT_APP_USERS_API_HOST}/token`; + const response = await fetch(url, { + credentials: "include", + }); if (response.ok) { const data = await response.json(); - // console.log("\n \n DATA", data); - setList(data); + const zipcode = data.account.zipcode; + setZip(zipcode); } } - fetchReps(`90017`); - // connect this with the user account zip code - }, []); + getZipFromUser(); + }, [token, zip]); - // console.log("\n \n REPS LIST", reps_list); + // to select the reps + useEffect(() => { + if (zip != null) { + async function fetchReps(zip) { + const urlCivics = `${process.env.REACT_APP_LETTERS_API_HOST}/civics?zipcode=${zip}`; + const response = await fetch(urlCivics, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.ok) { + const data = await response.json(); + setList(data); + } + } + fetchReps(zip); + // connects the reps fetch with the user's zip code + } + }, [token, zip]); // to get the rep data from the dropdown selection for the database: async function handleRepChange(event) { @@ -65,53 +102,87 @@ function RepForm() { setAddress( `${addressDict["line1"]} ${addressDict["city"]}, ${addressDict["state"]} ${addressDict["zip"]}` ); + setEmail(reps_list[i].email); } } } + // to create a rep: + async function postRep() { + const data = { office, level, name, party, address, email, letter_id }; + const url = `${process.env.REACT_APP_LETTERS_API_HOST}/api/reps`; + const fetchConfig = { + method: "post", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(data), + }; + const response = await fetch(url, fetchConfig); + if (response.ok) { + // eslint-disable-next-line no-unused-vars + const data = await response.json(); + } + } + // to send that data to the database via the store reducer: async function handleSubmit(e) { e.preventDefault(); - createRep({ office, level, name, party, address, letter_id }).then(() => - showReps(letter_id) + postRep({ office, level, name, party, address, email, letter_id }).then( + () => showReps(letter_id) ); } async function showReps(letter_id) { - const urlReps = `http://localhost:8090/reps/letter/${letter_id}`; - const response = await fetch(urlReps); + const response = await fetch( + `${process.env.REACT_APP_LETTERS_API_HOST}/reps/letter/${letter_id}`, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); if (response.ok) { const data = await response.json(); setSelection(data); } } - // to load it the first time the page renders - useEffect(() => { - showReps(letter_id); - }, [letter_id]); - - // to load it any time we add or delete a rep + // to load the Reps selected useEffect(() => { - showReps(letter_id); - }, []); + if (letter_id != null) { + async function showReps(letter_id) { + const response = await fetch( + `${process.env.REACT_APP_LETTERS_API_HOST}/reps/letter/${letter_id}`, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + if (response.ok) { + const data = await response.json(); + setSelection(data); + } + } + showReps(letter_id); + } + }, [letter_id, token]); async function deleteRep(id) { - // if (window.confirm("Are you sure: This Letter will be Deleted")) { await fetch( - `http://localhost:8090/reps/letters/${letter_id}?rep_id=${id}`, - { - method: "DELETE", - } + `${process.env.REACT_APP_LETTERS_API_HOST}/reps/letters/${letter_id}?rep_id=${id}`, + { headers: { Authorization: `Bearer ${token}` }, method: "DELETE" } ).then(() => showReps(letter_id)); } - // } + + async function finalPage(e) { + e.preventDefault(); + navigate("/review"); + } return (
-

Select your reps

+

Select Representatives

-

Reps selection

+
+

Representatives

+
@@ -150,22 +223,17 @@ function RepForm() { - {selection.map((rep, i, j) => { + {selection.map((rep) => { return ( - - - + + + ); @@ -173,9 +241,9 @@ function RepForm() {
- {rep.name} - - {rep.office} -
{rep.name}{rep.office} deleteRep(rep.rep_id)} - key={rep.rep_id} value={rep.rep_id} > - Delete {rep.rep_id} + Delete
- - - +
diff --git a/ghi/src/Review.js b/ghi/src/Review.js index 276e1e9..59f1459 100644 --- a/ghi/src/Review.js +++ b/ghi/src/Review.js @@ -1,131 +1,177 @@ import Card from "react-bootstrap/Card"; import { useEffect, useState } from "react"; -import { Link } from "react-router-dom"; import copy from "copy-to-clipboard"; +import { useAuthContext } from "./TokenTest.js"; +import { Link } from "react-router-dom"; function ReviewForm() { - const [letter_id, setLetterId] = useState(""); - const [oneLetter, setOneLetter] = useState([""]); - const [oneId, setId] = useState("Letter id"); - const [oneContent, setContent] = useState("Letter content"); - const [oneStance, setStance] = useState("Letter stance"); - const [oneTopic, setTopic] = useState("Letter topic"); - const [oneDate, setDate] = useState("Date"); + const { token } = useAuthContext(); + const [user, setUser] = useState(); + const [, setOneLetter] = useState([]); + const [oneId, setId] = useState(); + const [oneContent, setContent] = useState(); + const [oneStance, setStance] = useState(); + const [oneTopic, setTopic] = useState(); + const [oneDate, setDate] = useState(); const [repSelection, setSelection] = useState([]); + const options = { + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }; - // const letter_id = 4; + useEffect(() => { + async function getUserId() { + const url = `${process.env.REACT_APP_USERS_API_HOST}/token`; + const response = await fetch(url, { + credentials: "include", + }); + if (response.ok) { + const data = await response.json(); + setUser(data.account.id); + } + } + getUserId(); + }, [token, user]); // to get the id of the most recent letter created: useEffect(() => { async function fetchLetterId() { - const urlLetter = `http://localhost:8090/api/letters`; - const response = await fetch(urlLetter); + const urlLetter = `${process.env.REACT_APP_LETTERS_API_HOST}/api/letters`; + const response = await fetch(urlLetter, { + headers: { Authorization: `Bearer ${token}` }, + }); if (response.ok) { - const data = await response.json(); - // console.log("LETTER DATA", data); + const content = await response.json(); + const data = content.filter((c) => c["user_id"] === user); for (let i = 0; i < data.length; i++) { if (i === data.length - 1) { const lastId = data[i].id; - // console.log("LAST", lastId); - setLetterId(lastId); + setId(lastId); } } } } fetchLetterId(); - }, []); + }, [token, user]); useEffect(() => { - async function showLetter(id) { - const response = await fetch(`http://localhost:8090/letters/${id}`); - const content = await response.json(); - setOneLetter(content); - setId(content["id"]); - setContent(content["content"]); - setStance(content["stance"]); - setTopic(content["topic"]); - setDate(content["created"]); - // console.log("LETTER ONE CONTENT", content); + if (oneId != null) { + async function showLetter(oneId) { + const response = await fetch( + `${process.env.REACT_APP_LETTERS_API_HOST}/letters/${oneId}`, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + const content = await response.json(); + setOneLetter(content); + setId(content["id"]); + setContent(content["content"]); + setStance(content["stance"]); + setTopic(content["topic"]); + setDate(content["created"]); + } + showLetter(oneId); } - showLetter(letter_id); - }, [letter_id]); + }, [oneId, token]); useEffect(() => { // to show selected reps - async function seeReps(id) { - const urlReps = `http://localhost:8090/reps/letter/${id}`; - const response = await fetch(urlReps); - if (response.ok) { - const data = await response.json(); - // console.log("\n \n DATA", data); - setSelection(data); + if (oneId != null) { + async function seeReps(oneId) { + const urlReps = `${process.env.REACT_APP_LETTERS_API_HOST}/reps/letter/${oneId}`; + const response = await fetch(urlReps, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.ok) { + const data = await response.json(); + setSelection(data); + } } + seeReps(oneId); } - seeReps(letter_id); - }, [letter_id]); + }, [oneId, token]); const copyToClipboard = () => { copy(oneContent); - alert(`Your letter has been copied: ${oneContent}`); + alert(`Your letter has been copied:${oneContent}`); }; + const ButtonMailto = ({ mailto, text }) => { + return ( + { + window.location.href = mailto; + e.preventDefault(); + }} + > + {text} + + ); +}; + return (
-

Print letter

+
+

Final Letter

+
- Date created: {oneDate} + + Date created:{" "} + {oneDate + ? new Date(oneDate).toLocaleDateString(undefined, options) + : ""}{" "} + Write a letter{" "} {oneStance ? "in favor of" : "in opposition to"} {oneTopic} {oneContent} - {/* - - */} - - - - Reps selected for this letter - - - - - - - - - - - - {repSelection.map((rep, i, j, k, l) => { - return ( - - - - - - - ); - })} - -
NameOfficePartyAddress
- {rep.name} - - {rep.office} - - {rep.party} - - {rep.address} -
-
+
+

Reps selected for this letter

+ + + + + + + + + + + + + {repSelection.map((rep) => { + return ( + + + + + + + + + ); + })} + +
NameOfficePartyAddressEmailSend email
{rep.name}{rep.office}{rep.party}{rep.address}{rep.email} + { + rep.email === "N/A"? "N/A" : + + } +
+