diff --git a/webapp/src/features/event/messaging/MessagePasteCard.tsx b/webapp/src/components/MessagePasteCard.tsx similarity index 100% rename from webapp/src/features/event/messaging/MessagePasteCard.tsx rename to webapp/src/components/MessagePasteCard.tsx diff --git a/webapp/src/components/currentOrNextEvent/CurrentOrNextEvent.tsx b/webapp/src/components/currentOrNextEvent/CurrentOrNextEvent.tsx index b057e98e..56124142 100644 --- a/webapp/src/components/currentOrNextEvent/CurrentOrNextEvent.tsx +++ b/webapp/src/components/currentOrNextEvent/CurrentOrNextEvent.tsx @@ -78,7 +78,7 @@ export const CurrentOrNextEvent = () => { Event Setup Lineup - Public Announcements + Preflight Checklist diff --git a/webapp/src/features/event/EventAnnouncements.tsx b/webapp/src/features/event/EventAnnouncements.tsx deleted file mode 100644 index 6b1fcb23..00000000 --- a/webapp/src/features/event/EventAnnouncements.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Tabs, Tab, Form } from "react-bootstrap"; -import MessagePasteCard from "./messaging/MessagePasteCard"; -import { getDiscordMessage, getTwitterMessage } from "../../util/messageWriters"; -import { useEventOperations } from "./outletContext"; - -const EventAnnouncements = () => { - - const [eventScratchpad, proposeEventChange] = useEventOperations(); - - const discordMessage = getDiscordMessage(eventScratchpad); - const twitterMessage = getTwitterMessage(eventScratchpad); - - const discordFooterInstructions = <> -

Paste your announcement text to - #s4-vrchat-events - in Discord. -

-

Be sure to press 📢 Publish after posting, so other servers that subscribe to the channel get notified!

- - const twitterFooterInstructions =

Paste this text to Twitter and Bluesky.

- - return
-

Public Announcements

-
- - - Discord Announcement Message - - { proposeEventChange({ ...eventScratchpad, message: event.target.value }); }} - /> - -
- - - - - - - - -
-}; - -export default EventAnnouncements; diff --git a/webapp/src/features/event/EventRoot.tsx b/webapp/src/features/event/EventRoot.tsx index 50f0746a..a76f0cf8 100644 --- a/webapp/src/features/event/EventRoot.tsx +++ b/webapp/src/features/event/EventRoot.tsx @@ -205,12 +205,6 @@ const EventRoot = () => { Lineup - - {/* Verify DJs removed — verification is available via Verify Lineup modal */} - - - Messaging - Preflight diff --git a/webapp/src/features/event/PreflightChecklist.tsx b/webapp/src/features/event/PreflightChecklist.tsx deleted file mode 100644 index ab3956d7..00000000 --- a/webapp/src/features/event/PreflightChecklist.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import { Container, Card, ButtonGroup, Button, Alert, Stack, Badge } from 'react-bootstrap'; -import { useEventOperations } from './outletContext'; -import { useEventStore } from '../../hooks/useEventStore/useEventStore'; -import toast from 'react-hot-toast'; -import { EventPublishedStatusBadge } from './EventPublishedStatusBadge'; -import { Link } from 'react-router-dom'; - -// Add types for checklist items -type RequiredChecklistItem = { - label: string; - completed: boolean; - description: string; - link: string; - optional?: false; -}; - -type OptionalChecklistItem = { - label: string; - completed: boolean; - description: string; - link: string; - buttonLabel: string; - external: true; - optional: true; -}; - -type ChecklistItem = RequiredChecklistItem | OptionalChecklistItem; - -const PreflightChecklist = () => { - const [eventScratchpad, proposeEventChange] = useEventOperations(); - const { saveEvent } = useEventStore(); - - const handlePublish = async () => { - try { - const newEvent = { ...eventScratchpad, published: true }; - await saveEvent(newEvent, eventScratchpad); - proposeEventChange(newEvent); - toast.success('Event published successfully!'); - } catch (error) { - toast.error(`Error publishing event: ${(error as Error).message}`); - } - }; - - const handleUnpublish = async () => { - try { - const newEvent = { ...eventScratchpad, published: false }; - await saveEvent(newEvent, eventScratchpad); - proposeEventChange(newEvent); - toast.success('Event unpublished successfully!'); - } catch (error) { - toast.error(`Error unpublishing event: ${(error as Error).message}`); - } - }; - - const hasMinimumRequirements = () => { - return eventScratchpad.name && - eventScratchpad.host && - eventScratchpad.start_datetime && - eventScratchpad.slots.length > 0; - }; - - const getChecklistItems = (): ChecklistItem[] => { - const required: RequiredChecklistItem[] = [ - { - label: 'Event Name', - completed: !!eventScratchpad.name, - description: 'Event has a name assigned', - link: `/events/${eventScratchpad.id}/setup`, - optional: false, - }, - { - label: 'Host Assigned', - completed: !!eventScratchpad.host, - description: 'Event has a host assigned', - link: `/events/${eventScratchpad.id}/setup`, - optional: false, - }, - { - label: 'Date & Time Set', - completed: !!eventScratchpad.start_datetime, - description: 'Event has a start date and time', - link: `/events/${eventScratchpad.id}/setup`, - optional: false, - }, - { - label: 'Lineup Created', - completed: eventScratchpad.slots.length > 0, - description: `${eventScratchpad.slots.length} slot(s) in lineup`, - link: `/events/${eventScratchpad.id}/lineup`, - optional: false, - }, - ]; - - const optional: OptionalChecklistItem[] = [ - { - label: 'Add post to vrchat', - completed: false, - description: 'Post this event to VRChat.', - external: true, - link: 'https://vrchat.com/home/group/grp_626cd923-6dea-4583-abcb-8c09a765969f/posts', - buttonLabel: 'Go to VRChat', - optional: true, - }, - { - label: 'Add the event to vrc.tl', - completed: false, - description: 'Share your event on the VRCTL event listing site.', - external: true, - link: 'https://vrc.tl', - buttonLabel: 'Go to vrc.tl', - optional: true, - }, - { - label: 'Create a post on Bluesky', - completed: false, - description: 'Announce your event to the Bluesky community.', - external: true, - link: 'https://bsky.app/profile/sundayservice.bsky.social', - buttonLabel: 'Go to Bluesky', - optional: true, - }, - { - label: 'Update the assets repo', - completed: false, - description: 'Update posters and host images in the assets repository.', - external: true, - link: 'https://github.com/StrawbsProtato/strawbsprotato.github.io', - buttonLabel: 'Go to GitHub', - optional: true, - }, - ]; - - return [...required, ...optional]; - }; - - const checklistItems = getChecklistItems(); - const requiredItems = checklistItems.filter(item => !item.optional); - const optionalItems = checklistItems.filter(item => item.optional); - const completedCount = requiredItems.filter(item => item.completed).length; - const totalCount = requiredItems.length; - - return ( - -

Preflight Checklist

- - - -
Event Status
- -
- - - - Checklist Progress: {completedCount}/{totalCount} items completed - -
- - {Math.round((completedCount / totalCount) * 100)}% Complete - -
-
- -
-
-
- -
Publish/Unpublish Event
- - - - - - {!hasMinimumRequirements() && ( - - Requirements Not Met - Publishing requires: Event name, host, date/time, and at least one slot in the lineup. - - )} - - {eventScratchpad.published && ( - - Event is Live! - This event is currently published and visible to the public. - - )} - - - - - -
Preflight Checklist
-
- -
- {requiredItems.map((item, index) => ( -
-
- - {item.completed ? '✓' : '○'} - -
-
-
- {item.label} -
- {item.description} -
-
- - {item.completed ? 'Review' : 'Complete'} - -
-
- ))} -
-
-
- - - -
Additional Steps (Optional)
-
- -
- {optionalItems.map((item, index) => ( -
-
- - i - -
-
-
- {item.label} -
- {item.description} -
-
- {'buttonLabel' in item && ( - - {item.buttonLabel} - - )} -
-
- ))} -
-
-
- - ); -}; - -export default PreflightChecklist; \ No newline at end of file diff --git a/webapp/src/features/event/basic/EventDetails.tsx b/webapp/src/features/event/basic/EventDetails.tsx index 7378f28b..226fcfb7 100644 --- a/webapp/src/features/event/basic/EventDetails.tsx +++ b/webapp/src/features/event/basic/EventDetails.tsx @@ -2,7 +2,7 @@ import { useEventOperations } from "../outletContext"; import EventBasicDetailsForm from "./EventBasicDetailsForm"; import { Card, CardBody } from "react-bootstrap"; import { getSignupsPostedMessage } from "../../../util/messageWriters"; -import MessagePasteCard from "../messaging/MessagePasteCard"; +import MessagePasteCard from "../../../components/MessagePasteCard"; import { useParams } from "react-router-dom"; import "react-datepicker/dist/react-datepicker.css"; diff --git a/webapp/src/features/event/lineup/EventLineup.tsx b/webapp/src/features/event/lineup/EventLineup.tsx index f24613c2..959197e3 100644 --- a/webapp/src/features/event/lineup/EventLineup.tsx +++ b/webapp/src/features/event/lineup/EventLineup.tsx @@ -12,7 +12,7 @@ import { AddOrCreateDjModal } from "./components/AddOrCreateDjModal"; import { updateSignupForEvent } from "../util"; import { useReconciledEvent } from "../../../hooks/useEventStore/useReconciledEvent"; import { getProposedLineupMessage } from "../../../util/messageWriters"; -import MessagePasteCard from "../messaging/MessagePasteCard"; +import MessagePasteCard from "../../../components/MessagePasteCard"; type ViewMode = "signups" | "build" | "lineup"; diff --git a/webapp/src/features/event/lineup/EventLineupSlot.tsx b/webapp/src/features/event/lineup/EventLineupSlot.tsx index 72fd0e79..94ed743f 100644 --- a/webapp/src/features/event/lineup/EventLineupSlot.tsx +++ b/webapp/src/features/event/lineup/EventLineupSlot.tsx @@ -29,7 +29,7 @@ const EventLineupSlot = ({ onSlotMoveLater, onRemoveSlot, }: Props) => { - const { djCache } = useEventDjCache(); + const { djCache, getEventsByDjId } = useEventDjCache(); const hasConflict = hasAvailabilityConflict(slot, signup); @@ -106,7 +106,7 @@ const EventLineupSlot = ({ avatar: avatarUrl, // Pass the avatar URL }} djRef={djRef} - djEvents={[]} // No events available for `signup.dj_refs` + djEvents={getEventsByDjId(djRef.id)} // Fetch DJ events /> ); })} diff --git a/webapp/src/features/event/preflight/EventStatusCard.tsx b/webapp/src/features/event/preflight/EventStatusCard.tsx new file mode 100644 index 00000000..23985f5b --- /dev/null +++ b/webapp/src/features/event/preflight/EventStatusCard.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Card, ButtonGroup, Button, Alert, Stack, Badge } from 'react-bootstrap'; +import { EventPublishedStatusBadge } from '../EventPublishedStatusBadge'; +import { Event } from '../../../util/types'; + +interface EventStatusCardProps { + event: Event; + completedCount: number; + totalCount: number; + hasMinimumRequirements: boolean; + onPublish: () => void; + onUnpublish: () => void; +} + +export const EventStatusCard: React.FC = ({ + event, + completedCount, + totalCount, + hasMinimumRequirements, + onPublish, + onUnpublish, +}) => { + return ( + + +
Event Status
+ +
+ + + + Checklist Progress: {completedCount}/{totalCount} items completed + +
+ + {Math.round((completedCount / totalCount) * 100)}% Complete + +
+
+ +
+
+
+ +
Publish/Unpublish Event
+ + + + + + {!hasMinimumRequirements && ( + + Requirements Not Met + Publishing requires: Event name, host, date/time, and at least one slot in the lineup. + + )} + + {event.published && ( + + Event is Live! + This event is currently published and visible to the public. + + )} + + + ); +}; diff --git a/webapp/src/features/event/preflight/LineupPosterModal.tsx b/webapp/src/features/event/preflight/LineupPosterModal.tsx new file mode 100644 index 00000000..3bbec4a3 --- /dev/null +++ b/webapp/src/features/event/preflight/LineupPosterModal.tsx @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import { Modal, Button, Form } from 'react-bootstrap'; +import { Event } from '../../../util/types'; +import { toast } from 'react-hot-toast'; +import { LINEUP_POSTER_MAX_FILE_SIZE_MB, validateLineupPosterFile } from '../../../util/util'; + +interface LineupPosterModalProps { + show: boolean; + onHide: () => void; + event: Event; + onFileChange: (file: File | null) => void; +} + +export const LineupPosterModal: React.FC = ({ + show, + onHide, + event, + onFileChange, +}) => { + const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + + const handleFileChange = (e: React.ChangeEvent) => { + const input = e.target as HTMLInputElement; + const file = input.files?.[0] ?? null; + + const { valid, errorMessage } = validateLineupPosterFile(file); + if (!valid) { + if (errorMessage) { + toast.error(errorMessage); + } + input.value = ""; + return; + } + + setSelectedFile(file); + if (file) { + const url = URL.createObjectURL(file); + setPreviewUrl(url); + } else { + setPreviewUrl(null); + } + }; + + const handleSave = () => { + if (selectedFile) { + onFileChange(selectedFile); + toast.success('Lineup poster will be saved when you save the event'); + } + handleClose(); + }; + + const handleRemove = () => { + onFileChange(null); + toast.success('Lineup poster removed'); + handleClose(); + }; + + const handleClose = () => { + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + setSelectedFile(null); + setPreviewUrl(null); + onHide(); + }; + + return ( + + + Upload Lineup Poster + + + + Select Lineup Poster Image + + + Upload an image for the lineup whiteboard/OBS overlays. Maximum file size: {LINEUP_POSTER_MAX_FILE_SIZE_MB}MB. + + + + {(previewUrl || event.lineup_poster_url) && ( +
+ Preview +
+ Lineup poster preview +
+
+ )} + + {event.lineup_poster_url && !selectedFile && ( +
+

+ Current Status: A lineup poster is already uploaded for this event. +

+
+ )} +
+ + {event.lineup_poster_url && ( + + )} + + {selectedFile && ( + + )} + +
+ ); +}; diff --git a/webapp/src/features/event/preflight/OutdatedStepsAccordion.tsx b/webapp/src/features/event/preflight/OutdatedStepsAccordion.tsx new file mode 100644 index 00000000..72e4cb99 --- /dev/null +++ b/webapp/src/features/event/preflight/OutdatedStepsAccordion.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { Accordion, ButtonGroup, Button } from 'react-bootstrap'; + +interface OutdatedStepsAccordionProps { + onShowSocialMediaModal: () => void; +} + +export const OutdatedStepsAccordion: React.FC = ({ + onShowSocialMediaModal, +}) => { + const outdatedItems = [ + { + label: 'Add event to vrc.tl', + description: 'Share your event on the VRCTL event listing site.', + link: 'https://vrc.tl', + buttonLabel: 'Go to vrc.tl', + isMessaging: false, + }, + { + label: 'Social Media Announcement', + description: 'Share your event on Bluesky and other platforms.', + link: 'https://bsky.app/profile/sundayservice.bsky.social', + buttonLabel: 'Go to Bluesky', + isMessaging: true, + }, + { + label: 'Update the assets repo', + description: 'Update posters and host images in the assets repository.', + link: 'https://github.com/StrawbsProtato/strawbsprotato.github.io', + buttonLabel: 'Go to GitHub', + isMessaging: false, + }, + ]; + + return ( + + + +
Outdated Steps (No Longer Used)
+
+ +
+ {outdatedItems.map((item, index) => ( +
+
+ + ✕ + +
+
+
+ {item.label} +
+ {item.description} +
+
+ {item.isMessaging ? ( + + + + {item.buttonLabel} + + + ) : ( + + {item.buttonLabel} + + )} +
+
+ ))} +
+
+
+
+ ); +}; diff --git a/webapp/src/features/event/preflight/PreflightChecklist.tsx b/webapp/src/features/event/preflight/PreflightChecklist.tsx new file mode 100644 index 00000000..4b746032 --- /dev/null +++ b/webapp/src/features/event/preflight/PreflightChecklist.tsx @@ -0,0 +1,119 @@ +import { Container, Card } from 'react-bootstrap'; +import { useEventOperations } from '../outletContext'; +import { useEventStore } from '../../../hooks/useEventStore/useEventStore'; +import UnifiedMessageEditModal from './messaging/UnifiedMessageEditModal'; +import { EventStatusCard } from './EventStatusCard'; +import { PreflightChecklistStep } from './PreflightChecklistStep'; +import { OutdatedStepsAccordion } from './OutdatedStepsAccordion'; +import { usePreflightHandlers } from './usePreflightHandlers'; +import { useChecklistItems } from './useChecklistItems'; +import { getDiscordMessage, getTwitterMessage } from '../../../util/messageWriters'; +import { Event } from '../../../util/types'; +import { LineupPosterModal } from './LineupPosterModal'; + +const PreflightChecklist = () => { + const [eventScratchpad, proposeEventChange, onLineupPosterFileSelected] = useEventOperations(); + const { saveEvent } = useEventStore(); + + const handlers = usePreflightHandlers(eventScratchpad, proposeEventChange, saveEvent); + const checklistItems = useChecklistItems(eventScratchpad); + + const hasMinimumRequirements = () => { + return eventScratchpad.name != "" && + eventScratchpad.host != "" && + eventScratchpad.start_datetime && + eventScratchpad.slots.length > 0; + }; + + const requiredItems = checklistItems.filter(item => !item.optional); + const optionalItems = checklistItems.filter(item => item.optional); + const completedCount = requiredItems.filter(item => item.completed).length; + const totalCount = requiredItems.length; + + return ( + +

Preflight Checklist

+ + + + + +
Preflight Checklist
+
+ +
+ {requiredItems.map((item, index) => ( + handlers.setShowMessageModal(true)} + onUploadPoster={() => handlers.setShowPosterModal(true)} + /> + ))} + {optionalItems.map((item, index) => ( + handlers.setShowMessageModal(true) + : () => handlers.setShowSocialMediaModal(true) + } + onUploadPoster={() => handlers.setShowPosterModal(true)} + /> + ))} +
+
+
+ + handlers.setShowSocialMediaModal(true)} + /> + + handlers.setShowMessageModal(false)} + event={eventScratchpad} + onSave={handlers.handleSaveMessage} + messageType="discord" + getMessageFromEvent={(event: Event) => event.message} + generatePreview={(event: Event) => getDiscordMessage(event)} + title="Edit Discord Announcement Message" + label="Discord Announcement Message" + helpText="This message will be included in your Discord announcement." + previewLabel="Discord Announcement Preview" + previewHelpText="This is how your Discord announcement will look." + /> + + handlers.setShowSocialMediaModal(false)} + event={eventScratchpad} + onSave={handlers.handleSaveSocialMediaMessage} + messageType="social" + getMessageFromEvent={(event: Event) => event.socialMediaMessage || getTwitterMessage(event)} + title="Social Media / VRChat Message" + label="Social Media & VRChat Message" + helpText="Customize your message for VRChat and social media platforms. The default message is generated from your event details." + /> + + handlers.setShowPosterModal(false)} + event={eventScratchpad} + onFileChange={onLineupPosterFileSelected} + /> +
+ ); +}; + +export default PreflightChecklist; \ No newline at end of file diff --git a/webapp/src/features/event/preflight/PreflightChecklistStep.tsx b/webapp/src/features/event/preflight/PreflightChecklistStep.tsx new file mode 100644 index 00000000..b679f934 --- /dev/null +++ b/webapp/src/features/event/preflight/PreflightChecklistStep.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { ButtonGroup, Button } from 'react-bootstrap'; +import { Link } from 'react-router-dom'; +import { ChecklistItem } from './types'; +import { Event } from '../../../util/types'; + +interface PreflightChecklistStepProps { + item: ChecklistItem; + eventScratchpad: Event; + onEditMessage: () => void; + onUploadPoster?: () => void; +} + +export const PreflightChecklistStep: React.FC = ({ + item, + eventScratchpad, + onEditMessage, + onUploadPoster, +}) => { + const renderActions = () => { + if (item.action.type === 'internal') { + return ( + + {item.completed ? 'Review' : item.action.label} + + ); + } + + if (item.action.type === 'messaging') { + const isDiscord = item.action.platform === 'discord'; + return ( + + + + Open Page + + + ); + } + + if (item.action.type === 'poster') { + return ( + + ); + } + + if (item.action.type === 'external') { + return ( + + {item.action.label} + + ); + } + + return null; + }; + + const getBadgeInfo = () => { + if (item.optional) { + return { + className: 'badge bg-info', + content: 'i' + }; + } + return { + className: `badge ${item.completed ? 'bg-success' : 'bg-secondary'}`, + content: item.completed ? '✓' : '○' + }; + }; + + const badge = getBadgeInfo(); + + return ( +
+
+ + {badge.content} + +
+
+
+ {item.label} +
+ {item.description} +
+
+ {renderActions()} +
+
+ ); +}; diff --git a/webapp/src/features/event/preflight/messaging/UnifiedMessageEditModal.tsx b/webapp/src/features/event/preflight/messaging/UnifiedMessageEditModal.tsx new file mode 100644 index 00000000..6df5e5a4 --- /dev/null +++ b/webapp/src/features/event/preflight/messaging/UnifiedMessageEditModal.tsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { Modal, Button, Form, Tabs, Tab } from 'react-bootstrap'; +import toast from 'react-hot-toast'; +import { Event } from '../../../../util/types'; + +type MessageType = 'discord' | 'social'; + +type UnifiedMessageEditModalProps = { + show: boolean; + onHide: () => void; + event: Event; + onSave: (updatedMessage: string) => void | Promise; + messageType: MessageType; + getMessageFromEvent: (event: Event) => string; + generatePreview?: (event: Event, message: string) => string; + title: string; + label: string; + helpText: string; + previewLabel?: string; + previewHelpText?: string; +}; + +const UnifiedMessageEditModal = ({ + show, + onHide, + event, + onSave, + messageType, + getMessageFromEvent, + generatePreview, + title, + label, + helpText, + previewLabel, + previewHelpText +}: UnifiedMessageEditModalProps) => { + const [message, setMessage] = React.useState(''); + const [isSaving, setIsSaving] = React.useState(false); + + React.useEffect(() => { + if (show) { + setMessage(getMessageFromEvent(event)); + } + }, [show, event, getMessageFromEvent]); + + const handleSave = async () => { + setIsSaving(true); + try { + await onSave(message); + } catch (error) { + console.error('Error saving message:', error); + } finally { + setIsSaving(false); + } + }; + + const handleCopyAndClose = () => { + navigator.clipboard.writeText(generatePreview ? generatePreview(event, message) : message); + toast.success('Message copied to clipboard!'); + onHide(); + }; + + // Create a temporary event with the current message for preview + const previewEvent = { ...event }; + if (messageType === 'discord') { + previewEvent.message = message; + } else { + previewEvent.socialMediaMessage = message; + } + const previewMessage = generatePreview ? generatePreview(previewEvent, message) : message; + + return ( + + + {title} + + + + + + {label} + setMessage(e.target.value)} + className="has-fixed-size" + /> + + {helpText} + + + + {generatePreview && ( + + + + {previewLabel || 'Preview'} + + + + {previewHelpText || 'This is how your message will look.'} + + + + )} + + + + + + + + + ); +}; + +export default UnifiedMessageEditModal; diff --git a/webapp/src/features/event/preflight/types.ts b/webapp/src/features/event/preflight/types.ts new file mode 100644 index 00000000..bccbe426 --- /dev/null +++ b/webapp/src/features/event/preflight/types.ts @@ -0,0 +1,15 @@ +// Checklist item types +export type ChecklistItemAction = + | { type: 'internal'; label: string } // Navigate within app (Link) + | { type: 'external'; label: string } // Open external link + | { type: 'messaging'; platform: 'discord' | 'vrchat' | 'social' } // Special messaging actions + | { type: 'poster' }; // Poster upload modal action + +export type ChecklistItem = { + label: string; + completed: boolean; + description: string; + link: string; + optional: boolean; + action: ChecklistItemAction; +}; diff --git a/webapp/src/features/event/preflight/useChecklistItems.ts b/webapp/src/features/event/preflight/useChecklistItems.ts new file mode 100644 index 00000000..e38c6866 --- /dev/null +++ b/webapp/src/features/event/preflight/useChecklistItems.ts @@ -0,0 +1,52 @@ +import { useMemo } from 'react'; +import { ChecklistItem } from './types'; +import { Event } from '../../../util/types'; + +export const useChecklistItems = (eventScratchpad: Event) => { + return useMemo((): ChecklistItem[] => [ + { + label: 'Host Assigned', + completed: !!eventScratchpad.host, + description: 'Event has a host assigned', + link: `/events/${eventScratchpad.id}/setup`, + optional: false, + action: { type: 'internal', label: 'Complete' }, + }, + { + label: 'Lineup Created', + completed: eventScratchpad.slots.length > 0, + description: `${eventScratchpad.slots.length} slot(s) in lineup`, + link: `/events/${eventScratchpad.id}/lineup`, + optional: false, + action: { type: 'internal', label: 'Complete' }, + }, + { + label: 'Lineup Poster', + completed: !!eventScratchpad.lineup_poster_url, + description: eventScratchpad.lineup_poster_url + ? 'Lineup poster uploaded and ready' + : 'Upload a lineup poster image', + link: '', + optional: true, + action: { type: 'poster' }, + }, + { + label: 'Discord Announcement', + completed: false, + description: eventScratchpad.message && eventScratchpad.message.trim() !== '' + ? 'Message customized and ready' + : 'Customize your Discord announcement message', + link: 'https://discord.com/channels/1004489038159413248/1004489042890588165', + optional: true, + action: { type: 'messaging', platform: 'discord' }, + }, + { + label: 'VRChat Announcement', + completed: false, + description: 'Post this event to VRChat group.', + link: 'https://vrchat.com/home/group/grp_626cd923-6dea-4583-abcb-8c09a765969f/posts', + optional: true, + action: { type: 'messaging', platform: 'vrchat' }, + }, + ], [eventScratchpad.host, eventScratchpad.id, eventScratchpad.slots.length, eventScratchpad.message, eventScratchpad.lineup_poster_url]); +}; diff --git a/webapp/src/features/event/preflight/usePreflightHandlers.ts b/webapp/src/features/event/preflight/usePreflightHandlers.ts new file mode 100644 index 00000000..46d9943b --- /dev/null +++ b/webapp/src/features/event/preflight/usePreflightHandlers.ts @@ -0,0 +1,70 @@ +import { useState } from 'react'; +import toast from 'react-hot-toast'; +import { Event } from '../../../util/types'; + +export const usePreflightHandlers = ( + eventScratchpad: Event, + proposeEventChange: (event: Event) => void, + saveEvent: (newEvent: Event, oldEvent: Event) => Promise +) => { + const [showMessageModal, setShowMessageModal] = useState(false); + const [showSocialMediaModal, setShowSocialMediaModal] = useState(false); + const [showPosterModal, setShowPosterModal] = useState(false); + + const handlePublish = async () => { + try { + const newEvent = { ...eventScratchpad, published: true }; + await saveEvent(newEvent, eventScratchpad); + proposeEventChange(newEvent); + toast.success('Event published successfully!'); + } catch (error) { + toast.error(`Error publishing event: ${(error as Error).message}`); + } + }; + + const handleUnpublish = async () => { + try { + const newEvent = { ...eventScratchpad, published: false }; + await saveEvent(newEvent, eventScratchpad); + proposeEventChange(newEvent); + toast.success('Event unpublished successfully!'); + } catch (error) { + toast.error(`Error unpublishing event: ${(error as Error).message}`); + } + }; + + const handleSaveMessage = async (updatedMessage: string) => { + try { + const newEvent = { ...eventScratchpad, message: updatedMessage }; + await saveEvent(newEvent, eventScratchpad); + proposeEventChange(newEvent); + toast.success('Message updated successfully!'); + } catch (error) { + toast.error(`Error saving message: ${(error as Error).message}`); + } + }; + + const handleSaveSocialMediaMessage = async (updatedMessage: string) => { + try { + const newEvent = { ...eventScratchpad, socialMediaMessage: updatedMessage }; + await saveEvent(newEvent, eventScratchpad); + proposeEventChange(newEvent); + toast.success('Social media message updated successfully!'); + } catch (error) { + toast.error(`Error saving social media message: ${(error as Error).message}`); + } + }; + + return { + showMessageModal, + setShowMessageModal, + showSocialMediaModal, + setShowSocialMediaModal, + showPosterModal, + setShowPosterModal, + handlePublish, + handleUnpublish, + handleSaveMessage, + handleSaveSocialMediaMessage, + }; +}; diff --git a/webapp/src/features/event/routes.tsx b/webapp/src/features/event/routes.tsx index 8030b43c..2ada7fb1 100644 --- a/webapp/src/features/event/routes.tsx +++ b/webapp/src/features/event/routes.tsx @@ -1,9 +1,8 @@ import { Link, RouteObject } from "react-router-dom"; import EventDetails from "./basic/EventDetails"; -import EventAnnouncements from "./EventAnnouncements"; import EventLineup from "./lineup/EventLineup"; import DebuggingDetails from "./DebuggingDetails"; -import PreflightChecklist from "./PreflightChecklist"; +import PreflightChecklist from "./preflight/PreflightChecklist"; export const eventRoutes: RouteObject[] = [ { @@ -27,15 +26,10 @@ export const eventRoutes: RouteObject[] = [ element: , handle: { crumb: () => Preflight} }, - { - path: "announcements", - element: , - handle: { crumb: () => Announcements}, - }, { path: "debug", element: , - handle: { crumb: () => Debugging}, + handle: { crumb: () => Debugging}, }, ]; \ No newline at end of file diff --git a/webapp/src/util/types.ts b/webapp/src/util/types.ts index a1d0063f..d2851003 100644 --- a/webapp/src/util/types.ts +++ b/webapp/src/util/types.ts @@ -36,6 +36,7 @@ export type Event = { name: string; published: boolean, message: string; + socialMediaMessage?: string; start_datetime: Date; end_datetime?: Date; host: string;