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.
Browse files
{state.file && (
{state.file.name}
{(state.file.size / 1024 / 1024).toFixed(2)} MB
)}
Title
dispatch({ type: 'SET_METADATA', payload: { title: e.target.value } })}
className="mt-2 w-full rounded-xl border border-white/10 bg-white/10 px-3 py-2 text-white focus:border-sky-400 focus:outline-none"
placeholder="Name your artwork"
/>
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 (
dispatch({ type: 'SET_METADATA', payload: { type: String(ct.id), category: '' } })}
className={`group rounded-2xl border px-4 py-3 text-left transition ${active
? 'border-emerald-400/60 bg-emerald-500/10 shadow-[0_0_25px_rgba(16,185,129,0.25)]'
: 'border-white/10 bg-white/5 hover:border-sky-400/40 hover:bg-white/10'}`}
aria-pressed={active}
>
{
const letter = document.getElementById(`type-letter-${ct.id}`)
if (letter) letter.style.display = 'none'
}}
onError={(e) => {
e.currentTarget.style.display = 'none'
const letter = document.getElementById(`type-letter-${ct.id}`)
if (letter) letter.style.display = 'grid'
}}
/>
{ct.name?.slice(0, 1)?.toUpperCase() || '?'}
{ct.name}
Select to reveal categories
)
})}
)}
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 (
{
if (hasChildren) {
setSelectedParentCategory(String(cat.id))
dispatch({ type: 'SET_METADATA', payload: { category: '' } })
return
}
setSelectedParentCategory(null)
dispatch({ type: 'SET_METADATA', payload: { category: String(cat.id) } })
}}
className={`rounded-full border px-4 py-2 text-sm transition ${isSelected || isExpanded
? 'border-purple-300/60 bg-purple-400/20 text-purple-100'
: 'border-white/10 bg-white/5 text-white/70 hover:border-purple-300/50 hover:bg-purple-400/10'}`}
aria-pressed={isSelected || isExpanded}
>
{cat.name}
)
})}
{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
setSelectedParentCategory(null)} className="text-sm text-white/60">Back
{parent.children.map((child) => {
const activeChild = String(child.id) === String(state.metadata.category)
return (
{
// Select child but keep parent expanded so the panel remains visible
dispatch({ type: 'SET_METADATA', payload: { category: String(child.id) } })
}}
className={`rounded-full border px-4 py-2 text-sm transition ${activeChild
? 'border-purple-300/60 bg-purple-400/20 text-purple-100'
: 'border-white/10 bg-white/5 text-white/70 hover:border-purple-300/50 hover:bg-purple-400/10'}`}
aria-pressed={activeChild}
>
{child.name}
)
})}
)
})()}
)}
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)"
/>
Description
dispatch({ type: 'SET_METADATA', payload: { isMature: e.target.checked } })}
size={16}
variant="accent"
label="Mark this artwork as mature content."
/>
dispatch({ type: 'SET_METADATA', payload: { licenseAccepted: e.target.checked } })}
size={16}
variant="emerald"
label="I confirm I own the rights to this artwork."
/>
{state.cancelledAt && (
Upload cancelled.
)}
{state.error && (
{state.error}
)}
Start upload
{
if (!confirmCancel) {
setConfirmCancel(true)
return
}
setConfirmCancel(false)
cancelUpload()
}}
className="inline-flex items-center gap-2 rounded-full border border-red-400/40 px-4 py-2 text-sm text-red-100"
disabled={!state.sessionId || state.phase === phases.success}
aria-pressed={confirmCancel}
>
{confirmCancel ? 'Confirm cancel' : 'Cancel'}
{
if (userId) {
clearStoredSession(userId)
}
dispatch({ type: 'RESET' })
}}
className="inline-flex items-center gap-2 rounded-full border border-white/20 px-4 py-2 text-sm text-white/70"
>
Reset