diff --git a/apps/data_library/app-text.R b/apps/data_library/app-text.R
new file mode 100644
index 0000000..6e760c8
--- /dev/null
+++ b/apps/data_library/app-text.R
@@ -0,0 +1,25 @@
+# Demo App Text
+# Contains text displayed on the landing page tabs.
+
+# About Page Text
+welcome_text = "This demo application allows you to explore and search the reports and datasets
+ available in the Demo platform from one central location.
+
+ Select Reports or Data Catalog from the sidebar to begin
+ exploring, or click the '+' to expand the boxes below to learn more."
+
+reports_welcome_text = "The Reports page contains links to and information about the different
+ interactive dashboards and other analyses hosted in the Demo platform. Scroll to browse
+ through the reports or use the Technical Area and Tags filters in the sidebar
+ to narrow your selection.
+
+ Click on the report title to be directed to the report in a new tab."
+
+data_welcome_text = "The Data Catalog page contains information about datasets available in the Demo platform.
+ Scroll to browse through the datasets or use the Technical Area and Tags filters
+ in the sidebar to narrow your selection.
+
+ Each dataset card includes a brief description, last updated date, and access
+ restrictions."
+
+help_welcome_text = "This is a demo app that demonstrates how an organization could use the Civis Platform"
diff --git a/apps/data_library/app.R b/apps/data_library/app.R
new file mode 100644
index 0000000..1302731
--- /dev/null
+++ b/apps/data_library/app.R
@@ -0,0 +1,439 @@
+# M-DIVE Landing Page — Demo Version
+# Derived from apps/mdive_landing_page/app.R
+
+install.packages("shinycssloaders")
+
+library(shiny)
+library(shinydashboard)
+library(stringr)
+library(dplyr)
+library(shinyBS)
+library(shinyWidgets)
+library(shinycssloaders)
+library(shinyjs)
+
+source('components.R')
+source('app-text.R')
+source('demo-data.R')
+
+# ── Derived filter choices ────────────────────────────────────────────────────
+
+tag_choices <- get_unique_choices(reports_data, 'tags', data, 'tags')
+tag_choices_br <- stringr::str_wrap(tag_choices, width = 35)
+tag_choices_br <- stringr::str_replace_all(tag_choices_br, "\n", "
")
+
+tech_area_choices <- get_unique_choices(reports_data, 'technical_area', data, 'technical_area')
+tech_area_choices_br <- stringr::str_wrap(tech_area_choices, width = 35)
+tech_area_choices_br <- stringr::str_replace_all(tech_area_choices_br, "\n", "
")
+
+# ── UI ────────────────────────────────────────────────────────────────────────
+
+ui <- function(request) {
+ dashboardPage(skin = 'black',
+
+ dashboardHeader(title = "Welcome to the Demo Dashboard", titleWidth = 350),
+
+ dashboardSidebar(
+ sidebarMenu(
+ id = "tabs",
+ div(class = 'sidebar-menu', id = "sidebar_nav",
+ menuItem("About", tabName = "home"),
+ menuItem("Demo Reports", tabName = "reports", selected = TRUE),
+ menuItem("Demo Data Catalog", tabName = "overview"),
+ # Hidden — only reachable by clicking a dataset card
+ conditionalPanel("input.tabs == 'something'",
+ menuItem("Data Details", tabName = "data_details"))
+ ),
+ div(class = 'sidebar-menu', id = 'sidebar_filters',
+ conditionalPanel(
+ "input.tabs == 'reports' | input.tabs == 'overview'",
+ pickerInput(
+ width = '200px',
+ inputId = "tech_area",
+ label = HTML("Filter by
Technical Area"),
+ choices = sort_other(tech_area_choices),
+ multiple = TRUE,
+ selected = NULL,
+ choicesOpt = list(
+ style = rep_len("color:black;", length(tech_area_choices)),
+ content = sort_other(tech_area_choices_br)
+ ),
+ options = pickerOptions(
+ actionsBox = TRUE,
+ liveSearch = TRUE,
+ dropupAuto = FALSE,
+ size = 8,
+ iconBase = "",
+ tickIcon = "fa fa-check",
+ `selected-text-format` = "count",
+ `count-selected-text` = "{0} of {1} Technical Areas Chosen"
+ )
+ ),
+ pickerInput(
+ width = '200px',
+ inputId = "dataset_choice",
+ label = HTML("Or filter by
Tags"),
+ choices = sort_other(tag_choices),
+ multiple = TRUE,
+ selected = NULL,
+ choicesOpt = list(
+ style = rep_len("color:black;", length(tag_choices)),
+ content = sort_other(tag_choices_br)
+ ),
+ options = pickerOptions(
+ actionsBox = TRUE,
+ liveSearch = TRUE,
+ dropupAuto = FALSE,
+ size = 8,
+ iconBase = "",
+ tickIcon = "fa fa-check",
+ `selected-text-format` = "count",
+ `count-selected-text` = "{0} of {1} Tags Chosen"
+ )
+ )
+ )
+ )
+ )
+ ),
+
+ dashboardBody(
+ shinyjs::useShinyjs(),
+ tags$script("document.getElementsByClassName('sidebar-toggle')[0].style.visibility = 'hidden';"),
+ includeCSS('styles.css'),
+
+ tabItems(
+
+ # ── About Tab ──────────────────────────────────────────────────────
+ tabItem(tabName = 'home',
+ fluidRow(shinydashboard::box(
+ id = 'welcome_flip', status = "info",
+ title = span(icon("home", lib = "font-awesome"), ' Welcome'),
+ width = 12, closable = FALSE, collapsible = TRUE,
+ solidHeader = TRUE, collapsed = FALSE,
+ p(HTML(welcome_text))
+ )),
+ br(),
+ fluidRow(shinydashboard::box(
+ id = 'report_flip', status = "info",
+ title = span(icon("chart-line", lib = "font-awesome"),
+ actionLink('reports_go', ' Demo Reports',
+ style = 'text-decoration:underline')),
+ width = 12, closable = FALSE, collapsible = TRUE,
+ solidHeader = TRUE, collapsed = TRUE,
+ p(HTML(reports_welcome_text))
+ )),
+ br(),
+ fluidRow(shinydashboard::box(
+ id = 'data_flip', status = "info",
+ title = span(icon("database", lib = "font-awesome"),
+ actionLink('data_go', ' Demo Data Catalog',
+ style = 'text-decoration:underline')),
+ width = 12, closable = FALSE, collapsible = TRUE,
+ solidHeader = TRUE, collapsed = TRUE,
+ p(HTML(data_welcome_text))
+ )),
+ br(),
+ fluidRow(shinydashboard::box(
+ id = 'help_flip', status = "info",
+ title = span(icon("question", lib = "font-awesome"), " Help"),
+ width = 12, closable = FALSE, collapsible = TRUE,
+ solidHeader = TRUE, collapsed = TRUE,
+ p(HTML(help_welcome_text))
+ ))
+ ),
+
+ # ── Demo Reports Tab ─────────────────────────────────────────────
+ tabItem(tabName = 'reports',
+ div(id = "demo_reports",
+ style = 'overflow-x:hidden;overflow-y:scroll;max-height:600px;width:100%;padding-right:4px',
+ fluidRow(
+ column(6,
+ textInput("search_box_reports", label = "Search",
+ placeholder = "Search...", width = '86.5%')),
+ column(6,
+ pickerInput(
+ inputId = 'order_results_reports',
+ label = "Order by",
+ choices = c(
+ 'Featured' = 'featured',
+ 'Report Name: A-Z' = 'name',
+ 'Category' = 'category'
+ ),
+ choicesOpt = list(style = rep_len("color:black;", 3))
+ ))
+ ),
+ uiOutput('reportHeading'),
+ uiOutput('reports_page') %>% withSpinner(color = "#2474a4")
+ )
+ ),
+
+ # ── Data Details Tab ───────────────────────────────────────────────
+ tabItem(tabName = 'data_details',
+ dataDetailsUI(id = 'data_details_tab',
+ dataset_options = unique(data$name))
+ ),
+
+ # ── Data Catalog Tab ───────────────────────────────────────────────
+ tabItem(tabName = 'overview',
+ fluidPage(
+ fluidRow(
+ column(6,
+ textInput("search_box", label = "Search",
+ placeholder = "Search...", width = '86.5%')),
+ column(6,
+ selectInput('order_results', "Order by",
+ choices = c(
+ 'Dataset Name: A-Z' = 'name',
+ 'Last Updated: Oldest to Newest' = 'last_data_update'
+ )))
+ )
+ ),
+ fluidPage(
+ div(style = 'overflow-x:hidden;overflow-y:scroll;max-height:600px;width:100%;padding-right:4px',
+ id = "data_catalog",
+ uiOutput('cardHeading'),
+ uiOutput('cardContents') %>% withSpinner(color = "#2474a4"))
+ )
+ )
+
+ ) # end tabItems
+ ) # end dashboardBody
+ )
+}
+
+# ── Server ────────────────────────────────────────────────────────────────────
+
+server <- function(input, output, session) {
+
+ # ── Filtered Data Reactives ───────────────────────────────────────────────
+
+ filtered_data <- reactive({
+ tag_selections <- add_backslash_to_string(input$dataset_choice)
+ tech_area_selections <- add_backslash_to_string(input$tech_area)
+
+ output <- data
+
+ if (!is.null(input$dataset_choice)) {
+ output <- output %>% dplyr::filter(str_detect(tags, tag_selections))
+ }
+
+ if (!is.null(input$tech_area)) {
+ output <- output %>% dplyr::filter(str_detect(technical_area, tech_area_selections))
+ }
+
+ if (!is.na(input$search_box) & input$search_box != "") {
+ output <- output %>%
+ filter_all(any_vars(agrepl(input$search_box, ., ignore.case = TRUE)))
+ }
+
+ if (!is.null(input$order_results)) {
+ output <- output %>% arrange_at(.vars = c(input$order_results, 'name'))
+ }
+
+ output
+ })
+
+ filtered_reports <- reactive({
+ tag_selections <- add_backslash_to_string(input$dataset_choice)
+ tech_area_selections <- add_backslash_to_string(input$tech_area)
+
+ output <- reports_data
+
+ if (!is.null(input$dataset_choice)) {
+ output <- output %>% dplyr::filter(str_detect(tags, tag_selections))
+ }
+
+ if (!is.null(input$tech_area)) {
+ output <- output %>% dplyr::filter(str_detect(technical_area, tech_area_selections))
+ }
+
+ if (!is.na(input$search_box_reports) & input$search_box_reports != "") {
+ output <- output %>%
+ filter_all(any_vars(agrepl(input$search_box_reports, ., ignore.case = TRUE)))
+ }
+
+ if (!is.null(input$order_results_reports)) {
+ output <- output %>% arrange_at(.vars = c(input$order_results_reports, 'name'))
+ }
+
+ output
+ })
+
+ # ── Reports Tab ───────────────────────────────────────────────────────────
+
+ output$reports_page <- renderUI({
+ if (nrow(filtered_reports()) >= 1) {
+
+ results <- lapply(1:nrow(filtered_reports()), function(rownum) {
+ boxButtonUI(
+ id = str_replace_all(
+ filtered_reports()[rownum, 'name'],
+ c(':' = '', ',' = '', '-' = '', ' ' = '_', '\\(' = '', '\\)' = '', '&' = '')
+ ),
+ box_link = filtered_reports()[rownum, 'location_link'],
+ development_status = filtered_reports()[rownum, 'development_status'],
+ description = filtered_reports()[rownum, 'description'],
+ tooltip_text = "Click to open this report in a new tab",
+ tag_list = strsplit(filtered_reports()[rownum, 'tags'], ';') %>% unlist() %>% trimws('both'),
+ tag_filter_list = input$dataset_choice,
+ tech_area_list = strsplit(filtered_reports()[rownum, 'technical_area'], ';') %>% unlist() %>% trimws('both'),
+ tech_area_filter_list = input$tech_area
+ )
+ })
+
+ # Insert category/featured headers when sorting by category or featured
+ if (input$order_results_reports %in% c('category')) {
+ category_counts <- filtered_reports() %>%
+ dplyr::mutate(position = row_number()) %>%
+ dplyr::group_by(!!dplyr::sym(input$order_results_reports)) %>%
+ dplyr::mutate(group_start_position = row_number()) %>%
+ dplyr::filter(group_start_position == 1) %>%
+ dplyr::select(!!sym(input$order_results_reports), position)
+
+ for (i in 1:nrow(category_counts)) {
+ insert_position <- category_counts$position[i] + i - 1
+ html_headers <- strong(h3(category_counts[[input$order_results_reports]][[i]]))
+ results <- append(results, list(html_headers), insert_position - 1)
+ }
+ }
+
+ } else {
+ results <- fluidRow(width = 12,
+ actionLink("reset_input", "Reset your filters to view more results.",
+ style = "margin-left: 15px"))
+ }
+ results
+ })
+
+ dataset_choices <- reactive(input$dataset_choice)
+ tech_area_input_choices <- reactive(input$tech_area)
+
+ lapply(1:nrow(reports_data), function(rownum) {
+ boxButtonServer(
+ id = str_replace_all(
+ reports_data[rownum, 'name'],
+ c(':' = '', ',' = '', '-' = '', ' ' = '_', '\\(' = '', '\\)' = '', '&' = '')
+ ),
+ link = reports_data[rownum, 'location_link'],
+ box_title_text = h3(span(reports_data[rownum, 'name'],
+ icon("external-link-alt", lib = "font-awesome"))),
+ tag_list = strsplit(reports_data[rownum, 'tags'], ';') %>% unlist() %>% trimws('both'),
+ tag_filter_list = dataset_choices,
+ tech_area_list = strsplit(reports_data[rownum, 'technical_area'], ';') %>% unlist() %>% trimws('both'),
+ tech_area_filter_list = tech_area_input_choices,
+ parent_session = session
+ )
+ })
+
+ # ── Data Catalog Tab ──────────────────────────────────────────────────────
+
+ output$cardContents <- renderUI({
+ if (nrow(filtered_data()) >= 1) {
+ lapply(1:nrow(filtered_data()), function(rownum) {
+ resultsCardUI(
+ str_replace_all(
+ filtered_data()[rownum, 'name'],
+ c(':' = '', ',' = '', '-' = '', ' ' = '_', '\\(' = '', '\\)' = '', '&' = '')
+ ),
+ filtered_data(), rownum,
+ tag_filter_list = input$dataset_choice,
+ tech_area_filter_list = input$tech_area
+ )
+ })
+ } else {
+ fluidRow(width = 12,
+ actionLink("reset_input", "Reset your filters to view more results.",
+ style = "margin-left: 15px"))
+ }
+ })
+
+ lapply(1:nrow(data), function(rownum) {
+ resultsCardServer(
+ str_replace_all(
+ data[rownum, 'name'],
+ c(':' = '', ',' = '', '-' = '', ' ' = '_', '\\(' = '', '\\)' = '', '&' = '')
+ ),
+ parent_session = session,
+ data = data,
+ row = rownum,
+ target_module = 'data_details_tab',
+ tag_filter_list = dataset_choices,
+ tech_area_filter_list = tech_area_input_choices
+ )
+ })
+
+ dataDetailsServer('data_details_tab', data = data, reports_data = reports_data)
+
+ # ── Headings ──────────────────────────────────────────────────────────────
+
+ output$cardHeading <- renderUI({
+ tech_picks <- paste(input$tech_area, collapse = ', ')
+ tag_picks <- paste(input$dataset_choice, collapse = ', ')
+
+ if (nrow(filtered_data()) == 0) {
+ out <- "Sorry! No datasets match selected filters."
+ } else {
+ out <- paste0("Showing ", nrow(filtered_data()), " dataset(s)")
+ }
+
+ if (tech_picks != "" & tag_picks == "" & nrow(filtered_data()) > 0)
+ out <- HTML(out, " tagged with ", tech_picks, " technical area(s)")
+ if (tech_picks == "" & tag_picks != "" & nrow(filtered_data()) > 0)
+ out <- HTML(out, " tagged with ", tag_picks, ".")
+ if (tag_picks != "" & tech_picks != "" & nrow(filtered_data()) > 0)
+ out <- HTML(out, " tagged with ", tag_picks, " AND ", tech_picks, " technical area(s)")
+
+ out
+ })
+
+ output$reportHeading <- renderUI({
+ tech_picks <- paste(input$tech_area, collapse = ', ')
+ tag_picks <- paste(input$dataset_choice, collapse = ', ')
+
+ if (nrow(filtered_reports()) == 0) {
+ out <- "Sorry! No reports match selected filters."
+ } else {
+ out <- paste0("Showing ", nrow(filtered_reports()), " report(s)")
+ }
+
+ if (tech_picks != "" & tag_picks == "" & nrow(filtered_reports()) > 0)
+ out <- HTML(out, " tagged with ", tech_picks, " technical area(s)")
+ if (tech_picks == "" & tag_picks != "" & nrow(filtered_reports()) > 0)
+ out <- HTML(out, " tagged with ", tag_picks, ".")
+ if (tag_picks != "" & tech_picks != "" & nrow(filtered_reports()) > 0)
+ out <- HTML(out, " tagged with ", tag_picks, " AND ", tech_picks, " technical area(s)")
+
+ out
+ })
+
+ # ── Navigation observers ──────────────────────────────────────────────────
+
+ observeEvent(input$reset_input, {
+ updatePickerInput(session, "dataset_choice", selected = "")
+ updatePickerInput(session, "tech_area", selected = "")
+ updateTextInput(session, "search_box", value = "")
+ updateTextInput(session, "search_box_reports", value = "")
+ })
+
+ observeEvent(input$reports_go, {
+ updateTabItems(session, "tabs", "reports")
+ })
+
+ observeEvent(input$data_go, {
+ updateTabItems(session, "tabs", "overview")
+ })
+
+ observeEvent(input$go_back_button, {
+ updateTabItems(session, "tabs", "overview")
+ })
+
+ # Tab bookmarking
+ observeEvent(reactiveValuesToList(input), session$doBookmark())
+ onBookmarked(updateQueryString)
+ observe({
+ setBookmarkExclude(dplyr::setdiff(names(input), "tabs"))
+ })
+}
+
+#### App ####
+shinyApp(ui, server, enableBookmarking = "url")
diff --git a/apps/data_library/components.R b/apps/data_library/components.R
new file mode 100644
index 0000000..af551cb
--- /dev/null
+++ b/apps/data_library/components.R
@@ -0,0 +1,277 @@
+# Demo App - UI Components
+# Derived from apps/mdive_landing_page/components.R
+
+# Inlined from style_files/18f_custom_ui.R
+actionButton18F <- function(..., theme = 'light') {
+ actionButton(..., class = ifelse(theme == 'light', 'btn-18f-light', 'btn-18f-dark'))
+}
+
+
+#' Adds "//" to "(" and ")" characters so str_detect works correctly for filtering
+#' @param x a string
+add_backslash_to_string = function(x){
+ v = paste(x, collapse = '|')
+ v = gsub("(", "\\(", v, fixed = TRUE)
+ v = gsub(")", "\\)", v, fixed = TRUE)
+}
+
+
+#' Gets unique categories from a ';'-separated column across both reports and data inventories.
+get_unique_choices = function(reports_data, report_col, data, data_col){
+
+ report_choices <- unlist(lapply(reports_data[report_col], function(x) strsplit(x, ';'))) %>%
+ trimws('both') %>%
+ unique()
+
+ catalog_choices <- unlist(lapply(data[data_col], function(x) strsplit(x, ';'))) %>%
+ trimws('both') %>%
+ unique()
+
+ all_choices <- unique(c(report_choices, catalog_choices))
+
+ return(all_choices)
+}
+
+
+#' Orders a vector so 'Other' appears at the bottom.
+sort_other = function(unique_choices){
+ if ('Other' %in% unique_choices){
+ choices = unique_choices[!unique_choices == 'Other']
+ choices = sort(choices)
+ choices = c(choices, 'Other')
+ } else {
+ choices = sort(unique_choices)
+ }
+ return(choices)
+}
+
+
+create_buttons = function(text_list, filter_list, ns, tag_separator = ';'){
+
+ button_tags <- tagList()
+
+ if (length(text_list) == 0){
+ button_tags = "None"
+ } else {
+ text_list_length <- length(text_list)
+ i <- 1
+ for (text in text_list) {
+ if (i == text_list_length) {
+ text_label <- text
+ } else {
+ text_label <- paste0(text, tag_separator)
+ }
+ if (text != '') {
+ btn_class <- ifelse(text %in% filter_list,
+ 'btn-18f-selected',
+ 'btn-18f')
+ button_text <- ifelse(
+ text %in% filter_list,
+ 'Click to remove from filter',
+ 'Click to add to filter'
+ )
+ button_tags <- tagList(button_tags,
+ actionLink(
+ inputId = ns(str_replace_all(text, "[^A-Za-z]", "")),
+ label = text_label,
+ ),
+ bsTooltip(ns(str_replace_all(text, "[^A-Za-z]", "")), button_text)
+ )
+ }
+ i <- i + 1
+ }
+ }
+
+ return(button_tags)
+}
+
+
+update_input = function(input, text_list, filter_list, parent_session, input_id) {
+ lapply(
+ text_list,
+ function(text) {
+ observeEvent(input[[str_replace_all(text, "[^A-Za-z,]", "")]], {
+ if (text %in% filter_list()) {
+ new_list <- filter_list()[!filter_list() == text]
+ } else {
+ new_list <- c(filter_list(), text)
+ }
+ updatePickerInput(parent_session, input_id, selected = new_list)
+ })
+ }
+ )
+}
+
+
+resultsCardUI <- function(id, input_data, rownum,
+ tag_filter_list, tech_area_filter_list) {
+
+ ns <- NS(id)
+
+ tag_list <- input_data[rownum, 'tags'] %>% str_split(';') %>% unlist() %>% trimws('both')
+ tech_area_list <- input_data[rownum, 'technical_area'] %>% str_split(';') %>% unlist() %>% trimws('both')
+
+ tag_buttons = create_buttons(tag_list, tag_filter_list, ns)
+ tech_area_buttons = create_buttons(tech_area_list, tech_area_filter_list, ns)
+
+ wellPanel(style = "background-color:white; color:black",
+ bsTooltip(id = ns('link_title'), title = 'Click to view full description and data details',
+ placement = "right", trigger = 'hover', options = list(container = "body")),
+ div(class = 'row results-header', style = 'padding-left:12px',
+ actionLink(inputId = ns('link_title'), label = input_data[rownum, "name"],
+ style = 'margin-top:0px; text-decoration:underline')
+ ),
+ div(class = 'row results-body',
+ div(class = 'col-sm-8 results-body-main',
+ p(HTML(str_trunc(gsub("\n", "
", input_data[rownum, "description"]),
+ width = 450,
+ ellipsis = "...(Click Dataset Name to See Full Description)")))
+ ),
+ div(class = 'col-sm-4 results-body-side border-left',
+ h4("Data Updated on:"),
+ p(input_data[rownum, "clean_last_data_update"]),
+ h4("Access Restrictions"),
+ p(input_data[rownum, "access_restrictions"]))
+ ),
+ div(class = 'results-details',
+ 'Technical area:', tech_area_buttons,
+ br(),
+ 'Tags:', tag_buttons
+ )
+ )
+}
+
+
+resultsCardServer <- function(id, parent_session, data, row, target_module,
+ tag_filter_list, tech_area_filter_list) {
+ ns <- NS(target_module)
+
+ moduleServer(id, function(input, output, session) {
+ tag_list <- data[row, 'tags'] %>% str_split(';') %>% unlist() %>% trimws('both')
+ tech_area_list <- data[row, 'technical_area'] %>% str_split(';') %>% unlist() %>% trimws('both')
+
+ update_input(input, tag_list, tag_filter_list, parent_session, 'dataset_choice')
+ update_input(input, tech_area_list, tech_area_filter_list, parent_session, 'tech_area')
+
+ table_selection <- reactive({ data[row, "name"] })
+
+ observeEvent(input$link_title, {
+ updateTabItems(session = parent_session, "tabs", "data_details")
+ updateSelectInput(session = parent_session, ns('dataset'), selected = table_selection())
+ })
+ })
+}
+
+
+boxButtonUI <- function(id, box_link, description, tooltip_text, development_status,
+ tag_list, tag_filter_list = c(),
+ tech_area_list, tech_area_filter_list = c()) {
+ ns <- NS(id)
+
+ tag_buttons = create_buttons(tag_list, tag_filter_list, ns)
+ tech_area_buttons = create_buttons(tech_area_list, tech_area_filter_list, ns)
+
+ box(
+ id = ns('box_info'),
+ status = 'primary',
+ title = tags$div(
+ style = 'display:inline-block',
+ uiOutput(ns("box_title")),
+ class = 'report-header'
+ ),
+ div(class = 'results-body-report-main', style = 'border-bottom:1px solid rgb(241, 241, 241)',
+ tags$p(HTML(' Status: ', development_status)),
+ tags$p(description)
+ ),
+ bsTooltip(id = ns('box_title'), title = tooltip_text,
+ placement = "right", trigger = 'hover', options = list(container = "body")),
+ div(class = 'results-body-report-details',
+ div(id = ns("icon_box"), 'Technical Area:', tech_area_buttons),
+ div(id = ns("icon_box"), 'Tags:', tag_buttons)
+ ),
+ width = 12
+ )
+}
+
+
+# ── Data Details Tab ──────────────────────────────────────────────────────────
+
+dataDetailsUI <- function(id, dataset_options) {
+ ns <- NS(id)
+ fluidPage(
+ fluidRow(
+ column(8, actionButton("go_back_button", "← Back to Data Catalog")),
+ column(4, selectInput(ns("dataset"), "Select dataset",
+ choices = sort(dataset_options)))
+ ),
+ br(),
+ uiOutput(ns("details_content"))
+ )
+}
+
+dataDetailsServer <- function(id, data, reports_data) {
+ moduleServer(id, function(input, output, session) {
+
+ output$details_content <- renderUI({
+ req(input$dataset)
+ r <- data[data$name == input$dataset, ]
+ req(nrow(r) > 0)
+
+ assoc <- r$associated_reports[1]
+ has_assoc <- length(assoc) == 1 && !is.na(assoc) && nchar(trimws(assoc)) > 0
+ assoc_ui <- if (has_assoc) {
+ report_names <- trimws(strsplit(assoc, ";")[[1]])
+ tags$ul(lapply(report_names, function(rn) {
+ report_row <- reports_data[reports_data$name == rn, ]
+ if (nrow(report_row) > 0) {
+ tags$li(tags$a(href = report_row$location_link[1], rn, target = "_blank"))
+ } else {
+ tags$li(rn)
+ }
+ }))
+ } else {
+ p("None")
+ }
+
+ box(
+ width = 12, status = "primary",
+ h3(r$name),
+ p(HTML(gsub("\n", "
", r$full_description))),
+ tags$hr(),
+ fluidRow(
+ column(6,
+ h4("Technical Area"), p(r$technical_area),
+ h4("Last Updated"), p(r$clean_last_data_update),
+ h4("Access Restrictions"), p(r$access_restrictions)
+ ),
+ column(6,
+ h4("Source"), p(r$source),
+ h4("Unit of Analysis"), p(r$unit_of_analysis),
+ h4("Associated Reports"), assoc_ui
+ )
+ )
+ )
+ })
+ })
+}
+
+
+# Simplified: all reports are always accessible in the demo (no access gate)
+boxButtonServer <- function(id, link, box_title_text,
+ tag_list = c(), tag_filter_list = NULL,
+ tech_area_list = c(), tech_area_filter_list = NULL,
+ parent_session = NULL) {
+ moduleServer(id, function(input, output, session) {
+
+ update_input(input, tag_list, tag_filter_list, parent_session, 'dataset_choice')
+ update_input(input, tech_area_list, tech_area_filter_list, parent_session, 'tech_area')
+
+ output$box_title <- renderUI({
+ tags$a(
+ href = link,
+ HTML(paste(tags$u(box_title_text))),
+ target = '_blank'
+ )
+ })
+ })
+}
diff --git a/apps/data_library/demo-data.R b/apps/data_library/demo-data.R
new file mode 100644
index 0000000..7e37ae5
--- /dev/null
+++ b/apps/data_library/demo-data.R
@@ -0,0 +1,119 @@
+# Demo App - Mock Data
+# All static data used by the demo app lives here.
+
+# ── Reports ───────────────────────────────────────────────────────────────────
+
+reports_data <- data.frame(
+ name = c(
+ "Quarterly Report",
+ "Facility Level Dashboard",
+ "Cases Dashboard",
+ "Disease in Pregnancy Dashboard",
+ "Treatment Dashboard"
+ ),
+ description = c(
+ "Tracks confirmed cases and reporting rates across a selection of countries on a quarterly basis.",
+ "Monitors reporting rates and case data at the facility level for targeted follow-up.",
+ "Summarizes case data across supported regions.",
+ "Monitors disease in pregnancy indicators across supported regions.",
+ "Tracks treatment indicators across supported regions."
+ ),
+ report_type = c("Shiny", "Shiny", "Shiny", "Shiny", "Tableau"),
+ development_status = c("Production", "Production", "In Development", "Production", "Production"),
+ technical_area = c("Reporting", "Reporting", "Measurement", "Measurement", "Treatment"),
+ tags = c(
+ "quarterly;cases;reporting",
+ "facility;reporting;cases",
+ "cases;measurement",
+ "disease;pregnancy;measurement",
+ "treatment;cases"
+ ),
+ category = c("General", "General", "Interventions", "Interventions", "Interventions"),
+ featured = c(1, 2, 3, 4, 5),
+ location_link = rep("#", 5),
+ stringsAsFactors = FALSE
+)
+# ── Data Catalog ──────────────────────────────────────────────────────────────
+
+data <- data.frame(
+ name = c(
+ "QR Reporting Table",
+ "Facility Master List",
+ "Inventory Stock Data",
+ "Program Coverage Data",
+ "Population Estimates"
+ ),
+ description = c(
+ "Monthly case and reporting data aggregated from national health information systems across supported countries.",
+ "Master list of all supported health facilities including location and administrative hierarchy.",
+ "Logistics data including commodity stockouts, consumption, and stock on hand.",
+ "Campaign coverage data by cycle and administrative unit.",
+ "Annual population estimates by administrative unit used for rate calculations."
+ ),
+ full_description = c(
+ "The QR Reporting Table contains monthly surveillance data compiled from national health information systems
+ across all supported countries. It includes confirmed cases, suspected cases, and testing data broken down
+ by administrative unit (admin 1 and admin 2), facility, and month. Reporting rates and completeness
+ indicators are also included to help users assess data quality. This table is the primary data source
+ for the Quarterly Report dashboard.",
+ "The Facility Master List is the authoritative reference for all supported health facilities.
+ It includes facility names, unique identifiers, administrative hierarchy (country, admin 1, admin 2),
+ GPS coordinates where available, facility type, and ownership category.
+ It is used as a lookup table across multiple dashboards to ensure consistent facility naming
+ and geographic attribution.",
+ "The Inventory Stock Data table contains commodity tracking information from national logistics
+ information systems. It covers stock on hand, quantities received, quantities consumed, and stockout
+ days for key commodities. Data is reported at the facility level on a monthly basis.
+ Access is restricted due to the sensitivity of supply chain information.",
+ "The Program Coverage Data table contains campaign-level coverage data for seasonal intervention
+ programs. Each row represents one administrative unit in one campaign cycle, with fields for
+ target population, individuals reached, and coverage percentage.
+ Data is available for all supported countries with active programs.",
+ "The Population Estimates table provides annual population denominators by administrative unit
+ used across the platform for calculating rate-based indicators (e.g. cases per 1,000 population).
+ Estimates are derived from national census projections and are updated annually."
+ ),
+ technical_area = c("Case Management", "Health Systems", "Supply Chain", "Campaigns", "Cross-cutting"),
+ tags = c(
+ "surveillance;cases;reporting",
+ "facilities;geo;admin",
+ "stockouts;commodities;logistics",
+ "coverage;campaigns",
+ "population;denominators"
+ ),
+ access_restrictions = c(
+ "Open use within platform",
+ "Open use within platform",
+ "Restricted",
+ "Open use within platform",
+ "Publicly Available"
+ ),
+ clean_last_data_update = c(
+ "January 2026", "December 2025", "November 2025", "October 2025", "January 2026"
+ ),
+ last_data_update = as.Date(c(
+ "2026-01-01", "2025-12-01", "2025-11-01", "2025-10-01", "2026-01-01"
+ )),
+ source = c(
+ "National Health Information System",
+ "Country Program Teams",
+ "National Logistics System",
+ "National Program Office",
+ "National Census Projections"
+ ),
+ unit_of_analysis = c(
+ "Country / Admin 1 / Admin 2 / Month",
+ "Facility",
+ "Facility / Month",
+ "Admin unit / Campaign cycle",
+ "Admin unit / Year"
+ ),
+ associated_reports = c(
+ "Quarterly Report; Facility Level Dashboard",
+ "Facility Level Dashboard",
+ "",
+ "Cases Dashboard",
+ "Quarterly Report; Disease in Pregnancy Dashboard"
+ ),
+ stringsAsFactors = FALSE
+)
diff --git a/apps/data_library/styles.css b/apps/data_library/styles.css
new file mode 100644
index 0000000..fc242a4
--- /dev/null
+++ b/apps/data_library/styles.css
@@ -0,0 +1,565 @@
+/*
+This file specifies 18F guidelines for the MDIVE landing page
+It adjusts the outline color of boxes, dashboard sidebar color,
+hover color for dashboard navigation, etc. It is similar to the file
+used in the QR Dashboard and contains some specifications that will only
+be necessary if we start using submenus in the MDIVE landing page
+*/
+
+/****************************** Navbar/Top Header ******************************/
+ /* Header background color for logo and text formatting */
+ .main-header .logo {
+ background-color: #1c304a !important;
+ color:white !important;
+ height: 70px;
+ font-size: 30px;
+ font-weight: 500;
+ padding-top: 10px;
+ border:0 !important;
+ }
+
+ .main-header .logo:hover {
+ background-color: #f4b943;
+ }
+
+ /* navbar (rest of the header) */
+ .main-header .navbar {
+ background-color: #1c304a !important;
+ color:white !important;
+ height: 70px;
+ }
+
+ /* Sidebar toggle button - 18F white */
+ .main-header .navbar .sidebar-toggle{
+ color: white !important;
+ border:0 !important;
+ padding-top: 30px;
+ }
+
+ /* Sidebar toggle button when hovered - 18Fmedium blue hover */
+ .main-header .navbar .sidebar-toggle:hover{
+ color: white !important;
+ background-color:#034c6d !important;
+ border:0;
+ }
+
+ /* header button - 18F Dark */
+ .btn-18f-header {
+ background: none;
+ color:white;
+ padding-top:30px;
+ margin-right:40px;
+ border: none;
+ touch-action: manipulation;
+ }
+
+ .btn-header-lessmarg {
+ background: none;
+ color:white;
+ padding-top:10px;
+ border: none;
+ touch-action: manipulation;
+ }
+ .btn-header-lessmarg:hover {
+ background: none;
+ color:white;
+ padding-top:10px;
+ border: none;
+ touch-action: manipulation;
+ }
+
+
+
+ .btn-18f-header:hover {
+ background: none;
+ color:white;
+ padding-top:30px;
+ margin-right:40px;
+ border: none;
+ touch-action: manipulation;
+ }
+
+/****************************** Sidebar ******************************/
+ /* Sidebar background color - 18F medium blue*/
+ .main-sidebar {
+ background-color: #046b99 !important;
+ padding-top: 70px;
+ }
+ /* Sidebar navigation font - 18F h2*/
+ .main-sidebar .sidebar .sidebar-menu #sidebar_nav li{
+ font-size: 20px;
+ font-weight: 600;
+ }
+
+ /* Sidebar filters title font - 18F p*/
+ .main-sidebar .sidebar .sidebar-menu .control-label {
+ font-size: 14px;
+ font-weight: normal;
+ }
+
+ /* Sidebar filters display font - 18F p*/
+ .main-sidebar .sidebar .sidebar-menu .filter-option {
+ font-size: 14px;
+ font-weight: normal;
+ margin-left: 10px;
+ }
+
+
+ /* Sidebar filters dropdown font - 18F p*/
+ .main-sidebar .sidebar .sidebar-menu #sidebar_filters li{
+ font-size: 14px;
+ }
+
+ /* Activate selected tab in sidebar - 18F medium blue hover */
+ .main-sidebar .sidebar .sidebar-menu .active a{
+ background-color: #034c6d;
+ }
+
+ /* Other links in sidebar when hovered - 18F medium blue hover */
+ .main-sidebar .sidebar .sidebar-menu a:hover{
+ background-color: #034c6d;
+ }
+
+ /* Treeview - 18F medium blue*/
+ .main-sidebar .sidebar .sidebar-menu .treeview-menu {
+ background-color: #046b99;
+ font-size:14px;
+ }
+
+ .main-sidebar .sidebar .sidebar-menu .treeview-menu .active {
+ background-color: #034c6d;
+ font-size:14px;
+ color: white;
+ font-weight: 600;
+
+ }
+ /* Treeview active - 18F medium blue*/
+ .main-sidebar .sidebar .sidebar-menu .treeview li.active a {
+ background-color: #034c6d !important;
+ font-size:14px;
+ color: white;
+ font-weight: 600;
+ }
+
+ /* Treeview menu sidebar - 18F medium hover blue*/
+ .main-sidebar .sidebar .sidebar-menu .treeview-menu a {
+ background-color: #046b99 !important;
+ }
+
+ /* Treeview menu hover - 18F medium blue*/
+ .main-sidebar .sidebar .sidebar-menu .treeview-menu a:hover {
+ background-color: #034c6d !important;
+ }
+
+
+ /* Sidebar menu dropdown hover - 18F lightest gray */
+ .main-sidebar .sidebar .sidebar-menu .dropdown{
+ background-color: #f1f1f1;
+ color: black !important;
+ }
+
+/*Remove arrow from sidebar */
+.main-sidebar .sidebar .sidebar-menu .fa-angle-left:before {
+ content: "";
+}
+ /* Sidebar menu dropdown hover - 18F lightest gray */
+ .main-sidebar .sidebar .sidebar-menu .dropdown a:hover{
+ background-color: #f1f1f1;
+ color: black !important;
+ }
+
+ /* Sidebar menu dropdown active - 18F lightest gray */
+ .main-sidebar .sidebar .sidebar-menu .dropdown-menu .active a{
+ background-color: #f1f1f1;
+ color: black !important;
+ }
+
+/****************************** Body ******************************/
+
+ /* Backgrond color for body - 18F white */
+ .content-wrapper {
+ background-color: white !important;
+ }
+ .wrapper {
+ background-color: white !important;
+ }
+ label{
+ font-weight: 400;
+ }
+
+ /* Margins for tabs and default text set to p */
+ .tab-content .tab-pane {
+ padding-top: 5px;
+ padding-left: 20px;
+ padding-right: 10px;
+ font-size: 14px;
+ }
+
+
+ /* Border for boxes - 18F lightest gray */
+ .box.box-primary {
+ margin-top: 10px;
+ margin-bottom: 25px;
+ border:3px solid #f1f1f1;
+ border-left-width:3px;
+ }
+
+ /* Well border (in case the app calls something a well instead of box) - 18F lightest gray */
+ .tab-content .tab-pane .well {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ border: 3px solid #f1f1f1;
+ padding-bottom: 5px;
+ }
+
+ /*Boxes for 'home' page*/
+ .box.box-solid.box-info>.box-header {
+ color:white;
+ background:#046b99
+}
+ .box.box-solid.box-info>.box-header .box-title {
+ font-size: 20px;
+}
+
+ .box.box-solid.box-info{
+ background-color: #e6e6e6;
+ border-bottom-color:#046b99;
+ border-left-color:#046b99;
+ border-right-color:#046b99;
+ border-top-color:#046b99;
+ }
+
+ /* Font size for header of boxes - h4 */
+ .tab-content .tab-pane .box.box-primary .box-header {
+ font-size: 21px;
+ font-weight: 600;
+ padding-bottom: 5px;
+ }
+
+ /* Font size for body of boxes - p */
+ .tab-content .tab-pane .box.box-primary .box-body {
+ font-size: 14px;
+ font-weight: normal;
+ color: black;
+ padding-top: 5px;
+ }
+
+ /*Warning boxes*/
+ .box.box-solid.box-warning>.box-header {
+ color:white;
+ background:#f39c12;
+}
+
+ .box.box-solid.box-warning>.box-header .btn{
+ background:#f39c12 !important;
+ }
+
+ /* Font size for header of wells - h4 */
+ .tab-content .tab-pane .well .row.results-header {
+ font-size: 21px;
+ font-weight: 600;
+ padding-bottom: 5px;
+ }
+
+ /* Font size for body of wells - p */
+ .tab-content .tab-pane .well .row.results-body {
+ font-size: 14px;
+ font-weight: normal;
+ color: black;
+ }
+
+ .tab-content .tab-pane .well .results-details {
+ font-size: 14px;
+ font-weight: normal;
+ color: black;
+ border-top:1px solid rgb(241, 241, 241);
+ padding-left:12px;
+ }
+
+/* Body filters title font - 18F p*/
+ .tab-content .tab-pane .control-label {
+ color: black;
+ font-size: 14px;
+ font-weight: normal;
+ }
+
+/* Body filters display font - 18F p*/
+ .tab-content .tab-pane .selectize-input{
+ color: black;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: #f1f1f1;
+ }
+
+/* Body filters dropdown font - 18F p*/
+ .tab-content .tab-pane .selectize-dropdown{
+ color: black;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: white;
+ }
+
+/* Body filters dropdown hover - 18F p*/
+ .tab-content .tab-pane .selectize-dropdown .active{
+ color: black;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: #f1f1f1;
+ }
+
+/* DataTables text default size and weight - 18F p*/
+
+ .tab-content .tab-pane .dataTables_wrapper{
+ font-size: 14px;
+ font-weight: normal;
+ }
+
+ .card .avatar {
+ background: #E0E0E0 !important;
+ color: black !important;
+ border: 0px;
+ border-radius:0px;
+ }
+ .card {
+ background-color: #E0E0E0 !important;
+ font: white !important;
+ }
+
+ /*Slider selection on Sidebar */
+ .irs-grid-text {
+ color: white !important;
+ }
+
+ .irs-grid-pol {
+ background-color: white !important;
+}
+/****************************** Font Style and Sizes ******************************/
+ /* font - 18F Helvetica */
+ .sansserif {
+ font-family: Helvetica, sans-serif;
+ }
+
+
+ h1 {
+ font-size: 34px;
+ font-weight: 600;
+ }
+
+ h2 {
+ font-size: 26px;
+ font-weight: 600;
+ }
+
+ h3 {
+ font-size: 18px;
+ font-weight: 600;
+ margin-top:0px;
+ }
+
+ h4 {
+ font-size: 17px;
+ font-weight: 600;
+ }
+
+ h5 {
+ font-size: 16px;
+ font-weight: 600;
+ }
+
+
+/****************************** Objects ******************************/
+ /* align text to center */
+ .center {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ width: 80%;
+ }
+
+ /* hyperlink color - 18F medium blue*/
+ a {
+ color:#046b99;
+ }
+
+ /* hyperlink hover color - 18F medium hover blue*/
+ a:hover {
+ color:#034c6d;
+ }
+
+ /* Style for scrollbar */
+ ::-webkit-scrollbar {
+ -webkit-appearance: none;
+ width: 7px;
+ }
+
+ /* Style for the scrolling part of the scrolbar */
+ ::-webkit-scrollbar-thumb {
+ border-radius: 4px;
+ background-color: rgba(0, 0, 0, .5);
+ box-shadow: 0 0 1px rgba(255, 255, 255, .5);
+ }
+
+
+ /* medium button w/ white background (for preview buttons) */
+ .btn-18f-medium {
+ color: #046b99;
+ font-size: 14px;
+ background: none;
+ padding: 0;
+ border: none;
+ }
+ /* medium button w/ white background (for preview buttons) - hover */
+ .btn-18f-medium:hover {
+ color: #034c6d;
+ font-size: 14px;
+ background: none;
+ padding: 0;
+ border: none;
+ }
+
+ /* dark button - 18F medium */
+ .btn-18f-dark {
+ background-color:#046b99;
+ color:white;
+ font-size: 14px;
+ }
+
+ /* dark button hover - 18F medium hover */
+ .btn-18f-dark:hover {
+ background-color:#034c6d;
+ color:white;
+ font-size: 14px;
+ }
+
+ /* sidebar dark button hover */
+ .btn-18f-side {
+ background-color:#034c6d;
+ color:white;
+ border: none;
+ font-size: 14px;
+ }
+ /* sidebar dark button - hover */
+ .btn-18f-side:hover {
+ background-color:white;
+ color:#034c6d;
+ border: none;
+ font-size: 14px;
+ }
+ /* sidebar dark button - selected */
+ .btn-18f-side:focus {
+ background-color: white;
+ color:#034c6d;
+ border: none;
+ }
+
+
+ /* light button - 18F medium */
+ .btn-18f-light {
+ background-color:#046b99;
+ color:white;
+ font-size: 14px;
+ }
+
+ /* light button hover - 18F medium hover */
+ .btn-18f-light:hover {
+ background-color:#034c6d;
+ color:white;
+ font-size: 14px;
+ }
+
+
+/* tooltips - white background and black outline and text */
+ .tooltip-inner {
+ background-color: white;
+ color: black;
+ font-size: 14px;
+ border: 1px solid black;
+ }
+
+ /* light button in main tab pane - 18F medium*/
+ .tab-content .tab-pane .btn{
+ color: white;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: #046b99;
+ }
+
+
+/* light button hover in main tab pane - 18F medium hover*/
+ .tab-content .tab-pane .btn:hover {
+ color: white;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: #034c6d;
+ }
+
+ .tab-content .tab-pane .btn-18f-selected {
+ color: white;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: #034c6d;
+ }
+ .tab-content .tab-pane .btn-default.active{
+ color: white;
+ font-size: 14px;
+ font-weight: normal;
+ background-color: #034c6d;
+ }
+
+/*Dropdown menu in main pane for pickerInput style */
+ .tab-content .tab-pane .dropdown-toggle {
+ background-color: #f1f1f1;
+ color: black;
+ }
+ .tab-content .tab-pane .dropdown-toggle:hover {
+ background-color: #f1f1f1;
+ color: black;
+ }
+ .tab-content .tab-pane .dropdown-menu .inner {
+ font-size: 14px;
+ }
+ .tab-content .tab-pane .dropdown-menu .li .a:hover {
+ font-size: 14px;
+ background-color: #f1f1f1 !important;
+ }
+
+ .dropdown-menu li a {
+ font-size: 14px;
+ color: black;
+ }
+ .tab-content .tab-pane .dropdown-menu .active {
+ font-size: 14px;
+ background-color: white;
+ color: #777;
+ }
+ .tab-content .tab-pane .dropdown-menu .active:hover {
+ font-size: 14px;
+ background-color: #f1f1f1;
+ }
+
+ /*Leaflet options*/
+ .leaflet-top, .leaflet-bottom {
+ z-index: unset !important;
+ }
+
+ .leaflet-touch .leaflet-control-layers .leaflet-touch .leaflet-bar {
+ z-index: 10000000000 !important;
+ }
+
+ .row{
+ margin-left: 5px;
+ margin-right: 5px;
+ }
+
+ .sticky-footer {
+ position:fixed;
+ bottom:0;
+ right:0;
+ left:265px;
+ background:white;
+ padding:10px;
+ margin-top: 15px;
+ border-top-width: 1px;
+ border-top-color: #f1f1f1;
+ border-top-style: solid;
+ }