Community Stories Platform - R Shiny Version

ADA-compliant community storytelling platform

library(shiny) library(shinydashboard) library(shinyWidgets) library(DT) library(plotly) library(dplyr) library(stringr) library(reactable) library(htmltools) library(shinycssloaders)

Initialize data storage

stories_data <- data.frame( id = integer(), title = character(), description = character(), category = character(), author = character(), file_name = character(), file_type = character(), upload_date = as.Date(character()), transcript = character(), duration = numeric(), stringsAsFactors = FALSE )

Categories for stories

story_categories <- c( “Success Stories”, “Community Impact”, “Volunteer Experiences”, “Program Highlights” )

Define UI

ui <- dashboardPage( skin = “blue”,

# Header dashboardHeader( title = “Community Stories Platform”, titleWidth = 300, tags\(li( class = "dropdown", tags\)a( href = “#”, class = “dropdown-toggle”, data-toggle = “dropdown”, role = “button”, “Accessibility”, tags\(span(class = "caret") ), tags\)ul( class = “dropdown-menu”, tags\(li( actionButton("increase_font", "Increase Font Size", class = "btn-link", style = "width: 100%;") ), tags\)li( actionButton(“decrease_font”, “Decrease Font Size”, class = “btn-link”, style = “width: 100%;”) ), tags$li( actionButton(“toggle_contrast”, “Toggle High Contrast”, class = “btn-link”, style = “width: 100%;”) ) ) ) ),

# Sidebar dashboardSidebar( width = 300, sidebarMenu( id = “sidebar_menu”, menuItem(“Home”, tabName = “home”, icon = icon(“home”)), menuItem(“Upload Story”, tabName = “upload”, icon = icon(“upload”)), menuItem(“Browse Stories”, tabName = “browse”, icon = icon(“search”)), menuItem(“Analytics”, tabName = “analytics”, icon = icon(“chart-bar”)), menuItem(“Accessibility”, tabName = “accessibility”, icon = icon(“universal-access”)) ) ),

# Body dashboardBody( # Custom CSS for accessibility and styling tags\(head( tags\)style(HTML(” .main-header .navbar { background-color: #2c3e50 !important; }

    .accessibility-toolbar {
      position: fixed;
      bottom: 20px;
      right: 20px;
      z-index: 1000;
      background: white;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
      padding: 10px;
    }
    
    .story-card {
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 15px;
      margin-bottom: 15px;
      background: white;
      box-shadow: 0 2px 5px rgba(0,0,0,0.1);
    }
    
    .story-card:hover {
      box-shadow: 0 4px 15px rgba(0,0,0,0.15);
    }
    
    .category-badge {
      display: inline-block;
      padding: 4px 8px;
      border-radius: 4px;
      font-size: 12px;
      font-weight: bold;
      margin-right: 5px;
    }
    
    .success-stories { background-color: #e3f2fd; color: #1976d2; }
    .community-impact { background-color: #e8f5e8; color: #2e7d32; }
    .volunteer-experiences { background-color: #fff3e0; color: #f57c00; }
    .program-highlights { background-color: #f3e5f5; color: #7b1fa2; }
    
    .upload-area {
      border: 2px dashed #ccc;
      border-radius: 8px;
      padding: 40px;
      text-align: center;
      margin-bottom: 20px;
      transition: border-color 0.3s;
    }
    
    .upload-area:hover {
      border-color: #2196f3;
    }
    
    .high-contrast {
      background-color: #000000 !important;
      color: #ffffff !important;
    }
    
    .high-contrast .box {
      background-color: #333333 !important;
      color: #ffffff !important;
    }
    
    .high-contrast .content-wrapper {
      background-color: #000000 !important;
    }
    
    .newsletter-header {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 30px;
      border-radius: 8px;
      margin-bottom: 20px;
    }
    
    .hero-section {
      background: linear-gradient(135deg, #2196f3 0%, #4caf50 100%);
      color: white;
      padding: 50px 20px;
      text-align: center;
      border-radius: 8px;
      margin-bottom: 30px;
    }
    
    .stats-box {
      background: #f8f9fa;
      padding: 20px;
      border-radius: 8px;
      text-align: center;
      margin-bottom: 20px;
    }
    
    .transcript-box {
      background: #f5f5f5;
      padding: 15px;
      border-radius: 8px;
      margin-top: 10px;
      border-left: 4px solid #2196f3;
    }
  "))
),

tabItems(
  # Home Tab
  tabItem(
    tabName = "home",
    fluidRow(
      column(12,
             div(class = "hero-section",
                 h1("Share Your Community's Stories", style = "margin-bottom: 20px;"),
                 p("Amplify voices, inspire change, and build stronger connections through storytelling", 
                   style = "font-size: 18px; margin-bottom: 30px;"),
                 actionButton("go_to_upload", "Share Your Story", 
                              class = "btn btn-light btn-lg", 
                              style = "margin-right: 10px;"),
                 actionButton("browse_stories", "Browse Stories", 
                              class = "btn btn-outline-light btn-lg")
             )
      )
    ),
    
    fluidRow(
      column(12,
             div(class = "newsletter-header",
                 h2(icon("newspaper"), " Latest Community Stories"),
                 p(paste("Issue #42 -", format(Sys.Date(), "%B %Y")))
             )
      )
    ),
    
    fluidRow(
      column(4,
             div(class = "stats-box",
                 h3(textOutput("total_stories"), style = "color: #2196f3; margin: 0;"),
                 p("Total Stories")
             )
      ),
      column(4,
             div(class = "stats-box",
                 h3(textOutput("total_authors"), style = "color: #4caf50; margin: 0;"),
                 p("Contributors")
             )
      ),
      column(4,
             div(class = "stats-box",
                 h3(textOutput("recent_uploads"), style = "color: #ff9800; margin: 0;"),
                 p("This Month")
             )
      )
    ),
    
    fluidRow(
      column(12,
             h3("Recent Stories"),
             withSpinner(uiOutput("recent_stories_cards"))
      )
    )
  ),
  
  # Upload Tab
  tabItem(
    tabName = "upload",
    fluidRow(
      column(12,
             h2(icon("upload"), " Share Your Story"),
             p("Upload your audio or video story to share with the community.")
      )
    ),
    
    fluidRow(
      column(6,
             div(class = "upload-area",
                 icon("file-upload", style = "font-size: 48px; color: #ccc; margin-bottom: 20px;"),
                 h4("Drop your file here or click to browse"),
                 p("Supports MP3, MP4, WAV, MOV files up to 100MB"),
                 fileInput("story_file", NULL, 
                           accept = c("audio/*", "video/*"),
                           width = "100%")
             ),
             
             conditionalPanel(
               condition = "output.file_uploaded",
               div(class = "alert alert-success",
                   h5("File uploaded successfully!"),
                   textOutput("uploaded_file_info")
               )
             )
      ),
      
      column(6,
             wellPanel(
               textInput("story_title", "Story Title*", 
                         placeholder = "Enter a compelling title for your story"),
               
               selectInput("story_category", "Category*", 
                           choices = c("Select a category" = "", story_categories)),
               
               textInput("story_author", "Author*", 
                         placeholder = "Your name"),
               
               textAreaInput("story_description", "Description", 
                             placeholder = "Provide a brief description of your story...",
                             rows = 4),
               
               textAreaInput("story_transcript", "Transcript (Optional)", 
                             placeholder = "Add transcript for accessibility...",
                             rows = 6),
               
               checkboxInput("has_captions", "Story has captions/subtitles", FALSE),
               
               br(),
               
               div(style = "text-align: center;",
                   actionButton("save_draft", "Save Draft", 
                                class = "btn btn-secondary",
                                style = "margin-right: 10px;"),
                   actionButton("publish_story", "Publish Story", 
                                class = "btn btn-primary")
               )
             )
      )
    )
  ),
  
  # Browse Tab
  tabItem(
    tabName = "browse",
    fluidRow(
      column(12,
             h2(icon("search"), " Browse Stories")
      )
    ),
    
    fluidRow(
      column(4,
             textInput("search_text", "Search Stories", 
                       placeholder = "Search by title, author, or description...")
      ),
      column(4,
             selectInput("filter_category", "Filter by Category", 
                         choices = c("All Categories" = "", story_categories))
      ),
      column(4,
             selectInput("sort_by", "Sort by", 
                         choices = c("Date (Newest First)" = "date_desc",
                                     "Date (Oldest First)" = "date_asc",
                                     "Title A-Z" = "title_asc",
                                     "Author A-Z" = "author_asc"),
                         selected = "date_desc")
      )
    ),
    
    fluidRow(
      column(12,
             withSpinner(uiOutput("filtered_stories"))
      )
    )
  ),
  
  # Analytics Tab
  tabItem(
    tabName = "analytics",
    fluidRow(
      column(12,
             h2(icon("chart-bar"), " Story Analytics")
      )
    ),
    
    fluidRow(
      column(6,
             h4("Stories by Category"),
             withSpinner(plotlyOutput("category_chart"))
      ),
      column(6,
             h4("Upload Timeline"),
             withSpinner(plotlyOutput("timeline_chart"))
      )
    ),
    
    fluidRow(
      column(12,
             h4("Story Statistics"),
             withSpinner(DT::dataTableOutput("stories_table"))
      )
    )
  ),
  
  # Accessibility Tab
  tabItem(
    tabName = "accessibility",
    fluidRow(
      column(12,
             h2(icon("universal-access"), " Accessibility Features")
      )
    ),
    
    fluidRow(
      column(6,
             wellPanel(
               h4("Visual Accessibility"),
               
               sliderInput("font_size", "Font Size", 
                           min = 12, max = 24, value = 16, step = 1),
               
               checkboxInput("high_contrast", "High Contrast Mode", FALSE),
               
               br(),
               
               h4("Audio Features"),
               checkboxInput("screen_reader", "Screen Reader Support", TRUE),
               checkboxInput("keyboard_nav", "Keyboard Navigation", TRUE)
             )
      ),
      
      column(6,
             wellPanel(
               h4("Accessibility Guidelines"),
               
               tags$ul(
                 tags$li("All images include alternative text"),
                 tags$li("Proper heading structure (H1, H2, H3)"),
                 tags$li("High contrast color combinations"),
                 tags$li("Keyboard navigation support"),
                 tags$li("Screen reader compatibility"),
                 tags$li("Transcript support for all audio/video content"),
                 tags$li("Clear focus indicators"),
                 tags$li("Semantic HTML structure")
               ),
               
               br(),
               
               div(class = "alert alert-info",
                   strong("WCAG 2.1 AA Compliance"),
                   p("This platform follows Web Content Accessibility Guidelines 2.1 Level AA standards.")
               )
             )
      )
    )
  )
),

# Accessibility Toolbar
div(class = "accessibility-toolbar",
    actionButton("acc_font_plus", "+", title = "Increase Font Size"),
    actionButton("acc_font_minus", "-", title = "Decrease Font Size"),
    actionButton("acc_contrast", "◐", title = "Toggle High Contrast")
)

) )

Define Server

server <- function(input, output, session) {

# Reactive values values <- reactiveValues( stories = stories_data, font_size = 16, high_contrast = FALSE )

# File upload handling output\(file_uploaded <- reactive({ return(!is.null(input\)story_file)) }) outputOptions(output, “file_uploaded”, suspendWhenHidden = FALSE)

output\(uploaded_file_info <- renderText({ if (!is.null(input\)story_file)) { paste(“File:”, input\(story_file\)name, “- Size:”, round(input\(story_file\)size / 1024^2, 2), “MB”) } })

# Story publication observeEvent(input\(publish_story, { if (is.null(input\)story_file) || input\(story_title == "" || input\)story_category == “” || input$story_author == ““) { showNotification(”Please fill in all required fields and upload a file.”, type = “error”) return() }

new_story <- data.frame(
  id = nrow(values$stories) + 1,
  title = input$story_title,
  description = ifelse(input$story_description == "", "No description provided", input$story_description),
  category = input$story_category,
  author = input$story_author,
  file_name = input$story_file$name,
  file_type = ifelse(grepl("audio", input$story_file$type), "Audio", "Video"),
  upload_date = Sys.Date(),
  transcript = ifelse(input$story_transcript == "", "No transcript provided", input$story_transcript),
  duration = round(runif(1, 60, 600), 0), # Random duration for demo
  stringsAsFactors = FALSE
)

values$stories <- rbind(values$stories, new_story)

# Reset form
updateTextInput(session, "story_title", value = "")
updateSelectInput(session, "story_category", selected = "")
updateTextInput(session, "story_author", value = "")
updateTextAreaInput(session, "story_description", value = "")
updateTextAreaInput(session, "story_transcript", value = "")
updateCheckboxInput(session, "has_captions", value = FALSE)

showNotification("Story published successfully!", type = "success")

})

# Draft saving observeEvent(input$save_draft, { showNotification(“Draft saved successfully!”, type = “success”) })

# Navigation observeEvent(input$go_to_upload, { updateTabItems(session, “sidebar_menu”, “upload”) })

observeEvent(input$browse_stories, { updateTabItems(session, “sidebar_menu”, “browse”) })

# Home page statistics output\(total_stories <- renderText({ nrow(values\)stories) })

output\(total_authors <- renderText({ length(unique(values\)stories$author)) })

output\(recent_uploads <- renderText({ sum(values\)stories$upload_date >= (Sys.Date() - 30)) })

# Recent stories cards output\(recent_stories_cards <- renderUI({ if (nrow(values\)stories) == 0) { return(div(class = “alert alert-info”, h4(“No stories yet!”), p(“Be the first to share your story with the community.”))) }

recent_stories <- values$stories %>%
  arrange(desc(upload_date)) %>%
  head(6)

story_cards <- lapply(1:nrow(recent_stories), function(i) {
  story <- recent_stories[i, ]
  
  category_class <- switch(story$category,
                           "Success Stories" = "success-stories",
                           "Community Impact" = "community-impact",
                           "Volunteer Experiences" = "volunteer-experiences",
                           "Program Highlights" = "program-highlights",
                           "success-stories"
  )
  
  div(class = "col-md-4",
      div(class = "story-card",
          h4(story$title),
          span(class = paste("category-badge", category_class), story$category),
          p(paste("By", story$author, "•", format(story$upload_date, "%B %d, %Y"))),
          p(substr(story$description, 1, 100), if(nchar(story$description) > 100) "..." else ""),
          div(class = "transcript-box",
              strong("Transcript Available: "),
              ifelse(story$transcript != "No transcript provided", "Yes", "No")
          ),
          br(),
          actionButton(paste0("play_", i), "Play Story", class = "btn btn-primary btn-sm")
      )
  )
})

div(class = "row", story_cards)

})

# Filtered stories for browse page filtered_stories <- reactive({ stories <- values$stories

if (input$search_text != "") {
  stories <- stories %>%
    filter(
      str_detect(tolower(title), tolower(input$search_text)) |
        str_detect(tolower(author), tolower(input$search_text)) |
        str_detect(tolower(description), tolower(input$search_text))
    )
}

if (input$filter_category != "") {
  stories <- stories %>%
    filter(category == input$filter_category)
}

# Sort stories
stories <- switch(input$sort_by,
                  "date_desc" = stories %>% arrange(desc(upload_date)),
                  "date_asc" = stories %>% arrange(upload_date),
                  "title_asc" = stories %>% arrange(title),
                  "author_asc" = stories %>% arrange(author),
                  stories %>% arrange(desc(upload_date))
)

return(stories)

})

output$filtered_stories <- renderUI({ stories <- filtered_stories()

if (nrow(stories) == 0) {
  return(div(class = "alert alert-warning", 
             h4("No stories found"), 
             p("Try adjusting your search or filter criteria.")))
}

story_cards <- lapply(1:nrow(stories), function(i) {
  story <- stories[i, ]
  
  category_class <- switch(story$category,
                           "Success Stories" = "success-stories",
                           "Community Impact" = "community-impact",
                           "Volunteer Experiences" = "volunteer-experiences",
                           "Program Highlights" = "program-highlights",
                           "success-stories"
  )
  
  div(class = "col-md-6",
      div(class = "story-card",
          h4(story$title),
          span(class = paste("category-badge", category_class), story$category),
          p(paste("By", story$author, "•", format(story$upload_date, "%B %d, %Y"))),
          p(story$description),
          div(class = "transcript-box",
              strong("Transcript: "),
              substr(story$transcript, 1, 200),
              if(nchar(story$transcript) > 200) "..." else ""
          ),
          br(),
          actionButton(paste0("play_filtered_", i), "Play Story", class = "btn btn-primary btn-sm"),
          actionButton(paste0("download_", i), "Download", class = "btn btn-secondary btn-sm")
      )
  )
})

div(class = "row", story_cards)

})

# Analytics charts output\(category_chart <- renderPlotly({ if (nrow(values\)stories) == 0) { return(plot_ly() %>% add_text(text = “No data available”, x = 0.5, y = 0.5) %>% layout(showlegend = FALSE, xaxis = list(showticklabels = FALSE), yaxis = list(showticklabels = FALSE))) }

category_counts <- values$stories %>%
  count(category) %>%
  arrange(desc(n))

plot_ly(category_counts, x = ~category, y = ~n, type = "bar",
        marker = list(color = c("#2196f3", "#4caf50", "#ff9800", "#9c27b0"))) %>%
  layout(title = "Stories by Category",
         xaxis = list(title = "Category"),
         yaxis = list(title = "Number of Stories"))

})

output\(timeline_chart <- renderPlotly({ if (nrow(values\)stories) == 0) { return(plot_ly() %>% add_text(text = “No data available”, x = 0.5, y = 0.5) %>% layout(showlegend = FALSE, xaxis = list(showticklabels = FALSE), yaxis = list(showticklabels = FALSE))) }

timeline_data <- values$stories %>%
  count(upload_date) %>%
  arrange(upload_date)

plot_ly(timeline_data, x = ~upload_date, y = ~n, type = "scatter", mode = "lines+markers",
        line = list(color = "#2196f3")) %>%
  layout(title = "Upload Timeline",
         xaxis = list(title = "Date"),
         yaxis = list(title = "Stories Uploaded"))

})

# Stories data table output\(stories_table <- DT::renderDataTable({ if (nrow(values\)stories) == 0) { return(data.frame(Message = “No stories available”)) }

display_data <- values$stories %>%
  select(title, category, author, file_type, upload_date, duration) %>%
  mutate(
    duration = paste(duration, "seconds"),
    upload_date = format(upload_date, "%Y-%m-%d")
  )

DT::datatable(display_data, 
              options = list(pageLength = 10, scrollX = TRUE),
              rownames = FALSE,
              colnames = c("Title", "Category", "Author", "Type", "Upload Date", "Duration"))

})

# Accessibility features observeEvent(input\(acc_font_plus, { values\)font_size <- min(values\(font_size + 2, 24) runjs(paste0("document.body.style.fontSize = '", values\)font_size, “px’;”)) })

observeEvent(input\(acc_font_minus, { values\)font_size <- max(values\(font_size - 2, 12) runjs(paste0("document.body.style.fontSize = '", values\)font_size, “px’;”)) })

observeEvent(input\(acc_contrast, { values\)high_contrast <- !values\(high_contrast if (values\)high_contrast) { runjs(“document.body.classList.add(‘high-contrast’);”) } else { runjs(“document.body.classList.remove(‘high-contrast’);”) } })

# Font size slider observeEvent(input\(font_size, { values\)font_size <- input\(font_size runjs(paste0("document.body.style.fontSize = '", input\)font_size, “px’;”)) })

# High contrast checkbox observeEvent(input\(high_contrast, { values\)high_contrast <- input\(high_contrast if (input\)high_contrast) { runjs(“document.body.classList.add(‘high-contrast’);”) } else { runjs(“document.body.classList.remove(‘high-contrast’);”) } }) }

Run the application

shinyApp(ui = ui, server = server)