Files
SkinbaseNova/resources/js/components/upload/StudioStatusBar.jsx
Gregor Klevze dc51d65440 feat: forum rich-text editor, emoji picker, mentions, discover nav, feed, uploads, profile
Forum:
- TipTap WYSIWYG editor with full toolbar
- @emoji-mart/react emoji picker (consistent with tweets)
- @mention autocomplete with user search API
- Fix PHP 8.4 parse errors in Blade templates
- Fix thread data display (paginator items)
- Align forum page widths to max-w-5xl

Discover:
- Extract shared _nav.blade.php partial
- Add missing nav links to for-you page
- Add Following link for authenticated users

Feed/Posts:
- Post model, controllers, policies, migrations
- Feed page components (PostComposer, FeedCard, etc)
- Post reactions, comments, saves, reports, sharing
- Scheduled publishing support
- Link preview controller

Profile:
- Profile page components (ProfileHero, ProfileTabs)
- Profile API controller

Uploads:
- Upload wizard enhancements
- Scheduled publish picker
- Studio status bar and readiness checklist
2026-03-03 09:48:31 +01:00

142 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react'
import { motion, useReducedMotion } from 'framer-motion'
/**
* StudioStatusBar
*
* Sticky header beneath the main nav that shows:
* - Step pills (reuse UploadStepper visual style but condensed)
* - Upload progress bar (visible while uploading/processing)
* - Machine-state pill
* - Back / Next primary actions
*/
const STATE_LABELS = {
idle: null,
initializing: 'Initializing…',
uploading: 'Uploading',
finishing: 'Finishing…',
processing: 'Processing',
ready_to_publish: 'Ready',
publishing: 'Publishing…',
complete: 'Published',
error: 'Error',
cancelled: 'Cancelled',
}
const STATE_COLORS = {
idle: '',
initializing: 'bg-sky-500/20 text-sky-200 border-sky-300/30',
uploading: 'bg-sky-500/25 text-sky-100 border-sky-300/40',
finishing: 'bg-sky-400/20 text-sky-200 border-sky-300/30',
processing: 'bg-amber-500/20 text-amber-100 border-amber-300/30',
ready_to_publish: 'bg-emerald-500/20 text-emerald-100 border-emerald-300/35',
publishing: 'bg-sky-500/25 text-sky-100 border-sky-300/40',
complete: 'bg-emerald-500/25 text-emerald-100 border-emerald-300/50',
error: 'bg-red-500/20 text-red-200 border-red-300/30',
cancelled: 'bg-white/8 text-white/50 border-white/15',
}
export default function StudioStatusBar({
steps = [],
activeStep = 1,
highestUnlockedStep = 1,
machineState = 'idle',
progress = 0,
showProgress = false,
onStepClick,
}) {
const prefersReducedMotion = useReducedMotion()
const transition = prefersReducedMotion ? { duration: 0 } : { duration: 0.3, ease: 'easeOut' }
const stateLabel = STATE_LABELS[machineState] ?? machineState
const stateColor = STATE_COLORS[machineState] ?? 'bg-white/8 text-white/50 border-white/15'
return (
<div className="sticky top-0 z-20 -mx-4 px-4 pt-2 pb-0 sm:-mx-6 sm:px-6">
{/* Blur backdrop */}
<div className="absolute inset-0 bg-slate-950/80 backdrop-blur-md" aria-hidden="true" />
<div className="relative">
{/* Step pills row */}
<nav aria-label="Upload steps">
<ol className="flex flex-nowrap items-center gap-2 overflow-x-auto py-3 pr-1 sm:gap-3">
{steps.map((step, index) => {
const number = index + 1
const isActive = number === activeStep
const isComplete = number < activeStep
const isLocked = number > highestUnlockedStep
const canNavigate = !isLocked && number < activeStep
const btnClass = [
'inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[11px] sm:text-xs transition',
isActive
? 'border-sky-300/70 bg-sky-500/25 text-white'
: isComplete
? 'border-emerald-300/30 bg-emerald-500/15 text-emerald-100 hover:bg-emerald-500/25 cursor-pointer'
: isLocked
? 'cursor-default border-white/10 bg-white/5 text-white/35 pointer-events-none'
: 'border-white/15 bg-white/6 text-white/70 hover:bg-white/12 cursor-pointer',
].join(' ')
const circleClass = isComplete
? 'border-emerald-300/50 bg-emerald-500/20 text-emerald-100'
: isActive
? 'border-sky-300/50 bg-sky-500/25 text-white'
: 'border-white/20 bg-white/6 text-white/60'
return (
<li key={step.key} className="flex shrink-0 items-center gap-2">
<button
type="button"
onClick={() => canNavigate && onStepClick?.(number)}
disabled={isLocked}
aria-disabled={isLocked}
aria-current={isActive ? 'step' : undefined}
className={btnClass}
>
<span className={`grid h-4 w-4 place-items-center rounded-full border text-[10px] shrink-0 ${circleClass}`}>
{isComplete ? '✓' : number}
</span>
<span className="whitespace-nowrap">{step.label}</span>
</button>
{index < steps.length - 1 && (
<span className="text-white/30 select-none text-xs" aria-hidden="true"></span>
)}
</li>
)
})}
{/* Spacer */}
<li className="flex-1" aria-hidden="true" />
{/* State pill */}
{stateLabel && (
<li className="shrink-0">
<span className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] ${stateColor}`}>
{['uploading', 'initializing', 'finishing', 'processing', 'publishing'].includes(machineState) && (
<span className="relative flex h-2 w-2 shrink-0" aria-hidden="true">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-300 opacity-60" />
<span className="relative inline-flex h-2 w-2 rounded-full bg-sky-300" />
</span>
)}
{stateLabel}
</span>
</li>
)}
</ol>
</nav>
{/* Progress bar (shown during upload/processing) */}
{showProgress && (
<div className="h-0.5 w-full overflow-hidden rounded-full bg-white/8">
<motion.div
className="h-full rounded-full bg-gradient-to-r from-sky-400 via-cyan-300 to-emerald-300"
animate={{ width: `${progress}%` }}
transition={transition}
/>
</div>
)}
</div>
</div>
)
}