From 2100894475d9a29dff7d88d32b4ba7022f6f26a5 Mon Sep 17 00:00:00 2001 From: 24c02 <163450896+24c02@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:50:21 -0500 Subject: [PATCH] wowa --- app/components/admin/search/index.rb | 112 +++++++++--------- app/components/admin/users/show.rb | 79 ++++++------ app/components/api_keys/_row.rb | 48 +++----- app/components/api_keys/index.rb | 88 +++++--------- app/components/uploads/_row.rb | 80 ++++++------- app/components/uploads/index.rb | 44 +++---- app/components/uploads/new.rb | 59 ++++----- app/controllers/api/v4/api_keys_controller.rb | 22 ++++ config/routes.rb | 1 + 9 files changed, 257 insertions(+), 276 deletions(-) create mode 100644 app/controllers/api/v4/api_keys_controller.rb diff --git a/app/components/admin/search/index.rb b/app/components/admin/search/index.rb index 63640e3..fb28dcb 100644 --- a/app/components/admin/search/index.rb +++ b/app/components/admin/search/index.rb @@ -12,7 +12,7 @@ class Components::Admin::Search::Index < Components::Base end def view_template - div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do + div(class: "container-lg p-4") do header_section tabs_section search_form @@ -23,47 +23,37 @@ class Components::Admin::Search::Index < Components::Base private def header_section - header(style: "margin-bottom: 24px;") do - h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { "Admin Search" } - p(style: "color: var(--fgColor-muted, #656d76); margin: 8px 0 0; font-size: 14px;") do + div(class: "mb-4") do + h1(class: "h2 mb-1") { "Admin Search" } + p(class: "color-fg-muted f5") do "Search users and uploads by ID, email, filename, URL, etc." end end end def tabs_section - div(style: "display: flex; gap: 4px; margin-bottom: 16px; border-bottom: 1px solid var(--borderColor-default, #d0d7de);") do - tab_link("All", "all") - tab_link("Users", "users") - tab_link("Uploads", "uploads") - end - end - - def tab_link(label, type) - active = @type == type - base_style = "padding: 8px 16px; text-decoration: none; border-bottom: 2px solid transparent; margin-bottom: -1px;" - active_style = active ? "border-color: #0969da; color: #0969da; font-weight: 500;" : "color: var(--fgColor-muted);" - - link_to admin_search_path(type: type, q: @query), style: "#{base_style} #{active_style}" do - plain label + render Primer::Alpha::UnderlineNav.new(label: "Search type") do |nav| + nav.with_tab(selected: @type == "all", href: admin_search_path(type: "all", q: @query)) { "All" } + nav.with_tab(selected: @type == "users", href: admin_search_path(type: "users", q: @query)) { "Users" } + nav.with_tab(selected: @type == "uploads", href: admin_search_path(type: "uploads", q: @query)) { "Uploads" } end end def search_form - div(style: "margin-bottom: 24px;") do - form_with url: admin_search_path, method: :get, style: "display: flex; gap: 8px;" do + div(class: "mb-4 mt-3") do + form_with url: admin_search_path, method: :get, class: "d-flex gap-2" do input(type: "hidden", name: "type", value: @type) input( type: "search", name: "q", placeholder: search_placeholder, value: @query, - class: "form-control", - style: "flex: 1; max-width: 600px;", + class: "form-control flex-1", + style: "max-width: 600px;", autofocus: true ) - button(type: "submit", class: "btn btn-primary") do - render Primer::Beta::Octicon.new(icon: :search, mr: 1) + render Primer::Beta::Button.new(type: :submit, scheme: :primary) do |button| + button.with_leading_visual_icon(icon: :search) plain "Search" end end @@ -88,43 +78,55 @@ class Components::Admin::Search::Index < Components::Base end def users_section - div(style: "margin-bottom: 32px;") do - h2(style: "font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;") do + div(class: "mb-5") do + h2(class: "h4 mb-3") do plain "Users " - span(style: "color: var(--fgColor-muted); font-weight: normal;") { "(#{@users.size})" } + render Primer::Beta::Label.new(scheme: :secondary) { @users.size.to_s } end - div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do + render Primer::Beta::BorderBox.new do |box| @users.each do |user| - user_row(user) + box.with_row do + user_row(user) + end end end end end def user_row(user) - div(style: "padding: 12px 16px; border-bottom: 1px solid var(--borderColor-default, #d0d7de); display: flex; justify-content: space-between; align-items: center;") do + div(class: "d-flex flex-justify-between flex-items-center") do div do - div(style: "font-weight: 500;") { user.name || "Unnamed" } - div(style: "font-size: 12px; color: var(--fgColor-muted);") do + div(class: "text-bold") { user.name || "Unnamed" } + div(class: "f6 color-fg-muted") do plain user.email plain " · " - code(style: "font-size: 11px;") { user.public_id } + code(class: "f6") { user.public_id } end end - div(style: "display: flex; align-items: center; gap: 16px;") do - div(style: "text-align: right; font-size: 12px; color: var(--fgColor-muted);") do + div(class: "d-flex flex-items-center gap-3") do + div(class: "text-right f6 color-fg-muted") do div { "#{user.total_files} files" } div { user.total_storage_formatted } - if user.is_admin? - span(style: "background: #8250df; color: white; padding: 2px 6px; border-radius: 4px; font-size: 10px; margin-left: 8px;") { "ADMIN" } - end end - div(style: "display: flex; gap: 8px;") do - link_to admin_user_path(user), class: "btn btn-sm" do - render Primer::Beta::Octicon.new(icon: :eye, size: :small) - end - button_to admin_user_path(user), method: :delete, class: "btn btn-sm btn-danger", data: { turbo_confirm: "Delete user #{user.name || user.email} and all their uploads?" } do - render Primer::Beta::Octicon.new(icon: :trash, size: :small) + if user.is_admin? + render Primer::Beta::Label.new(scheme: :accent) { "ADMIN" } + end + div(class: "d-flex gap-2") do + render Primer::Beta::IconButton.new( + icon: :eye, + "aria-label": "View user", + href: admin_user_path(user), + tag: :a, + size: :small + ) + button_to admin_user_path(user), method: :delete, class: "d-inline", data: { turbo_confirm: "Delete user #{user.name || user.email} and all their uploads?" } do + render Primer::Beta::IconButton.new( + icon: :trash, + "aria-label": "Delete user", + scheme: :danger, + size: :small, + tag: :span + ) end end end @@ -133,27 +135,25 @@ class Components::Admin::Search::Index < Components::Base def uploads_section div do - h2(style: "font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;") do + h2(class: "h4 mb-3") do plain "Uploads " - span(style: "color: var(--fgColor-muted); font-weight: normal;") { "(#{@uploads.size})" } + render Primer::Beta::Label.new(scheme: :secondary) { @uploads.size.to_s } end - div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do + render Primer::Beta::BorderBox.new do |box| @uploads.each do |upload| - upload_row(upload) + box.with_row do + render Components::Uploads::Row.new(upload: upload, compact: true, admin: true) + end end end end end - def upload_row(upload) - render Components::Uploads::Row.new(upload: upload, compact: true, admin: true) - end - def empty_state - div(style: "text-align: center; padding: 64px 24px; color: var(--fgColor-muted, #656d76);") do - render Primer::Beta::Octicon.new(icon: :search, size: :medium) - h2(style: "font-size: 20px; font-weight: 600; margin: 16px 0 8px;") { "No results found" } - p(style: "margin: 0;") { "Try a different search query" } + render Primer::Beta::Blankslate.new(border: true) do |component| + component.with_visual_icon(icon: :search) + component.with_heading(tag: :h2) { "No results found" } + component.with_description { "Try a different search query" } end end end diff --git a/app/components/admin/users/show.rb b/app/components/admin/users/show.rb index 96d812b..dfdd5d9 100644 --- a/app/components/admin/users/show.rb +++ b/app/components/admin/users/show.rb @@ -9,7 +9,7 @@ class Components::Admin::Users::Show < Components::Base end def view_template - div(style: "max-width: 800px; margin: 0 auto; padding: 24px;") do + div(class: "container-md p-4") do header_section stats_section api_keys_section @@ -20,36 +20,37 @@ class Components::Admin::Users::Show < Components::Base private def header_section - header(style: "margin-bottom: 24px;") do - div(style: "display: flex; justify-content: space-between; align-items: flex-start;") do + div(class: "mb-4") do + div(class: "d-flex flex-justify-between flex-items-start") do div do - h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { @user.name || "Unnamed User" } - p(style: "color: var(--fgColor-muted, #656d76); margin: 8px 0 0; font-size: 14px;") do + div(class: "d-flex flex-items-center gap-2 mb-1") do + h1(class: "h2 mb-0") { @user.name || "Unnamed User" } + if @user.is_admin? + render Primer::Beta::Label.new(scheme: :accent) { "ADMIN" } + end + end + div(class: "color-fg-muted f5") do plain @user.email plain " · " - code(style: "font-size: 12px;") { @user.public_id } + code(class: "f6") { @user.public_id } end if @user.slack_id.present? - p(style: "color: var(--fgColor-muted); margin: 4px 0 0; font-size: 12px;") do + div(class: "color-fg-muted f6 mt-1") do plain "Slack: " code { @user.slack_id } end end end - div(style: "display: flex; gap: 8px;") do - if @user.is_admin? - span(style: "background: #8250df; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;") { "ADMIN" } - end - link_to admin_search_path, class: "btn" do - plain "Back to Search" - end + render Primer::Beta::Button.new(href: admin_search_path, tag: :a) do |button| + button.with_leading_visual_icon(icon: :"arrow-left") + plain "Back to Search" end end end end def stats_section - div(style: "display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px;") do + div(class: "d-grid gap-3 mb-4", style: "grid-template-columns: repeat(3, 1fr);") do stat_card("Total Files", @user.total_files.to_s) stat_card("Total Storage", @user.total_storage_formatted) stat_card("Member Since", @user.created_at.strftime("%b %d, %Y")) @@ -57,9 +58,11 @@ class Components::Admin::Users::Show < Components::Base end def stat_card(label, value) - div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; padding: 16px;") do - div(style: "font-size: 12px; color: var(--fgColor-muted);") { label } - div(style: "font-size: 24px; font-weight: 600; margin-top: 4px;") { value } + render Primer::Beta::BorderBox.new do |box| + box.with_body(padding: :normal) do + div(class: "f6 color-fg-muted") { label } + div(class: "h3 mt-1") { value } + end end end @@ -67,30 +70,34 @@ class Components::Admin::Users::Show < Components::Base api_keys = @user.api_keys.recent return if api_keys.empty? - div(style: "margin-bottom: 24px;") do - h2(style: "font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;") { "API Keys" } - div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do + div(class: "mb-4") do + h2(class: "h4 mb-3") { "API Keys" } + render Primer::Beta::BorderBox.new do |box| api_keys.each do |api_key| - api_key_row(api_key) + box.with_row do + api_key_row(api_key) + end end end end end def api_key_row(api_key) - div(style: "display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--borderColor-default, #d0d7de);") do + div(class: "d-flex flex-justify-between flex-items-center") do div do - div(style: "font-weight: 500;") { api_key.name } - code(style: "font-size: 12px; color: var(--fgColor-muted);") { api_key.masked_token } + div(class: "text-bold") { api_key.name } + code(class: "f6 color-fg-muted") { api_key.masked_token } end - div(style: "display: flex; align-items: center; gap: 12px;") do + div(class: "d-flex flex-items-center gap-3") do if api_key.revoked? - span(style: "background: #cf222e; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px;") { "REVOKED" } + render Primer::Beta::Label.new(scheme: :danger) { "REVOKED" } else - span(style: "background: #1a7f37; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px;") { "ACTIVE" } - button_to "Revoke", helpers.admin_api_key_path(api_key), method: :delete, class: "btn btn-sm btn-danger", data: { confirm: "Revoke this API key?" } + render Primer::Beta::Label.new(scheme: :success) { "ACTIVE" } + button_to helpers.admin_api_key_path(api_key), method: :delete, class: "d-inline", data: { confirm: "Revoke this API key?" } do + render Primer::Beta::Button.new(scheme: :danger, size: :small, tag: :span) { "Revoke" } + end end - span(style: "font-size: 12px; color: var(--fgColor-muted);") { api_key.created_at.strftime("%b %d, %Y") } + span(class: "f6 color-fg-muted") { api_key.created_at.strftime("%b %d, %Y") } end end end @@ -100,16 +107,14 @@ class Components::Admin::Users::Show < Components::Base return if uploads.empty? div do - h2(style: "font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;") { "Recent Uploads" } - div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do + h2(class: "h4 mb-3") { "Recent Uploads" } + render Primer::Beta::BorderBox.new do |box| uploads.each do |upload| - upload_row(upload) + box.with_row do + render Components::Uploads::Row.new(upload: upload, compact: true, admin: true) + end end end end end - - def upload_row(upload) - render Components::Uploads::Row.new(upload: upload, compact: true, admin: true) - end end diff --git a/app/components/api_keys/_row.rb b/app/components/api_keys/_row.rb index dc4418e..817f5ae 100644 --- a/app/components/api_keys/_row.rb +++ b/app/components/api_keys/_row.rb @@ -3,40 +3,30 @@ class Components::APIKeys::Row < Components::Base include Phlex::Rails::Helpers::FormWith - def initialize(api_key:, index: 0) + def initialize(api_key:) @api_key = api_key - @index = index end def view_template - div( - style: "padding: 16px; #{index > 0 ? 'border-top: 1px solid var(--borderColor-default, #d0d7de);' : ''}", - data: { api_key_id: api_key.id } - ) do - div(style: "display: flex; justify-content: space-between; align-items: flex-start; gap: 16px;") do - div(style: "flex: 1; min-width: 0;") do - div(style: "display: flex; align-items: center; gap: 8px; margin-bottom: 8px;") do - render Primer::Beta::Octicon.new(icon: :key, size: :small) - div(style: "font-size: 14px; font-weight: 500;") do - plain api_key.name - end - end - div(style: "font-size: 12px; color: var(--fgColor-muted, #656d76); font-family: monospace;") do - plain api_key.masked_token - end - div(style: "font-size: 12px; color: var(--fgColor-muted, #656d76); margin-top: 4px;") do - plain "Created #{time_ago_in_words(api_key.created_at)} ago" - end + div(class: "d-flex flex-justify-between flex-items-start gap-3") do + div(class: "flex-1 min-width-0") do + div(class: "d-flex flex-items-center gap-2 mb-1") do + render Primer::Beta::Octicon.new(icon: :key, size: :small, color: :muted) + span(class: "f5 text-bold") { api_key.name } + end + code(class: "f6 color-fg-muted") { api_key.masked_token } + div(class: "f6 color-fg-muted mt-1") do + plain "Created #{time_ago_in_words(api_key.created_at)} ago" end - - render_revoke_dialog end + + render_revoke_dialog end end private - attr_reader :api_key, :index + attr_reader :api_key def render_revoke_dialog render Primer::Alpha::Dialog.new(title: "Revoke API key?", size: :medium) do |dialog| @@ -44,19 +34,17 @@ class Components::APIKeys::Row < Components::Base render Primer::Beta::Octicon.new(icon: :trash) end dialog.with_header(variant: :large) do - h1(style: "margin: 0;") { "Revoke \"#{api_key.name}\"?" } + h1(class: "h3") { "Revoke \"#{api_key.name}\"?" } end dialog.with_body do - p(style: "margin: 0;") do + p(class: "color-fg-muted") do plain "This action cannot be undone. Any applications using this API key will immediately lose access." end end dialog.with_footer do - div(style: "display: flex; justify-content: flex-end; gap: 8px;") do - form_with url: api_key_path(api_key), method: :delete, style: "display: inline;" do - button(type: "submit", class: "btn btn-danger") do - plain "Revoke key" - end + div(class: "d-flex flex-justify-end gap-2") do + form_with url: api_key_path(api_key), method: :delete, class: "d-inline" do + render Primer::Beta::Button.new(type: :submit, scheme: :danger) { "Revoke key" } end end end diff --git a/app/components/api_keys/index.rb b/app/components/api_keys/index.rb index c31b143..62ed047 100644 --- a/app/components/api_keys/index.rb +++ b/app/components/api_keys/index.rb @@ -9,7 +9,7 @@ class Components::APIKeys::Index < Components::Base end def view_template - div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do + div(class: "container-lg p-4") do header_section new_token_alert if new_token create_form @@ -22,73 +22,47 @@ class Components::APIKeys::Index < Components::Base attr_reader :api_keys, :new_token def header_section - header(style: "margin-bottom: 24px;") do - h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { "API Keys" } - p(style: "color: var(--fgColor-muted, #656d76); margin: 8px 0 0; font-size: 14px;") do + div(class: "mb-4") do + h1(class: "h2 mb-1") { "API Keys" } + p(class: "color-fg-muted f5") do plain "Manage your API keys for programmatic access. " - a(href: "/docs/api", style: "color: var(--fgColor-accent, #0969da);") { "View API documentation" } + a(href: "/docs/api", class: "Link") { "View API documentation" } end end end def new_token_alert - div( - style: "background: var(--bgColor-success-muted, #dafbe1); border: 1px solid var(--borderColor-success-emphasis, #1a7f37); border-radius: 6px; padding: 16px; margin-bottom: 24px;" - ) do - div(style: "display: flex; align-items: flex-start; gap: 12px;") do - render Primer::Beta::Octicon.new(icon: :check, size: :medium, color: :success) - div(style: "flex: 1;") do - p(style: "margin: 0 0 8px; font-weight: 600; color: var(--fgColor-success, #1a7f37);") do - plain "API key created successfully!" - end - p(style: "margin: 0 0 8px; color: var(--fgColor-default, #1f2328); font-size: 14px;") do - plain "Copy your API key now. You won't be able to see it again!" - end - div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; padding: 12px; font-family: monospace; font-size: 14px; word-break: break-all;") do - plain new_token - end - end + render Primer::Beta::Flash.new(scheme: :success, mb: 4) do |component| + component.with_icon(icon: :check) + div do + p(class: "text-bold mb-1") { "API key created successfully!" } + p(class: "mb-2") { "Copy your API key now. You won't be able to see it again!" } + code(class: "d-block p-2 color-bg-subtle rounded-2 f5 text-mono") { new_token } end end end def create_form - div(style: "background: linear-gradient(135deg, var(--bgColor-default, #fff) 0%, var(--bgColor-muted, #f6f8fa) 100%); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 12px; overflow: hidden; margin-bottom: 24px;") do - # Header with icon - div(style: "padding: 20px 24px; border-bottom: 1px solid var(--borderColor-muted, #d0d7de); display: flex; align-items: center; gap: 12px;") do - div(style: "width: 40px; height: 40px; border-radius: 10px; background: linear-gradient(135deg, #0969da 0%, #0550ae 100%); display: flex; align-items: center; justify-content: center; color: #fff;") do - render Primer::Beta::Octicon.new(icon: :key, size: :small) - end - div do - h2(style: "font-size: 1.125rem; font-weight: 600; margin: 0; color: var(--fgColor-default, #1f2328);") { "Create new API key" } - p(style: "font-size: 13px; color: var(--fgColor-muted, #656d76); margin: 2px 0 0;") { "Generate a key to access the CDN API programmatically" } - end + render Primer::Beta::BorderBox.new(mb: 4) do |box| + box.with_header do + h2(class: "f5 text-bold") { "Create new API key" } end - - # Form body - div(style: "padding: 24px;") do + box.with_body do form_with url: api_keys_path, method: :post do - div(style: "margin-bottom: 20px;") do - label(for: "api_key_name", style: "display: block; font-size: 14px; font-weight: 600; margin-bottom: 8px; color: var(--fgColor-default, #1f2328);") do - plain "Key name" - end - p(style: "font-size: 12px; color: var(--fgColor-muted, #656d76); margin: 0 0 8px;") do - plain "Give your key a memorable name so you can identify it later" - end + div(class: "mb-3", style: "max-width: 400px;") do + label(for: "api_key_name", class: "f5 text-bold d-block mb-2") { "Key name" } input( type: "text", name: "api_key[name]", id: "api_key_name", - placeholder: "e.g., Production server, CI/CD pipeline, Local dev", + placeholder: "e.g., The Coolest App That's Ever Lived", required: true, - class: "form-control", - style: "max-width: 400px; padding: 10px 12px; font-size: 14px;" + class: "form-control width-full" ) end - - button(type: "submit", class: "btn btn-primary", style: "padding: 10px 20px; font-size: 14px; font-weight: 600;") do - render Primer::Beta::Octicon.new(icon: :plus, mr: 2) - plain "Generate API key" + render Primer::Beta::Button.new(type: :submit, scheme: :primary) do |button| + button.with_leading_visual_icon(icon: :key) + plain "Create key" end end end @@ -97,12 +71,14 @@ class Components::APIKeys::Index < Components::Base def api_keys_list div do - h2(style: "font-size: 1.25rem; font-weight: 600; margin: 0 0 16px;") { "Your API keys" } + h2(class: "h4 mb-3") { "Your API keys" } if api_keys.any? - div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do - api_keys.each_with_index do |api_key, index| - render Components::APIKeys::Row.new(api_key: api_key, index: index) + render Primer::Beta::BorderBox.new do |box| + api_keys.each do |api_key| + box.with_row do + render Components::APIKeys::Row.new(api_key: api_key) + end end end else @@ -112,10 +88,10 @@ class Components::APIKeys::Index < Components::Base end def empty_state - div(style: "text-align: center; padding: 64px 24px; color: var(--fgColor-muted, #656d76); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px;") do - render Primer::Beta::Octicon.new(icon: :key, size: :medium) - h3(style: "font-size: 20px; font-weight: 600; margin: 16px 0 8px;") { "No API keys yet" } - p(style: "margin: 0;") { "Create your first API key to get started with the API" } + render Primer::Beta::Blankslate.new(border: true) do |component| + component.with_visual_icon(icon: :key) + component.with_heading(tag: :h3) { "No API keys yet" } + component.with_description { "Create your first API key to get started with the API" } end end end diff --git a/app/components/uploads/_row.rb b/app/components/uploads/_row.rb index 49691ea..a573af4 100644 --- a/app/components/uploads/_row.rb +++ b/app/components/uploads/_row.rb @@ -4,73 +4,71 @@ class Components::Uploads::Row < Components::Base include Phlex::Rails::Helpers::FormWith include Phlex::Rails::Helpers::LinkTo - def initialize(upload:, index: 0, compact: false, admin: false) + def initialize(upload:, compact: false, admin: false) @upload = upload - @index = index @compact = compact @admin = admin end def view_template - div( - style: "padding: #{compact ? '12px 16px' : '16px'}; #{index > 0 ? 'border-top: 1px solid var(--borderColor-default, #d0d7de);' : ''}", - data: { upload_id: upload.id } - ) do - div(style: "display: flex; justify-content: space-between; align-items: #{compact ? 'center' : 'flex-start'}; gap: 16px;") do - if compact - compact_content - else - full_content - end + div(class: "d-flex flex-justify-between flex-items-#{compact ? 'center' : 'start'} gap-3") do + if compact + compact_content + else + full_content end end end private - attr_reader :upload, :index, :compact, :admin + attr_reader :upload, :compact, :admin def compact_content - div(style: "flex: 1; min-width: 0;") do - div(style: "font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;") do + div(class: "flex-1 min-width-0") do + div(class: "f5 text-bold text-truncate") do render Primer::Beta::Octicon.new(icon: file_icon_for(upload.content_type), size: :small, mr: 1) plain upload.filename.to_s end - div(style: "font-size: 12px; color: var(--fgColor-muted, #656d76); margin-top: 4px;") do + div(class: "f6 color-fg-muted mt-1") do plain "#{upload.human_file_size} • #{time_ago_in_words(upload.created_at)} ago" end end - - div(style: "display: flex; gap: 8px; align-items: center;") do - a(href: upload.cdn_url, target: "_blank", rel: "noopener", class: "btn btn-sm") do - plain "View" - end + + div(class: "d-flex gap-2 flex-items-center") do + render Primer::Beta::Button.new( + href: upload.cdn_url, + tag: :a, + size: :small, + target: "_blank", + rel: "noopener" + ) { "View" } render_delete_dialog end end def full_content - div(style: "flex: 1; min-width: 0;") do - div(style: "display: flex; align-items: center; gap: 8px; margin-bottom: 8px;") do + div(class: "flex-1 min-width-0") do + div(class: "d-flex flex-items-center gap-2 mb-1") do render Primer::Beta::Octicon.new(icon: file_icon_for(upload.content_type), size: :small) - div(style: "font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;") do - plain upload.filename.to_s - end - span( - style: "font-size: 12px; padding: 2px 8px; background: var(--bgColor-muted, #f6f8fa); border-radius: 12px; color: var(--fgColor-muted, #656d76);" - ) do - plain upload.provenance.titleize - end + span(class: "f5 text-bold text-truncate") { upload.filename.to_s } + render Primer::Beta::Label.new(scheme: :secondary, size: :small) { upload.provenance.titleize } end - div(style: "font-size: 12px; color: var(--fgColor-muted, #656d76);") do + div(class: "f6 color-fg-muted") do plain "#{upload.human_file_size} • #{upload.content_type} • #{time_ago_in_words(upload.created_at)} ago" end end - div(style: "display: flex; gap: 8px; align-items: center;") do - a(href: upload.cdn_url, target: "_blank", rel: "noopener", class: "btn btn-sm") do - render Primer::Beta::Octicon.new(icon: :link, mr: 1) + div(class: "d-flex gap-2 flex-items-center") do + render Primer::Beta::Button.new( + href: upload.cdn_url, + tag: :a, + size: :small, + target: "_blank", + rel: "noopener" + ) do |button| + button.with_leading_visual_icon(icon: :link) plain "View" end @@ -84,19 +82,17 @@ class Components::Uploads::Row < Components::Base render Primer::Beta::Octicon.new(icon: :trash) end dialog.with_header(variant: :large) do - h1(style: "margin: 0;") { "Delete #{upload.filename}?" } + h1(class: "h3") { "Delete #{upload.filename}?" } end dialog.with_body do - p(style: "margin: 0;") do + p(class: "color-fg-muted") do plain "This action cannot be undone. The file will be permanently removed from the CDN." end end dialog.with_footer do - div(style: "display: flex; justify-content: flex-end; gap: 8px;") do - form_with url: (admin ? admin_upload_path(upload) : upload_path(upload)), method: :delete, style: "display: inline;" do - button(type: "submit", class: "btn btn-danger") do - plain "Delete" - end + div(class: "d-flex flex-justify-end gap-2") do + form_with url: (admin ? admin_upload_path(upload) : upload_path(upload)), method: :delete, class: "d-inline" do + render Primer::Beta::Button.new(type: :submit, scheme: :danger) { "Delete" } end end end diff --git a/app/components/uploads/index.rb b/app/components/uploads/index.rb index ff98cd4..73de7bf 100644 --- a/app/components/uploads/index.rb +++ b/app/components/uploads/index.rb @@ -12,7 +12,7 @@ class Components::Uploads::Index < Components::Base end def view_template - div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do + div(class: "container-lg p-4") do header_section search_section uploads_list @@ -25,35 +25,35 @@ class Components::Uploads::Index < Components::Base attr_reader :uploads, :query def header_section - header(style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;") do + div(class: "d-flex flex-justify-between flex-items-center mb-4") do div do - h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { "Your Uploads" } - p(style: "color: var(--fgColor-muted, #656d76); margin: 8px 0 0; font-size: 14px;") do + h1(class: "h2 mb-1") { "Your Uploads" } + p(class: "color-fg-muted f5 mb-0") do count = uploads.respond_to?(:total_count) ? uploads.total_count : uploads.size plain "#{count} file#{count == 1 ? '' : 's'}" end end - link_to new_upload_path, class: "btn btn-primary" do - render Primer::Beta::Octicon.new(icon: :upload, mr: 1) + render Primer::Beta::Button.new(href: new_upload_path, tag: :a, scheme: :primary) do |button| + button.with_leading_visual_icon(icon: :upload) plain "Upload File" end end end def search_section - div(style: "margin-bottom: 24px;") do - form_with url: uploads_path, method: :get, style: "display: flex; gap: 8px;" do + div(class: "mb-4") do + form_with url: uploads_path, method: :get, class: "d-flex gap-2" do input( type: "search", name: "query", placeholder: "Search files...", value: query, - class: "form-control", - style: "flex: 1; max-width: 400px;" + class: "form-control flex-1", + style: "max-width: 400px;" ) - button(type: "submit", class: "btn") do - render Primer::Beta::Octicon.new(icon: :search, mr: 1) + render Primer::Beta::Button.new(type: :submit) do |button| + button.with_leading_visual_icon(icon: :search) plain "Search" end end @@ -62,9 +62,11 @@ class Components::Uploads::Index < Components::Base def uploads_list if uploads.any? - div(style: "background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default, #d0d7de); border-radius: 6px; overflow: hidden;") do - uploads.each_with_index do |upload, index| - render Components::Uploads::Row.new(upload: upload, index: index, compact: false) + render Primer::Beta::BorderBox.new do |box| + uploads.each do |upload| + box.with_row do + render Components::Uploads::Row.new(upload: upload, compact: false) + end end end else @@ -73,16 +75,16 @@ class Components::Uploads::Index < Components::Base end def empty_state - div(style: "text-align: center; padding: 64px 24px; color: var(--fgColor-muted, #656d76);") do - render Primer::Beta::Octicon.new(icon: :inbox, size: :medium) - h2(style: "font-size: 20px; font-weight: 600; margin: 16px 0 8px;") do + render Primer::Beta::Blankslate.new(border: true) do |component| + component.with_visual_icon(icon: :inbox) + component.with_heading(tag: :h2) do query.present? ? "No files found" : "No uploads yet" end - p(style: "margin: 0 0 24px;") do + component.with_description do query.present? ? "Try a different search query" : "Upload your first file to get started" end unless query.present? - link_to new_upload_path, class: "btn btn-primary" do + component.with_primary_action(href: new_upload_path) do render Primer::Beta::Octicon.new(icon: :upload, mr: 1) plain "Upload File" end @@ -91,7 +93,7 @@ class Components::Uploads::Index < Components::Base end def pagination_section - div(style: "margin-top: 24px; text-align: center;") do + div(class: "mt-4 text-center") do paginate uploads end end diff --git a/app/components/uploads/new.rb b/app/components/uploads/new.rb index 0d121d8..31481b6 100644 --- a/app/components/uploads/new.rb +++ b/app/components/uploads/new.rb @@ -5,7 +5,7 @@ class Components::Uploads::New < Components::Base include Phlex::Rails::Helpers::LinkTo def view_template - div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do + div(class: "container-lg p-4") do header_section upload_form end @@ -14,14 +14,14 @@ class Components::Uploads::New < Components::Base private def header_section - header(style: "margin-bottom: 32px;") do - div(style: "display: flex; align-items: center; gap: 8px; margin-bottom: 16px;") do - link_to uploads_path, style: "color: var(--fgColor-muted, #656d76); text-decoration: none;" do + div(class: "mb-4") do + div(class: "d-flex flex-items-center gap-2 mb-2") do + render Primer::Beta::Button.new(href: uploads_path, tag: :a, scheme: :invisible, size: :small) do render Primer::Beta::Octicon.new(icon: :"arrow-left") end - h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { "Upload File" } + h1(class: "h2 mb-0") { "Upload File" } end - p(style: "color: var(--fgColor-muted, #656d76); margin: 0; font-size: 14px;") do + p(class: "color-fg-muted f5 mb-0") do plain "Drop a file anywhere on this page or click to browse" end end @@ -29,22 +29,21 @@ class Components::Uploads::New < Components::Base def upload_form form_with url: uploads_path, method: :post, multipart: true, data: { dropzone_form: true } do - # Main upload area - drag anywhere on page for full-screen overlay div( - class: "upload-area", - style: upload_area_styles + class: "upload-area rounded-3 p-6 text-center", + style: "border: 3px dashed var(--borderColor-default); transition: all 0.2s ease;" ) do - div(style: "text-align: center;") do - render Primer::Beta::Octicon.new(icon: :upload, size: :medium) - h2(style: "font-size: 32px; font-weight: 600; margin: 24px 0 16px;") { "Drag & Drop" } - p(style: "color: var(--fgColor-muted, #656d76); margin: 0 0 32px; font-size: 16px;") do + div(class: "py-6") do + render Primer::Beta::Octicon.new(icon: :upload, size: :medium, color: :muted) + h2(class: "h1 mt-4 mb-3") { "Drag & Drop" } + p(class: "color-fg-muted f4 mb-4") do plain "Drop a file anywhere on this page to upload instantly" end label( for: "file-input", class: "btn btn-primary btn-large", - style: "cursor: pointer; display: inline-block; font-size: 16px; padding: 12px 24px;" + style: "cursor: pointer;" ) do render Primer::Beta::Octicon.new(icon: :file, mr: 2) plain "Choose File" @@ -55,32 +54,24 @@ class Components::Uploads::New < Components::Base name: "file", id: "file-input", data: { dropzone_input: true }, - style: "display: none;" + class: "d-none" ) end end - # Tips section - div(style: "margin-top: 48px; padding: 24px; background: var(--bgColor-muted, #f6f8fa); border-radius: 8px;") do - h3(style: "font-size: 16px; font-weight: 600; margin: 0 0 16px;") { "How it works" } - ul(style: "margin: 0; padding-left: 24px; font-size: 14px; color: var(--fgColor-muted, #656d76); line-height: 1.8;") do - li { "Drag and drop a file anywhere on this page for instant upload" } - li { "Or click the button above to browse and select a file" } - li { "Files are stored securely and accessible via CDN URLs" } - li { "Supports images, videos, documents, and more" } + render Primer::Beta::BorderBox.new(mt: 5) do |box| + box.with_header do + h3(class: "f5 text-bold") { "How it works" } + end + box.with_body do + ul(class: "color-fg-muted f5 pl-4 mb-0", style: "line-height: 1.8;") do + li { "Drag and drop a file anywhere on this page for instant upload" } + li { "Or click the button above to browse and select a file" } + li { "Files are stored securely and accessible via CDN URLs" } + li { "Supports images, videos, documents, and more" } + end end end end end - - def upload_area_styles - <<~CSS.strip - border: 3px dashed var(--borderColor-default, #d0d7de); - border-radius: 16px; - padding: 96px 48px; - background: var(--bgColor-default, #fff); - text-align: center; - transition: all 0.2s ease; - CSS - end end diff --git a/app/controllers/api/v4/api_keys_controller.rb b/app/controllers/api/v4/api_keys_controller.rb new file mode 100644 index 0000000..58f298c --- /dev/null +++ b/app/controllers/api/v4/api_keys_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module API + module V4 + class APIKeysController < ApplicationController + def revoke + api_key = current_token + owner_email = current_user.email + key_name = api_key.name + + api_key.revoke! + + render json: { + success: true, + owner_email: owner_email, + key_name: key_name, + status: "complete" + }, status: :ok + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 7c8de12..4f22063 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,7 @@ Rails.application.routes.draw do get "me", to: "users#show" post "upload", to: "uploads#create" post "upload_from_url", to: "uploads#create_from_url" + post "revoke", to: "api_keys#revoke" end end