This commit is contained in:
24c02 2026-01-29 17:06:26 -05:00
parent 2100894475
commit bbf815cf30
9 changed files with 166 additions and 174 deletions

View file

@ -12,7 +12,7 @@ class Components::Admin::Search::Index < Components::Base
end end
def view_template def view_template
div(class: "container-lg p-4") do div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do
header_section header_section
tabs_section tabs_section
search_form search_form
@ -23,9 +23,9 @@ class Components::Admin::Search::Index < Components::Base
private private
def header_section def header_section
div(class: "mb-4") do header(style: "margin-bottom: 24px;") do
h1(class: "h2 mb-1") { "Admin Search" } h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { "Admin Search" }
p(class: "color-fg-muted f5") do p(style: "color: var(--fgColor-muted, #656d76); margin: 8px 0 0; font-size: 14px;") do
"Search users and uploads by ID, email, filename, URL, etc." "Search users and uploads by ID, email, filename, URL, etc."
end end
end end
@ -40,20 +40,20 @@ class Components::Admin::Search::Index < Components::Base
end end
def search_form def search_form
div(class: "mb-4 mt-3") do div(style: "margin-bottom: 24px; margin-top: 16px;") do
form_with url: admin_search_path, method: :get, class: "d-flex gap-2" do form_with url: admin_search_path, method: :get, style: "display: flex; gap: 8px;" do
input(type: "hidden", name: "type", value: @type) input(type: "hidden", name: "type", value: @type)
input( input(
type: "search", type: "search",
name: "q", name: "q",
placeholder: search_placeholder, placeholder: search_placeholder,
value: @query, value: @query,
class: "form-control flex-1", class: "form-control",
style: "max-width: 600px;", style: "flex: 1; max-width: 600px;",
autofocus: true autofocus: true
) )
render Primer::Beta::Button.new(type: :submit, scheme: :primary) do |button| button(type: "submit", class: "btn btn-primary") do
button.with_leading_visual_icon(icon: :search) render Primer::Beta::Octicon.new(icon: :search, mr: 1)
plain "Search" plain "Search"
end end
end end
@ -78,10 +78,10 @@ class Components::Admin::Search::Index < Components::Base
end end
def users_section def users_section
div(class: "mb-5") do div(style: "margin-bottom: 32px;") do
h2(class: "h4 mb-3") do h2(style: "font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;") do
plain "Users " plain "Users "
render Primer::Beta::Label.new(scheme: :secondary) { @users.size.to_s } render(Primer::Beta::Label.new(scheme: :secondary)) { plain @users.size.to_s }
end end
render Primer::Beta::BorderBox.new do |box| render Primer::Beta::BorderBox.new do |box|
@users.each do |user| @users.each do |user|
@ -94,40 +94,25 @@ class Components::Admin::Search::Index < Components::Base
end end
def user_row(user) def user_row(user)
div(class: "d-flex flex-justify-between flex-items-center") do div(style: "display: flex; justify-content: space-between; align-items: center;") do
div do div do
div(class: "text-bold") { user.name || "Unnamed" } div(style: "font-weight: 500;") { user.name || "Unnamed" }
div(class: "f6 color-fg-muted") do div(style: "font-size: 12px; color: var(--fgColor-muted);") do
plain user.email plain user.email
plain " · " plain " · "
code(class: "f6") { user.public_id } code(style: "font-size: 11px;") { user.public_id }
end end
end end
div(class: "d-flex flex-items-center gap-3") do div(style: "display: flex; align-items: center; gap: 16px;") do
div(class: "text-right f6 color-fg-muted") do div(style: "text-align: right; font-size: 12px; color: var(--fgColor-muted);") do
div { "#{user.total_files} files" } div { "#{user.total_files} files" }
div { user.total_storage_formatted } div { user.total_storage_formatted }
end end
if user.is_admin? if user.is_admin?
render Primer::Beta::Label.new(scheme: :accent) { "ADMIN" } render(Primer::Beta::Label.new(scheme: :accent)) { plain "ADMIN" }
end end
div(class: "d-flex gap-2") do link_to admin_user_path(user), class: "btn btn-sm", title: "View user" do
render Primer::Beta::IconButton.new( render Primer::Beta::Octicon.new(icon: :eye, size: :small)
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
end end
end end
@ -135,9 +120,9 @@ class Components::Admin::Search::Index < Components::Base
def uploads_section def uploads_section
div do div do
h2(class: "h4 mb-3") do h2(style: "font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;") do
plain "Uploads " plain "Uploads "
render Primer::Beta::Label.new(scheme: :secondary) { @uploads.size.to_s } render(Primer::Beta::Label.new(scheme: :secondary)) { plain @uploads.size.to_s }
end end
render Primer::Beta::BorderBox.new do |box| render Primer::Beta::BorderBox.new do |box|
@uploads.each do |upload| @uploads.each do |upload|

View file

@ -9,7 +9,7 @@ class Components::Admin::Users::Show < Components::Base
end end
def view_template def view_template
div(class: "container-md p-4") do div(style: "max-width: 800px; margin: 0 auto; padding: 24px;") do
header_section header_section
stats_section stats_section
api_keys_section api_keys_section
@ -20,29 +20,29 @@ class Components::Admin::Users::Show < Components::Base
private private
def header_section def header_section
div(class: "mb-4") do header(style: "margin-bottom: 24px;") do
div(class: "d-flex flex-justify-between flex-items-start") do div(style: "display: flex; justify-content: space-between; align-items: flex-start;") do
div do div do
div(class: "d-flex flex-items-center gap-2 mb-1") do div(style: "display: flex; align-items: center; gap: 8px; margin-bottom: 8px;") do
h1(class: "h2 mb-0") { @user.name || "Unnamed User" } h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { @user.name || "Unnamed User" }
if @user.is_admin? if @user.is_admin?
render Primer::Beta::Label.new(scheme: :accent) { "ADMIN" } render(Primer::Beta::Label.new(scheme: :accent)) { plain "ADMIN" }
end end
end end
div(class: "color-fg-muted f5") do p(style: "color: var(--fgColor-muted, #656d76); margin: 0; font-size: 14px;") do
plain @user.email plain @user.email
plain " · " plain " · "
code(class: "f6") { @user.public_id } code(style: "font-size: 12px;") { @user.public_id }
end end
if @user.slack_id.present? if @user.slack_id.present?
div(class: "color-fg-muted f6 mt-1") do p(style: "color: var(--fgColor-muted); margin: 4px 0 0; font-size: 12px;") do
plain "Slack: " plain "Slack: "
code { @user.slack_id } code { @user.slack_id }
end end
end end
end end
render Primer::Beta::Button.new(href: admin_search_path, tag: :a) do |button| link_to admin_search_path, class: "btn" do
button.with_leading_visual_icon(icon: :"arrow-left") render Primer::Beta::Octicon.new(icon: :"arrow-left", mr: 1)
plain "Back to Search" plain "Back to Search"
end end
end end
@ -50,7 +50,7 @@ class Components::Admin::Users::Show < Components::Base
end end
def stats_section def stats_section
div(class: "d-grid gap-3 mb-4", style: "grid-template-columns: repeat(3, 1fr);") do div(style: "display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px;") do
stat_card("Total Files", @user.total_files.to_s) stat_card("Total Files", @user.total_files.to_s)
stat_card("Total Storage", @user.total_storage_formatted) stat_card("Total Storage", @user.total_storage_formatted)
stat_card("Member Since", @user.created_at.strftime("%b %d, %Y")) stat_card("Member Since", @user.created_at.strftime("%b %d, %Y"))
@ -60,8 +60,8 @@ class Components::Admin::Users::Show < Components::Base
def stat_card(label, value) def stat_card(label, value)
render Primer::Beta::BorderBox.new do |box| render Primer::Beta::BorderBox.new do |box|
box.with_body(padding: :normal) do box.with_body(padding: :normal) do
div(class: "f6 color-fg-muted") { label } div(style: "font-size: 12px; color: var(--fgColor-muted);") { label }
div(class: "h3 mt-1") { value } div(style: "font-size: 24px; font-weight: 600; margin-top: 4px;") { value }
end end
end end
end end
@ -70,8 +70,8 @@ class Components::Admin::Users::Show < Components::Base
api_keys = @user.api_keys.recent api_keys = @user.api_keys.recent
return if api_keys.empty? return if api_keys.empty?
div(class: "mb-4") do div(style: "margin-bottom: 24px;") do
h2(class: "h4 mb-3") { "API Keys" } h2(style: "font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;") { "API Keys" }
render Primer::Beta::BorderBox.new do |box| render Primer::Beta::BorderBox.new do |box|
api_keys.each do |api_key| api_keys.each do |api_key|
box.with_row do box.with_row do
@ -83,21 +83,21 @@ class Components::Admin::Users::Show < Components::Base
end end
def api_key_row(api_key) def api_key_row(api_key)
div(class: "d-flex flex-justify-between flex-items-center") do div(style: "display: flex; justify-content: space-between; align-items: center;") do
div do div do
div(class: "text-bold") { api_key.name } div(style: "font-weight: 500;") { api_key.name }
code(class: "f6 color-fg-muted") { api_key.masked_token } code(style: "font-size: 12px; color: var(--fgColor-muted);") { api_key.masked_token }
end end
div(class: "d-flex flex-items-center gap-3") do div(style: "display: flex; align-items: center; gap: 12px;") do
if api_key.revoked? if api_key.revoked?
render Primer::Beta::Label.new(scheme: :danger) { "REVOKED" } render(Primer::Beta::Label.new(scheme: :danger)) { plain "REVOKED" }
else else
render Primer::Beta::Label.new(scheme: :success) { "ACTIVE" } render(Primer::Beta::Label.new(scheme: :success)) { plain "ACTIVE" }
button_to helpers.admin_api_key_path(api_key), method: :delete, class: "d-inline", data: { confirm: "Revoke this API key?" } do button_to helpers.admin_api_key_path(api_key), method: :delete, class: "btn btn-sm btn-danger", data: { confirm: "Revoke this API key?" } do
render Primer::Beta::Button.new(scheme: :danger, size: :small, tag: :span) { "Revoke" } plain "Revoke"
end end
end end
span(class: "f6 color-fg-muted") { api_key.created_at.strftime("%b %d, %Y") } span(style: "font-size: 12px; color: var(--fgColor-muted);") { api_key.created_at.strftime("%b %d, %Y") }
end end
end end
end end
@ -107,7 +107,7 @@ class Components::Admin::Users::Show < Components::Base
return if uploads.empty? return if uploads.empty?
div do div do
h2(class: "h4 mb-3") { "Recent Uploads" } h2(style: "font-size: 1.25rem; font-weight: 600; margin-bottom: 12px;") { "Recent Uploads" }
render Primer::Beta::BorderBox.new do |box| render Primer::Beta::BorderBox.new do |box|
uploads.each do |upload| uploads.each do |upload|
box.with_row do box.with_row do

View file

@ -8,14 +8,14 @@ class Components::APIKeys::Row < Components::Base
end end
def view_template def view_template
div(class: "d-flex flex-justify-between flex-items-start gap-3") do div(style: "display: flex; justify-content: space-between; align-items: flex-start; gap: 16px;") do
div(class: "flex-1 min-width-0") do div(style: "flex: 1; min-width: 0;") do
div(class: "d-flex flex-items-center gap-2 mb-1") do div(style: "display: flex; align-items: center; gap: 8px; margin-bottom: 8px;") do
render Primer::Beta::Octicon.new(icon: :key, size: :small, color: :muted) render Primer::Beta::Octicon.new(icon: :key, size: :small, color: :muted)
span(class: "f5 text-bold") { api_key.name } span(style: "font-size: 14px; font-weight: 500;") { api_key.name }
end end
code(class: "f6 color-fg-muted") { api_key.masked_token } code(style: "font-size: 12px; color: var(--fgColor-muted, #656d76);") { api_key.masked_token }
div(class: "f6 color-fg-muted mt-1") do 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" plain "Created #{time_ago_in_words(api_key.created_at)} ago"
end end
end end
@ -34,17 +34,19 @@ class Components::APIKeys::Row < Components::Base
render Primer::Beta::Octicon.new(icon: :trash) render Primer::Beta::Octicon.new(icon: :trash)
end end
dialog.with_header(variant: :large) do dialog.with_header(variant: :large) do
h1(class: "h3") { "Revoke \"#{api_key.name}\"?" } h1(style: "margin: 0;") { "Revoke \"#{api_key.name}\"?" }
end end
dialog.with_body do dialog.with_body do
p(class: "color-fg-muted") do p(style: "margin: 0;") do
plain "This action cannot be undone. Any applications using this API key will immediately lose access." plain "This action cannot be undone. Any applications using this API key will immediately lose access."
end end
end end
dialog.with_footer do dialog.with_footer do
div(class: "d-flex flex-justify-end gap-2") do div(style: "display: flex; justify-content: flex-end; gap: 8px;") do
form_with url: api_key_path(api_key), method: :delete, class: "d-inline" do form_with url: api_key_path(api_key), method: :delete, style: "display: inline;" do
render Primer::Beta::Button.new(type: :submit, scheme: :danger) { "Revoke key" } button(type: "submit", class: "btn btn-danger") do
plain "Revoke key"
end
end end
end end
end end

View file

@ -9,7 +9,7 @@ class Components::APIKeys::Index < Components::Base
end end
def view_template def view_template
div(class: "container-lg p-4") do div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do
header_section header_section
new_token_alert if new_token new_token_alert if new_token
create_form create_form
@ -22,11 +22,11 @@ class Components::APIKeys::Index < Components::Base
attr_reader :api_keys, :new_token attr_reader :api_keys, :new_token
def header_section def header_section
div(class: "mb-4") do header(style: "margin-bottom: 24px;") do
h1(class: "h2 mb-1") { "API Keys" } h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { "API Keys" }
p(class: "color-fg-muted f5") do p(style: "color: var(--fgColor-muted, #656d76); margin: 8px 0 0; font-size: 14px;") do
plain "Manage your API keys for programmatic access. " plain "Manage your API keys for programmatic access. "
a(href: "/docs/api", class: "Link") { "View API documentation" } a(href: "/docs/api", style: "color: var(--fgColor-accent, #0969da);") { "View API documentation" }
end end
end end
end end
@ -35,9 +35,11 @@ class Components::APIKeys::Index < Components::Base
render Primer::Beta::Flash.new(scheme: :success, mb: 4) do |component| render Primer::Beta::Flash.new(scheme: :success, mb: 4) do |component|
component.with_icon(icon: :check) component.with_icon(icon: :check)
div do div do
p(class: "text-bold mb-1") { "API key created successfully!" } p(style: "margin: 0 0 8px; font-weight: 600;") { "API key created successfully!" }
p(class: "mb-2") { "Copy your API key now. You won't be able to see it again!" } p(style: "margin: 0 0 8px;") { "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 } code(style: "display: block; padding: 12px; background: var(--bgColor-default, #fff); border: 1px solid var(--borderColor-default); border-radius: 6px; font-size: 14px; word-break: break-all;") do
plain new_token
end
end end
end end
end end
@ -45,23 +47,25 @@ class Components::APIKeys::Index < Components::Base
def create_form def create_form
render Primer::Beta::BorderBox.new(mb: 4) do |box| render Primer::Beta::BorderBox.new(mb: 4) do |box|
box.with_header do box.with_header do
h2(class: "f5 text-bold") { "Create new API key" } h2(style: "font-size: 14px; font-weight: 600; margin: 0;") { "Create new API key" }
end end
box.with_body do box.with_body do
form_with url: api_keys_path, method: :post do form_with url: api_keys_path, method: :post do
div(class: "mb-3", style: "max-width: 400px;") do div(style: "margin-bottom: 12px; max-width: 400px;") do
label(for: "api_key_name", class: "f5 text-bold d-block mb-2") { "Key name" } label(for: "api_key_name", style: "display: block; font-size: 14px; font-weight: 600; margin-bottom: 8px;") do
plain "Key name"
end
input( input(
type: "text", type: "text",
name: "api_key[name]", name: "api_key[name]",
id: "api_key_name", id: "api_key_name",
placeholder: "e.g., The Coolest App That's Ever Lived", placeholder: "e.g., My App",
required: true, required: true,
class: "form-control width-full" class: "form-control"
) )
end end
render Primer::Beta::Button.new(type: :submit, scheme: :primary) do |button| button(type: "submit", class: "btn btn-primary") do
button.with_leading_visual_icon(icon: :key) render Primer::Beta::Octicon.new(icon: :key, mr: 1)
plain "Create key" plain "Create key"
end end
end end
@ -71,7 +75,7 @@ class Components::APIKeys::Index < Components::Base
def api_keys_list def api_keys_list
div do div do
h2(class: "h4 mb-3") { "Your API keys" } h2(style: "font-size: 1.25rem; font-weight: 600; margin: 0 0 16px;") { "Your API keys" }
if api_keys.any? if api_keys.any?
render Primer::Beta::BorderBox.new do |box| render Primer::Beta::BorderBox.new do |box|

View file

@ -61,9 +61,11 @@ class Components::StaticPages::Home < Components::StaticPages::Base
end end
def recent_uploads_list def recent_uploads_list
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|
stats[:recent_uploads].each_with_index do |upload, index| stats[:recent_uploads].each do |upload|
render Components::Uploads::Row.new(upload: upload, index: index, compact: true) box.with_row do
render Components::Uploads::Row.new(upload: upload, compact: true)
end
end end
end end
end end

View file

@ -11,7 +11,7 @@ class Components::Uploads::Row < Components::Base
end end
def view_template def view_template
div(class: "d-flex flex-justify-between flex-items-#{compact ? 'center' : 'start'} gap-3") do div(style: "display: flex; justify-content: space-between; align-items: #{compact ? 'center' : 'flex-start'}; gap: 16px;") do
if compact if compact
compact_content compact_content
else else
@ -25,50 +25,42 @@ class Components::Uploads::Row < Components::Base
attr_reader :upload, :compact, :admin attr_reader :upload, :compact, :admin
def compact_content def compact_content
div(class: "flex-1 min-width-0") do div(style: "flex: 1; min-width: 0;") do
div(class: "f5 text-bold text-truncate") do div(style: "font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;") do
render Primer::Beta::Octicon.new(icon: file_icon_for(upload.content_type), size: :small, mr: 1) render Primer::Beta::Octicon.new(icon: file_icon_for(upload.content_type), size: :small, mr: 1)
plain upload.filename.to_s plain upload.filename.to_s
end end
div(class: "f6 color-fg-muted mt-1") do div(style: "font-size: 12px; color: var(--fgColor-muted, #656d76); margin-top: 4px;") do
plain "#{upload.human_file_size}#{time_ago_in_words(upload.created_at)} ago" plain "#{upload.human_file_size}#{time_ago_in_words(upload.created_at)} ago"
end end
end end
div(class: "d-flex gap-2 flex-items-center") do div(style: "display: flex; gap: 8px; align-items: center;") do
render Primer::Beta::Button.new( a(href: upload.cdn_url, target: "_blank", rel: "noopener", class: "btn btn-sm") do
href: upload.cdn_url, plain "View"
tag: :a, end
size: :small,
target: "_blank",
rel: "noopener"
) { "View" }
render_delete_dialog render_delete_dialog
end end
end end
def full_content def full_content
div(class: "flex-1 min-width-0") do div(style: "flex: 1; min-width: 0;") do
div(class: "d-flex flex-items-center gap-2 mb-1") do div(style: "display: flex; align-items: center; gap: 8px; margin-bottom: 8px;") do
render Primer::Beta::Octicon.new(icon: file_icon_for(upload.content_type), size: :small) render Primer::Beta::Octicon.new(icon: file_icon_for(upload.content_type), size: :small)
span(class: "f5 text-bold text-truncate") { upload.filename.to_s } div(style: "font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;") do
render Primer::Beta::Label.new(scheme: :secondary, size: :small) { upload.provenance.titleize } plain upload.filename.to_s
end
render(Primer::Beta::Label.new(scheme: :secondary, size: :small)) { plain upload.provenance.titleize }
end end
div(class: "f6 color-fg-muted") do div(style: "font-size: 12px; color: var(--fgColor-muted, #656d76);") do
plain "#{upload.human_file_size}#{upload.content_type}#{time_ago_in_words(upload.created_at)} ago" plain "#{upload.human_file_size}#{upload.content_type}#{time_ago_in_words(upload.created_at)} ago"
end end
end end
div(class: "d-flex gap-2 flex-items-center") do div(style: "display: flex; gap: 8px; align-items: center;") do
render Primer::Beta::Button.new( a(href: upload.cdn_url, target: "_blank", rel: "noopener", class: "btn btn-sm") do
href: upload.cdn_url, render Primer::Beta::Octicon.new(icon: :link, mr: 1)
tag: :a,
size: :small,
target: "_blank",
rel: "noopener"
) do |button|
button.with_leading_visual_icon(icon: :link)
plain "View" plain "View"
end end
@ -82,17 +74,19 @@ class Components::Uploads::Row < Components::Base
render Primer::Beta::Octicon.new(icon: :trash) render Primer::Beta::Octicon.new(icon: :trash)
end end
dialog.with_header(variant: :large) do dialog.with_header(variant: :large) do
h1(class: "h3") { "Delete #{upload.filename}?" } h1(style: "margin: 0;") { "Delete #{upload.filename}?" }
end end
dialog.with_body do dialog.with_body do
p(class: "color-fg-muted") do p(style: "margin: 0;") do
plain "This action cannot be undone. The file will be permanently removed from the CDN." plain "This action cannot be undone. The file will be permanently removed from the CDN."
end end
end end
dialog.with_footer do dialog.with_footer do
div(class: "d-flex flex-justify-end gap-2") 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, class: "d-inline" do form_with url: (admin ? admin_upload_path(upload) : upload_path(upload)), method: :delete, style: "display: inline;" do
render Primer::Beta::Button.new(type: :submit, scheme: :danger) { "Delete" } button(type: "submit", class: "btn btn-danger") do
plain "Delete"
end
end end
end end
end end

View file

@ -12,7 +12,7 @@ class Components::Uploads::Index < Components::Base
end end
def view_template def view_template
div(class: "container-lg p-4") do div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do
header_section header_section
search_section search_section
uploads_list uploads_list
@ -25,35 +25,35 @@ class Components::Uploads::Index < Components::Base
attr_reader :uploads, :query attr_reader :uploads, :query
def header_section def header_section
div(class: "d-flex flex-justify-between flex-items-center mb-4") do header(style: "display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;") do
div do div do
h1(class: "h2 mb-1") { "Your Uploads" } h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { "Your Uploads" }
p(class: "color-fg-muted f5 mb-0") do p(style: "color: var(--fgColor-muted, #656d76); margin: 8px 0 0; font-size: 14px;") do
count = uploads.respond_to?(:total_count) ? uploads.total_count : uploads.size count = uploads.respond_to?(:total_count) ? uploads.total_count : uploads.size
plain "#{count} file#{count == 1 ? '' : 's'}" plain "#{count} file#{count == 1 ? '' : 's'}"
end end
end end
render Primer::Beta::Button.new(href: new_upload_path, tag: :a, scheme: :primary) do |button| link_to new_upload_path, class: "btn btn-primary" do
button.with_leading_visual_icon(icon: :upload) render Primer::Beta::Octicon.new(icon: :upload, mr: 1)
plain "Upload File" plain "Upload File"
end end
end end
end end
def search_section def search_section
div(class: "mb-4") do div(style: "margin-bottom: 24px;") do
form_with url: uploads_path, method: :get, class: "d-flex gap-2" do form_with url: uploads_path, method: :get, style: "display: flex; gap: 8px;" do
input( input(
type: "search", type: "search",
name: "query", name: "query",
placeholder: "Search files...", placeholder: "Search files...",
value: query, value: query,
class: "form-control flex-1", class: "form-control",
style: "max-width: 400px;" style: "flex: 1; max-width: 400px;"
) )
render Primer::Beta::Button.new(type: :submit) do |button| button(type: "submit", class: "btn") do
button.with_leading_visual_icon(icon: :search) render Primer::Beta::Octicon.new(icon: :search, mr: 1)
plain "Search" plain "Search"
end end
end end
@ -93,7 +93,7 @@ class Components::Uploads::Index < Components::Base
end end
def pagination_section def pagination_section
div(class: "mt-4 text-center") do div(style: "margin-top: 24px; text-align: center;") do
paginate uploads paginate uploads
end end
end end

View file

@ -5,7 +5,7 @@ class Components::Uploads::New < Components::Base
include Phlex::Rails::Helpers::LinkTo include Phlex::Rails::Helpers::LinkTo
def view_template def view_template
div(class: "container-lg p-4") do div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do
header_section header_section
upload_form upload_form
end end
@ -14,14 +14,14 @@ class Components::Uploads::New < Components::Base
private private
def header_section def header_section
div(class: "mb-4") do header(style: "margin-bottom: 32px;") do
div(class: "d-flex flex-items-center gap-2 mb-2") do div(style: "display: flex; align-items: center; gap: 8px; margin-bottom: 16px;") do
render Primer::Beta::Button.new(href: uploads_path, tag: :a, scheme: :invisible, size: :small) do link_to uploads_path, style: "color: var(--fgColor-muted, #656d76); text-decoration: none;" do
render Primer::Beta::Octicon.new(icon: :"arrow-left") render Primer::Beta::Octicon.new(icon: :"arrow-left")
end end
h1(class: "h2 mb-0") { "Upload File" } h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { "Upload File" }
end end
p(class: "color-fg-muted f5 mb-0") do p(style: "color: var(--fgColor-muted, #656d76); margin: 0; font-size: 14px;") do
plain "Drop a file anywhere on this page or click to browse" plain "Drop a file anywhere on this page or click to browse"
end end
end end
@ -30,20 +30,20 @@ class Components::Uploads::New < Components::Base
def upload_form def upload_form
form_with url: uploads_path, method: :post, multipart: true, data: { dropzone_form: true } do form_with url: uploads_path, method: :post, multipart: true, data: { dropzone_form: true } do
div( div(
class: "upload-area rounded-3 p-6 text-center", class: "upload-area",
style: "border: 3px dashed var(--borderColor-default); transition: all 0.2s ease;" style: upload_area_styles
) do ) do
div(class: "py-6") do div(style: "text-align: center;") do
render Primer::Beta::Octicon.new(icon: :upload, size: :medium, color: :muted) render Primer::Beta::Octicon.new(icon: :upload, size: :medium)
h2(class: "h1 mt-4 mb-3") { "Drag & Drop" } h2(style: "font-size: 32px; font-weight: 600; margin: 24px 0 16px;") { "Drag & Drop" }
p(class: "color-fg-muted f4 mb-4") do p(style: "color: var(--fgColor-muted, #656d76); margin: 0 0 32px; font-size: 16px;") do
plain "Drop a file anywhere on this page to upload instantly" plain "Drop a file anywhere on this page to upload instantly"
end end
label( label(
for: "file-input", for: "file-input",
class: "btn btn-primary btn-large", class: "btn btn-primary btn-large",
style: "cursor: pointer;" style: "cursor: pointer; display: inline-block; font-size: 16px; padding: 12px 24px;"
) do ) do
render Primer::Beta::Octicon.new(icon: :file, mr: 2) render Primer::Beta::Octicon.new(icon: :file, mr: 2)
plain "Choose File" plain "Choose File"
@ -54,17 +54,17 @@ class Components::Uploads::New < Components::Base
name: "file", name: "file",
id: "file-input", id: "file-input",
data: { dropzone_input: true }, data: { dropzone_input: true },
class: "d-none" style: "display: none;"
) )
end end
end end
render Primer::Beta::BorderBox.new(mt: 5) do |box| render Primer::Beta::BorderBox.new(mt: 5) do |box|
box.with_header do box.with_header do
h3(class: "f5 text-bold") { "How it works" } h3(style: "font-size: 16px; font-weight: 600; margin: 0;") { "How it works" }
end end
box.with_body do box.with_body do
ul(class: "color-fg-muted f5 pl-4 mb-0", style: "line-height: 1.8;") do 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 { "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 { "Or click the button above to browse and select a file" }
li { "Files are stored securely and accessible via CDN URLs" } li { "Files are stored securely and accessible via CDN URLs" }
@ -74,4 +74,15 @@ class Components::Uploads::New < Components::Base
end 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 end

View file

@ -4,46 +4,40 @@ icon: rocket
order: 1 order: 1
--- ---
# Getting Started~! ✨ # Getting Started
Welcome to Hack Club CDN, nya~! This guide will help you get your paws on file hosting in no time! Welcome to Hack Club CDN. This guide will help you get started with file hosting.
## Sign In ## Sign In
Click the **Sign in with Hack Club** button on the homepage to authenticate with your Hack Club account! It's super easy, I promise~ 🐱 Click the **Sign in with Hack Club** button on the homepage to authenticate with your Hack Club account.
## Uploading Files ## Uploading Files
Once you're logged in, here's how to share your creations with the world: Once you're logged in:
1. Pounce over to **My Files** 1. Navigate to **My Files**
2. Click **Upload** or drag and drop files (like a cat batting at a toy~) 2. Click **Upload** or drag and drop files
3. Your file gets a purrmanent URL! How neat is that? 3. Your file receives a permanent URL
## Sharing Files ## Sharing Files
Every uploaded file gets its own special URL you can share anywhere, nya~! Every uploaded file gets a unique URL you can share anywhere:
``` ```
https://cdn.hackclub.com/your-file-id https://cdn.hackclub.com/your-file-id
``` ```
Copy it, share it, do whatever you want with it! It's *your* file now~ ✨
## File Limits ## File Limits
- Maximum file size: varies by account (no eating too much at once!) - Maximum file size: varies by account
- Supported formats: images, documents, archives, and more~ - Supported formats: images, documents, archives, and more
## API Usage ## API Usage
For those of you who want to get technical (*adjusts glasses*), check out the [API documentation](https://github.com/hackclub/cdn#-api-usage) for programmatic uploads! For programmatic uploads, check out the [API documentation](/docs/api).
## Need Help? ## Need Help?
Don't be shy, nya~! We're here for you: - Join the [#cdn-dev channel on Slack](https://hackclub.slack.com/archives/C08RYDPS36V)
- Open an issue on [GitHub](https://github.com/hackclub/cdn/issues)
- Join the [#cdn channel on Slack](https://hackclub.enterprise.slack.com/archives/C016DEDUL87) and say hi!
- Open an issue on [GitHub](https://github.com/hackclub/cdn/issues) if something's broken
Now go upload something cool! I believe in you~ 💖