Commit workspace changes

This commit is contained in:
2026-04-05 19:42:33 +02:00
parent 148a3bbe43
commit 08ad757bcb
312 changed files with 35149 additions and 399 deletions

View File

@@ -36,6 +36,61 @@ const wizardSteps = [
{ key: 'publish', label: 'Publish' },
]
function createInitialMetadata(initialGroupSlug = '', currentUserId = null, contributorOptionsByGroup = {}) {
const normalizedGroupSlug = String(initialGroupSlug || '').trim()
const contributors = Array.isArray(contributorOptionsByGroup?.[normalizedGroupSlug])
? contributorOptionsByGroup[normalizedGroupSlug]
: []
const defaultPrimaryAuthor = contributors.some((user) => Number(user.id) === Number(currentUserId))
? Number(currentUserId)
: Number(contributors[0]?.id || 0) || null
return {
title: '',
rootCategoryId: '',
subCategoryId: '',
tags: [],
description: '',
isMature: false,
rightsAccepted: false,
contentType: '',
group: normalizedGroupSlug,
primaryAuthorUserId: defaultPrimaryAuthor,
contributorUserIds: [],
contributorCredits: {},
}
}
function normalizeContributorCredits(contributorIds = [], contributorCredits = {}) {
const normalized = {}
const ids = Array.isArray(contributorIds)
? contributorIds.map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0)
: []
ids.forEach((id) => {
const current = contributorCredits?.[id] || contributorCredits?.[String(id)] || {}
normalized[id] = {
creditRole: typeof current.creditRole === 'string' ? current.creditRole : '',
isPrimary: Boolean(current.isPrimary),
}
})
const leadIds = Object.entries(normalized)
.filter(([, value]) => value.isPrimary)
.map(([id]) => Number(id))
if (leadIds.length > 1) {
leadIds.slice(1).forEach((id) => {
normalized[id] = {
...normalized[id],
isPrimary: false,
}
})
}
return normalized
}
// ─── Helpers ─────────────────────────────────────────────────────────────────
function isValidForUpload(primaryFile, primaryErrors, isArchive, screenshotErrors) {
if (!primaryFile) return false
@@ -44,17 +99,6 @@ function isValidForUpload(primaryFile, primaryErrors, isArchive, screenshotError
return true
}
const initialMetadata = {
title: '',
rootCategoryId: '',
subCategoryId: '',
tags: [],
description: '',
isMature: false,
rightsAccepted: false,
contentType: '',
}
// ─── Component ────────────────────────────────────────────────────────────────
export default function UploadWizard({
onValidationStateChange,
@@ -62,6 +106,10 @@ export default function UploadWizard({
chunkSize,
contentTypes = [],
suggestedTags = [],
groupOptions = [],
contributorOptionsByGroup = {},
initialGroupSlug = '',
currentUserId = null,
}) {
const [notices, setNotices] = useState([])
// ── UI state ──────────────────────────────────────────────────────────────
@@ -88,7 +136,7 @@ export default function UploadWizard({
const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0)
// ── Metadata state ────────────────────────────────────────────────────────
const [metadata, setMetadata] = useState(initialMetadata)
const [metadata, setMetadata] = useState(() => createInitialMetadata(initialGroupSlug, currentUserId, contributorOptionsByGroup))
// ── Refs ──────────────────────────────────────────────────────────────────
const prefersReducedMotion = useReducedMotion()
@@ -189,6 +237,27 @@ export default function UploadWizard({
return categoryTreeByType[selected] || []
}, [categoryTreeByType, metadata.contentType])
const selectedGroupOption = useMemo(() => {
const selectedSlug = String(metadata.group || '')
if (!selectedSlug) return null
return (Array.isArray(groupOptions) ? groupOptions : []).find((group) => String(group.slug || '') === selectedSlug) || null
}, [groupOptions, metadata.group])
const reviewSubmissionMode = Boolean(
selectedGroupOption &&
!selectedGroupOption?.permissions?.can_publish_artworks &&
selectedGroupOption?.permissions?.can_submit_artwork_for_review
)
const publishActionLabel = reviewSubmissionMode
? 'Submit for review'
: (publishMode === 'schedule' ? 'Schedule publish' : 'Publish now')
const currentContributorOptions = useMemo(() => {
const selectedSlug = String(metadata.group || '')
return Array.isArray(contributorOptionsByGroup?.[selectedSlug]) ? contributorOptionsByGroup[selectedSlug] : []
}, [contributorOptionsByGroup, metadata.group])
const allRootCategoryOptions = useMemo(() => {
const items = []
Object.entries(categoryTreeByType).forEach(([contentTypeValue, roots]) => {
@@ -213,6 +282,45 @@ export default function UploadWizard({
selectedRootCategory.children.length > 0
)
useEffect(() => {
const selectedSlug = String(metadata.group || '')
if (!selectedSlug) {
if (metadata.primaryAuthorUserId || metadata.contributorUserIds.length > 0 || Object.keys(metadata.contributorCredits || {}).length > 0) {
setMetadata((current) => ({ ...current, primaryAuthorUserId: null, contributorUserIds: [], contributorCredits: {} }))
}
return
}
const validGroup = (Array.isArray(groupOptions) ? groupOptions : []).some((group) => String(group.slug || '') === selectedSlug)
if (!validGroup) {
setMetadata((current) => ({ ...current, group: '', primaryAuthorUserId: null, contributorUserIds: [], contributorCredits: {} }))
return
}
const validContributorIds = currentContributorOptions.map((user) => Number(user.id)).filter((id) => Number.isFinite(id) && id > 0)
const nextPrimaryAuthorId = validContributorIds.includes(Number(metadata.primaryAuthorUserId))
? Number(metadata.primaryAuthorUserId)
: (validContributorIds.includes(Number(currentUserId)) ? Number(currentUserId) : (validContributorIds[0] || null))
const nextContributorIds = (Array.isArray(metadata.contributorUserIds) ? metadata.contributorUserIds : [])
.map((id) => Number(id))
.filter((id) => validContributorIds.includes(id) && id !== nextPrimaryAuthorId)
const nextContributorCredits = normalizeContributorCredits(nextContributorIds, metadata.contributorCredits)
const currentPrimary = metadata.primaryAuthorUserId ? Number(metadata.primaryAuthorUserId) : null
const currentContributors = (Array.isArray(metadata.contributorUserIds) ? metadata.contributorUserIds : []).map((id) => Number(id))
const contributorsChanged = nextContributorIds.length !== currentContributors.length || nextContributorIds.some((id, index) => id !== currentContributors[index])
const contributorCreditsChanged = JSON.stringify(nextContributorCredits) !== JSON.stringify(normalizeContributorCredits(currentContributors, metadata.contributorCredits))
if (currentPrimary !== nextPrimaryAuthorId || contributorsChanged || contributorCreditsChanged) {
setMetadata((current) => ({
...current,
primaryAuthorUserId: nextPrimaryAuthorId,
contributorUserIds: nextContributorIds,
contributorCredits: nextContributorCredits,
}))
}
}, [groupOptions, currentContributorOptions, currentUserId, metadata.group, metadata.primaryAuthorUserId, metadata.contributorUserIds, metadata.contributorCredits])
// ── Metadata validation ───────────────────────────────────────────────────
const metadataErrors = useMemo(() => {
const errors = {}
@@ -274,9 +382,10 @@ export default function UploadWizard({
const canScheduleSubmit = useMemo(() => {
if (!canPublish) return false
if (reviewSubmissionMode) return true
if (publishMode === 'schedule') return Boolean(scheduledAt)
return true
}, [canPublish, publishMode, scheduledAt])
}, [canPublish, reviewSubmissionMode, publishMode, scheduledAt])
// ── Validation surface for parent ────────────────────────────────────────
const validationErrors = useMemo(
@@ -338,7 +447,7 @@ export default function UploadWizard({
setPrimaryFile(null)
setScreenshots([])
setSelectedScreenshotIndex(0)
setMetadata(initialMetadata)
setMetadata(createInitialMetadata(initialGroupSlug, currentUserId, contributorOptionsByGroup))
setIsUploadLocked(false)
hasAutoAdvancedRef.current = false
setPublishMode('now')
@@ -350,7 +459,7 @@ export default function UploadWizard({
return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : null
})
setActiveStep(1)
}, [resetMachine, initialDraftId])
}, [resetMachine, initialDraftId, initialGroupSlug, currentUserId, contributorOptionsByGroup])
const goToStep = useCallback((step) => {
if (step >= 1 && step <= highestUnlockedStep) setActiveStep(step)
@@ -454,6 +563,60 @@ export default function UploadWizard({
onContentTypeChange={(value) => setMeta({ contentType: value, rootCategoryId: '', subCategoryId: '' })}
onRootCategoryChange={(rootId) => setMeta({ rootCategoryId: rootId, subCategoryId: '' })}
onSubCategoryChange={(subId) => setMeta({ subCategoryId: subId })}
groupOptions={groupOptions}
currentContributorOptions={currentContributorOptions}
onGroupChange={(groupSlug) => setMeta({ group: groupSlug })}
onPrimaryAuthorChange={(authorId) => setMeta({ primaryAuthorUserId: authorId ? Number(authorId) : null })}
onContributorToggle={(contributorId) => setMetadata((current) => {
const normalizedId = Number(contributorId)
const nextIds = new Set((Array.isArray(current.contributorUserIds) ? current.contributorUserIds : []).map((id) => Number(id)).filter((id) => id !== Number(current.primaryAuthorUserId)))
if (nextIds.has(normalizedId)) {
nextIds.delete(normalizedId)
} else {
nextIds.add(normalizedId)
}
const contributorUserIds = Array.from(nextIds).filter((id) => id !== Number(current.primaryAuthorUserId))
return {
...current,
contributorUserIds,
contributorCredits: normalizeContributorCredits(contributorUserIds, current.contributorCredits),
}
})}
onContributorRoleChange={(contributorId, creditRole) => setMetadata((current) => {
const contributorUserIds = (Array.isArray(current.contributorUserIds) ? current.contributorUserIds : []).map((id) => Number(id))
if (!contributorUserIds.includes(Number(contributorId))) return current
const contributorCredits = normalizeContributorCredits(contributorUserIds, current.contributorCredits)
return {
...current,
contributorCredits: {
...contributorCredits,
[Number(contributorId)]: {
...(contributorCredits[Number(contributorId)] || { isPrimary: false }),
creditRole,
},
},
}
})}
onContributorPrimaryChange={(contributorId) => setMetadata((current) => {
const contributorUserIds = (Array.isArray(current.contributorUserIds) ? current.contributorUserIds : []).map((id) => Number(id))
const contributorCredits = normalizeContributorCredits(contributorUserIds, current.contributorCredits)
contributorUserIds.forEach((id) => {
contributorCredits[id] = {
...(contributorCredits[id] || { creditRole: '' }),
isPrimary: id === Number(contributorId),
}
})
return {
...current,
contributorCredits,
}
})}
suggestedTags={mergedSuggestedTags}
publishMode={publishMode}
scheduledAt={scheduledAt}
@@ -486,7 +649,11 @@ export default function UploadWizard({
scheduledAt={scheduledAt}
timezone={userTimezone}
visibility={visibility}
actionLabel={publishActionLabel}
showScheduleSummary={!reviewSubmissionMode}
onVisibilityChange={setVisibility}
selectedGroup={selectedGroupOption}
currentContributorOptions={currentContributorOptions}
allRootCategoryOptions={allRootCategoryOptions}
filteredCategoryTree={filteredCategoryTree}
/>
@@ -608,14 +775,15 @@ export default function UploadWizard({
disableReason={disableReason}
onStart={runUploadFlow}
onContinue={() => detailsValid && setActiveStep(3)}
onPublish={() => handlePublish(canPublish, { mode: publishMode, publishAt: scheduledAt, timezone: userTimezone, visibility })}
onPublish={() => handlePublish(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })}
onBack={() => setActiveStep((s) => Math.max(1, s - 1))}
onCancel={handleCancel}
onReset={handleReset}
onRetry={() => handleRetry(canPublish, { mode: publishMode, publishAt: scheduledAt, timezone: userTimezone, visibility })}
onRetry={() => handleRetry(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })}
onSaveDraft={() => {}}
showSaveDraft={activeStep === 2}
resetLabel={isUploadLocked ? 'Reset upload' : 'Reset'}
publishLabel={publishActionLabel}
mobileSticky
/>
</div>
@@ -641,13 +809,15 @@ export default function UploadWizard({
scheduledAt={scheduledAt}
timezone={userTimezone}
visibility={visibility}
actionLabel={publishActionLabel}
showScheduleControls={!reviewSubmissionMode}
showRightsConfirmation={activeStep === 3}
showVisibility={false}
onPublishModeChange={setPublishMode}
onScheduleAt={setScheduledAt}
onVisibilityChange={setVisibility}
onToggleRights={(checked) => setMeta({ rightsAccepted: Boolean(checked) })}
onPublish={() => handlePublish(canPublish, { mode: publishMode, publishAt: scheduledAt, timezone: userTimezone, visibility })}
onPublish={() => handlePublish(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })}
onCancel={handleCancel}
onGoToStep={goToStep}
allRootCategoryOptions={allRootCategoryOptions}
@@ -668,7 +838,7 @@ export default function UploadWizard({
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
Publish
{reviewSubmissionMode ? 'Review' : 'Publish'}
{!canPublish && (
<span className="ml-1 rounded-full bg-white/20 px-1.5 text-[10px]">
{[
@@ -725,6 +895,8 @@ export default function UploadWizard({
scheduledAt={scheduledAt}
timezone={userTimezone}
visibility={visibility}
actionLabel={publishActionLabel}
showScheduleControls={!reviewSubmissionMode}
showRightsConfirmation={activeStep === 3}
showVisibility={false}
onPublishModeChange={setPublishMode}
@@ -733,7 +905,7 @@ export default function UploadWizard({
onToggleRights={(checked) => setMeta({ rightsAccepted: Boolean(checked) })}
onPublish={() => {
setShowMobilePublishPanel(false)
handlePublish(canPublish, { mode: publishMode, publishAt: scheduledAt, timezone: userTimezone, visibility })
handlePublish(canPublish, { mode: reviewSubmissionMode ? 'now' : publishMode, publishAt: reviewSubmissionMode ? null : scheduledAt, timezone: userTimezone, visibility, action: reviewSubmissionMode ? 'submit_review' : 'publish' })
}}
onCancel={() => {
setShowMobilePublishPanel(false)