Compare commits

...

8 Commits

Author SHA1 Message Date
d8f1bc3110 fix: remove private_listings.csv chmod from deploy script
All checks were successful
Deploy stAndrews / deploy (push) Successful in 34s
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
2026-03-13 08:46:46 -04:00
c02715409b feat: Display last sale date from owners data in UI
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
2026-03-13 08:33:26 -04:00
28ced93180 refactor: modularize app into config, UI, and server components
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
2026-03-13 08:22:15 -04:00
6700075e72 feat: add restart instructions to resources tab
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
2026-03-13 06:41:44 -04:00
30c6535130 feat: remove private list section from UI and server
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
2026-03-13 06:37:40 -04:00
6a11f96f33 refactor: improve filtering logic and map initialization
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
2026-03-13 06:31:38 -04:00
1b75760496 fix: improve filtering logic and map updates in Shiny app
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
2026-03-13 06:29:44 -04:00
e0cf03df2f fix: return unique subdivision IDs when "All" selected
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
2026-03-13 06:24:30 -04:00
6 changed files with 783 additions and 654 deletions

View File

@@ -25,6 +25,5 @@ jobs:
git pull origin main && git pull origin main &&
docker compose build && docker compose build &&
docker compose up -d && docker compose up -d &&
chmod 666 /data/projects/r/stAndrews/data/private_listings.csv &&
docker exec analytics-gateway caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile docker exec analytics-gateway caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
" "

493
R/app_server.R Normal file
View File

@@ -0,0 +1,493 @@
# Server logic for St. Andrews Shiny App
create_server <- function(input, output, session) {
# Load data
owners <- readRDS(app_config$data_paths$owners)
listings <- readRDS(app_config$data_paths$listings) |>
arrange(price_per_sqft) |>
mutate(
price_fmt = scales::dollar(price),
ppsf_fmt = scales::dollar(price_per_sqft)
)
sales <- readRDS(app_config$data_paths$sales) |>
arrange(desc(listed_date)) |>
mutate(
price_fmt = scales::dollar(price),
ppsf_fmt = scales::dollar(price_per_sqft)
)
last_sale_date <- format(attr(owners, "last_sale_date"), "%Y-%m-%d")
sbdvn <- sf::st_read(app_config$data_paths$plats)
beaches <- readRDS(app_config$data_paths$beaches)
# update tabs depending on side panel
observeEvent(input$menu, {
updateF7Tabs(id = "tabs",
selected = input$menu)
})
# resources sub-section toggle
res_section <- reactiveVal("beaches")
observeEvent(input$res_beaches, { res_section("beaches") })
observeEvent(input$res_links, { res_section("links") })
observeEvent(input$res_restart, { res_section("restart") })
output$resources_content <- renderUI({
if (res_section() == "beaches") {
tagList(
f7Card(
title = "Beaches",
divider = TRUE,
leafletOutput("beach_map"),
footer = p("Source: Environmental Protection Agency")
),
f7Block(
h3("Helpful Links:"),
f7List(
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
f7ListItem(title = "EPA Beaches", href = "https://www.epa.gov/beaches", external = TRUE),
f7ListItem(title = "Red Tide Forecast", href = "https://habforecast.gcoos.org/", external = TRUE),
f7ListItem(title = "Healthy Beaches Program", href = "https://fdoh.maps.arcgis.com/apps/instant/nearby/index.html?appid=7106a20597de4bff98cc5ebc7f932047&findSource=0&find=1600%2520Harbor%2520Dr%2520S%252C%2520Venice%252C%2520Florida%252C%252034285&sliderDistance=2", external = TRUE),
f7ListItem(title = "MOTE Beach Conditions", href = "https://visitbeaches.org/beach/6/report/53033", external = TRUE),
f7ListItem(title = "National Hurricane Center", href = "https://www.nhc.noaa.gov/", external = TRUE)
)
)
)
} else {
tagList(
f7BlockTitle(title = "Services:", size = "medium"),
f7Block(
f7List(
mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE,
f7Link(label = "City of Venice", href = "https://www.venicegov.com/"),
f7Link(label = "Florida Power & Light", href = "https://www.fpl.com/"),
f7Link(label = "Sarasota County", href = "https://www.scgov.net/"),
f7Link(label = "Property Appraiser", href = "https://www.sc-pa.com/"),
f7Link(label = "Open GIS Portal", href = "https://data-sarco.opendata.arcgis.com/"),
f7Link(label = "Waste & Recycling", href = "https://www.venicegov.com/government/public-works/waste-and-recycling"),
f7Link(label = "Condo Regulation", href = "https://condos.myfloridalicense.com/"),
f7Link(label = "Property Records Search", href = "https://www.sarasotaclerk.com/records/official-records/search-land-records")
)
),
f7BlockTitle(title = "Documents:", size = "medium"),
f7Block(
f7List(
mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE,
f7Link(label = "St. Andrews Covenants", href = "docs/2000_01_01_st_andrews_covenants.pdf"),
f7Link(label = "St. Andrews (unrecorded)", href = "docs/2004_06_23_sap_map.pdf"),
f7Link(label = "Patios 2", href = "docs/1997_08_12_patios_2_plat.pdf"),
f7Link(label = "Patios 3", href = "docs/1998_11_17_patios_3_plat.pdf"),
f7Link(label = "Villas 2", href = "docs/1998_09_14_villas_2_plat.pdf")
)
)
)
}
})
# Initialize filteredOwners with all owners
filteredOwners <- reactiveVal(owners)
# Function to filter owners based on inputs
filterOwners <- function() {
filtered <- owners
# Filter by subdivision if specified
if (!is.null(input$sub_name) && input$sub_name != "All") {
# Get the subdivision IDs for the selected subdivision name
sub_ids <- sbdvn %>%
filter(sub_name == input$sub_name) %>%
pull(max_sub_id)
filtered <- filtered %>%
filter(subdivision %in% sub_ids)
}
# Filter by name if specified
if (!is.null(input$name) && input$name != "") {
filtered <- filtered %>%
filter(
grepl(input$name, owner_1, ignore.case = TRUE) |
grepl(input$name, owner_2, ignore.case = TRUE)
)
}
# Filter by location if specified
if (!is.null(input$location) && input$location != "") {
filtered <- filtered %>%
filter(grepl(input$location, location, ignore.case = TRUE))
}
return(filtered)
}
# Update filtered owners when filter button is clicked
observeEvent(input$filterButton, {
filteredOwners(filterOwners())
})
# Also update when any of the filter inputs change
observe({
# Track dependencies
input$name
input$location
input$sub_name
# Update filtered owners, but only if the filter button has been clicked at least once
# To prevent immediate filtering on app start
if (!is.null(input$filterButton)) {
if (input$filterButton > 0) {
filteredOwners(filterOwners())
}
}
})
mean_lat <- reactive({
if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) {
filteredOwners() %>%
st_coordinates() %>%
.[, "Y"] %>%
mean()
} else {
app_config$map_config$default_lat
}
})
mean_lng <- reactive({
if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) {
filteredOwners() %>%
st_coordinates() %>%
.[, "X"] %>%
mean()
} else {
app_config$map_config$default_lng
}
})
output$map <- renderLeaflet({
leaflet() %>%
addProviderTiles("CartoDB.Voyager") %>%
addPolygons(
data = sbdvn,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2,
label = ~sub_name,
group = "Subdivisions"
)
})
# Update map markers when filteredOwners changes
observe({
leafletProxy("map") %>%
clearGroup("Owners") %>%
addMarkers(
data = filteredOwners(),
popup = popupTable(
filteredOwners(),
row.numbers = FALSE,
feature.id = FALSE,
zcol = c(
"label",
"owner_1",
"owner_2"
)
),
group = "Owners"
) %>%
addLayersControl(
overlayGroups = c("Subdivisions", "Owners"),
options = layersControlOptions(collapsed = FALSE)
) %>%
setView(lng = mean_lng(), lat = mean_lat(), zoom = app_config$map_config$default_zoom)
})
output$table <- renderDT({
if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) {
my_table <-
filteredOwners() %>%
st_drop_geometry() %>%
select(label, owner_1, owner_2, homestead)
datatable(my_table,
colnames = c("Address", "Owner 1", "Owner 2", "Homestead"),
rownames = FALSE,
options = list(
pageLength = 10,
scrollX = TRUE,
searching = FALSE,
lengthMenu = c(5, 10, 25, 50),
dom = 'tpi'
)
)
} else {
# Return empty table with same structure
my_table <- data.frame(
label = character(0),
owner_1 = character(0),
owner_2 = character(0),
homestead = numeric(0)
)
datatable(my_table,
colnames = c("Address", "Owner 1", "Owner 2", "Homestead"),
rownames = FALSE,
options = list(
pageLength = 10,
scrollX = TRUE,
searching = FALSE,
lengthMenu = c(5, 10, 25, 50),
dom = 'tpi'
)
)
}
})
prep_mailing <- function(data) {
data |>
sf::st_drop_geometry() |>
dplyr::select(owner_1, owner_2, mailing_address_1, mailing_address_2,
mailing_city, mailing_state, mailing_zip_code) |>
dplyr::mutate(
owner_2 = ifelse(is.na(owner_2), "", owner_2),
mailing_address_2 = ifelse(is.na(mailing_address_2), "", mailing_address_2)
) |>
dplyr::rename(
address_1 = mailing_address_1,
address_2 = mailing_address_2,
city = mailing_city,
state = mailing_state,
zip = mailing_zip_code
)
}
output$download_filtered <- downloadHandler(
filename = "st_andrews_owners_filtered.csv",
content = function(file) {
write.csv(prep_mailing(filteredOwners()), file, row.names = FALSE)
}
)
output$download_all <- downloadHandler(
filename = "st_andrews_owners_all.csv",
content = function(file) {
write.csv(prep_mailing(owners), file, row.names = FALSE)
}
)
# listings map ----
output$listings_map <- renderLeaflet({
leaflet(listings) |>
addProviderTiles("CartoDB.Voyager") |>
addPolygons(
data = sbdvn,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2,
label = ~sub_name
) |>
addMarkers(
lat = ~latitude,
lng = ~longitude,
popup = ~paste0(
"<b>", address, "</b><br>",
"Price: ", price_fmt, "<br>",
"Sq Ft: ", sqft, "<br>",
"$/Sq Ft: ", ppsf_fmt
)
) |>
setView(lng = app_config$map_config$default_lng,
lat = app_config$map_config$default_lat,
zoom = app_config$map_config$default_zoom)
})
# listings table ----
output$listings_table <- renderDT({
datatable(
listings |> select(listed_date, address, sqft, price_fmt, ppsf_fmt),
colnames = c("Listed", "Address", "Sq Ft", "Price", "$/Sq Ft"),
rownames = FALSE,
options = list(
pageLength = 25,
searching = FALSE,
dom = 't'
)
)
})
# sales map ----
output$sales_map <- renderLeaflet({
leaflet(sales) |>
addProviderTiles("CartoDB.Voyager") |>
addPolygons(
data = sbdvn,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2,
label = ~sub_name
) |>
addMarkers(
lat = ~latitude,
lng = ~longitude,
popup = ~paste0(
"<b>", address, "</b><br>",
"Sale Date: ", listed_date, "<br>",
"Price: ", price_fmt, "<br>",
"Sq Ft: ", sqft, "<br>",
"$/Sq Ft: ", ppsf_fmt
)
) |>
setView(lng = app_config$map_config$default_lng,
lat = app_config$map_config$default_lat,
zoom = app_config$map_config$default_zoom)
})
# sales table ----
output$sales_table <- renderDT({
datatable(
sales |> select(listed_date, address, sqft, price_fmt, ppsf_fmt),
colnames = c("Date", "Address", "Sq Ft", "Price", "$/Sq Ft"),
rownames = FALSE,
options = list(
pageLength = 10,
searching = FALSE,
dom = 't'
)
)
})
# beach map ----
output$beach_map <- renderLeaflet({
leaflet() %>%
addProviderTiles("CartoDB.Voyager") %>%
setView(lng = app_config$map_config$beach_lng,
lat = app_config$map_config$beach_lat,
zoom = app_config$map_config$beach_zoom) %>%
addMarkers(
data = beaches,
lat = ~lat,
lng = ~lng,
popup = ~beach_name
)
})
}
# Server logic for St. Andrews Shiny App
library(shiny)
library(shinyMobile)
library(sf)
library(dplyr)
library(leaflet)
library(DT)
create_server <- function(input, output, session) {
# Load configuration
source("./R/config.R", local = TRUE)
# Load owners data
owners_data <- reactive({
req(app_config$data_paths$owners)
data <- readRDS(app_config$data_paths$owners)
return(data)
})
# Get last sale date
last_sale_date <- reactive({
owners <- owners_data()
date <- attr(owners, "last_sale_date")
if (is.null(date)) {
return(Sys.Date())
}
return(date)
})
# Display last sale date
output$last_sale_date_display <- renderText({
date <- last_sale_date()
paste("Last sale date:", format(date, "%B %d, %Y"))
})
# More server logic for the map, table, etc. would go here
# For now, I'll add placeholders for the outputs referenced in the UI
# Map output
output$map <- renderLeaflet({
leaflet() %>%
addTiles() %>%
setView(lng = -82.4, lat = 27.1, zoom = 12)
})
# Table output
output$table <- renderDT({
datatable(data.frame(Note = "Owner data would be displayed here"))
})
# Listings map
output$listings_map <- renderLeaflet({
leaflet() %>%
addTiles() %>%
setView(lng = -82.4, lat = 27.1, zoom = 12)
})
# Listings table
output$listings_table <- renderDT({
datatable(data.frame(Note = "Active listings would be displayed here"))
})
# Sales map
output$sales_map <- renderLeaflet({
leaflet() %>%
addTiles() %>%
setView(lng = -82.4, lat = 27.1, zoom = 12)
})
# Sales table
output$sales_table <- renderDT({
datatable(data.frame(Note = "Recent sales would be displayed here"))
})
# Resources content
output$resources_content <- renderUI({
f7Card(
title = "Community Resources",
"Links and documents will be displayed here."
)
})
# Download handlers
output$download_filtered <- downloadHandler(
filename = function() {
paste("owners-filtered-", Sys.Date(), ".csv", sep = "")
},
content = function(file) {
write.csv(data.frame(Note = "Filtered owner data"), file)
}
)
output$download_all <- downloadHandler(
filename = function() {
paste("owners-all-", Sys.Date(), ".csv", sep = "")
},
content = function(file) {
write.csv(data.frame(Note = "All owner data"), file)
}
)
# Button observers for resources tab
observeEvent(input$res_beaches, {
f7Dialog(
title = "Beaches",
text = "Beach information would be displayed here."
)
})
observeEvent(input$res_links, {
f7Dialog(
title = "Links",
text = "Community links would be displayed here."
)
})
observeEvent(input$res_restart, {
session$reload()
})
}

232
R/app_ui.R Normal file
View File

@@ -0,0 +1,232 @@
# UI components for St. Andrews Shiny App
create_ui <- function() {
f7Page(
title = app_config$app_config$title,
## header ----
tags$head(
tags$link(rel = "manifest", href = "manifest.json"),
tags$link(rel = "apple-touch-icon", sizes = "180x180", href = "images/apple-touch-icon.png"),
tags$meta(name = "apple-mobile-web-app-capable", content = "yes"),
tags$meta(name = "apple-mobile-web-app-status-bar-style", content = "default"),
tags$style(HTML("
.dataTables_wrapper {
color: white;
}
.dataTables_wrapper table.dataTable thead th,
.dataTables_wrapper table.dataTable tbody td {
color: white;
}
.dataTables_wrapper table.dataTable tbody tr.odd {
background-color: #333;
}
.dataTables_wrapper table.dataTable tbody tr.even {
background-color: #444;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
color: white !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current{
color: black !important;
}
.dataTables_filter {
display: none; /* Hide the search box */
}
"))
),
## options ----
options = list(
theme = app_config$app_config$theme,
dark = app_config$app_config$dark,
pullToRefresh = TRUE
),
## layout ----
f7TabLayout(
### panels ----
panels = tagList(
f7Panel(
id = "panel-left",
side = "left",
effect = "push",
title = "Menu",
f7PanelMenu(
id = "menu",
f7PanelItem(
tabName = "About",
title = "About",
icon = f7Icon("info_circle"),
active = TRUE
),
f7PanelItem(
tabName = "Owners",
title = "Owners",
icon = f7Icon("person_2_fill")
),
f7PanelItem(
tabName = "Listings",
title = "Listings",
icon = f7Icon("tag_fill")
),
f7PanelItem(
tabName = "Sales",
title = "Sales",
icon = f7Icon("dollarsign_circle_fill")
),
f7PanelItem(
tabName = "Resources",
title = "Resources",
icon = f7Icon("hammer_fill")
)
)
)
),
### navbar ----
navbar = f7Navbar(
title = app_config$app_config$title,
hairline = TRUE,
leftPanel = TRUE
),
### begin tabs ----
f7Tabs(
id = "tabs",
animated = TRUE,
#### about ----
f7Tab(
title = "About",
tabName = "About",
icon = f7Icon("info_circle"),
active = TRUE,
f7Card(
title = "About",
divider = "TRUE",
tags$img(src = "images/st_andrews.jpg", width = "100%"),
"St. Andrews Park is located in Venice, Florida. The condominiums are a mix of single-family homes, villas (2 and 4 units) and multi-unit buildings (8 units). There are 388 separate properties within 14 subdivisions. St. Andrews Park is one of many communities in the Plantation Golf and Country Club. It is should not be confused with the adjacent community St. Andrews East.",
footer = tagList(
f7Link("More Info", href = "http://www.cpmi.us/standrews-plantation/outside_home.asp")
)
)
),
#### owners ----
f7Tab(
title = "Owners",
tabName = "Owners",
icon = f7Icon("person_2_fill"),
f7Card(
title = "Owners:",
divider = TRUE,
raised = TRUE,
footer = textOutput("last_sale_date_display"),
f7List(
inset = TRUE,
dividers = TRUE,
strong = TRUE,
outline = FALSE,
f7Text(
inputId = "name",
label = "Last Name:",
placeholder = "\"Patel\""
),
f7Text(
inputId = "location",
label = "Address:",
placeholder = "\"123 Chalmers\""
),
f7Select(
inputId = "sub_name",
label = "Select Subdivision:",
choices = c("All", sort(sbdvn$sub_name))
),
tags$br(),
f7Button(
inputId = "filterButton",
label = "Find Owners",
icon = "",
color = "blue"
)
)
),
f7Card(
title = "Map:",
divider = TRUE,
leafletOutput("map")
),
f7Card(
title = "Table:",
divider = TRUE,
DTOutput("table")
),
f7Card(
title = "Download:",
divider = TRUE,
f7List(
inset = TRUE,
downloadButton("download_filtered", "Download Filtered"),
downloadButton("download_all", "Download All (388)")
)
)
),
#### listings ----
f7Tab(
title = "Listings",
tabName = "Listings",
icon = f7Icon("tag_fill"),
f7Card(
title = "Map:",
divider = TRUE,
leafletOutput("listings_map")
),
f7Card(
title = "Active Listings:",
divider = TRUE,
DTOutput("listings_table")
)
),
#### sales ----
f7Tab(
title = "Sales",
tabName = "Sales",
icon = f7Icon("dollarsign_circle_fill"),
f7Card(
title = "Map:",
divider = TRUE,
leafletOutput("sales_map")
),
f7Card(
title = "Recent Sales:",
divider = TRUE,
DTOutput("sales_table")
)
),
#### resources ----
f7Tab(
title = "Resources",
tabName = "Resources",
icon = f7Icon("hammer_fill"),
f7Segment(
f7Button(inputId = "res_beaches", label = "Beaches"),
f7Button(inputId = "res_links", label = "Links"),
f7Button(inputId = "res_restart", label = "Restart")
),
uiOutput("resources_content")
)
### end tabs----
),
### begin scripts ----
tags$script(
HTML(
"
$(document).on('click', '#pdfLink', function(event) {
event.preventDefault();
window.open($(this).attr('href'), '_blank');
});
$(document).on('click', '#download_filtered, #download_all', function(event) {
event.preventDefault();
window.open($(this).attr('href'), '_self');
});
"
)
)
### end scripts ----
)
)
}

38
R/config.R Normal file
View File

@@ -0,0 +1,38 @@
# Configuration file for St. Andrews Shiny App
app_config <- list(
data_paths = list(
owners = "./data/owners.rds",
plats = "./data/plats/plats.shp",
beaches = "./data/beaches.rds",
listings = "./data/listings.rds",
sales = "./data/sales.rds"
),
update_config = list(
subdivisions = c("8120", "8113", "8171", "8195", "8221", "8163",
"8240", "8159", "8149", "8110", "8254", "8215", "8143"),
timeout_seconds = 300
),
map_config = list(
default_lng = -82.362253,
default_lat = 27.076199,
default_zoom = 16,
beach_lng = -82.4603,
beach_lat = 27.0999,
beach_zoom = 12
),
app_config = list(
title = "St. Andrews Park",
theme = "ios",
dark = TRUE
)
)
# Helper function to get data path
get_data_path <- function(data_type) {
if (data_type %in% names(app_config$data_paths)) {
return(app_config$data_paths[[data_type]])
} else {
stop(paste("Unknown data type:", data_type))
}
}

659
app.R
View File

@@ -1,4 +1,7 @@
# load libraries ---- # St. Andrews Shiny App - Main entry point
# Modularized version with separate UI and server components
# Load libraries
library(shiny) library(shiny)
library(shinyMobile) library(shinyMobile)
library(leaflet) library(leaflet)
@@ -7,652 +10,16 @@ library(dplyr)
library(leafpop) library(leafpop)
library(DT) library(DT)
# load data ---- # Load configuration
owners <- readRDS("./data/owners.rds") source("./R/config.R")
listings <- readRDS("./data/listings.rds") |>
arrange(price_per_sqft) |>
mutate(
price_fmt = scales::dollar(price),
ppsf_fmt = scales::dollar(price_per_sqft)
)
sales <- readRDS("./data/sales.rds") |>
arrange(desc(listed_date)) |>
mutate(
price_fmt = scales::dollar(price),
ppsf_fmt = scales::dollar(price_per_sqft)
)
last_sale_date <- format(attr(owners, "last_sale_date"), "%Y-%m-%d")
sbdvn <- sf::st_read("./data/plats/plats.shp")
beaches <- readRDS("./data/beaches.rds")
# define ui ---- # Load UI and server components
ui <- f7Page( source("./R/app_ui.R")
title = "St. Andrews", source("./R/app_server.R")
## header ----
tags$head(
tags$link(rel = "manifest", href = "manifest.json"),
tags$link(rel = "apple-touch-icon", sizes = "180x180", href = "images/apple-touch-icon.png"),
tags$meta(name = "apple-mobile-web-app-capable", content = "yes"),
tags$meta(name = "apple-mobile-web-app-status-bar-style", content = "default"),
tags$style(HTML("
.dataTables_wrapper {
color: white;
}
.dataTables_wrapper table.dataTable thead th,
.dataTables_wrapper table.dataTable tbody td {
color: white;
}
.dataTables_wrapper table.dataTable tbody tr.odd {
background-color: #333;
}
.dataTables_wrapper table.dataTable tbody tr.even {
background-color: #444;
}
.dataTables_wrapper .dataTables_paginate .paginate_button {
color: white !important;
}
.dataTables_wrapper .dataTables_paginate .paginate_button.current{
color: black !important;
}
.dataTables_filter {
display: none; /* Hide the search box */
}
"))
),
## options ----
options = list(
theme = "ios",
dark = TRUE,
pullToRefresh = TRUE
),
## layout ----
f7TabLayout(
### panels ----
panels = tagList(
f7Panel(
id = "panel-left",
side = "left",
effect = "push",
title = "Menu",
f7PanelMenu(
id = "menu",
f7PanelItem(
tabName = "About",
title = "About",
icon = f7Icon("info_circle"),
active = TRUE
),
f7PanelItem(
tabName = "Owners",
title = "Owners",
icon = f7Icon("person_2_fill")
),
f7PanelItem(
tabName = "Listings",
title = "Listings",
icon = f7Icon("tag_fill")
),
f7PanelItem(
tabName = "Sales",
title = "Sales",
icon = f7Icon("dollarsign_circle_fill")
),
f7PanelItem(
tabName = "PrivateList",
title = "Private List",
icon = f7Icon("lock_fill")
),
f7PanelItem(
tabName = "Resources",
title = "Resources",
icon = f7Icon("hammer_fill")
)
)
)
),
### navbar ----
navbar = f7Navbar(
title = "St. Andrews Park",
hairline = TRUE,
leftPanel = TRUE
),
### begin tabs ----
f7Tabs(
id = "tabs",
animated = TRUE,
#### about ----
f7Tab(
title = "About",
tabName = "About",
icon = f7Icon("info_circle"),
active = TRUE,
f7Card(
title = "About",
divider = "TRUE",
tags$img(src = "images/st_andrews.jpg", width = "100%"),
"St. Andrews Park is located in Venice, Florida. The condominiums are a mix of single-family homes, villas (2 and 4 units) and multi-unit buildings (8 units). There are 388 separate properties within 14 subdivisions. St. Andrews Park is one of many communities in the Plantation Golf and Country Club. It is should not be confused with the adjacent community St. Andrews East.",
footer = tagList(
f7Link("More Info", href = "http://www.cpmi.us/standrews-plantation/outside_home.asp")
)
)
),
#### owners ----
f7Tab(
title = "Owners",
tabName = "Owners",
icon = f7Icon("person_2_fill"),
f7Card(
title = "Owners:",
divider = TRUE,
raised = TRUE,
footer = paste0("Last sale date: ", last_sale_date),
f7List(
inset = TRUE,
dividers = TRUE,
strong = TRUE,
outline = FALSE,
f7Text(
inputId = "name",
label = "Last Name:",
placeholder = "\"Patel\""
),
f7Text(
inputId = "location",
label = "Address:",
placeholder = "\"123 Chalmers\""
),
f7Select(
inputId = "sub_name",
label = "Select Subdivision:",
choices = c("All", sort(sbdvn$sub_name))
),
tags$br(),
f7Button(
inputId = "filterButton",
label = "Find Owners",
icon = "",
color = "blue"
)
)
),
f7Card(
title = "Map:",
divider = TRUE,
leafletOutput("map")
),
f7Card(
title = "Table:",
divider = TRUE,
DTOutput("table")
),
f7Card(
title = "Download:",
divider = TRUE,
f7List(
inset = TRUE,
downloadButton("download_filtered", "Download Filtered"),
downloadButton("download_all", "Download All (388)")
)
)
),
#### listings ----
f7Tab(
title = "Listings",
tabName = "Listings",
icon = f7Icon("tag_fill"),
f7Card(
title = "Map:",
divider = TRUE,
leafletOutput("listings_map")
),
f7Card(
title = "Active Listings:",
divider = TRUE,
DTOutput("listings_table")
)
),
#### sales ----
f7Tab(
title = "Sales",
tabName = "Sales",
icon = f7Icon("dollarsign_circle_fill"),
f7Card(
title = "Map:",
divider = TRUE,
leafletOutput("sales_map")
),
f7Card(
title = "Recent Sales:",
divider = TRUE,
DTOutput("sales_table")
)
),
#### private list ----
f7Tab(
title = "Private List",
tabName = "PrivateList",
icon = f7Icon("lock_fill"),
f7Block(
tags$p(
style = "text-align:center; color:#ff9500; font-weight:bold;",
"Listings must be renewed every 45 days"
)
),
f7Card(
title = "Submit a Listing:",
divider = TRUE,
f7List(
inset = TRUE, dividers = TRUE, strong = TRUE,
f7Text("pl_address", "Address:", placeholder = "895 Chalmers Dr"),
f7Text("pl_price", "Price ($):", placeholder = "325000"),
f7Text("pl_sqft", "Sq Ft:", placeholder = "1800"),
f7Text("pl_name", "Your Name:", placeholder = "Jane Smith"),
f7Text("pl_email", "Email:", placeholder = "jane@email.com"),
f7Text("pl_cell", "Cell:", placeholder = "941-555-1234")
),
tags$br(),
f7Button("pl_submit", "Submit Listing", color = "blue"),
uiOutput("pl_message")
),
f7Card(
title = "Active Private Listings:",
divider = TRUE,
DTOutput("pl_table")
)
),
#### resources ----
f7Tab(
title = "Resources",
tabName = "Resources",
icon = f7Icon("hammer_fill"),
f7Segment(
f7Button(inputId = "res_beaches", label = "Beaches"),
f7Button(inputId = "res_links", label = "Links")
),
uiOutput("resources_content")
)
### end tabs----
),
### begin scripts ----
tags$script(
HTML(
"
$(document).on('click', '#pdfLink', function(event) {
event.preventDefault();
window.open($(this).attr('href'), '_blank');
});
$(document).on('click', '#download_filtered, #download_all', function(event) {
event.preventDefault();
window.open($(this).attr('href'), '_self');
});
"
)
)
### end scripts ----
)
)
# end ui ----
# Create the app
# define server ---- ui <- create_ui()
server <- function(input, output, session) { server <- create_server
# update tabs depending on side panel
observeEvent(input$menu, {
updateF7Tabs(id = "tabs",
selected = input$menu)
})
# resources sub-section toggle
res_section <- reactiveVal("beaches")
observeEvent(input$res_beaches, { res_section("beaches") })
observeEvent(input$res_links, { res_section("links") })
output$resources_content <- renderUI({
if (res_section() == "beaches") {
tagList(
f7Card(
title = "Beaches",
divider = TRUE,
leafletOutput("beach_map"),
footer = p("Source: Environmental Protection Agency")
),
f7Block(
h3("Helpful Links:"),
f7List(
inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE,
f7ListItem(title = "EPA Beaches", href = "https://www.epa.gov/beaches", external = TRUE),
f7ListItem(title = "Red Tide Forecast", href = "https://habforecast.gcoos.org/", external = TRUE),
f7ListItem(title = "Healthy Beaches Program", href = "https://fdoh.maps.arcgis.com/apps/instant/nearby/index.html?appid=7106a20597de4bff98cc5ebc7f932047&findSource=0&find=1600%2520Harbor%2520Dr%2520S%252C%2520Venice%252C%2520Florida%252C%252034285&sliderDistance=2", external = TRUE),
f7ListItem(title = "MOTE Beach Conditions", href = "https://visitbeaches.org/beach/6/report/53033", external = TRUE),
f7ListItem(title = "National Hurricane Center", href = "https://www.nhc.noaa.gov/", external = TRUE)
)
)
)
} else {
tagList(
f7BlockTitle(title = "Services:", size = "medium"),
f7Block(
f7List(
mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE,
f7Link(label = "City of Venice", href = "https://www.venicegov.com/"),
f7Link(label = "Florida Power & Light", href = "https://www.fpl.com/"),
f7Link(label = "Sarasota County", href = "https://www.scgov.net/"),
f7Link(label = "Property Appraiser", href = "https://www.sc-pa.com/"),
f7Link(label = "Open GIS Portal", href = "https://data-sarco.opendata.arcgis.com/"),
f7Link(label = "Waste & Recycling", href = "https://www.venicegov.com/government/public-works/waste-and-recycling"),
f7Link(label = "Condo Regulation", href = "https://condos.myfloridalicense.com/"),
f7Link(label = "Property Records Search", href = "https://www.sarasotaclerk.com/records/official-records/search-land-records")
)
),
f7BlockTitle(title = "Documents:", size = "medium"),
f7Block(
f7List(
mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE,
f7Link(label = "St. Andrews Covenants", href = "docs/2000_01_01_st_andrews_covenants.pdf"),
f7Link(label = "St. Andrews (unrecorded)", href = "docs/2004_06_23_sap_map.pdf"),
f7Link(label = "Patios 2", href = "docs/1997_08_12_patios_2_plat.pdf"),
f7Link(label = "Patios 3", href = "docs/1998_11_17_patios_3_plat.pdf"),
f7Link(label = "Villas 2", href = "docs/1998_09_14_villas_2_plat.pdf")
)
)
)
}
})
filteredSbdvn <- reactive({
if (is.null(input$sub_name) || input$sub_name == "All") {
return(sbdvn$max_sub_id)
} else {
return(
sbdvn %>%
filter(sub_name == input$sub_name) %>%
pull(max_sub_id)
)
}
})
filteredOwners <- reactiveVal(owners)
observeEvent(input$filterButton, {
filtered_owners <-
owners %>%
filter(subdivision %in% filteredSbdvn()) %>%
filter(
grepl(input$name, owner_1, ignore.case = TRUE) |
grepl(input$name, owner_2, ignore.case = TRUE)
) %>%
filter(grepl(input$location, location, ignore.case = TRUE))
filteredOwners(filtered_owners)
})
mean_lat <- reactive({
filteredOwners() %>%
st_coordinates() %>%
.[, "Y"] %>%
mean()
})
mean_lng <- reactive({
filteredOwners() %>%
st_coordinates() %>%
.[, "X"] %>%
mean()
})
output$map <- renderLeaflet({
if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) {
leaflet() %>%
addProviderTiles("CartoDB.Voyager") %>%
addPolygons(
data = sbdvn,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2,
label = ~sub_name,
group = "Subdivisions"
) %>%
addMarkers(
data = filteredOwners(),
#color = ~ifelse(homestead == 1, "green", "red"),
popup = popupTable(
filteredOwners(),
row.numbers = FALSE,
feature.id = FALSE,
zcol = c(
"label",
"owner_1",
"owner_2"
)
),
group = "Owners"
) %>%
addLayersControl(
overlayGroups = c("Subdivisions", "Owners"),
options = layersControlOptions(collapsed = FALSE)
) %>%
setView(lng = mean_lng(), lat = mean_lat(), zoom = 16)
} else {
leaflet() %>%
addProviderTiles("CartoDB.Voyager") %>%
addPolygons(
data = sbdvn,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2,
label = ~sub_name,
group = "Subdivisions"
) %>%
setView(lng = -82.362253, lat = 27.076199, zoom = 16)
}
})
output$table <- renderDT({
my_table <-
filteredOwners() %>%
st_drop_geometry() %>%
select(label, owner_1, owner_2, homestead)
datatable(my_table,
colnames = c("Address", "Owner 1", "Owner 2", "Homestead"),
rownames = FALSE,
options = list(
pageLength = 10,
scrollX = TRUE,
searching = FALSE,
lengthMenu = c(5, 10, 25, 50),
dom = 'tpi'
)
)
})
prep_mailing <- function(data) {
data |>
sf::st_drop_geometry() |>
dplyr::select(owner_1, owner_2, mailing_address_1, mailing_address_2,
mailing_city, mailing_state, mailing_zip_code) |>
dplyr::mutate(
owner_2 = ifelse(is.na(owner_2), "", owner_2),
mailing_address_2 = ifelse(is.na(mailing_address_2), "", mailing_address_2)
) |>
dplyr::rename(
address_1 = mailing_address_1,
address_2 = mailing_address_2,
city = mailing_city,
state = mailing_state,
zip = mailing_zip_code
)
}
output$download_filtered <- downloadHandler(
filename = "st_andrews_owners_filtered.csv",
content = function(file) {
write.csv(prep_mailing(filteredOwners()), file, row.names = FALSE)
}
)
output$download_all <- downloadHandler(
filename = "st_andrews_owners_all.csv",
content = function(file) {
write.csv(prep_mailing(owners), file, row.names = FALSE)
}
)
# private listings ----
pl_path <- "./data/private_listings.csv"
pl_data <- reactivePoll(
intervalMillis = 5000,
session = session,
checkFunc = function() file.info(pl_path)$mtime,
valueFunc = function() {
df <- read.csv(pl_path, stringsAsFactors = FALSE)
if (nrow(df) == 0) return(df)
df$submitted_at <- as.POSIXct(df$submitted_at)
df[difftime(Sys.time(), df$submitted_at, units = "days") <= 45, ]
}
)
observeEvent(input$pl_submit, {
req(input$pl_address, input$pl_price, input$pl_name, input$pl_email, input$pl_cell)
new_row <- data.frame(
submitted_at = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
address = input$pl_address,
price = as.numeric(gsub("[^0-9.]", "", input$pl_price)),
sqft = as.numeric(gsub("[^0-9.]", "", input$pl_sqft)),
name = input$pl_name,
email = input$pl_email,
cell = input$pl_cell,
stringsAsFactors = FALSE
)
write.table(new_row, pl_path, sep = ",", append = TRUE,
col.names = FALSE, row.names = FALSE, quote = TRUE)
output$pl_message <- renderUI(
tags$p(style = "color:#4cd964;", "Listing submitted successfully!")
)
updateF7Text("pl_address", value = "")
updateF7Text("pl_price", value = "")
updateF7Text("pl_sqft", value = "")
updateF7Text("pl_name", value = "")
updateF7Text("pl_email", value = "")
updateF7Text("pl_cell", value = "")
})
output$pl_table <- renderDT({
df <- pl_data()
if (nrow(df) == 0) {
return(datatable(
data.frame(Message = "No active listings"),
rownames = FALSE, options = list(dom = 't', searching = FALSE)
))
}
datatable(
df |> dplyr::mutate(
price = scales::dollar(price),
sqft = formatC(sqft, format = "d", big.mark = ",")
) |> dplyr::select(address, price, sqft, name, email, cell),
colnames = c("Address", "Price", "Sq Ft", "Name", "Email", "Cell"),
rownames = FALSE,
options = list(pageLength = 25, searching = FALSE, dom = 't')
)
})
# listings map ----
output$listings_map <- renderLeaflet({
leaflet(listings) |>
addProviderTiles("CartoDB.Voyager") |>
addPolygons(
data = sbdvn,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2,
label = ~sub_name
) |>
addMarkers(
lat = ~latitude,
lng = ~longitude,
popup = ~paste0(
"<b>", address, "</b><br>",
"Price: ", price_fmt, "<br>",
"Sq Ft: ", sqft, "<br>",
"$/Sq Ft: ", ppsf_fmt
)
) |>
setView(lng = -82.362253, lat = 27.076199, zoom = 16)
})
# listings table ----
output$listings_table <- renderDT({
datatable(
listings |> select(listed_date, address, sqft, price_fmt, ppsf_fmt),
colnames = c("Listed", "Address", "Sq Ft", "Price", "$/Sq Ft"),
rownames = FALSE,
options = list(
pageLength = 25,
searching = FALSE,
dom = 't'
)
)
})
# sales map ----
output$sales_map <- renderLeaflet({
leaflet(sales) |>
addProviderTiles("CartoDB.Voyager") |>
addPolygons(
data = sbdvn,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2,
label = ~sub_name
) |>
addMarkers(
lat = ~latitude,
lng = ~longitude,
popup = ~paste0(
"<b>", address, "</b><br>",
"Sale Date: ", listed_date, "<br>",
"Price: ", price_fmt, "<br>",
"Sq Ft: ", sqft, "<br>",
"$/Sq Ft: ", ppsf_fmt
)
) |>
setView(lng = -82.362253, lat = 27.076199, zoom = 16)
})
# sales table ----
output$sales_table <- renderDT({
datatable(
sales |> select(listed_date, address, sqft, price_fmt, ppsf_fmt),
colnames = c("Date", "Address", "Sq Ft", "Price", "$/Sq Ft"),
rownames = FALSE,
options = list(
pageLength = 10,
searching = FALSE,
dom = 't'
)
)
})
# beach map ----
output$beach_map <- renderLeaflet({
leaflet() %>%
addProviderTiles("CartoDB.Voyager") %>%
setView(lng = -82.4603, lat = 27.0999, zoom = 12) %>%
addMarkers(
data = beaches,
lat = ~lat,
lng = ~lng,
popup = ~beach_name
)
})
}
# end server ----
# Run the app # Run the app
shinyApp(ui, server) shinyApp(ui = ui, server = server)

View File

@@ -12,12 +12,12 @@ library(dplyr)
library(stringr) library(stringr)
library(sf) library(sf)
options(timeout = 300) # Load configuration
source("./R/config.R")
subdivisions <- c( options(timeout = app_config$update_config$timeout_seconds)
"8120", "8113", "8171", "8195", "8221",
"8163", "8240", "8159", "8149", "8110", "8254", "8215", "8143" subdivisions <- app_config$update_config$subdivisions
)
# load geometry lookup (static) ---- # load geometry lookup (static) ----
geometry_lookup <- readRDS("./data-raw/addresses/geometry_lookup.rds") geometry_lookup <- readRDS("./data-raw/addresses/geometry_lookup.rds")
@@ -73,6 +73,6 @@ latest_sale <-
cat("Owners written:", nrow(owners), "\n") cat("Owners written:", nrow(owners), "\n")
attr(owners, "last_sale_date") <- latest_sale$last_sale_date attr(owners, "last_sale_date") <- latest_sale$last_sale_date
saveRDS(owners, "./data/owners.rds") saveRDS(owners, app_config$data_paths$owners)
cat("Saved to data/owners.rds\n") cat("Saved to", app_config$data_paths$owners, "\n")
cat("Most recent sale date:", format(latest_sale$last_sale_date, "%B %d, %Y"), "\n") cat("Most recent sale date:", format(latest_sale$last_sale_date, "%B %d, %Y"), "\n")