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
142 lines
5.6 KiB
JavaScript
142 lines
5.6 KiB
JavaScript
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>
|
||
)
|
||
}
|