more more more

This commit is contained in:
24c02 2026-01-29 17:32:31 -05:00
parent bbf815cf30
commit 4619ba5944
9 changed files with 108 additions and 340 deletions

View file

@ -36,6 +36,8 @@ class Components::Uploads::Row < Components::Base
end
div(style: "display: flex; gap: 8px; align-items: center;") do
render(Primer::Beta::ClipboardCopyButton.new(value: upload.cdn_url, size: :small, "aria-label": "Copy link")) { "Copy link" }
a(href: upload.cdn_url, target: "_blank", rel: "noopener", class: "btn btn-sm") do
plain "View"
end
@ -51,7 +53,7 @@ class Components::Uploads::Row < Components::Base
div(style: "font-size: 14px; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;") do
plain upload.filename.to_s
end
render(Primer::Beta::Label.new(scheme: :secondary, size: :small)) { plain upload.provenance.titleize }
render(Primer::Beta::Label.new(scheme: :secondary)) { plain upload.provenance.titleize }
end
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"
@ -59,6 +61,8 @@ class Components::Uploads::Row < Components::Base
end
div(style: "display: flex; gap: 8px; align-items: center;") do
render(Primer::Beta::ClipboardCopyButton.new(value: upload.cdn_url, size: :small, "aria-label": "Copy link")) { "Copy link" }
a(href: upload.cdn_url, target: "_blank", rel: "noopener", class: "btn btn-sm") do
render Primer::Beta::Octicon.new(icon: :link, mr: 1)
plain "View"

View file

@ -12,6 +12,7 @@ class Components::Uploads::Index < Components::Base
end
def view_template
dropzone_form
div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do
header_section
search_section
@ -34,7 +35,7 @@ class Components::Uploads::Index < Components::Base
end
end
link_to new_upload_path, class: "btn btn-primary" do
label(for: "dropzone-file-input", class: "btn btn-primary", style: "cursor: pointer;") do
render Primer::Beta::Octicon.new(icon: :upload, mr: 1)
plain "Upload File"
end
@ -43,18 +44,20 @@ class Components::Uploads::Index < Components::Base
def search_section
div(style: "margin-bottom: 24px;") do
form_with url: uploads_path, method: :get, style: "display: flex; gap: 8px;" do
input(
type: "search",
name: "query",
placeholder: "Search files...",
value: query,
class: "form-control",
style: "flex: 1; max-width: 400px;"
)
button(type: "submit", class: "btn") do
render Primer::Beta::Octicon.new(icon: :search, mr: 1)
plain "Search"
form_with url: uploads_path, method: :get do
div(style: "display: flex; gap: 12px;") do
input(
type: "search",
name: "query",
placeholder: "Search files...",
value: query,
class: "form-control",
style: "flex: 1; max-width: 400px;"
)
button(type: "submit", class: "btn") do
render Primer::Beta::Octicon.new(icon: :search, mr: 1)
plain "Search"
end
end
end
end
@ -76,17 +79,15 @@ class Components::Uploads::Index < Components::Base
def empty_state
render Primer::Beta::Blankslate.new(border: true) do |component|
component.with_visual_icon(icon: :inbox)
component.with_visual_icon(icon: query.present? ? :search : :upload, size: :medium)
component.with_heading(tag: :h2) do
query.present? ? "No files found" : "No uploads yet"
query.present? ? "No files found" : "Drop files here"
end
component.with_description do
query.present? ? "Try a different search query" : "Upload your first file to get started"
end
unless query.present?
component.with_primary_action(href: new_upload_path) do
render Primer::Beta::Octicon.new(icon: :upload, mr: 1)
plain "Upload File"
if query.present?
"Try a different search query"
else
"Drag and drop files anywhere on this page, or use the Upload button"
end
end
end
@ -97,4 +98,10 @@ class Components::Uploads::Index < Components::Base
paginate uploads
end
end
def dropzone_form
form_with url: uploads_path, method: :post, multipart: true, data: { dropzone_form: true } do
input(type: "file", name: "file", id: "dropzone-file-input", data: { dropzone_input: true }, style: "display: none;")
end
end
end

View file

@ -1,88 +0,0 @@
# frozen_string_literal: true
class Components::Uploads::New < Components::Base
include Phlex::Rails::Helpers::FormWith
include Phlex::Rails::Helpers::LinkTo
def view_template
div(style: "max-width: 1200px; margin: 0 auto; padding: 24px;") do
header_section
upload_form
end
end
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
render Primer::Beta::Octicon.new(icon: :"arrow-left")
end
h1(style: "font-size: 2rem; font-weight: 600; margin: 0;") { "Upload File" }
end
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"
end
end
end
def upload_form
form_with url: uploads_path, method: :post, multipart: true, data: { dropzone_form: true } do
div(
class: "upload-area",
style: upload_area_styles
) 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
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;"
) do
render Primer::Beta::Octicon.new(icon: :file, mr: 2)
plain "Choose File"
end
input(
type: "file",
name: "file",
id: "file-input",
data: { dropzone_input: true },
style: "display: none;"
)
end
end
render Primer::Beta::BorderBox.new(mt: 5) do |box|
box.with_header do
h3(style: "font-size: 16px; font-weight: 600; margin: 0;") { "How it works" }
end
box.with_body 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 { "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

View file

@ -13,14 +13,11 @@ class UploadsController < ApplicationController
@uploads = @uploads.page(params[:page]).per(50)
end
def new
end
def create
uploaded_file = params[:file]
if uploaded_file.blank?
redirect_to new_upload_path, alert: "Please select a file to upload."
redirect_to uploads_path, alert: "Please select a file to upload."
return
end
@ -37,7 +34,7 @@ class UploadsController < ApplicationController
redirect_to uploads_path, notice: "File uploaded successfully!"
rescue StandardError => e
redirect_to new_upload_path, alert: "Upload failed: #{e.message}"
redirect_to uploads_path, alert: "Upload failed: #{e.message}"
end
def destroy

View file

@ -2,26 +2,31 @@
let dropzone;
let counter = 0;
let fileInput, form;
let initialized = false;
function init() {
const formElement = document.querySelector("[data-dropzone-form]");
if (!formElement) {
fileInput = null;
form = null;
initialized = false;
return;
}
if (initialized && form === formElement) return;
form = formElement;
fileInput = form.querySelector("[data-dropzone-input]");
if (!fileInput) return;
initialized = true;
// Handle file input change
fileInput.addEventListener("change", (e) => {
const file = e.target.files[0];
if (file) {
// Auto-submit on file selection
form.submit();
form.requestSubmit();
}
});
}
@ -61,8 +66,7 @@
const files = e.dataTransfer.files;
if (files.length > 0) {
fileInput.files = files;
// Auto-submit on drop
form.submit();
form.requestSubmit();
}
});
@ -96,10 +100,13 @@
}
}
// Initialize
// Initialize on first load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
// Re-initialize on Turbo navigations
document.addEventListener("turbo:load", init);
})();

View file

@ -4,198 +4,69 @@ icon: code
order: 3
---
# API Documentation 🔧
# API Documentation
Want to upload files programmatically? You've come to the right place! Our API lets you integrate CDN uploads directly into your apps.
Upload images programmatically using the CDN API.
## Authentication
First, you'll need an API key! Head over to [API Keys](/api_keys) to create one.
Create an API key at [API Keys](/api_keys). Keys are shown once, so copy it immediately.
Your API key will look something like this:
Include the key in the `Authorization` header:
```
sk_cdn_a1b2c3d4e5f6...
```
**Important**: Copy it immediately after creation—you won't be able to see it again.
### Using Your API Key
Include your API key in the `Authorization` header with the `Bearer` prefix:
```bash
Authorization: Bearer sk_cdn_your_key_here
```
## Endpoints
## POST /api/v4/upload
### GET /api/v4/me
Get information about the currently authenticated user!
**Response:**
```json
{
"id": "usr_abc123",
"email": "cat@hackclub.com",
"name": "Cool Cat"
}
```
**Examples:**
#### cURL
```bash
curl -H "Authorization: Bearer sk_cdn_your_key_here" \
https://cdn.hackclub.com/api/v4/me
```
#### JavaScript
```javascript
const response = await fetch('https://cdn.hackclub.com/api/v4/me', {
headers: {
'Authorization': 'Bearer sk_cdn_your_key_here'
}
});
const user = await response.json();
console.log(user);
```
#### Ruby
```ruby
require 'faraday'
require 'json'
conn = Faraday.new(url: 'https://cdn.hackclub.com')
response = conn.get('/api/v4/me') do |req|
req.headers['Authorization'] = 'Bearer sk_cdn_your_key_here'
end
user = JSON.parse(response.body)
puts user
```
---
### POST /api/v4/upload
Upload a file directly! This endpoint accepts multipart form data.
**Parameters:**
- `file` (required): The file to upload
**Response:**
```json
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"filename": "cat.png",
"size": 12345,
"content_type": "image/png",
"url": "https://cdn.hackclub.com/01234567-89ab-cdef-0123-456789abcdef/cat.png",
"created_at": "2026-01-29T12:00:00Z"
}
```
**Examples:**
#### cURL
Upload a file via multipart form data.
```bash
curl -X POST \
-H "Authorization: Bearer sk_cdn_your_key_here" \
-F "file=@/path/to/cat.png" \
-F "file=@photo.jpg" \
https://cdn.hackclub.com/api/v4/upload
```
#### JavaScript
```javascript
const formData = new FormData();
formData.append('file', fileInput.files[0]);
const response = await fetch('https://cdn.hackclub.com/api/v4/upload', {
method: 'POST',
headers: {
'Authorization': 'Bearer sk_cdn_your_key_here'
},
headers: { 'Authorization': 'Bearer sk_cdn_your_key_here' },
body: formData
});
const upload = await response.json();
console.log('Uploaded to:', upload.url);
const { url } = await response.json();
```
#### Ruby
```ruby
require 'faraday'
require 'faraday/multipart'
require 'json'
conn = Faraday.new(url: 'https://cdn.hackclub.com') do |f|
f.request :multipart
f.adapter Faraday.default_adapter
end
response = conn.post('/api/v4/upload') do |req|
req.headers['Authorization'] = 'Bearer sk_cdn_your_key_here'
req.body = {
file: Faraday::Multipart::FilePart.new(
'/path/to/cat.png',
'image/png'
)
}
end
upload = JSON.parse(response.body)
puts "Uploaded to: #{upload['url']}"
```
---
### POST /api/v4/upload\_from\_url
Upload a file from a URL. Perfect for grabbing images from the internet.
**Parameters:**
- `url` (required): The URL of the file to upload
**Response:**
```json
{
"id": "01234567-89ab-cdef-0123-456789abcdef",
"filename": "image.jpg",
"size": 54321,
"filename": "photo.jpg",
"size": 12345,
"content_type": "image/jpeg",
"url": "https://cdn.hackclub.com/01234567-89ab-cdef-0123-456789abcdef/image.jpg",
"url": "https://cdn.hackclub.com/01234567-89ab-cdef-0123-456789abcdef/photo.jpg",
"created_at": "2026-01-29T12:00:00Z"
}
```
**Examples:**
## POST /api/v4/upload\_from\_url
#### cURL
Upload an image from a URL.
```bash
curl -X POST \
-H "Authorization: Bearer sk_cdn_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/cat.jpg"}' \
-d '{"url":"https://example.com/image.jpg"}' \
https://cdn.hackclub.com/api/v4/upload_from_url
```
#### JavaScript
```javascript
const response = await fetch('https://cdn.hackclub.com/api/v4/upload_from_url', {
method: 'POST',
@ -203,48 +74,37 @@ const response = await fetch('https://cdn.hackclub.com/api/v4/upload_from_url',
'Authorization': 'Bearer sk_cdn_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://example.com/cat.jpg'
})
body: JSON.stringify({ url: 'https://example.com/image.jpg' })
});
const upload = await response.json();
console.log('Uploaded to:', upload.url);
const { url } = await response.json();
```
#### Ruby
## GET /api/v4/me
```ruby
require 'faraday'
require 'json'
Get the authenticated user.
conn = Faraday.new(url: 'https://cdn.hackclub.com')
response = conn.post('/api/v4/upload_from_url') do |req|
req.headers['Authorization'] = 'Bearer sk_cdn_your_key_here'
req.headers['Content-Type'] = 'application/json'
req.body = { url: 'https://example.com/cat.jpg' }.to_json
end
upload = JSON.parse(response.body)
puts "Uploaded to: #{upload['url']}"
```bash
curl -H "Authorization: Bearer sk_cdn_your_key_here" \
https://cdn.hackclub.com/api/v4/me
```
---
```json
{
"id": "usr_abc123",
"email": "you@hackclub.com",
"name": "Your Name"
}
```
## Error Handling
## Errors
When something goes wrong, you'll get an error response with details.
**Status Codes:**
- `200 OK` - Success!
- `201 Created` - File uploaded successfully
- `400 Bad Request` - Missing required parameters
- `401 Unauthorized` - Invalid or missing API key
- `404 Not Found` - Resource not found
- `422 Unprocessable Entity` - Validation failed
**Error Response Format:**
| Status | Meaning |
|--------|---------|
| 400 | Missing required parameters |
| 401 | Invalid or missing API key |
| 404 | Resource not found |
| 422 | Validation failed |
```json
{
@ -252,26 +112,7 @@ When something goes wrong, you'll get an error response with details.
}
```
Or with validation details:
## Help
```json
{
"error": "Validation failed",
"details": ["Name can't be blank"]
}
```
---
## Rate Limiting
Be nice to our servers. While we don't enforce strict rate limits yet, please use the API responsibly.
## Need Help?
Got questions? Found a bug? Let us know!
- Join the [#cdn channel on Slack](https://hackclub.enterprise.slack.com/archives/C016DEDUL87)
- Open an issue on [GitHub](https://github.com/hackclub/cdn/issues)
Happy uploading!
- [#cdn-dev on Slack](https://hackclub.slack.com/archives/C08RYDPS36V)
- [GitHub Issues](https://github.com/hackclub/cdn/issues)

View file

@ -6,38 +6,39 @@ order: 1
# Getting Started
Welcome to Hack Club CDN. This guide will help you get started with file hosting.
Hack Club CDN is image hosting for your HTML pages. Upload files, get permanent URLs, embed them anywhere.
## Sign In
Click the **Sign in with Hack Club** button on the homepage to authenticate with your Hack Club account.
Click **Sign in with Hack Club** on the homepage to authenticate.
## Uploading Files
## Upload an Image
Once you're logged in:
1. Go to **My Files**
2. Drag and drop images or click **Upload**
3. Copy the URL
1. Navigate to **My Files**
2. Click **Upload** or drag and drop files
3. Your file receives a permanent URL
## Use in HTML
## Sharing Files
Every uploaded file gets a unique URL you can share anywhere:
```
https://cdn.hackclub.com/your-file-id
```html
<img src="https://cdn.hackclub.com/019505e2-c85b-7f80-9c31-4b2e5a8d9f12/photo.jpg" alt="My image">
```
## File Limits
## Use in Markdown
- Maximum file size: varies by account
- Supported formats: images, documents, archives, and more
```markdown
![](https://cdn.hackclub.com/019505e2-c85b-7f80-9c31-4b2e5a8d9f12/photo.jpg)
```
## API Usage
## Hotlinking
For programmatic uploads, check out the [API documentation](/docs/api).
URLs work everywhere—GitHub READMEs, Notion, Discord, Slack, personal websites, etc.
## Programmatic Uploads
Need to upload from code? See the [API documentation](/docs/api).
## Need Help?
- Join the [#cdn-dev channel on Slack](https://hackclub.slack.com/archives/C08RYDPS36V)
- Open an issue on [GitHub](https://github.com/hackclub/cdn/issues)
- [#cdn-dev on Slack](https://hackclub.slack.com/archives/C08RYDPS36V)
- [GitHub Issues](https://github.com/hackclub/cdn/issues)

View file

@ -1 +0,0 @@
<%= render Components::Uploads::New.new %>

View file

@ -13,7 +13,7 @@ Rails.application.routes.draw do
get "/auth/hack_club/callback", to: "sessions#create"
get "/auth/failure", to: "sessions#failure"
resources :uploads, only: [:index, :new, :create, :destroy]
resources :uploads, only: [:index, :create, :destroy]
resources :api_keys, only: [:index, :create, :destroy]