diff --git a/app.R b/app.R index eb417d9..716506e 100644 --- a/app.R +++ b/app.R @@ -8,7 +8,8 @@ library(leafpop) library(DT) # load data ---- -owners <- readRDS("./data/owners.rds") +owners <- readRDS("./data/owners.rds") +listings <- readRDS("./data/listings.rds") last_sale_date <- format(attr(owners, "last_sale_date"), "%Y-%m-%d") sbdvn <- sf::st_read("./data/plats/plats.shp") venice_bndry <- readRDS("./data/venice.rds") @@ -87,6 +88,11 @@ ui <- f7Page( title = "Owners", icon = f7Icon("person_2_fill") ), + f7PanelItem( + tabName = "Listings", + title = "Listings", + icon = f7Icon("tag_fill") + ), f7PanelItem( tabName = "Resources", title = "Resources", @@ -244,6 +250,17 @@ ui <- f7Page( ) ) ), + #### listings ---- + f7Tab( + title = "Listings", + tabName = "Listings", + icon = f7Icon("tag_fill"), + f7Card( + title = "Active Listings:", + divider = TRUE, + DTOutput("listings_table") + ) + ), #### services ---- f7Tab( title = "Resources", @@ -456,6 +473,25 @@ server <- function(input, output) { } ) +# listings table ---- + output$listings_table <- renderDT({ + datatable( + listings |> + select(listed_date, address, sqft, price, price_per_sqft) |> + mutate( + price = scales::dollar(price), + price_per_sqft = scales::dollar(price_per_sqft) + ), + colnames = c("Listed", "Address", "Sq Ft", "Price", "$/Sq Ft"), + rownames = FALSE, + options = list( + pageLength = 25, + searching = FALSE, + dom = 't' + ) + ) + }) + # venice map ---- output$venice_map <- renderLeaflet({ leaflet() %>% diff --git a/data-raw/update_listings.R b/data-raw/update_listings.R new file mode 100644 index 0000000..a5b0ba2 --- /dev/null +++ b/data-raw/update_listings.R @@ -0,0 +1,68 @@ +# update_listings.R +# Weekly update: pull active listings from RentCast within 1 mile of +# St. Andrews center, clip to plat boundary, save data/listings.rds. +# Input: .Renviron (RENTCAST_API_KEY) +# data/plats/plats.shp +# Output: data/listings.rds + +library(httr2) +library(jsonlite) +library(sf) +library(dplyr) + +# ── API key ─────────────────────────────────────────────────────────────────── +api_key <- Sys.getenv("RENTCAST_API_KEY") +if (identical(api_key, "")) stop("Missing RENTCAST_API_KEY in .Renviron") + +# ── Pull listings ───────────────────────────────────────────────────────────── +resp <- request("https://api.rentcast.io/v1/listings/sale") |> + req_headers( + "Accept" = "application/json", + "X-Api-Key" = api_key + ) |> + req_url_query( + latitude = 27.076199, + longitude = -82.362253, + radius = 1, + status = "Active", + limit = 500 + ) |> + req_retry(max_tries = 3) |> + req_perform() + +if (resp_status(resp) >= 400) + stop("RentCast API error: HTTP ", resp_status(resp), "\n", resp_body_string(resp)) + +listings_raw <- jsonlite::fromJSON(resp_body_string(resp), flatten = TRUE) +cat("Fetched:", nrow(listings_raw), "listings\n") + +# ── Clip to St. Andrews plat boundary ──────────────────────────────────────── +plats <- sf::st_read("./data/plats/plats.shp", quiet = TRUE) |> + sf::st_union() + +listings_sf <- sf::st_as_sf( + listings_raw, + coords = c("longitude", "latitude"), + crs = 4326, + remove = FALSE +) + +in_plat <- lengths(sf::st_within(listings_sf, plats)) > 0 +listings <- listings_raw[in_plat, ] +cat("After plat clip:", nrow(listings), "listings\n") + +# ── Select and clean columns ────────────────────────────────────────────────── +listings <- listings |> + transmute( + listed_date = as.Date(listedDate), + address = formattedAddress, + sqft = as.numeric(squareFootage), + price = as.numeric(price), + price_per_sqft = round(price / sqft, 0), + latitude, + longitude + ) |> + arrange(desc(listed_date)) + +cat("Listings saved:", nrow(listings), "\n") +saveRDS(listings, "./data/listings.rds") diff --git a/renv.lock b/renv.lock index 6f8fad9..2263f89 100644 --- a/renv.lock +++ b/renv.lock @@ -1625,6 +1625,66 @@ "Maintainer": "Hadley Wickham ", "Repository": "P3M" }, + "httr2": { + "Package": "httr2", + "Version": "1.2.2", + "Source": "Repository", + "Title": "Perform HTTP Requests and Process the Responses", + "Authors@R": "c( person(\"Hadley\", \"Wickham\", , \"hadley@posit.co\", role = c(\"aut\", \"cre\")), person(\"Posit Software, PBC\", role = c(\"cph\", \"fnd\")), person(\"Maximilian\", \"Girlich\", role = \"ctb\") )", + "Description": "Tools for creating and modifying HTTP requests, then performing them and processing the results. 'httr2' is a modern re-imagining of 'httr' that uses a pipe-based interface and solves more of the problems that API wrapping packages face.", + "License": "MIT + file LICENSE", + "URL": "https://httr2.r-lib.org, https://github.com/r-lib/httr2", + "BugReports": "https://github.com/r-lib/httr2/issues", + "Depends": [ + "R (>= 4.1)" + ], + "Imports": [ + "cli (>= 3.0.0)", + "curl (>= 6.4.0)", + "glue", + "lifecycle", + "magrittr", + "openssl", + "R6", + "rappdirs", + "rlang (>= 1.1.0)", + "vctrs (>= 0.6.3)", + "withr" + ], + "Suggests": [ + "askpass", + "bench", + "clipr", + "covr", + "docopt", + "httpuv", + "jose", + "jsonlite", + "knitr", + "later (>= 1.4.0)", + "nanonext", + "otel (>= 0.2.0)", + "otelsdk (>= 0.2.0)", + "paws.common (>= 0.8.0)", + "promises", + "rmarkdown", + "testthat (>= 3.1.8)", + "tibble", + "webfakes (>= 1.4.0)", + "xml2" + ], + "VignetteBuilder": "knitr", + "Config/Needs/website": "tidyverse/tidytemplate", + "Config/testthat/edition": "3", + "Config/testthat/parallel": "true", + "Config/testthat/start-first": "resp-stream, req-perform", + "Encoding": "UTF-8", + "RoxygenNote": "7.3.3", + "NeedsCompilation": "no", + "Author": "Hadley Wickham [aut, cre], Posit Software, PBC [cph, fnd], Maximilian Girlich [ctb]", + "Maintainer": "Hadley Wickham ", + "Repository": "CRAN" + }, "janitor": { "Package": "janitor", "Version": "2.2.1",