feat: daily timeline advanced UI and calendar integration

This commit is contained in:
2026-06-03 04:08:13 -05:00
parent e944594e06
commit ad8622e243
34 changed files with 2088 additions and 788 deletions
+39 -5
View File
@@ -1,9 +1,10 @@
import React, { useCallback } from 'react';
import { Film, Volume2, Music, X, Upload, Wand2, Maximize2, Minimize2, Move, Pipette } from 'lucide-react';
import { DesignMD } from '../../types';
import { DesignMD, CompanyProfile } from '../../types';
import { FileDropZone } from '../ui/FileDropZone';
interface BrandTabMediaProps {
company: CompanyProfile;
designMD: DesignMD;
handleDesignChange: (key: keyof DesignMD, value: string | number | string[] | boolean) => void;
}
@@ -15,7 +16,7 @@ interface BrandTabMediaProps {
* All positioning, fit, duration, and blend controls live in the TemplateBuilder
* (per-template segment configuration), avoiding collisions.
*/
export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type: keyof DesignMD, url: string) => void }> = ({ designMD, handleDesignChange, onEditAsset }) => {
export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type: keyof DesignMD, url: string) => void }> = ({ company, designMD, handleDesignChange, onEditAsset }) => {
/** Auto-detect video duration and store it in DesignMD (for BrandPreview playback) */
const probeVideoDuration = useCallback((url: string, key: 'introDurationFrames' | 'outroDurationFrames') => {
@@ -48,6 +49,7 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
{/* ═══ Intro Video ═══ */}
<VideoUploadSimple
company={company}
label="Video de Cabezote (Intro)"
description="Se usará automáticamente en plantillas que incluyan segmento de intro de marca"
videoUrl={designMD.introVideoUrl || ''}
@@ -70,6 +72,7 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
{/* ═══ Outro Video ═══ */}
<VideoUploadSimple
company={company}
label="Video de Cierre (Outro)"
description="Se usará automáticamente en plantillas que incluyan segmento de outro de marca"
videoUrl={designMD.outroVideoUrl || ''}
@@ -144,10 +147,25 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
accept="audio/*"
label="Subir audio"
onFiles={async (files) => {
let workspacePath = '';
if (window.electronAPI) {
workspacePath = await window.electronAPI.fs.getWorkspacePath();
}
const formData = new FormData();
formData.append('file', files[0]);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
let res;
if (workspacePath && company.id) {
formData.append('brandId', company.id);
formData.append('workspacePath', workspacePath);
res = await fetch('/api/upload/brand', { method: 'POST', body: formData });
} else {
res = await fetch('/api/upload', { method: 'POST', body: formData });
}
if (!res.ok) throw new Error('Upload failed');
const data = await res.json();
if (data.url) handleDesignChange('brandAudioUrl', data.url);
} catch (err) {
@@ -184,6 +202,7 @@ export const BrandTabMedia: React.FC<BrandTabMediaProps & { onEditAsset?: (type:
/* ── Simple Video Upload Card ── */
const VideoUploadSimple: React.FC<{
company: CompanyProfile;
label: string;
description: string;
videoUrl: string;
@@ -196,7 +215,7 @@ const VideoUploadSimple: React.FC<{
onFitChange?: (fit: 'cover' | 'contain' | 'fill') => void;
bgColor?: string | null;
onBgColorChange?: (color: string | null) => void;
}> = ({ label, description, videoUrl, accentColor, onUrlChange, onClear, onEdit, showEdit, fit = 'cover', onFitChange, bgColor, onBgColorChange }) => {
}> = ({ company, label, description, videoUrl, accentColor, onUrlChange, onClear, onEdit, showEdit, fit = 'cover', onFitChange, bgColor, onBgColorChange }) => {
const hasVideo = !!videoUrl && videoUrl.trim().length > 0;
const colorInputRef = React.useRef<HTMLInputElement>(null);
@@ -258,10 +277,25 @@ const VideoUploadSimple: React.FC<{
accept="video/*"
label="Subir archivo"
onFiles={async (files) => {
let workspacePath = '';
if (window.electronAPI) {
workspacePath = await window.electronAPI.fs.getWorkspacePath();
}
const formData = new FormData();
formData.append('file', files[0]);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
let res;
if (workspacePath && company.id) {
formData.append('brandId', company.id);
formData.append('workspacePath', workspacePath);
res = await fetch('/api/upload/brand', { method: 'POST', body: formData });
} else {
res = await fetch('/api/upload', { method: 'POST', body: formData });
}
if (!res.ok) throw new Error('Upload failed');
const data = await res.json();
if (data.url) onUrlChange(data.url);
} catch (err) {