EmailFormPopup.js
199 lines
| 1 | /** |
| 2 | * Add/Edit email submission popup: email, status, visibility (public/private only), published date/time. |
| 3 | * Visibility drives effectiveStatus (private => post status 'private'); password protection was removed. |
| 4 | */ |
| 5 | // eslint-disable-next-line import/no-extraneous-dependencies |
| 6 | import React, { useState, useEffect } from 'react'; |
| 7 | import { __ } from '@wordpress/i18n'; |
| 8 | import { Dialog, Button, Container, Input, Loader, Select, Tooltip } from '@bsf/force-ui'; |
| 9 | import { Info, Settings } from 'lucide-react'; |
| 10 | import PostScheduleField from '../PostScheduleField'; |
| 11 | import { |
| 12 | DEFAULT_STATE, |
| 13 | getInitialState, |
| 14 | getOptionLabel, |
| 15 | toPublishedDateTime, |
| 16 | STATUS_OPTIONS, |
| 17 | VISIBILITY_OPTIONS, |
| 18 | } from './EmailFormPopupUtils'; |
| 19 | |
| 20 | function OptionSelect( { options, value, onChange, label, tooltip } ) { |
| 21 | return ( |
| 22 | <div> |
| 23 | { label && ( |
| 24 | <div className="flex items-center gap-1 mb-1"> |
| 25 | <span className="text-sm font-medium text-field-label">{ label }</span> |
| 26 | { tooltip && ( |
| 27 | <Tooltip content={ tooltip } placement="right"> |
| 28 | <Info className="size-3.5 text-icon-secondary cursor-help shrink-0" /> |
| 29 | </Tooltip> |
| 30 | ) } |
| 31 | </div> |
| 32 | ) } |
| 33 | <Select by="value" size="md" value={ { value, label: getOptionLabel( options, value ) } } onChange={ ( o ) => o && onChange( o.value ) }> |
| 34 | <Select.Button render={ ( v ) => v?.label } /> |
| 35 | <Select.Options> |
| 36 | { options.map( ( o ) => <Select.Option key={ o.value } value={ { value: o.value, label: o.label } }>{ o.label }</Select.Option> ) } |
| 37 | </Select.Options> |
| 38 | </Select> |
| 39 | </div> |
| 40 | ); |
| 41 | } |
| 42 | |
| 43 | const EmailFormPopup = ( { open, onClose, initialData, onSuccess } ) => { |
| 44 | const [ email, setEmail ] = useState( '' ); |
| 45 | const [ status, setStatus ] = useState( 'publish' ); |
| 46 | const [ visibility, setVisibility ] = useState( 'public' ); |
| 47 | const [ publishedDate, setPublishedDate ] = useState( null ); |
| 48 | const [ publishedTimeValue, setPublishedTimeValue ] = useState( '' ); |
| 49 | const [ publishedPeriod, setPublishedPeriod ] = useState( '-1' ); |
| 50 | const [ saving, setSaving ] = useState( false ); |
| 51 | const [ errorMessage, setErrorMessage ] = useState( '' ); |
| 52 | // Tracks whether the user touched the date/time/period picker. Without this, |
| 53 | // every save round-trips the time through the 10-min picker granularity and |
| 54 | // drifts the stored post_date by up to 10 minutes — losing precision on edits |
| 55 | // that didn't intend to change the timestamp at all. |
| 56 | const [ dateTouched, setDateTouched ] = useState( false ); |
| 57 | |
| 58 | const isEditMode = !! initialData?.id; |
| 59 | |
| 60 | useEffect( () => { |
| 61 | if ( open ) { |
| 62 | const s = getInitialState( initialData ); |
| 63 | setEmail( s.email ); |
| 64 | setStatus( s.status ); |
| 65 | setVisibility( s.visibility ); |
| 66 | setPublishedDate( s.publishedDate ); |
| 67 | setPublishedTimeValue( s.publishedTimeValue ); |
| 68 | setPublishedPeriod( s.publishedPeriod ); |
| 69 | setErrorMessage( '' ); |
| 70 | setSaving( false ); |
| 71 | setDateTouched( false ); |
| 72 | } |
| 73 | }, [ open, initialData ] ); |
| 74 | |
| 75 | const resetAndClose = () => { |
| 76 | if ( saving ) return; |
| 77 | setEmail( DEFAULT_STATE.email ); |
| 78 | setStatus( DEFAULT_STATE.status ); |
| 79 | setVisibility( DEFAULT_STATE.visibility ); |
| 80 | setPublishedDate( null ); |
| 81 | setPublishedTimeValue( '' ); |
| 82 | setPublishedPeriod( '-1' ); |
| 83 | setErrorMessage( '' ); |
| 84 | setDateTouched( false ); |
| 85 | onClose(); |
| 86 | }; |
| 87 | |
| 88 | // Client-side hint only; server validates with is_email() and is authoritative. |
| 89 | const isValidEmail = ( v ) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( ( v || '' ).trim() ); |
| 90 | |
| 91 | const handleSave = async () => { |
| 92 | const trimmed = ( email || '' ).trim(); |
| 93 | setErrorMessage( '' ); |
| 94 | if ( ! trimmed ) { |
| 95 | setErrorMessage( __( 'Please enter an email address.', 'presto-player' ) ); |
| 96 | return; |
| 97 | } |
| 98 | if ( ! isValidEmail( trimmed ) ) { |
| 99 | setErrorMessage( __( 'Please enter a valid email address.', 'presto-player' ) ); |
| 100 | return; |
| 101 | } |
| 102 | setSaving( true ); |
| 103 | try { |
| 104 | const effectiveStatus = visibility === 'private' ? 'private' : status; |
| 105 | const payload = { |
| 106 | ...( isEditMode && { id: initialData.id, email: trimmed, video_title: initialData.video_title, preset_name: initialData.preset_name } ), |
| 107 | ...( ! isEditMode && { email: trimmed } ), |
| 108 | status: effectiveStatus, |
| 109 | visibility, |
| 110 | ...( ( dateTouched || ! isEditMode ) && { |
| 111 | date: toPublishedDateTime( publishedDate, publishedTimeValue, publishedPeriod ), |
| 112 | } ), |
| 113 | }; |
| 114 | const result = onSuccess?.( payload ); |
| 115 | if ( result?.then ) await result; |
| 116 | } catch ( err ) { |
| 117 | setErrorMessage( err?.message || __( 'Something went wrong.', 'presto-player' ) ); |
| 118 | } finally { |
| 119 | setSaving( false ); |
| 120 | } |
| 121 | }; |
| 122 | |
| 123 | return ( |
| 124 | <Dialog design="simple" exitOnEsc scrollLock open={ open } setOpen={ ( v ) => ! v && resetAndClose() }> |
| 125 | <Dialog.Backdrop /> |
| 126 | <Dialog.Panel className="max-w-2xl w-full overflow-visible"> |
| 127 | <Dialog.Header> |
| 128 | <div className="flex items-center justify-between"> |
| 129 | <div className="flex items-center gap-2"> |
| 130 | <Settings size={ 20 } /> |
| 131 | <Dialog.Title> |
| 132 | { __( 'Post Settings', 'presto-player' ) } |
| 133 | </Dialog.Title> |
| 134 | </div> |
| 135 | <Dialog.CloseButton onClick={ resetAndClose } /> |
| 136 | </div> |
| 137 | </Dialog.Header> |
| 138 | |
| 139 | <Dialog.Body> |
| 140 | <Container containerType="flex" direction="column"> |
| 141 | <Container.Item> |
| 142 | <Input |
| 143 | type="email" |
| 144 | size="md" |
| 145 | label={ __( 'Email', 'presto-player' ) } |
| 146 | value={ email } |
| 147 | onChange={ ( value ) => setEmail( value ) } |
| 148 | placeholder={ __( 'Enter email address…', 'presto-player' ) } |
| 149 | className="w-full" |
| 150 | /> |
| 151 | { errorMessage && ( |
| 152 | <p className="text-sm text-text-error mt-1" role="alert">{ errorMessage }</p> |
| 153 | ) } |
| 154 | </Container.Item> |
| 155 | |
| 156 | <Container.Item> |
| 157 | <OptionSelect |
| 158 | options={ STATUS_OPTIONS } |
| 159 | value={ status } |
| 160 | onChange={ setStatus } |
| 161 | label={ __( 'Status', 'presto-player' ) } |
| 162 | tooltip={ __( 'Controls the publication state — Published (live), Pending Review (awaiting approval), or Draft (not live yet).', 'presto-player' ) } |
| 163 | /> |
| 164 | </Container.Item> |
| 165 | |
| 166 | <Container.Item> |
| 167 | <OptionSelect |
| 168 | options={ VISIBILITY_OPTIONS } |
| 169 | value={ visibility } |
| 170 | onChange={ setVisibility } |
| 171 | label={ __( 'Visibility', 'presto-player' ) } |
| 172 | tooltip={ __( 'Controls who can access this submission — Public (everyone) or Private (admins only). Setting Private overrides the Status.', 'presto-player' ) } |
| 173 | /> |
| 174 | </Container.Item> |
| 175 | |
| 176 | <PostScheduleField |
| 177 | date={ publishedDate } |
| 178 | time={ publishedTimeValue } |
| 179 | period={ publishedPeriod } |
| 180 | setDate={ ( v ) => { setDateTouched( true ); setPublishedDate( v ); } } |
| 181 | setTime={ ( v ) => { setDateTouched( true ); setPublishedTimeValue( v ); } } |
| 182 | setPeriod={ ( v ) => { setDateTouched( true ); setPublishedPeriod( v ); } } |
| 183 | /> |
| 184 | </Container> |
| 185 | </Dialog.Body> |
| 186 | |
| 187 | <Dialog.Footer className="pt-0 justify-between"> |
| 188 | <Button size="md" variant="outline" onClick={ resetAndClose } disabled={ saving }>{ __( 'Cancel', 'presto-player' ) }</Button> |
| 189 | <Button size="md" disabled={ saving } onClick={ handleSave } iconPosition="right" icon={ saving && <Loader icon={ null } size="sm" variant="primary" /> }> |
| 190 | { __( 'Save Settings', 'presto-player' ) } |
| 191 | </Button> |
| 192 | </Dialog.Footer> |
| 193 | </Dialog.Panel> |
| 194 | </Dialog> |
| 195 | ); |
| 196 | }; |
| 197 | |
| 198 | export default EmailFormPopup; |
| 199 |