# load libraries ---- library(shiny) library(shinyMobile) library(leaflet) library(sf) library(dplyr) library(leafpop) library(DT) # load data ---- owners <- readRDS("./data/owners.rds") 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 ---- ui <- f7Page( title = "St. Andrews", ## 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 ---- # define server ---- server <- function(input, output, session) { # 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") ) ) ) } }) # 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 { 27.076199 } }) mean_lng <- reactive({ if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) { filteredOwners() %>% st_coordinates() %>% .[, "X"] %>% mean() } else { -82.362253 } }) 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 = 16) }) 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) } ) # 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( "", address, "
", "Price: ", price_fmt, "
", "Sq Ft: ", sqft, "
", "$/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( "", address, "
", "Sale Date: ", listed_date, "
", "Price: ", price_fmt, "
", "Sq Ft: ", sqft, "
", "$/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 shinyApp(ui, server)