Darren Lin
  • Teaching
  • Projects
    • Race Results
    • Check Splitting Calculator
  • CV
  • Course Guides
    • Multivariate Biostatistics

Check Splitting Calculator

This is a simple Shiny app that calculates your portion of a check for when you eat out with friends.

#| standalone: true
#| viewerHeight: 600

library(shiny)
library(bslib)

ui <- fluidPage(
  theme = bs_theme(
    version = 5,
    bootswatch = "flatly",
    primary = "#3B7A57"
  ),
  titlePanel("Check Splitting Calculator"),
  sidebarLayout(
    sidebarPanel(
      numericInput("price0",
                   label = "Item Price ($)",
                   value = 10, min = 0, step = 0.01),
      div(id = "item-list"),
      div(
        style = "display: flex; gap: 8px; margin-bottom: 15px;",
        actionButton(inputId = "add", label = "+ Add item", class = "btn-primary btn-sm"),
        actionButton(inputId = "rmv", label = "- Remove item", class = "btn-outline-secondary btn-sm")
      ),
      hr(),
      numericInput("tax",
                   label = "Tax (%)",
                   value = 6, min = 0, step = 0.1),
      numericInput("tip",
                   label = "Tip (%)",
                   value = 15, min = 0, step = 1)
    ),
    mainPanel(
      uiOutput("summary_card")
    )
  )
)

server <- function(input, output, session) {
  add_rm_counter <- reactiveVal(0)

  observeEvent(input$add, {
    add_rm_counter(add_rm_counter() + 1)
    insertUI(
      selector = "#item-list",
      where = "beforeEnd",
      ui = div(id = paste0("inserttext", add_rm_counter()),
               numericInput(paste0("price", add_rm_counter()),
                            label = paste("Item", add_rm_counter() + 1, "Price ($)"),
                            value = 0, min = 0, step = 0.01))
    )
  })

  observeEvent(input$rmv, {
    req(add_rm_counter() > 0)
    removeUI(selector = paste0("#inserttext", add_rm_counter()))
    add_rm_counter(add_rm_counter() - 1)
  })

  total_sum <- reactive({
    sum(sapply(0:add_rm_counter(), function(i) {
      val <- input[[paste0("price", i)]]
      if (is.null(val) || is.na(val)) 0 else val
    }))
  })

  tax_amt <- reactive(total_sum() * (as.numeric(input$tax) / 100))
  tip_amt <- reactive(total_sum() * (as.numeric(input$tip) / 100))
  total_price <- reactive(total_sum() + tax_amt() + tip_amt())

  fmt <- function(x) sprintf("$%.2f", x)

  output$summary_card <- renderUI({
    if (is.na(total_price())) {
      div(class = "alert alert-warning", "Make sure all fields are numeric.")
    } else {
      div(
        class = "card shadow-sm",
        style = "max-width: 320px; padding: 20px;",
        div(style = "display:flex; justify-content:space-between;",
            span("Subtotal"), span(fmt(total_sum()))),
        div(style = "display:flex; justify-content:space-between;",
            span(paste0("Tax (", input$tax, "%)")), span(fmt(tax_amt()))),
        div(style = "display:flex; justify-content:space-between;",
            span(paste0("Tip (", input$tip, "%)")), span(fmt(tip_amt()))),
        hr(),
        div(style = "display:flex; justify-content:space-between; font-weight:700; font-size:1.2rem;",
            span("You owe"), span(fmt(total_price())))
      )
    }
  })
}

shinyApp(ui = ui, server = server)