Files
SkinbaseNova/resources/js/components/upload/UploadStepper.jsx
Gregor Klevze 1266f81d35 feat: upload wizard refactor + vision AI tags + artwork versioning
Upload wizard:
- Refactored UploadWizard into modular steps (Step1FileUpload, Step2Details, Step3Publish)
- Extracted reusable hooks: useUploadMachine, useFileValidation, useVisionTags
- Extracted reusable components: CategorySelector, ContentTypeSelector
- Added TagPicker component (studio-style list picker with AI badge + new-tag insertion)
- Fixed TagInput auto-open bug (hasFocusedRef guard)
- Replaced TagInput with TagPicker in UploadSidebar

Vision AI tag suggestions:
- Add UploadVisionSuggestController: sync POST /api/uploads/{id}/vision-suggest
- Calls vision.klevze.net/analyze/all on upload completion (before step 2 opens)
- Two-phase useVisionTags: immediate gateway call + background DB polling
- Trigger fires on uploadReady (not step change) so tags arrive before user sees step 2
- Added vision.gateway config block with VISION_GATEWAY_URL env

Artwork versioning system:
- ArtworkVersion / ArtworkVersionEvent models
- ArtworkVersioningService: createNewVersion, restoreVersion, rate limiting, ranking decay
- Migrations: artwork_versions, artwork_version_events, versioning columns on artworks
- Studio API routes: GET versions, POST restore/{version_id}
- Feature tests: ArtworkVersioningTest (13 cases)
2026-03-01 14:56:46 +01:00

54 lines
2.4 KiB
JavaScript

import React from 'react'
export default function UploadStepper({ steps = [], activeStep = 1, highestUnlockedStep = 1, onStepClick }) {
const safeActive = Math.max(1, Math.min(steps.length || 1, activeStep))
return (
<nav aria-label="Upload steps" className="rounded-xl ring-1 ring-white/10 bg-slate-900/70 px-3 py-3 sm:px-4">
<ol className="flex flex-nowrap items-center gap-3 overflow-x-auto sm:gap-4">
{steps.map((step, index) => {
const number = index + 1
const isActive = number === safeActive
const isComplete = number < safeActive
const isLocked = number > highestUnlockedStep
const canNavigate = number < safeActive && !isLocked
const baseBtn = 'inline-flex items-center gap-2 rounded-full border px-2.5 py-1.5 text-xs sm:px-3'
const stateClass = isActive
? 'border-sky-300/80 bg-sky-500/30 text-white shadow-[0_8px_24px_rgba(14,165,233,0.12)]'
: isComplete
? 'border-emerald-300/30 bg-emerald-500/15 text-emerald-100 hover:bg-emerald-500/25'
: isLocked
? 'cursor-default border-white/10 bg-white/5 text-white/40'
: 'border-white/10 bg-white/5 text-white/80 hover:bg-white/10'
const circleClass = isComplete
? 'border-emerald-300/60 bg-emerald-500/20 text-emerald-100'
: isActive
? 'border-sky-300/60 bg-sky-500/30 text-white'
: 'border-white/20 bg-white/5 text-white/80'
return (
<li key={step.key} className="flex-shrink-0 flex items-center gap-3">
<button
type="button"
onClick={() => canNavigate && onStepClick?.(number)}
disabled={isLocked}
aria-disabled={isLocked ? 'true' : 'false'}
aria-current={isActive ? 'step' : undefined}
className={`${baseBtn} ${stateClass} flex-shrink-0`}
>
<span className={`grid h-5 w-5 place-items-center rounded-full border text-[11px] ${circleClass}`}>
{isComplete ? '✓' : number}
</span>
<span className="whitespace-nowrap pr-3">{step.label}</span>
</button>
{index < steps.length - 1 && <span className="text-white/50 mx-1 select-none"></span>}
</li>
)
})}
</ol>
</nav>
)
}