identity-vault/app/components/sidebar.rb
nora 9998147a4e
epic: overhaul program management experience (#188)
* temp commit

* lemme do it

* nope

* let them do it too

* collab invite model

* better visuals on progman

* waow

* danger will robinson

* show apps on backend & link user

* first pass on app auditability!

* no lastnaming admins

* async frame that shit!

* waugh

* can't add yourself

* fix reinvite

* sidebar badging

* lint...

* gotta be on the app!

* let that get rescued by applcon

* already in revoke_all_authorizations

* woag

* the routes you grew up with no longer exist

* what would the UI for that even be?

* sorch

* much better!

* frickin validations
2026-03-02 22:15:13 -05:00

167 lines
5.1 KiB
Ruby

# frozen_string_literal: true
class Components::Sidebar < Components::Base
include Phlex::Rails::Helpers::LinkTo
include Phlex::Rails::Helpers::CurrentPage
register_value_helper :current_identity
register_value_helper :signed_in?
register_output_helper :copy_to_clipboard
register_output_helper :vite_image_tag
def initialize(current_path:)
@current_path = current_path
end
def view_template
render_mobile_toggle
render_overlay
nav(class: "sidebar", id: "sidebar") do
render_sidebar_brand
render_navigation
render_user_section if signed_in?
end
render_toggle_script
end
private
def nav_items
items = [
{ label: t("sidebar.home"), path: root_path, icon: "home" }
]
items << { label: t("sidebar.edit_info"), path: edit_identity_path, icon: "person" }
# Add verification link if user needs to submit or resubmit
if current_identity.present?
status = current_identity.verification_status
if status == "needs_submission" || status == "pending"
items << {
label: t("sidebar.verification"),
path: new_verifications_path,
icon: status == "pending" ? "clock" : "card-id"
}
end
end
items << { label: t("sidebar.addresses"), path: addresses_path, icon: "email" }
items << { label: t("sidebar.security"), path: security_path, icon: "private" }
# Add developer link if developer mode is enabled or user is a program manager/super admin
if current_identity.present? && (current_identity.developer_mode? || current_identity.backend_user&.program_manager? || current_identity.backend_user&.super_admin?)
pending_count = current_identity.pending_collaboration_invitations.count
items << { label: t("sidebar.developer"), path: developer_apps_path, icon: "code", badge: pending_count }
end
items << { label: t("sidebar.docs"), path: docs_path, icon: "docs" }
if current_identity.present? && current_identity.active_for_backend?
items << { label: t("sidebar.backend"), path: backend_root_path, icon: "admin" }
end
items
end
def render_mobile_toggle
button(class: "sidebar-toggle", onclick: safe("toggleSidebar()"), "aria-label": "Toggle sidebar") do
svg(
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
stroke_width: "2.5",
stroke_linecap: "round",
stroke_linejoin: "round"
) do |s|
s.line(x1: "4", y1: "6", x2: "20", y2: "6")
s.line(x1: "4", y1: "12", x2: "20", y2: "12")
s.line(x1: "4", y1: "18", x2: "20", y2: "18")
end
end
end
def render_overlay
div(class: "sidebar-overlay", onclick: safe("toggleSidebar()"))
end
def render_navigation
div(class: "sidebar-nav") do
nav_items.each do |item|
render_nav_item(**item)
end
end
end
def render_nav_item(label:, path:, icon: nil, badge: nil)
is_active = @current_path == path
link_to(path, class: [ "sidebar-nav-item", ("active" if is_active) ].compact.join(" ")) do
span(class: "nav-icon") { inline_icon(icon, size: 24) } if icon
span(class: "nav-label") { label }
span(class: "nav-badge") { badge.to_s } if badge && badge > 0
end
end
def render_user_section
div(class: "sidebar-user") do
render_user_info
render_logout_button
end
end
def render_user_info
div(class: "user-info") do
copy_to_clipboard current_identity.public_id, tooltip_direction: "e", label: "click to copy your account ID..." do
div(class: "user-avatar") do
span { current_identity.first_name&.first || "?" }
end
end
div(class: "user-details") do
div(class: "user-name") do
plain current_identity.first_name
whitespace
plain current_identity.last_name
end
div(class: "user-email") { current_identity.primary_email }
end
end
end
def render_logout_button
form_with(url: logout_path, method: :delete, class: "logout-form") do
button(type: "submit", class: "logout-button secondary") do
plain t "sidebar.logout"
span(class: "logout-icon") { inline_icon("door-leave", size: 18) }
end
end
end
def render_sidebar_brand
div(class: "sidebar-brand") do
vite_image_tag("images/hc-square.png", alt: "Hack Club logo", class: "brand-logo")
h1 { I18n.t(".brand") }
button(id: "lightswitch", class: "lightswitch-btn", type: "button", "aria-label": "Toggle theme") do
span(class: "lightswitch-moon") { inline_icon("moon-fill", size: 16) }
span(class: "lightswitch-sun", style: "display: none;") { inline_icon("sun", size: 16) }
end
end
render Components::EnvironmentBanner.new
end
def render_toggle_script
script do
raw safe <<~JS
function toggleSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.querySelector('.sidebar-overlay');
sidebar.classList.toggle('mobile-open');
overlay.classList.toggle('active');
}
JS
end
end
end