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
This commit is contained in:
141
resources/js/components/upload/StudioStatusBar.jsx
Normal file
141
resources/js/components/upload/StudioStatusBar.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user