Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature | Chicago Shotspotter Map #5

Merged
merged 64 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
bcc6027
Adding serde_json crate
JosephLai241 Jul 22, 2023
1479d65
Adding an error variant in case something fucks up while making a req…
JosephLai241 Jul 22, 2023
a5fbf54
Adding a new struct for Chicago map-related data
JosephLai241 Jul 22, 2023
5e2d6f5
Adding an endpoint for Chicago map data
JosephLai241 Jul 22, 2023
ceff3c7
Adding a check for the Socrata app token used for Chicago API calls
JosephLai241 Jul 22, 2023
d1929c2
Adding the Chicago module
JosephLai241 Jul 22, 2023
be5d3fc
Adding the Chicago endpoint route
JosephLai241 Jul 22, 2023
2ba323a
Minor formatting
JosephLai241 Jul 22, 2023
bfdd45d
Adding Leaflet CDN and formatting with Prettier
JosephLai241 Jan 12, 2024
6099e9b
Adding Leaflet crate
JosephLai241 Jan 12, 2024
c51bbff
Merge branch 'main' into update/shotspotter-map
JosephLai241 Jan 12, 2024
8693362
Incrementing version number
JosephLai241 Jan 12, 2024
b470618
Using the latest serde_json version
JosephLai241 Jan 12, 2024
bb5ffc7
Adding crates pertaining to the new violence page
JosephLai241 Jan 13, 2024
3243077
Adding the violence page and route
JosephLai241 Jan 13, 2024
e69b7d9
Background is set to always refresh for each page
JosephLai241 Jan 13, 2024
6219913
Formatting with Prettier and Adding styles for the Leaflet map
JosephLai241 Jan 13, 2024
4d268a7
Adding the violence link
JosephLai241 Jan 13, 2024
bb21b10
Adding error variants for the violence page
JosephLai241 Jan 13, 2024
27eed75
Adding error variants for the violence page
JosephLai241 Jan 13, 2024
20a0ddc
Adding the Chicago data models and updating derived traits for the Re…
JosephLai241 Jan 13, 2024
2d6a16a
Merge branch 'update/shotspotter-map' of github.com:JosephLai241/stac…
JosephLai241 Jan 13, 2024
c081617
Minor README overhaul. Adding information for where the violence data…
JosephLai241 Jan 13, 2024
fcc6eda
Adding additional information for where the Chicago UCR codes are ref…
JosephLai241 Jan 13, 2024
57ea4a8
Adding the violence page
JosephLai241 Jan 13, 2024
84bcfe8
Adding site link to the top of the README
JosephLai241 Jan 13, 2024
dc72ac8
Minor formatting
JosephLai241 Jan 13, 2024
4915ee8
Adding cleaned data structs and moving traits elsewhere
JosephLai241 Jan 14, 2024
16b5801
Adding traits for creating a popup from violence data as well as sort…
JosephLai241 Jan 14, 2024
0e0678d
Adding the traits module
JosephLai241 Jan 14, 2024
e5d4a38
the build_gunshot_injury_row() method doesn't need to be public. Remo…
JosephLai241 Jan 14, 2024
7124afc
Moving all popup creation functionality into the popup trait module
JosephLai241 Jan 14, 2024
3218639
Minor formatting
JosephLai241 Jan 14, 2024
b6063f7
Implementing HashMap sorting algorithm
JosephLai241 Jan 14, 2024
f507758
Minor formatting
JosephLai241 Jan 14, 2024
a83a3a8
DRYing more code
JosephLai241 Jan 14, 2024
0297a40
Moving date formatting functionality to a separate utility module
JosephLai241 Jan 14, 2024
4ecf809
Adding the violence page
JosephLai241 Jan 14, 2024
68191b1
Adding styles for Leaflet and formatting with Prettier (I thought it …
JosephLai241 Jan 14, 2024
d296f90
Adding an error variant for the AbstractableHashMap trait
JosephLai241 Jan 14, 2024
63d6381
Adding the AbstractableHashMap trait and removing the sortable trait
JosephLai241 Jan 14, 2024
154eb3b
Adding a sorted_dates field to both Cleaned data structs
JosephLai241 Jan 14, 2024
2f6ad5c
Adding a sorted_dates field to both Cleaned data structs
JosephLai241 Jan 14, 2024
e2d2ce2
Adding a function that builds a table from a provided Vec<(String, i3…
JosephLai241 Jan 21, 2024
49b2f95
Adding a to_vec() method to the trait
JosephLai241 Jan 21, 2024
fb10842
Removing references to sorted_dates
JosephLai241 Jan 21, 2024
5a95a89
Updating match catch to return 'UNKNOWN' instead of '???'
JosephLai241 Jan 21, 2024
599a928
Removing the sorted dates field
JosephLai241 Jan 21, 2024
845e1a0
Merge branch 'update/shotspotter-map' of github.com:JosephLai241/stac…
JosephLai241 Jan 21, 2024
a39fd9e
Adding a step to export the LEAFLET_ACCESS_TOKEN to the build environ…
JosephLai241 Jan 21, 2024
41ee836
Exporting the LEAFLET_ACCESS_TOKEN for clippy, otherwise running it o…
JosephLai241 Jan 21, 2024
0aabd69
Adding a clippy allow rule for enum_variant_names
JosephLai241 Jan 21, 2024
10b8a03
Formatting and reordering web-sys features. I wish Cargo would just d…
JosephLai241 Jan 21, 2024
f9190f7
Applying clippy suggestions throughout
JosephLai241 Jan 21, 2024
7d9f19d
Formatting and adding styles for violence/Shotspotter data-related sh…
JosephLai241 Jan 21, 2024
a320fa8
Formatting and adding styles for violence/Shotspotter data-related sh…
JosephLai241 Jan 21, 2024
4a57edd
Merge branch 'update/shotspotter-map' of github.com:JosephLai241/stac…
JosephLai241 Jan 21, 2024
cdf7ceb
Adding a fuckton of stuff to the page
JosephLai241 Jan 21, 2024
55f0b44
Adding a fuckton of stuff to the page
JosephLai241 Jan 21, 2024
f00d264
Merge branch 'update/shotspotter-map' of github.com:JosephLai241/stac…
JosephLai241 Jan 21, 2024
f8770da
Adding a small message in case no Shotspotter or violence data is ava…
JosephLai241 Jan 21, 2024
876a643
Removing some dead code
JosephLai241 Jan 21, 2024
6b3421d
Incrementing version number
JosephLai241 Jan 21, 2024
526e5bf
Incrementing version number
JosephLai241 Jan 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1 +1 @@
API_VERSION=1.0.0
API_VERSION=1.1.0
3 changes: 3 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
needs: [changes, set-up-scp]
if: ${{ needs.changes.outputs.frontend == 'true' }}

env:
LEAFLET_ACCESS_TOKEN: ${{ secrets.LEAFLET_ACCESS_TOKEN }}

steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
clippy:
runs-on: ubuntu-latest

env:
LEAFLET_ACCESS_TOKEN: ${{ secrets.LEAFLET_ACCESS_TOKEN }}

steps:
- uses: actions/checkout@v4

Expand Down
50 changes: 42 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,61 @@
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/JosephLai241/stacc/prettier.yml?logo=prettier&label=Prettier)](https://github.com/JosephLai241/stacc/actions/workflows/prettier.yml)
[![Docker Image Version (latest semver)](https://img.shields.io/docker/v/jlai241/stacc-api?logo=docker&label=Docker%20version)](https://hub.docker.com/repository/docker/jlai241/stacc-api/general)

# 👉👉👉 [josephlai.dev][josephlai.dev] 👈👈👈

# Table of Contents

- [What Is This?](#what-is-this)
- [Where Does the Violence Data Come From?](#where-does-the-violence-data-come-from)
- [The Stack](#the-stack)

# What Is This?

This is my full-stack portfolio site written entirely in Rust to prove the haters wrong -- Rust _is_ production ready.
This is my full-stack portfolio site written entirely in Rust to prove the haters wrong -- Rust _is_ production ready. This is both a place to blog about computer science and a form of artistic expression.

![crt tv gif][crt tv gif]

I implemented a retro, CRT TV theme. The background GIF changes on each page refresh and a static GIF loads between each GIF change, simulating flipping through TV channels. All GIFs are miscellaneous screen captures from many different animes -- some are mellow, while others are packed with action. There are many more GIFs I would have liked to include in the rotation, but I have restricted the GIFs to anime to keep consistent with a single theme. This list of GIFs will continue to grow over the years as I find more GIFs that suit the aesthetics of the site. Enjoy cycling through the GIFs!

The site consists of many little easter eggs, including but not limited to:

- A dynamic GIF background that changes on each page refresh.
- 404 page stories about a digital nomad getting lost, generated by ChatGPT.
- [Click around and find out]

# Where Does the Violence Data Come From?

My site pulls data from the City of Chicago's Socrata API. Here are links to the documentation:

- [Violence Reduction - Shotspotter Alerts][violence reduction - shotspotter alerts]
- [Violence Reduction - Victims of Homicides and Non-Fatal Shootings][violence reduction - victims of homicides and non-fatal shootings]

I thought it would be cool to implement a dynamic background for the site. A new background GIF is loaded each time you refresh the homepage. This list of GIFs will continue to grow over the years as I find more GIFs that embody the right vibes. Enjoy cycling through the GIFs!
The City of Chicago's Uniform Crime Reporting (UCR) codes and descriptions may be found [at this page][city of chicago ucr codes].

# The Stack

This project uses the following stack:

| | |
| -------- | ------------------------------------------------------------------------------------------------------------------------- |
| Frontend | [`Yew`][yew] |
| Backend | [`Actix Web`][actix web] |
| Database | [![MongoDB Badge](https://img.shields.io/badge/MongoDB-4EA94B?style=for-the-badge&logo=mongodb&logoColor=white)][mongodb] |
### Frontend

[`Yew`][yew]

### API

[`Actix Web`][actix web]

[![docker badge](https://img.shields.io/badge/docker-2ca5e0?style=for-the-badge&logo=docker&logocolor=white)][docker]

### Database

[![MongoDB Badge](https://img.shields.io/badge/MongoDB-4EA94B?style=for-the-badge&logo=mongodb&logoColor=white)][mongodb]

[yew]: https://yew.rs/
[actix web]: https://actix.rs/
[city of chicago ucr codes]: https://gis.chicagopolice.org/pages/crime_details
[crt tv gif]: https://i.imgur.com/8F4N34p.gif
[docker]: https://www.docker.com/
[josephlai.dev]: https://josephlai.dev/
[mongodb]: https://www.mongodb.com/
[violence reduction - shotspotter alerts]: https://dev.socrata.com/foundry/data.cityofchicago.org/3h7q-7mdb
[violence reduction - victims of homicides and non-fatal shootings]: https://dev.socrata.com/foundry/data.cityofchicago.org/gumc-mgzr
[yew]: https://yew.rs/
3 changes: 2 additions & 1 deletion api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
authors = ["Joseph Lai"]
edition = "2021"
name = "api"
version = "1.0.0"
version = "1.1.0"

[[bin]]
name = "api"
Expand All @@ -23,5 +23,6 @@ mongodb = "2.5.0"
rand = "0.8.5"
reqwest = { version = "0.11.18", features = ["json"] }
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.111"
thiserror = "1.0.40"
tokio = { version = "1.28.2", features = ["full"] }
11 changes: 11 additions & 0 deletions api/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ use crate::models::data::Response;
/// Contains all error variants for errors that may be raised by Actix Web endpoints.
#[derive(Debug, Display, derive_more::Error)]
pub enum StaccResponseError {
/// Something fucked up when pinging the Chicago map APIs.
#[display(fmt = "Chicago API error: {error}")]
ChicagoAPIError { error: String },

/// A generic error variant for MongoDB.
#[display(fmt = "MongoDB error: {error}")]
MongoDBError { error: String },
Expand All @@ -34,6 +38,9 @@ impl ResponseError for StaccResponseError {

fn status_code(&self) -> StatusCode {
match *self {
StaccResponseError::ChicagoAPIError { .. } => {
StatusCode::from_u16(500).unwrap_or(StatusCode::BAD_REQUEST)
}
StaccResponseError::MongoDBError { .. } => {
StatusCode::from_u16(500).unwrap_or(StatusCode::BAD_REQUEST)
}
Expand All @@ -57,4 +64,8 @@ pub enum StaccError {
/// Something fucked up while making a request with `reqwest`.
#[error("Reqwest error: {0}")]
Reqwest(#[from] reqwest::Error),

/// Something fucked up with `serde_json`.
#[error("Serde JSON error: {0}")]
SerdeJSONError(#[from] serde_json::Error),
}
2 changes: 2 additions & 0 deletions api/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//! The API for the `stacc`.
#![allow(clippy::enum_variant_names)]

use std::env;

Expand Down Expand Up @@ -49,6 +50,7 @@ async fn main() {
.app_data(mongo.clone())
.service(
web::scope("api")
.service(routes::misc::chiraq)
.service(routes::misc::get_background_gif)
.service(routes::misc::story)
.service(
Expand Down
10 changes: 10 additions & 0 deletions api/src/models/data.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Contains models for miscellaneous data.

use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Contains the Imgur link to the background GIF.
#[derive(Debug, Deserialize, Serialize)]
Expand All @@ -9,6 +10,15 @@ pub struct BackgroundGIF {
pub link: String,
}

/// Contains JSON data returned from Chicago map-related APIs.
#[derive(Debug, Deserialize, Serialize)]
pub struct ChicagoMapData {
/// Data returned from the Shotspotter Alert API endpoint.
pub shotspotter_data: Value,
/// Data returned from the Victims of Homicides and Non-Fatal Shootings API endpoint.
pub violence_data: Value,
}

/// Contains the story for the 404 page.
#[derive(Debug, Deserialize, Serialize)]
pub struct Story {
Expand Down
25 changes: 24 additions & 1 deletion api/src/routes/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::{
errors::StaccResponseError,
middleware,
models::data::{BackgroundGIF, Story},
utils::{environment::EnvironmentVariables, mongo::Mongo},
utils::{chicago::get_vhnfs_shotspotter_data, environment::EnvironmentVariables, mongo::Mongo},
};

lazy_static! {
Expand All @@ -28,6 +28,11 @@ lazy_static! {
static ref FALLBACK_GIF: &'static str = "https://imgur.com/FgJDNsx.gif";
/// The default fallback story if selecting a random story from MongoDB fails.
static ref FALLBACK_STORY: &'static str = "If you don’t like the road you’re walking, pave another one. Except for this one.";

/// The API endpoint for the ShotSpotter Alerts data.
static ref SHOTSPOTTER_ENDPOINT: &'static str = "https://data.cityofchicago.org/resource/3h7q-7mdb.json";
/// The API endpoint for the Victims of Homicides and Non-Fatal Shootings data.
static ref VHNFS_ENDPOINT: &'static str = "https://data.cityofchicago.org/resource/gumc-mgzr.json";
}

/// Create a cookie that stores the background GIF link. Returns `Some(Cookie)` if able to retrieve
Expand Down Expand Up @@ -98,6 +103,24 @@ pub async fn get_background_gif(
}
}

/// Get the data that will be plotted on the Chicago map on the `violence` page.
#[get("/chiraq")]
pub async fn chiraq(
mongo: Data<Mongo>,
request: HttpRequest,
) -> Result<HttpResponse, StaccResponseError> {
if let Err(error) = middleware::log_visitor_data(&mongo, &request).await {
error!("{}", error);
}

get_vhnfs_shotspotter_data()
.await
.map(|chicago_map_data| HttpResponse::Ok().json(chicago_map_data))
.map_err(|error| StaccResponseError::ChicagoAPIError {
error: error.to_string(),
})
}

/// Get a 404 page story by choosing a random story stored in the stories collection.
#[get("/story")]
pub async fn story(
Expand Down
1 change: 1 addition & 0 deletions api/src/utils/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ lazy_static! {
"STACC_POSTS_COLLECTION_NAME",
"STACC_STORIES_COLLECTION_NAME",
"STACC_VISITORS_COLLECTION_NAME",
"SOCRATA_APP_TOKEN"
];
}

Expand Down
54 changes: 54 additions & 0 deletions api/src/utils/chicago.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//! Contains miscellaneous utilities for Chicago-related functionality.

use lazy_static::lazy_static;
use reqwest::Client;
use serde_json::Value;

use crate::{errors::StaccError, models::data::ChicagoMapData};

use super::environment::EnvironmentVariables;

lazy_static! {
/// A `reqwest` `Client` that is reused for Chicago API requests.
static ref REQUEST_CLIENT: Client = Client::new();
/// The API endpoint for the ShotSpotter Alerts data.
static ref SHOTSPOTTER_ENDPOINT: &'static str = "https://data.cityofchicago.org/resource/3h7q-7mdb.json";
/// The API endpoint for the Victims of Homicides and Non-Fatal Shootings data.
static ref VHNFS_ENDPOINT: &'static str = "https://data.cityofchicago.org/resource/gumc-mgzr.json";
}

/// Get data for Victims of Homicides and Non-Fatal Shootings and Shotspotter Alert data from the
/// Chicago APIs.
pub async fn get_vhnfs_shotspotter_data() -> Result<ChicagoMapData, StaccError> {
let violence_data: Value = serde_json::from_str(
&REQUEST_CLIENT
.get(VHNFS_ENDPOINT.to_string())
.header(
"X-App-Token",
EnvironmentVariables::SocrataAppToken.env_var()?,
)
.send()
.await?
.text()
.await?,
)?;
let shotspotter_data: Value = serde_json::from_str(
&REQUEST_CLIENT
.get(SHOTSPOTTER_ENDPOINT.to_string())
.header(
"X-App-Token",
EnvironmentVariables::SocrataAppToken.env_var()?,
)
.send()
.await?
.text()
.await?,
)?;

let chicago_map_data = ChicagoMapData {
shotspotter_data,
violence_data,
};

Ok(chicago_map_data)
}
3 changes: 3 additions & 0 deletions api/src/utils/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub enum EnvironmentVariables {
MongoDBURI,
/// The MongoDB user.
MongoDBUser,
/// The Socrata app token for Chicago map-related data.
SocrataAppToken,
/// The port number the API runs on.
StaccAPIPortNumber,
/// The name of the collection that contains all backgrounds.
Expand All @@ -35,6 +37,7 @@ impl EnvironmentVariables {
Self::MongoDBPassword => Ok(env::var("MONGO_PASSWORD")?),
Self::MongoDBURI => Ok(env::var("MONGO_URI")?),
Self::MongoDBUser => Ok(env::var("MONGO_USER")?),
Self::SocrataAppToken => Ok(env::var("SOCRATA_APP_TOKEN")?),
Self::StaccAPIPortNumber => Ok(env::var("STACC_API_PORT_NUMBER")?),
Self::StaccBackgroundsCollectionName => {
Ok(env::var("STACC_BACKGROUNDS_COLLECTION_NAME")?)
Expand Down
1 change: 1 addition & 0 deletions api/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Contains miscellaneous utilities for `stacc`.

pub mod checks;
pub mod chicago;
pub mod environment;
pub mod mongo;
19 changes: 17 additions & 2 deletions frontend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
authors = ["Joseph Lai"]
edition = "2021"
name = "frontend"
version = "1.0.1"
version = "1.1.0"

[dependencies]
chrono = "0.4.24"
Expand All @@ -12,14 +12,29 @@ gloo-console = "0.2.3"
gloo-net = { version = "0.2.6", features = ["http"] }
gloo-timers = { version = "0.2.6", features = ["futures"] }
gloo-utils = "0.1.6"
hex = "0.4.3"
js-sys = "0.3.63"
lazy_static = "1.4.0"
leaflet = "0.4.0"
pulldown-cmark = "0.9.3"
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.111"
sha2 = "0.10.8"
thiserror = "1.0.40"
wasm-bindgen = "0.2.86"
wasm-bindgen-futures = "0.4.36"
wasm-cookies = "0.1.0"
web-sys = { version = "0.3.63", features = ["CssStyleDeclaration", "Document", "HtmlElement", "MutationObserver", "MutationObserverInit", "Node"] }
web-sys = { version = "0.3.63", features = [
"CssStyleDeclaration",
"Document",
"HtmlElement",
"HtmlInputElement",
"HtmlTableCellElement",
"HtmlTableElement",
"HtmlTableRowElement",
"MutationObserver",
"MutationObserverInit",
"Node",
] }
yew = { version = "0.20.0", features = ["csr"] }
yew-router = "0.17.0"
13 changes: 13 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,19 @@
rel="stylesheet"
/>

<!-- Add Leaflet CDN. -->
<link
rel="stylesheet"
href="https://unpkg.com/[email protected]/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
crossorigin=""
/>
<script
src="https://unpkg.com/[email protected]/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
crossorigin=""
></script>

<!-- Load the anime GIF of someone typing nonsense into a computer. -->
<link
data-trunk
Expand Down
Loading