Upload artwork
Secure pipeline
All uploads are scanned, re-encoded, and published through the Skinbase pipeline.
e.preventDefault()}
onDrop={onDrop}
>
Drag & drop your file
JPG, PNG, or WebP. Up to 50MB.
{state.file && (
{state.file.name}
{(state.file.size / 1024 / 1024).toFixed(2)} MB
)}
Choose a type
Step 1: Pick what kind of artwork this is.
Type
{availableTypes.length === 0 ? (
No content types available.
) : (
{availableTypes.map((ct) => {
const active = String(ct.id) === String(state.metadata.type)
const iconKey = getTypeKey(ct)
const iconPath = `/gfx/mascot_${iconKey}.webp`
return (
)
})}
)}
Choose a category
Step 2: Pick a subcategory inside the type.
Category
{!selectedType ? (
Select a type first to see categories.
) : categoryOptions.length === 0 ? (
No categories available for {selectedType.name}.
) : (
{categoryOptions.map((cat) => {
const isSelected = String(cat.id) === String(state.metadata.category)
const isExpanded = String(cat.id) === String(selectedParentCategory)
const hasChildren = Array.isArray(cat.children) && cat.children.length > 0
return (
)
})}
{selectedParentCategory && (() => {
const parent = categoryOptions.find((c) => String(c.id) === String(selectedParentCategory))
if (!parent || !Array.isArray(parent.children) || parent.children.length === 0) return null
return (
Subcategories for {parent.name}
Choose a subcategory below
{parent.children.map((child) => {
const activeChild = String(child.id) === String(state.metadata.category)
return (
)
})}
)
})()}
)}
Tags
{
dispatch({ type: 'SET_METADATA', payload: { tags: nextTags.join(', ') } })
}}
suggestedTags={suggestedTags}
maxTags={15}
minLength={2}
maxLength={32}
searchEndpoint="/api/tags/search"
popularEndpoint="/api/tags/popular"
placeholder="Type tags (e.g. cyberpunk, city)"
/>
{state.cancelledAt && (
Upload cancelled.
)}
{state.error && (
{state.error}
)}