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:
2026-03-03 09:48:31 +01:00
parent 1266f81d35
commit dc51d65440
178 changed files with 14308 additions and 665 deletions

View File

@@ -1,9 +1,6 @@
import React from 'react'
import UploadDropzone from '../UploadDropzone'
import ScreenshotUploader from '../ScreenshotUploader'
import UploadProgress from '../UploadProgress'
import { machineStates } from '../../../hooks/upload/useUploadMachine'
import { getProcessingTransparencyLabel } from '../../../lib/uploadUtils'
/**
* Step1FileUpload
@@ -28,24 +25,9 @@ export default function Step1FileUpload({
screenshotErrors,
screenshotPerFileErrors,
onScreenshotsChange,
// Machine state
// Machine state (passed for potential future use)
machine,
showProgress,
onRetry,
onReset,
}) {
const processingTransparencyLabel = getProcessingTransparencyLabel(
machine.processingStatus,
machine.state
)
const progressStatus = (() => {
if (machine.state === machineStates.ready_to_publish) return 'Ready'
if (machine.state === machineStates.uploading) return 'Uploading'
if (machine.state === machineStates.processing || machine.state === machineStates.finishing) return 'Processing'
return 'Idle'
})()
return (
<div className="bg-panel/80 backdrop-blur rounded-2xl shadow-xl shadow-black/40 ring-1 ring-white/10 p-6 space-y-5">
{/* Step header */}
@@ -107,23 +89,6 @@ export default function Step1FileUpload({
looksGoodText="Looks good"
onFilesChange={onScreenshotsChange}
/>
{/* Progress panel */}
{showProgress && (
<UploadProgress
title="Upload progress"
description="Upload and processing status"
status={progressStatus}
progress={machine.progress}
state={machine.state}
processingStatus={machine.processingStatus}
isCancelling={machine.isCancelling}
error={machine.error}
processingLabel={processingTransparencyLabel}
onRetry={onRetry}
onReset={onReset}
/>
)}
</div>
)
}

View File

@@ -24,7 +24,11 @@ function PublishCheckBadge({ label, ok }) {
* Step3Publish
*
* Step 3 of the upload wizard: review summary and publish action.
* Shows a compact artwork preview, metadata summary, and readiness badges.
* Shows a compact artwork preview, metadata summary, readiness badges,
* and a summary of publish mode / schedule + visibility.
*
* Publish controls (mode/schedule picker) live in PublishPanel (sidebar).
* This step serves as the final review before the user clicks Publish.
*/
export default function Step3Publish({
headingRef,
@@ -39,17 +43,36 @@ export default function Step3Publish({
// Readiness
canPublish,
uploadReady,
// Publish options (from wizard state, for summary display only)
publishMode = 'now',
scheduledAt = null,
timezone = null,
visibility = 'public',
// Category tree (for label lookup)
allRootCategoryOptions = [],
filteredCategoryTree = [],
}) {
const prefersReducedMotion = useReducedMotion()
const quickTransition = prefersReducedMotion ? { duration: 0 } : { duration: 0.2, ease: 'easeOut' }
const hasPreview = Boolean(primaryPreviewUrl && !isArchive)
// ── Category label lookup ────────────────────────────────────────────────
const rootCategory = allRootCategoryOptions.find(
(r) => String(r.id) === String(metadata.rootCategoryId)
) ?? null
const rootLabel = rootCategory?.name ?? null
const subCategory = rootCategory?.children?.find(
(c) => String(c.id) === String(metadata.subCategoryId)
) ?? null
const subLabel = subCategory?.name ?? null
const checks = [
{ label: 'File uploaded', ok: uploadReady },
{ label: 'Scan passed', ok: uploadReady },
{ label: 'Preview ready', ok: hasPreview || (isArchive && screenshots.length > 0) },
{ label: 'Rights confirmed', ok: Boolean(metadata.rightsAccepted) },
{ label: 'Tags added', ok: Array.isArray(metadata.tags) && metadata.tags.length > 0 },
]
return (
@@ -104,11 +127,11 @@ export default function Step3Publish({
{metadata.contentType && (
<span className="capitalize">Type: <span className="text-white/75">{metadata.contentType}</span></span>
)}
{metadata.rootCategoryId && (
<span>Category: <span className="text-white/75">{metadata.rootCategoryId}</span></span>
{rootLabel && (
<span>Category: <span className="text-white/75">{rootLabel}</span></span>
)}
{metadata.subCategoryId && (
<span>Sub: <span className="text-white/75">{metadata.subCategoryId}</span></span>
{subLabel && (
<span>Sub: <span className="text-white/75">{subLabel}</span></span>
)}
</div>
@@ -129,6 +152,32 @@ export default function Step3Publish({
</div>
</div>
{/* Publish summary: visibility + schedule */}
<div className="flex flex-wrap gap-3">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/15 bg-white/6 px-2.5 py-1 text-xs text-white/60">
👁 {visibility === 'public' ? 'Public' : visibility === 'unlisted' ? 'Unlisted' : 'Private'}
</span>
{publishMode === 'schedule' && scheduledAt ? (
<span className="inline-flex items-center gap-1.5 rounded-full border border-violet-300/30 bg-violet-500/15 px-2.5 py-1 text-xs text-violet-200">
🕐 Scheduled
{timezone && (
<span className="text-violet-300/70">
{' '}·{' '}
{new Intl.DateTimeFormat('en-GB', {
timeZone: timezone,
weekday: 'short', day: 'numeric', month: 'short',
hour: '2-digit', minute: '2-digit', hour12: false,
}).format(new Date(scheduledAt))}
</span>
)}
</span>
) : (
<span className="inline-flex items-center gap-1.5 rounded-full border border-emerald-300/30 bg-emerald-500/12 px-2.5 py-1 text-xs text-emerald-200">
Publish immediately
</span>
)}
</div>
{/* Readiness badges */}
<div>
<p className="mb-2.5 text-xs uppercase tracking-wide text-white/40">Readiness checks</p>