o2o-castad-frontend/src/components/SocialPostingModal.tsx

769 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import React, { useState, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { getSocialAccounts, uploadToSocial, waitForUploadComplete, TokenExpiredError, handleSocialReconnect, getAutoSeoYoutube } from '../utils/api';
import { SocialAccount, VideoListItem, SocialUploadStatusResponse } from '../types/api';
import UploadProgressModal, { UploadStatus } from './UploadProgressModal';
interface SocialPostingModalProps {
isOpen: boolean;
onClose: () => void;
video: VideoListItem | null;
}
type PrivacyType = 'public' | 'unlisted' | 'private';
type PublishTimeType = 'now' | 'schedule';
// 플랫폼별 아이콘 경로
const getPlatformIcon = (platform: string) => {
switch (platform) {
case 'youtube':
return '/assets/images/social-youtube.png';
case 'instagram':
return '/assets/images/social-instagram.png';
default:
return '/assets/images/social-youtube.png';
}
};
// 미니 캘린더 컴포넌트
const MiniCalendar: React.FC<{
selectedDate: Date | null;
onSelect: (date: Date) => void;
minDate?: Date;
}> = ({ selectedDate, onSelect, minDate }) => {
const today = new Date();
const [viewYear, setViewYear] = useState(selectedDate?.getFullYear() ?? today.getFullYear());
const [viewMonth, setViewMonth] = useState(selectedDate?.getMonth() ?? today.getMonth());
const DAY_LABELS = ['일', '월', '화', '수', '목', '금', '토'];
const firstDay = new Date(viewYear, viewMonth, 1).getDay();
const daysInMonth = new Date(viewYear, viewMonth + 1, 0).getDate();
const daysInPrevMonth = new Date(viewYear, viewMonth, 0).getDate();
const cells: { date: Date; currentMonth: boolean }[] = [];
for (let i = firstDay - 1; i >= 0; i--) {
cells.push({ date: new Date(viewYear, viewMonth - 1, daysInPrevMonth - i), currentMonth: false });
}
for (let d = 1; d <= daysInMonth; d++) {
cells.push({ date: new Date(viewYear, viewMonth, d), currentMonth: true });
}
while (cells.length % 7 !== 0) {
cells.push({ date: new Date(viewYear, viewMonth + 1, cells.length - daysInMonth - firstDay + 1), currentMonth: false });
}
const isSameDay = (a: Date, b: Date) =>
a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
const isDisabled = (date: Date) => {
if (!minDate) return false;
const min = new Date(minDate); min.setHours(0,0,0,0);
const d = new Date(date); d.setHours(0,0,0,0);
return d < min;
};
const prevMonth = () => {
if (viewMonth === 0) { setViewYear(y => y - 1); setViewMonth(11); }
else setViewMonth(m => m - 1);
};
const nextMonth = () => {
if (viewMonth === 11) { setViewYear(y => y + 1); setViewMonth(0); }
else setViewMonth(m => m + 1);
};
return (
<div className="mini-calendar">
<div className="mini-calendar-header">
<button type="button" className="mini-calendar-nav" onClick={prevMonth}></button>
<span className="mini-calendar-title">{viewYear} {viewMonth + 1}</span>
<button type="button" className="mini-calendar-nav" onClick={nextMonth}></button>
</div>
<div className="mini-calendar-grid">
{DAY_LABELS.map(d => (
<div key={d} className="mini-calendar-day-label">{d}</div>
))}
{cells.map((cell, i) => {
const disabled = isDisabled(cell.date);
const isSelected = selectedDate ? isSameDay(cell.date, selectedDate) : false;
const isToday = isSameDay(cell.date, today);
return (
<button
key={i}
type="button"
disabled={disabled}
className={[
'mini-calendar-cell',
cell.currentMonth ? '' : 'other-month',
isSelected ? 'selected' : '',
isToday && !isSelected ? 'today' : '',
disabled ? 'disabled' : '',
].join(' ')}
onClick={() => !disabled && onSelect(cell.date)}
>
{cell.date.getDate()}
</button>
);
})}
</div>
</div>
);
};
const SocialPostingModal: React.FC<SocialPostingModalProps> = ({
isOpen,
onClose,
video
}) => {
const { t } = useTranslation();
const [socialAccounts, setSocialAccounts] = useState<SocialAccount[]>([]);
const [selectedChannel, setSelectedChannel] = useState<string>('');
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [tags, setTags] = useState('');
const [privacy, setPrivacy] = useState<PrivacyType>('public');
const [publishTime, setPublishTime] = useState<PublishTimeType>('now');
const [scheduledDate, setScheduledDate] = useState<Date | null>(null);
const [scheduledHour, setScheduledHour] = useState<number>(12);
const [scheduledMinute, setScheduledMinute] = useState<number>(0);
const [isLoadingAccounts, setIsLoadingAccounts] = useState(false);
const [isPosting, setIsPosting] = useState(false);
const [isChannelDropdownOpen, setIsChannelDropdownOpen] = useState(false);
const [isPrivacyDropdownOpen, setIsPrivacyDropdownOpen] = useState(false);
const [isLoadingAutoDescription, setIsLoadingAutoDescription] = useState(false);
const [isHorizontalVideo, setIsHorizontalVideo] = useState(false);
const [videoMeta, setVideoMeta] = useState<{ width: number; height: number; duration: number } | null>(null);
const channelDropdownRef = useRef<HTMLDivElement>(null);
const privacyDropdownRef = useRef<HTMLDivElement>(null);
// Upload progress modal state
const [showUploadProgress, setShowUploadProgress] = useState(false);
const [uploadStatus, setUploadStatus] = useState<UploadStatus>('pending');
const [uploadProgress, setUploadProgress] = useState(0);
const [uploadYoutubeUrl, setUploadYoutubeUrl] = useState<string | undefined>();
const [uploadErrorMessage, setUploadErrorMessage] = useState<string | undefined>();
// 업로드 정보 (모달이 닫힌 후에도 유지)
const [uploadVideoTitle, setUploadVideoTitle] = useState<string>('');
const [uploadChannelName, setUploadChannelName] = useState<string>('');
// 드롭다운 외부 클릭 시 닫기
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (channelDropdownRef.current && !channelDropdownRef.current.contains(event.target as Node)) {
setIsChannelDropdownOpen(false);
}
if (privacyDropdownRef.current && !privacyDropdownRef.current.contains(event.target as Node)) {
setIsPrivacyDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// 모달 열릴 때 배경 스크롤 차단
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => { document.body.style.overflow = ''; };
}, [isOpen]);
// 소셜 계정 로드
useEffect(() => {
if (isOpen) {
loadSocialAccounts();
loadAutocomplete();
}
}, [isOpen, video]);
const loadSocialAccounts = async () => {
setIsLoadingAccounts(true);
try {
const response = await getSocialAccounts();
const activeAccounts = response.accounts?.filter(acc => acc.is_active) || [];
setSocialAccounts(activeAccounts);
if (activeAccounts.length > 0) {
setSelectedChannel(activeAccounts[0].platform_user_id);
}
} catch (error) {
if (error instanceof TokenExpiredError) {
alert(t('social.youtubeExpiredAlert'));
handleSocialReconnect(error.reconnectUrl);
return;
}
console.error('Failed to load social accounts:', error);
} finally {
setIsLoadingAccounts(false);
}
};
const loadAutocomplete = async () => {
if (!video?.task_id) return;
setIsLoadingAutoDescription(true);
try {
const requestPayload = {
task_id : video.task_id,
};
// Call autoSEO API
console.log('[Upload] Request payload:', requestPayload);
const autoSeoResponse = await getAutoSeoYoutube(requestPayload);
// 각 필드가 있을 때만 덮어씌움 (기존 값 보호)
if (autoSeoResponse.title) setTitle(autoSeoResponse.title);
if (autoSeoResponse.description) setDescription(autoSeoResponse.description);
if (autoSeoResponse.keywords) setTags(autoSeoResponse.keywords.join(','));
} catch (error) {
console.error('Failed to load autocomplete:', error);
// 실패해도 사용자에게 별도 알림 없이 조용히 처리
} finally {
setIsLoadingAutoDescription(false);
}
};
const handlePost = async () => {
if (!selectedChannel || !title.trim() || !video) {
alert(t('social.channelAndTitleRequired'));
return;
}
if (publishTime === 'schedule' && !scheduledDate) {
alert('게시 날짜를 선택해주세요.');
return;
}
if (publishTime === 'schedule' && scheduledDate) {
const dt = new Date(scheduledDate);
dt.setHours(scheduledHour, scheduledMinute, 0, 0);
if (dt <= new Date()) {
alert('현재 시각 이후의 시간을 선택해주세요.');
return;
}
}
const selectedAcc = socialAccounts.find(acc => acc.platform_user_id === selectedChannel);
if (!selectedAcc) {
alert(t('social.selectChannel'));
return;
}
// video.video_id 검증 - 반드시 존재해야 함
if (!video.video_id) {
console.error('Video object missing video_id:', video);
alert(t('social.invalidVideoInfo'));
return;
}
setIsPosting(true);
// Reset upload progress state
setUploadStatus('pending');
setUploadProgress(0);
setUploadYoutubeUrl(undefined);
setUploadErrorMessage(undefined);
// 업로드 정보 저장 (모달이 닫힌 후에도 유지)
setUploadVideoTitle(title.trim());
setUploadChannelName(selectedAcc.display_name);
setShowUploadProgress(true);
try {
// Parse tags from comma-separated string
const tagsArray = tags
.split(',')
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
// Request payload 로그
let scheduledAt: string | null = null;
if (publishTime === 'schedule' && scheduledDate) {
const dt = new Date(scheduledDate);
dt.setHours(scheduledHour, scheduledMinute, 0, 0);
// 로컬 시간 기준으로 전송 (Z 없이)
const pad = (n: number) => String(n).padStart(2, '0');
scheduledAt = `${dt.getFullYear()}-${pad(dt.getMonth()+1)}-${pad(dt.getDate())}T${pad(dt.getHours())}:${pad(dt.getMinutes())}:00`;
}
const requestPayload = {
video_id: video.video_id,
social_account_id: selectedAcc.id,
title: title.trim(),
description: description.trim(),
tags: tagsArray,
privacy_status: privacy,
scheduled_at: scheduledAt,
};
console.log('[Upload] Request payload:', requestPayload);
// Call upload API
const uploadResponse = await uploadToSocial(requestPayload);
if (!uploadResponse.success) {
throw new Error(uploadResponse.message || t('social.uploadStartFailed'));
}
if (publishTime === 'schedule') {
// 예약 업로드: 폴링 없이 바로 완료 처리
setUploadStatus('completed');
setUploadProgress(100);
onClose();
resetForm();
} else {
// 즉시 업로드: 완료될 때까지 폴링
const result = await waitForUploadComplete(
uploadResponse.upload_id,
(status, progress) => {
setUploadStatus(status as UploadStatus);
setUploadProgress(progress || 0);
}
);
// Upload completed successfully
setUploadStatus('completed');
setUploadProgress(100);
setUploadYoutubeUrl(result.platform_url);
// Close the posting modal (keep progress modal open)
onClose();
resetForm();
}
} catch (error) {
if (error instanceof TokenExpiredError) {
setShowUploadProgress(false);
alert(t('social.youtubeExpiredAlert'));
handleSocialReconnect(error.reconnectUrl);
return;
}
console.error('Upload failed:', error);
setUploadStatus('failed');
setUploadErrorMessage(error instanceof Error ? error.message : t('social.uploadFailed'));
} finally {
setIsPosting(false);
}
};
const resetForm = () => {
setTitle('');
setDescription('');
setTags('');
setPrivacy('public');
setPublishTime('now');
setScheduledDate(null);
setScheduledHour(12);
setScheduledMinute(0);
setSelectedChannel('');
setIsChannelDropdownOpen(false);
setIsPrivacyDropdownOpen(false);
};
const handleUploadProgressClose = () => {
setShowUploadProgress(false);
setUploadStatus('pending');
setUploadProgress(0);
setUploadYoutubeUrl(undefined);
setUploadErrorMessage(undefined);
setUploadVideoTitle('');
setUploadChannelName('');
};
const handleClose = () => {
onClose();
resetForm();
};
const selectedAccount = socialAccounts.find(acc => acc.platform_user_id === selectedChannel);
const privacyOptions = [
{ value: 'public', label: t('social.privacyPublic') },
{ value: 'unlisted', label: t('social.privacyUnlisted') },
{ value: 'private', label: t('social.privacyPrivate') }
];
const selectedPrivacyOption = privacyOptions.find(opt => opt.value === privacy);
// Always render UploadProgressModal, even when main modal is closed
const uploadProgressModalElement = (
<UploadProgressModal
isOpen={showUploadProgress}
onClose={handleUploadProgressClose}
status={uploadStatus}
progress={uploadProgress}
videoTitle={uploadVideoTitle || title || (video?.store_name || '')}
channelName={uploadChannelName || selectedAccount?.display_name || ''}
youtubeUrl={uploadYoutubeUrl}
errorMessage={uploadErrorMessage}
isScheduled={publishTime === 'schedule'}
/>
);
if (!isOpen || !video) {
// Still render upload progress modal even when main modal is closed
return showUploadProgress ? uploadProgressModalElement : null;
}
return (
<>
<div className="social-posting-overlay" onClick={handleClose}>
<div className="social-posting-modal" onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="social-posting-header">
<h2 className="social-posting-title">{t('social.title')}</h2>
<button className="social-posting-close" onClick={handleClose}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
{/* Content */}
<div className="social-posting-content">
{/* Left: Video Preview */}
<div className="social-posting-preview">
<div className="social-posting-video-container">
<video
src={video.result_movie_url}
className={`social-posting-video ${isHorizontalVideo ? 'horizontal' : 'vertical'}`}
controls
playsInline
onLoadedMetadata={(e) => {
const v = e.currentTarget;
setIsHorizontalVideo(v.videoWidth > v.videoHeight);
setVideoMeta({ width: v.videoWidth, height: v.videoHeight, duration: v.duration });
}}
/>
</div>
</div>
{/* Right: Form */}
<div className="social-posting-form">
{/* Video Info */}
{/* <div className="social-posting-video-info">
<span className="social-posting-label-badge">{t('social.postNumber')}</span>
<span className="social-posting-add-btn">+</span>
</div> */}
<div className="social-posting-video-meta">
<p className="social-posting-video-title">
{video.store_name} {new Date(video.created_at).toLocaleString('ko-KR')}
</p>
<p className="social-posting-video-specs">
{videoMeta
? `${videoMeta.width}×${videoMeta.height} · ${Math.floor(videoMeta.duration / 60)}:${String(Math.floor(videoMeta.duration % 60)).padStart(2, '0')}`
: t('social.videoSpecs')}
</p>
</div>
{/* Channel Selector - Custom Dropdown */}
<div className="social-posting-field">
<label className="social-posting-label">
{t('social.channelLabel')} <span className="required">*</span>
</label>
{isLoadingAccounts ? (
<div className="social-posting-loading">{t('social.loadingAccounts')}</div>
) : socialAccounts.length === 0 ? (
<div className="social-posting-no-accounts">
<p>{t('social.noAccounts')}</p>
<p className="social-posting-no-accounts-hint">{t('social.noAccountsHint')}</p>
</div>
) : (
<div className="social-posting-channel-dropdown" ref={channelDropdownRef}>
<button
type="button"
className={`social-posting-channel-trigger ${isChannelDropdownOpen ? 'open' : ''}`}
onClick={() => setIsChannelDropdownOpen(!isChannelDropdownOpen)}
>
<div className="social-posting-channel-selected">
{selectedAccount && (
<>
<img
src={getPlatformIcon(selectedAccount.platform)}
alt={selectedAccount.platform}
className="social-posting-channel-icon"
/>
<span>{selectedAccount.display_name}</span>
</>
)}
</div>
<svg className="social-posting-channel-arrow" viewBox="0 0 12 8" fill="none">
<path d="M1 1.5L6 6.5L11 1.5" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{isChannelDropdownOpen && (
<div className="social-posting-channel-menu">
{socialAccounts.map(account => (
<button
key={account.id}
type="button"
className={`social-posting-channel-option ${selectedChannel === account.platform_user_id ? 'selected' : ''}`}
onClick={() => {
setSelectedChannel(account.platform_user_id);
setIsChannelDropdownOpen(false);
}}
>
<img
src={getPlatformIcon(account.platform)}
alt={account.platform}
className="social-posting-channel-option-icon"
/>
<span>{account.display_name}</span>
</button>
))}
</div>
)}
</div>
)}
</div>
{/* Title */}
<div className="social-posting-field">
<label className="social-posting-label">
{t('social.postTitleLabel')} <span className="required">*</span>
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder={isLoadingAutoDescription ? t('social.autoSeoTitle') : t('social.postTitlePlaceholder')}
// placeholder={t('social.postTitlePlaceholder')}
className="social-posting-input"
maxLength={100}
disabled={isLoadingAutoDescription}
/>
<span className="social-posting-char-count">{title.length}/100</span>
</div>
{/* Description */}
<div className="social-posting-field">
<label className="social-posting-label">{t('social.postContentLabel')}</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder={t(isLoadingAutoDescription ? t('social.autoSeoDescription') : t('social.postContentPlaceholder'))}
// placeholder={t('social.postContentPlaceholder')}
className="social-posting-textarea"
maxLength={5000}
rows={4}
disabled={isLoadingAutoDescription}
/>
<span className="social-posting-char-count">{description.length}/5,000</span>
</div>
{/* Tags */}
<div className="social-posting-field">
<label className="social-posting-label">{t('social.tagsLabel')}</label>
<input
type="text"
value={tags}
onChange={(e) => setTags(e.target.value)}
placeholder={t(isLoadingAutoDescription ? t('social.autoSeoTags') : t('social.tagsPlaceholder'))}
// placeholder={t('social.tagsPlaceholder')}
className="social-posting-input"
maxLength={500}
disabled={isLoadingAutoDescription}
/>
<span className="social-posting-char-count">{tags.length}/500</span>
</div>
{/* Privacy - Custom Dropdown */}
<div className="social-posting-field">
<label className="social-posting-label">
{t('social.privacyLabel')} <span className="required">*</span>
</label>
<div className="social-posting-channel-dropdown" ref={privacyDropdownRef}>
<button
type="button"
className={`social-posting-channel-trigger ${isPrivacyDropdownOpen ? 'open' : ''}`}
onClick={() => setIsPrivacyDropdownOpen(!isPrivacyDropdownOpen)}
>
<div className="social-posting-channel-selected">
<span>{selectedPrivacyOption?.label}</span>
</div>
<svg className="social-posting-channel-arrow" viewBox="0 0 12 8" fill="none">
<path d="M1 1.5L6 6.5L11 1.5" stroke="rgba(255,255,255,0.5)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
{isPrivacyDropdownOpen && (
<div className="social-posting-channel-menu">
{privacyOptions.map(option => (
<button
key={option.value}
type="button"
className={`social-posting-channel-option ${privacy === option.value ? 'selected' : ''}`}
onClick={() => {
setPrivacy(option.value as PrivacyType);
setIsPrivacyDropdownOpen(false);
}}
>
<span>{option.label}</span>
</button>
))}
</div>
)}
</div>
</div>
{/* Publish Time */}
<div className="social-posting-field">
<label className="social-posting-label">
{t('social.publishTimeLabel')} <span className="required">*</span>
</label>
<div className="social-posting-radio-group">
<label className="social-posting-radio">
<input
type="radio"
name="publishTime"
value="now"
checked={publishTime === 'now'}
onChange={() => setPublishTime('now')}
/>
<span className="radio-label">{t('social.publishNow')}</span>
</label>
<label className="social-posting-radio">
<input
type="radio"
name="publishTime"
value="schedule"
checked={publishTime === 'schedule'}
onChange={() => setPublishTime('schedule')}
/>
<span className="radio-label">{t('social.publishSchedule')}</span>
</label>
</div>
{/* 시간 설정 UI */}
{publishTime === 'schedule' && (() => {
const now = new Date();
const isTodaySelected = scheduledDate
? scheduledDate.getFullYear() === now.getFullYear() &&
scheduledDate.getMonth() === now.getMonth() &&
scheduledDate.getDate() === now.getDate()
: false;
// 오늘 선택 시: 현재 시각 기준으로 선택 가능한 최소 시간 계산
// 현재 분을 10분 단위 올림하여 최소 분 결정
const nowMinute = now.getMinutes();
const minMinuteForCurrentHour = Math.ceil((nowMinute + 1) / 10) * 10;
const minHour = isTodaySelected
? (minMinuteForCurrentHour >= 60 ? now.getHours() + 1 : now.getHours())
: 0;
// 선택된 시간이 오늘 최소 시간보다 작으면 분 옵션 제한
const minMinuteForSelectedHour = isTodaySelected && scheduledHour === now.getHours()
? minMinuteForCurrentHour >= 60 ? 0 : minMinuteForCurrentHour
: 0;
const handleDateSelect = (date: Date) => {
const selectedIsToday =
date.getFullYear() === now.getFullYear() &&
date.getMonth() === now.getMonth() &&
date.getDate() === now.getDate();
setScheduledDate(date);
// 오늘을 선택했을 때 현재 설정된 시간이 과거라면 최솟값으로 보정
if (selectedIsToday) {
const minMin = Math.ceil((now.getMinutes() + 1) / 10) * 10;
const minH = minMin >= 60 ? now.getHours() + 1 : now.getHours();
const adjMin = minMin >= 60 ? 0 : minMin;
if (scheduledHour < minH || (scheduledHour === minH && scheduledMinute < adjMin)) {
setScheduledHour(minH);
setScheduledMinute(adjMin);
}
}
};
const handleHourChange = (h: number) => {
setScheduledHour(h);
// 시간 변경 시 분이 과거가 되면 최솟값으로 보정
if (isTodaySelected && h === now.getHours()) {
const minMin = Math.ceil((now.getMinutes() + 1) / 10) * 10;
const adjMin = minMin >= 60 ? 0 : minMin;
if (scheduledMinute < adjMin) setScheduledMinute(adjMin);
}
};
return (
<div className="schedule-datetime-picker">
<MiniCalendar
selectedDate={scheduledDate}
onSelect={handleDateSelect}
minDate={new Date()}
/>
<div className="schedule-time-picker">
<span className="schedule-time-label"> </span>
<div className="schedule-time-selects">
<select
className="schedule-time-select"
value={scheduledHour}
onChange={(e) => handleHourChange(Number(e.target.value))}
>
{Array.from({ length: 24 }, (_, i) => (
<option key={i} value={i} disabled={isTodaySelected && i < minHour}>
{String(i).padStart(2, '0')}
</option>
))}
</select>
<select
className="schedule-time-select"
value={scheduledMinute}
onChange={(e) => setScheduledMinute(Number(e.target.value))}
>
{[0, 10, 20, 30, 40, 50].map(m => (
<option key={m} value={m} disabled={isTodaySelected && m < minMinuteForSelectedHour}>
{String(m).padStart(2, '0')}
</option>
))}
</select>
</div>
{scheduledDate && (
<p className="schedule-datetime-preview">
{scheduledDate.getFullYear()}.{String(scheduledDate.getMonth()+1).padStart(2,'0')}.{String(scheduledDate.getDate()).padStart(2,'0')} {String(scheduledHour).padStart(2,'0')}:{String(scheduledMinute).padStart(2,'0')}
</p>
)}
</div>
</div>
);
})()}
</div>
</div>
</div>
{/* Footer */}
<div className="social-posting-footer">
<p className="social-posting-footer-note">
{t('social.footerNote', { link: '' })}<a href="#" className="social-posting-link">{t('social.footerNoteLink')}</a>
</p>
<div className="social-posting-actions">
<button
className="social-posting-btn cancel"
onClick={handleClose}
disabled={isPosting}
>
{t('social.cancel')}
</button>
<button
className="social-posting-btn submit"
onClick={handlePost}
disabled={
isPosting ||
socialAccounts.length === 0 ||
!title.trim() ||
(publishTime === 'schedule' && !scheduledDate)
}
>
{isPosting ? t('social.posting') : t('social.post')}
</button>
</div>
</div>
</div>
</div>
{uploadProgressModalElement}
</>
);
};
export default SocialPostingModal;