Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | 1x 11x 11x 11x 11x 1x 10x 11x 11x 11x 11x 11x 11x 11x | /**
* EventPublishingTab Component (Story 5.7 - Updated)
*
* Progressive publishing controls with validation, timeline, and version control
* Now uses real data from usePublishing hook instead of mocked data
*/
import React from 'react';
import { Stack, Skeleton, Box, Typography, Paper } from '@mui/material';
import { useTranslation } from 'react-i18next';
import type { Event, EventDetailUI, PublishingPhase } from '@/types/event.types';
import { ValidationDashboard } from '@/components/Publishing/ValidationDashboard/ValidationDashboard';
import { PublishingControls } from '@/components/Publishing/PublishingControls/PublishingControls';
import { LivePreview } from '@/components/Publishing/LivePreview/LivePreview';
import { PublishingTimeline } from '@/components/Publishing/PublishingTimeline/PublishingTimeline';
import { usePublishing } from '@/hooks/usePublishing/usePublishing';
import { useSlotAssignment } from '@/hooks/useSlotAssignment/useSlotAssignment';
interface EventPublishingTabProps {
event: Event | EventDetailUI;
eventCode: string;
}
export const EventPublishingTab: React.FC<EventPublishingTabProps> = ({ event, eventCode }) => {
const { t } = useTranslation('events');
const { publishingStatus, isLoadingStatus, validationErrors } = usePublishing(eventCode);
const { unassignedSessions } = useSlotAssignment(eventCode);
// Loading state
if (isLoadingStatus) {
return (
<Stack spacing={3}>
<Skeleton variant="rectangular" height={200} />
<Skeleton variant="rectangular" height={150} />
<Skeleton variant="rectangular" height={100} />
<Skeleton variant="rectangular" height={300} />
</Stack>
);
}
// Extract data from status response, with fallbacks
// Convert uppercase phase from API to lowercase (API returns 'SPEAKERS', we need 'speakers')
const currentPhase: PublishingPhase =
(publishingStatus?.currentPhase?.toLowerCase() as PublishingPhase) || 'topic';
const publishedPhases: PublishingPhase[] =
publishingStatus?.publishedPhases?.map((p) => p.toLowerCase() as PublishingPhase) || [];
const eventDate = event.date || new Date().toISOString();
// Build validation data from status response and slot assignment
const mappedUnassignedSessions =
unassignedSessions?.map((session) => ({
sessionSlug: session.sessionSlug,
title: session.title || session.sessionSlug,
})) || [];
// Frontend safeguard: Check if event actually has a topic
const hasTopicCode = 'topicCode' in event && event.topicCode;
const topicValidation = publishingStatus?.topic || { isValid: true, errors: [] };
const validationData = {
topic: {
...topicValidation,
// Override if backend incorrectly reports valid but no topicCode exists
isValid: !!(topicValidation.isValid && hasTopicCode),
errors:
!hasTopicCode && topicValidation.isValid
? ['Event topic must be defined']
: topicValidation.errors,
},
speakers: publishingStatus?.speakers || { isValid: true, errors: [] },
sessions: {
...(publishingStatus?.sessions || {
isValid: true,
errors: [],
assignedCount: 0,
totalCount: 0,
}),
// Override isValid: only valid if no unassigned sessions AND there are sessions total
isValid:
mappedUnassignedSessions.length === 0 && (publishingStatus?.sessions?.totalCount || 0) > 0,
// Use actual unassigned sessions from slot assignment hook
unassignedSessions: mappedUnassignedSessions,
},
};
return (
<Stack spacing={3}>
{/* Top row: Validation · Publishing phases · Publish actions. On desktop a
40/40/20 three-column grid (publish buttons stay beside the phases); on
mobile the columns stack full-width (Phase G — three columns don't fit a phone). */}
<Box
sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
md: 'minmax(0, 2fr) minmax(0, 2fr) minmax(0, 1fr)',
},
gap: 3,
alignItems: 'stretch',
}}
>
{/* Validation Dashboard - Shows content validation status */}
<Box
data-testid="validation-dashboard-container"
sx={{ display: 'flex', flexDirection: 'column' }}
>
<Typography variant="subtitle2" gutterBottom>
{t('publishing.sections.validation', 'Validation')}
</Typography>
<ValidationDashboard
eventCode={eventCode}
phase={currentPhase}
validation={validationData}
/>
</Box>
{/* Publishing Timeline - Visual timeline of phases */}
<Box
data-testid="publishing-timeline-container"
sx={{ display: 'flex', flexDirection: 'column' }}
>
<Typography variant="subtitle2" gutterBottom>
{t('publishing.sections.phases', 'Publishing phase')}
</Typography>
<Paper sx={{ p: 2, flexGrow: 1 }}>
<PublishingTimeline
eventCode={eventCode}
currentPhase={currentPhase}
publishedPhases={publishedPhases}
eventDate={eventDate}
/>
</Paper>
</Box>
{/* Publishing Controls - phase publish buttons, stacked vertically */}
<Box
data-testid="publishing-controls-container"
sx={{ display: 'flex', flexDirection: 'column' }}
>
<Typography variant="subtitle2" gutterBottom>
{t('publishing.sections.publishNext', 'Publish next phase')}
</Typography>
<Paper sx={{ p: 2, flexGrow: 1 }}>
<PublishingControls
eventCode={eventCode}
currentPhase={currentPhase}
validationErrors={validationErrors}
/>
</Paper>
</Box>
</Box>
{/* Live Preview - full width below the three columns */}
<Box data-testid="live-preview-container">
<LivePreview eventCode={eventCode} phase={currentPhase} />
</Box>
</Stack>
);
};
export default EventPublishingTab;
|