| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028 |
- import * as React from 'react';
- // FIX: Import FormBlock and related types to handle form creation and editing.
- import { Block, BlockType, LinkBlock, HeaderBlock, SocialBlock, VideoBlock, ImageBlock, TextBlock, MapBlock, PdfBlock, SocialLink, MediaSource, EmailBlock, PhoneBlock, AIGCVideo, AIGCArticle, NewsBlock, NewsItemFromUrl, ProductBlock, ProductItem, SocialPlatform, ChatBlock, EnterpriseInfoBlock, EnterpriseInfoItem, EnterpriseInfoIcon, FormBlock, FormField, FormFieldId, FormPurposeOption, AwardBlock, AwardItem, FooterBlock, FooterLink } from '../types';
- import { Icon } from './ui/Icon';
- import { parseProductUrl, mockProductDatabase } from '../services/geminiService';
- const blockTypeMetadata: Record<BlockType, { icon: JSX.Element; name: string }> = {
- link: { icon: <path d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />, name: 'Link' },
- header: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m-4-16v16m8-16v16M4 4h16M4 20h16" />, name: 'Header' },
- text: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h7" />, name: 'Text' },
- social: { icon: <path d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />, name: 'Social Icons' },
- chat: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />, name: 'Chat Button' },
- enterprise_info: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h6.375a.375.375 0 01.375.375v1.5a.375.375 0 01-.375.375H9a.375.375 0 01-.375-.375v-1.5A.375.375 0 019 6.75zM9 12.75h6.375a.375.375 0 01.375.375v1.5a.375.375 0 01-.375.375H9a.375.375 0 01-.375-.375v-1.5A.375.375 0 019 12.75z" />, name: 'Enterprise Info'},
- video: { icon: <path d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />, name: 'Video' },
- image: { icon: <path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />, name: 'Image' },
- map: { icon: <path d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-1.447-.894L15 9m-6 3l6-3" />, name: 'Map' },
- pdf: { icon: <path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />, name: 'PDF' },
- email: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />, name: 'Email' },
- phone: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />, name: 'Phone' },
- news: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3h3m-3 4h3m-3 4h3" />, name: 'News' },
- product: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />, name: 'Product' },
- award: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M9 21h6m-3-4v4m-3-11h6m-3 0V4M3 11h18M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />, name: 'Awards' },
- form: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />, name: 'Form' },
- footer: { icon: <path strokeLinecap="round" strokeLinejoin="round" d="M5 21V3m0 18h14V3m-7 18v-8" />, name: 'Footer' },
- };
- const AddBlockModal: React.FC<{ onSelect: (type: BlockType) => void; onClose: () => void; hasChatBlock: boolean; hasFooterBlock: boolean; }> = ({ onSelect, onClose, hasChatBlock, hasFooterBlock }) => {
- return (
- <div className="fixed inset-0 bg-black/70 z-50 flex items-center justify-center backdrop-blur-sm" onClick={onClose}>
- <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-2xl p-8 border border-gray-200 dark:border-gray-700" onClick={e => e.stopPropagation()}>
- <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6">Add Block</h2>
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
- {(Object.keys(blockTypeMetadata) as BlockType[]).map(type => {
- const isDisabled = (type === 'chat' && hasChatBlock) || (type === 'footer' && hasFooterBlock);
- return (
- <button key={type} onMouseDown={(e) => e.preventDefault()} onClick={() => !isDisabled && onSelect(type)} className={`flex flex-col items-center justify-center p-4 bg-gray-100 dark:bg-gray-800 rounded-lg transition-colors text-gray-700 dark:text-gray-300 aspect-square ${isDisabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-200 dark:hover:bg-gray-700 hover:text-brand-primary'}`}>
- <Icon className="h-8 w-8 mb-2">{blockTypeMetadata[type].icon}</Icon>
- <span className="text-sm font-semibold">{blockTypeMetadata[type].name}</span>
- </button>
- )
- })}
- </div>
- </div>
- </div>
- );
- };
- const LayoutToggle: React.FC<{ label: string; options: {label: string, value: string}[]; value: string; onLayoutChange: (value: any) => void; }> = ({ label, options, value, onLayoutChange }) => (
- <div>
- <label className="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2 block">{label}</label>
- <div className="flex items-center gap-1 bg-gray-200 dark:bg-gray-700 p-1 rounded-lg">
- {options.map(opt => (
- <button key={opt.value} onMouseDown={(e) => e.preventDefault()} onClick={() => onLayoutChange(opt.value)} className={`flex-1 px-3 py-1 text-sm rounded-md capitalize transition-colors ${value === opt.value ? 'bg-brand-primary text-white shadow' : 'hover:bg-gray-300 dark:hover:bg-gray-600'}`}>{opt.label}</button>
- ))}
- </div>
- </div>
- );
- const SourceTypeToggle: React.FC<{ isVideo: boolean, type: 'url' | 'file' | 'aigc'; onTypeChange: (type: any) => void; onAIGCOpen: () => void; }> = ({ isVideo, type, onTypeChange, onAIGCOpen }) => (
- <div className="flex items-center gap-2">
- <label className="text-sm font-semibold text-gray-600 dark:text-gray-300">Source:</label>
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => onTypeChange('url')} className={`px-3 py-1 text-sm rounded-md ${type === 'url' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>URL</button>
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => onTypeChange('file')} className={`px-3 py-1 text-sm rounded-md ${type === 'file' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>Upload</button>
- {isVideo && <button onMouseDown={(e) => e.preventDefault()} onClick={onAIGCOpen} className={`px-3 py-1 text-sm rounded-md ${type === 'aigc' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>AIGC Library</button>}
- </div>
- );
- const AIGCNewsLibraryModal: React.FC<{
- articles: AIGCArticle[];
- selectedArticleIds: string[];
- onClose: () => void;
- onSave: (selectedIds: string[]) => void;
- }> = ({ articles, selectedArticleIds, onClose, onSave }) => {
- const [localSelected, setLocalSelected] = React.useState(selectedArticleIds);
- const toggleSelection = (id: string) => {
- setLocalSelected(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]);
- };
- return (
- <div className="fixed inset-0 bg-black/70 z-50 flex items-center justify-center backdrop-blur-sm" onClick={onClose}>
- <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-4xl h-[80vh] p-8 border border-gray-200 dark:border-gray-700 flex flex-col" onClick={e => e.stopPropagation()}>
- <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6 flex-shrink-0">Select News Articles</h2>
- <div className="flex-1 overflow-y-auto pr-4 -mr-4">
- {articles.length === 0 ? (
- <p className="text-center text-gray-400 dark:text-gray-500 py-12">No articles in your library. Create some in the AIGC News Creator!</p>
- ) : (
- <div className="space-y-3">
- {articles.map(article => (
- <button key={article.id} onClick={() => toggleSelection(article.id)} className={`w-full text-left p-4 rounded-lg border transition-colors flex items-center gap-4 ${localSelected.includes(article.id) ? 'bg-brand-primary/10 border-brand-primary' : 'bg-gray-100 dark:bg-gray-900/50 border-gray-200 dark:border-gray-700 hover:border-gray-400'}`}>
- <div className={`w-5 h-5 rounded-sm flex-shrink-0 flex items-center justify-center border-2 ${localSelected.includes(article.id) ? 'bg-brand-primary border-brand-primary' : 'bg-white dark:bg-gray-800 border-gray-300'}`}>
- {localSelected.includes(article.id) && <Icon className="h-4 w-4 text-white"><path d="M5 13l4 4L19 7" /></Icon>}
- </div>
- <div className="flex-1 min-w-0">
- <p className="font-semibold truncate">{article.title}</p>
- <p className="text-sm text-gray-500 dark:text-gray-400 truncate">{article.summary}</p>
- </div>
- </button>
- ))}
- </div>
- )}
- </div>
- <div className="flex justify-end gap-4 mt-6 flex-shrink-0">
- <button onClick={onClose} className="px-4 py-2 rounded-md font-semibold bg-gray-200 dark:bg-gray-700">Cancel</button>
- <button onClick={() => onSave(localSelected)} className="px-4 py-2 rounded-md font-semibold bg-brand-primary text-white">Save Selection</button>
- </div>
- </div>
- </div>
- );
- };
- const SparkleIcon = () => (
- <Icon className="h-4 w-4 text-brand-primary">
- <path strokeLinecap="round" strokeLinejoin="round" d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09zM18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456zM18 15.75l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 20l-1.035.259a3.375 3.375 0 00-2.456 2.456L18 23.75l-.259-1.035a3.375 3.375 0 00-2.456-2.456L14.25 20l1.035-.259a3.375 3.375 0 002.456-2.456L18 15.75z" />
- </Icon>
- );
- const BlockItemEditor: React.FC<{ block: Block; onUpdate: (updatedBlock: Block) => void; onAIGCOpen: () => void, aigcVideos: AIGCVideo[]; aigcArticles: AIGCArticle[] }> = ({ block, onUpdate, onAIGCOpen, aigcVideos, aigcArticles }) => {
- const inputClasses = "w-full bg-gray-100 dark:bg-gray-700 p-2 rounded-md border-2 border-gray-300 dark:border-gray-600 focus:outline-none focus:border-brand-primary";
- const labelClasses = "text-sm font-semibold text-gray-600 dark:text-gray-300";
-
- const handleUrlBlur = (e: React.FocusEvent<HTMLInputElement>) => {
- const { value: url } = e.target;
- const linkBlock = block as LinkBlock;
- if (!url || !url.startsWith('http')) {
- onUpdate({ ...linkBlock, iconUrl: undefined, url });
- return;
- }
- try {
- const domain = new URL(url).hostname;
- const iconUrl = `https://www.google.com/s2/favicons?domain=${domain}&sz=64`;
- onUpdate({ ...linkBlock, iconUrl, url });
- } catch (error) {
- console.error("Invalid URL for favicon:", error);
- onUpdate({ ...linkBlock, iconUrl: undefined, url });
- }
- };
- switch (block.type) {
- case 'header': {
- const headerBlock = block as HeaderBlock;
- return <div className="space-y-4">
- <div><label className={labelClasses}>Header Text</label><input type="text" value={headerBlock.text} onChange={e => onUpdate({ ...block, text: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- <div><LayoutToggle label="Alignment" options={[{label: 'Left', value: 'left'}, {label: 'Center', value: 'center'}]} value={headerBlock.titleAlignment || 'left'} onLayoutChange={align => onUpdate({ ...headerBlock, titleAlignment: align })} /></div>
- </div>
- }
- case 'link': {
- const linkBlock = block as LinkBlock;
- return <div className="space-y-4">
- <div><label className={labelClasses}>Title</label><input type="text" value={linkBlock.title} onChange={e => onUpdate({ ...block, title: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- <div><label className={labelClasses}>URL</label><input type="url" defaultValue={linkBlock.url} onBlur={handleUrlBlur} className={`${inputClasses} mt-1`} /></div>
- <div><label className={labelClasses}>Thumbnail URL (Optional)</label><input type="url" placeholder="https://..." value={linkBlock.thumbnailUrl || ''} onChange={e => onUpdate({ ...block, thumbnailUrl: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- </div>
- }
- case 'chat': {
- const chatBlock = block as ChatBlock;
- return (
- <div className="space-y-4">
- <LayoutToggle
- label="Display Style"
- options={[
- {label: 'Under Avatar', value: 'under_avatar'},
- {label: 'Block Button', value: 'button'},
- {label: 'Floating Widget', value: 'widget'}
- ]}
- value={chatBlock.layout}
- onLayoutChange={(layout: 'button' | 'under_avatar' | 'widget') => onUpdate({ ...chatBlock, layout })}
- />
- <p className="text-xs text-gray-500 dark:text-gray-400 -mt-2">
- <strong>Under Avatar:</strong> Only visible in Personal mode.<br/>
- <strong>Floating Widget:</strong> Recommended for Enterprise mode.<br/>
- <strong>Block Button:</strong> A standard button for any mode.
- </p>
- </div>
- )
- }
- case 'enterprise_info': {
- const infoBlock = block as EnterpriseInfoBlock;
- const availableIcons: { value: EnterpriseInfoIcon; label: string }[] = [
- { value: 'building', label: 'Building' },
- { value: 'bank', label: 'Bank' },
- { value: 'money', label: 'Money' },
- { value: 'location', label: 'Location' },
- { value: 'calendar', label: 'Calendar' },
- { value: 'users', label: 'Users' },
- { value: 'lightbulb', label: 'Lightbulb' },
- ];
-
- const handleUpdateItem = (id: string, field: 'icon' | 'label' | 'value', value: string) => {
- const updatedItems = infoBlock.items.map(item => item.id === id ? { ...item, [field]: value } : item);
- onUpdate({ ...infoBlock, items: updatedItems });
- };
- const handleAddItem = () => {
- const newItem: EnterpriseInfoItem = {
- id: `ei-${Date.now()}`,
- icon: 'lightbulb',
- label: 'New Item',
- value: 'Value'
- };
- onUpdate({ ...infoBlock, items: [...infoBlock.items, newItem] });
- };
- const handleRemoveItem = (id: string) => {
- onUpdate({ ...infoBlock, items: infoBlock.items.filter(item => item.id !== id) });
- };
- return (
- <div className="space-y-4">
- <LayoutToggle
- label="Alignment"
- options={[
- {label: 'Left', value: 'left'},
- {label: 'Center', value: 'center'},
- ]}
- value={infoBlock.alignment || 'left'}
- onLayoutChange={(align: 'left' | 'center') => onUpdate({ ...infoBlock, alignment: align })}
- />
- {infoBlock.items.map(item => (
- <div key={item.id} className="p-3 bg-gray-100 dark:bg-gray-900 rounded-md space-y-2 relative">
- <button onClick={() => handleRemoveItem(item.id)} className="absolute top-2 right-2 text-gray-400 hover:text-red-500"><Icon className="h-4 w-4"><path d="M6 18L18 6M6 6l12 12" /></Icon></button>
- <div className="grid grid-cols-1 md:grid-cols-3 gap-2">
- <div>
- <label className="text-xs font-semibold text-gray-500">Icon</label>
- <select value={item.icon} onChange={e => handleUpdateItem(item.id, 'icon', e.target.value)} className="w-full capitalize bg-white dark:bg-gray-700 p-1 text-sm rounded border border-gray-300 dark:border-gray-600">
- {availableIcons.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
- </select>
- </div>
- <div className="md:col-span-2">
- <label className="text-xs font-semibold text-gray-500">Label</label>
- <input type="text" value={item.label} onChange={e => handleUpdateItem(item.id, 'label', e.target.value)} className="w-full bg-white dark:bg-gray-700 p-1 text-sm rounded border border-gray-300 dark:border-gray-600" />
- </div>
- </div>
- <div>
- <label className="text-xs font-semibold text-gray-500">Value</label>
- <input type="text" value={item.value} onChange={e => handleUpdateItem(item.id, 'value', e.target.value)} className="w-full bg-white dark:bg-gray-700 p-1 text-sm rounded border border-gray-300 dark:border-gray-600" />
- </div>
- </div>
- ))}
- <button onClick={handleAddItem} className="w-full justify-center p-2 bg-brand-primary/10 rounded-md text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1">
- <Icon className="h-4 w-4"><path d="M12 4v16m8-8H4" /></Icon>Add Info Item
- </button>
- </div>
- )
- }
- case 'social': {
- const socialBlock = block as SocialBlock;
- const availablePlatforms: SocialPlatform[] = ['twitter', 'instagram', 'facebook', 'linkedin', 'youtube', 'tiktok', 'github'];
- const handleAddLink = () => {
- const usedPlatforms = new Set(socialBlock.links.map(l => l.platform));
- const nextPlatform = availablePlatforms.find(p => !usedPlatforms.has(p)) || 'twitter';
- const newLink: SocialLink = { id: `sl-${Date.now()}`, platform: nextPlatform, url: '' };
- onUpdate({ ...socialBlock, links: [...socialBlock.links, newLink] });
- };
- const handleAddMockLinks = () => {
- const mockLinks: SocialLink[] = [
- { id: 'sl-mock-twitter', platform: 'twitter', url: 'https://twitter.com/johndoe' },
- { id: 'sl-mock-github', platform: 'github', url: 'https://github.com/johndoe' },
- { id: 'sl-mock-linkedin', platform: 'linkedin', url: 'https://linkedin.com/in/johndoe' },
- ];
- const existingPlatforms = new Set(socialBlock.links.map(l => l.platform));
- const newMockLinks = mockLinks.filter(ml => !existingPlatforms.has(ml.platform));
- onUpdate({ ...socialBlock, links: [...socialBlock.links, ...newMockLinks] });
- };
- const handleRemoveLink = (linkId: string) => {
- onUpdate({ ...socialBlock, links: socialBlock.links.filter(l => l.id !== linkId) });
- };
- const handleUpdateLink = (linkId: string, field: 'platform' | 'url', value: string) => {
- onUpdate({ ...socialBlock, links: socialBlock.links.map(l => l.id === linkId ? { ...l, [field]: value } as SocialLink : l) });
- };
-
- return <div className="space-y-3">
- {socialBlock.links.map(link =>
- <div key={link.id} className="flex items-center gap-2">
- <select value={link.platform} onChange={e => handleUpdateLink(link.id, 'platform', e.target.value)} className="capitalize bg-gray-100 dark:bg-gray-700 p-2 rounded-md border-2 border-gray-300 dark:border-gray-600">
- {availablePlatforms.map(p => <option key={p} value={p}>{p}</option>)}
- </select>
- <input type="url" value={link.url} onChange={e => handleUpdateLink(link.id, 'url', e.target.value)} placeholder="https://..." className={`flex-1 ${inputClasses}`} />
- <button onClick={() => handleRemoveLink(link.id)} className="text-gray-400 hover:text-red-500"><Icon className="h-5 w-5"><path d="M6 18L18 6M6 6l12 12" /></Icon></button>
- </div>
- )}
- <div className="flex gap-4 pt-2">
- <button onClick={handleAddLink} className="text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1">
- <Icon className="h-4 w-4"><path d="M12 4v16m8-8H4" /></Icon>Add Link
- </button>
- <button onClick={handleAddMockLinks} className="text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1">
- <SparkleIcon />Add Mock Links
- </button>
- </div>
- </div>
- }
- case 'image':
- case 'video': {
- const mediaBlock = block as ImageBlock | VideoBlock;
- const addMedia = () => {
- const newSource: MediaSource = mediaBlock.type === 'image' ? { type: 'url', value: '' } : { type: 'url', value: '' };
- onUpdate({ ...mediaBlock, sources: [...mediaBlock.sources, newSource] });
- };
- const updateMediaSource = (index: number, newSource: MediaSource) => onUpdate({ ...mediaBlock, sources: mediaBlock.sources.map((s, i) => i === index ? newSource : s) });
- const removeMedia = (index: number) => onUpdate({ ...mediaBlock, sources: mediaBlock.sources.filter((_, i) => i !== index) });
- const displayClasses = "w-full text-sm p-2 bg-gray-100 dark:bg-gray-700 rounded-md border-2 border-gray-300 dark:border-gray-600";
-
- const handleAddMockMedia = () => {
- const newSources: MediaSource[] = Array.from({ length: 3 }).map((_, i) => {
- if (mediaBlock.type === 'image') {
- return { type: 'url', value: `https://picsum.photos/seed/${Date.now() + i}/400/300` };
- } else {
- const mockVideoUrls = [
- 'https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4',
- 'https://storage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4',
- 'https://storage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4'
- ];
- return { type: 'url', value: mockVideoUrls[i % mockVideoUrls.length] };
- }
- });
- onUpdate({ ...mediaBlock, sources: [...mediaBlock.sources, ...newSources] });
- };
- return <div className="space-y-6">
- <LayoutToggle label="Layout Mode" options={[{label: 'Single Column', value: 'single'}, {label: 'Grid', value: 'grid'}]} value={mediaBlock.layout} onLayoutChange={layout => onUpdate({ ...mediaBlock, layout })} />
- <div className="space-y-4">
- {mediaBlock.sources.map((source, index) => {
- const videoFromAIGC = source.type === 'aigc' ? aigcVideos.find(v => v.id === source.videoId) : null;
- return (
- <div key={index} className="bg-gray-100 dark:bg-gray-900 p-4 rounded-lg space-y-3 border border-gray-200 dark:border-gray-700">
- <div className="flex justify-between items-center">
- <SourceTypeToggle isVideo={mediaBlock.type === 'video'} type={source.type} onTypeChange={type => {
- const newSource: MediaSource = type === 'url' ? { type: 'url', value: '' } : { type: 'file', value: { name: 'example.file', size: 12345, previewUrl: `https://picsum.photos/seed/upload-${Date.now()}/200` } };
- updateMediaSource(index, newSource);
- }} onAIGCOpen={onAIGCOpen} />
- <button onMouseDown={(e) => e.preventDefault()} onClick={() => removeMedia(index)} className="text-gray-400 hover:text-red-500 flex-shrink-0"><Icon className="h-5 w-5"><path d="M6 18L18 6M6 6l12 12" /></Icon></button>
- </div>
-
- {source.type === 'url' ?
- <input type="url" value={source.value} onChange={e => updateMediaSource(index, { ...source, value: e.target.value })} className={inputClasses} placeholder="https://..." /> :
- source.type === 'file' ?
- <div className={displayClasses}>Simulated Upload: {source.value.name}</div> :
- videoFromAIGC ?
- <div className={`${displayClasses} flex items-center gap-2`}>
- <img src={videoFromAIGC.thumbnailUrl} className="h-8 w-12 rounded-sm object-cover" />
- <span>{videoFromAIGC.title}</span>
- </div> :
- <div className="text-sm p-2 bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200 rounded-md">AIGC video not found.</div>
- }
- </div>
- );
- })}
- </div>
- <div className="flex gap-4">
- <button onClick={addMedia} className="text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1">
- <Icon className="h-4 w-4"><path d="M12 4v16m8-8H4" /></Icon>Add {mediaBlock.type}
- </button>
- <button onClick={handleAddMockMedia} className="text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1">
- <SparkleIcon />Add 3 Mock {mediaBlock.type === 'image' ? 'Images' : 'Videos'}
- </button>
- </div>
- </div>
- }
- case 'text': {
- const textBlock = block as TextBlock;
- return <div className="space-y-4">
- <div className="flex items-center gap-2 p-2 bg-gray-100 dark:bg-gray-800 rounded-md">
- <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => onUpdate({ ...textBlock, textAlign: 'left' })} className={`p-2 rounded ${textBlock.textAlign === 'left' ? 'bg-brand-primary text-white' : ''}`}><Icon className="h-4 w-4"><path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12" /></Icon></button>
- <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => onUpdate({ ...textBlock, textAlign: 'center' })} className={`p-2 rounded ${textBlock.textAlign === 'center' ? 'bg-brand-primary text-white' : ''}`}><Icon className="h-4 w-4"><path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></Icon></button>
- <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => onUpdate({ ...textBlock, textAlign: 'right' })} className={`p-2 rounded ${textBlock.textAlign === 'right' ? 'bg-brand-primary text-white' : ''}`}><Icon className="h-4 w-4"><path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M12 17.25h8.25" /></Icon></button>
- <div className="h-6 border-l border-gray-300 dark:border-gray-600 mx-2" />
- <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => onUpdate({ ...textBlock, isBold: !textBlock.isBold })} className={`p-2 rounded ${textBlock.isBold ? 'bg-brand-primary text-white' : ''}`}><strong>B</strong></button>
- <button type="button" onMouseDown={(e) => e.preventDefault()} onClick={() => onUpdate({ ...textBlock, isItalic: !textBlock.isItalic })} className={`p-2 rounded ${textBlock.isItalic ? 'bg-brand-primary text-white' : ''}`}><em>I</em></button>
- <div className="h-6 border-l border-gray-300 dark:border-gray-600 mx-2" />
- <input type="text" value={textBlock.fontSize} onChange={e => onUpdate({ ...textBlock, fontSize: e.target.value })} className="w-16 p-1 text-sm bg-white dark:bg-gray-700 rounded" placeholder="16px" />
- <input type="color" value={textBlock.fontColor} onChange={e => onUpdate({ ...textBlock, fontColor: e.target.value })} className="h-8 w-8 rounded-md" />
- </div>
- <textarea value={textBlock.content} onChange={e => onUpdate({ ...textBlock, content: e.target.value })} rows={5} className={inputClasses} placeholder="Enter your text here..." />
- </div>
- }
- case 'map': {
- const mapBlock = block as MapBlock;
- const displayStyle = mapBlock.displayStyle || 'interactiveMap';
- const bgSource = mapBlock.backgroundImageSource;
- return <div className="space-y-4">
- <div>
- <label className={labelClasses}>Address or Location</label>
- <input type="text" value={mapBlock.address} onChange={e => onUpdate({ ...block, address: e.target.value })} className={`${inputClasses} mt-1`} placeholder="e.g., Eiffel Tower, Paris" />
- </div>
- <LayoutToggle
- label="Display Style"
- options={[{label: 'Map Preview', value: 'interactiveMap'}, {label: 'Image Overlay', value: 'imageOverlay'}]}
- value={displayStyle}
- onLayoutChange={style => onUpdate({ ...mapBlock, displayStyle: style })}
- />
- {displayStyle === 'imageOverlay' && (
- <div className="bg-gray-100 dark:bg-gray-900 p-4 rounded-lg space-y-3 border border-gray-200 dark:border-gray-700">
- <h4 className="text-sm font-semibold text-gray-600 dark:text-gray-300">Background Image</h4>
- <SourceTypeToggle
- isVideo={false}
- type={bgSource?.type || 'url'}
- onTypeChange={type => {
- const newSource: MediaSource = type === 'url' ? { type: 'url', value: '' } : { type: 'file', value: { name: 'background.jpg', size: 12345, previewUrl: `https://picsum.photos/seed/mapbg-${Date.now()}/400` } };
- onUpdate({ ...mapBlock, backgroundImageSource: newSource });
- }}
- onAIGCOpen={() => {}}
- />
- {bgSource?.type === 'url' &&
- <input type="url" value={bgSource.value} onChange={e => onUpdate({ ...mapBlock, backgroundImageSource: { type: 'url', value: e.target.value } })} className={inputClasses} placeholder="https://..."/>
- }
- {bgSource?.type === 'file' &&
- <div className="w-full text-sm p-2 bg-gray-100 dark:bg-gray-700 rounded-md border-2 border-gray-300 dark:border-gray-600">Simulated Upload: {bgSource.value.name}</div>
- }
- </div>
- )}
- </div>
- }
- case 'pdf': {
- const pdfBlock = block as PdfBlock;
- const source = pdfBlock.source;
- const displayClasses = "w-full text-sm p-2 bg-gray-100 dark:bg-gray-700 rounded-md border-2 border-gray-300 dark:border-gray-600";
- return <div className="bg-gray-100 dark:bg-gray-900 p-4 rounded-lg space-y-3 border border-gray-200 dark:border-gray-700">
- <SourceTypeToggle isVideo={false} type={source.type} onTypeChange={type => {
- const newSource: MediaSource = type === 'url' ? { type: 'url', value: '' } : { type: 'file', value: { name: 'document.pdf', size: 123456, previewUrl: `https://picsum.photos/seed/pdf-${Date.now()}/200/280` } };
- onUpdate({ ...pdfBlock, source: newSource });
- }} onAIGCOpen={() => {}} />
- {source.type === 'url' &&
- <input type="url" value={source.value} onChange={e => onUpdate({ ...pdfBlock, source: { type: 'url', value: e.target.value } })} className={inputClasses} placeholder="https://example.com/document.pdf"/>
- }
- {source.type === 'file' &&
- <div className={displayClasses}>Simulated Upload: {source.value.name}</div>
- }
- </div>
- }
- case 'email': {
- const emailBlock = block as EmailBlock;
- return <div className="space-y-4">
- <div><label className={labelClasses}>Button Label</label><input type="text" value={emailBlock.label} onChange={e => onUpdate({ ...emailBlock, label: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- <div><label className={labelClasses}>Email Address</label><input type="email" value={emailBlock.email} onChange={e => onUpdate({ ...emailBlock, email: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- <LayoutToggle label="Display Mode" options={[{label: 'Label Only', value: 'labelOnly'}, {label: 'Label + Email', value: 'labelAndValue'}]} value={emailBlock.displayMode} onLayoutChange={mode => onUpdate({ ...emailBlock, displayMode: mode })} />
- </div>
- }
- case 'phone': {
- const phoneBlock = block as PhoneBlock;
- return <div className="space-y-4">
- <div><label className={labelClasses}>Button Label</label><input type="text" value={phoneBlock.label} onChange={e => onUpdate({ ...phoneBlock, label: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- <div><label className={labelClasses}>Phone Number</label><input type="tel" value={phoneBlock.phone} onChange={e => onUpdate({ ...phoneBlock, phone: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- <LayoutToggle label="Display Mode" options={[{label: 'Label Only', value: 'labelOnly'}, {label: 'Label + Phone', value: 'labelAndValue'}]} value={phoneBlock.displayMode} onLayoutChange={mode => onUpdate({ ...phoneBlock, displayMode: mode })} />
- </div>
- }
- case 'news': {
- const newsBlock = block as NewsBlock;
- const [isNewsSelectorOpen, setIsNewsSelectorOpen] = React.useState(false);
- const selectedArticles = (newsBlock.source === 'aigc' && newsBlock.articleIds) ? aigcArticles.filter(a => newsBlock.articleIds.includes(a.id)) : [];
- const handleSaveSelection = (selectedIds: string[]) => {
- const { customItems, ...rest } = newsBlock;
- onUpdate({ ...rest, source: 'aigc', articleIds: selectedIds });
- setIsNewsSelectorOpen(false);
- };
-
- const handleSourceChange = (newSource: 'aigc' | 'custom') => {
- if (newSource === 'aigc') {
- const { customItems, ...rest } = newsBlock;
- onUpdate({ ...rest, source: 'aigc', articleIds: [] });
- } else {
- const { articleIds, ...rest } = newsBlock;
- onUpdate({ ...rest, source: 'custom', customItems: [] });
- }
- };
-
- const handleUpdateCustomItem = (itemId: string, field: 'title' | 'summary' | 'url', value: string) => {
- if(newsBlock.source !== 'custom') return;
- const updatedItems = newsBlock.customItems.map(item =>
- item.id === itemId ? { ...item, [field]: value } : item
- );
- onUpdate({ ...newsBlock, customItems: updatedItems });
- };
-
- const handleAddCustomItem = () => {
- if(newsBlock.source !== 'custom') return;
- const newItem: NewsItemFromUrl = { id: `custom-news-${Date.now()}`, title: 'New Article', summary: '', url: '' };
- onUpdate({ ...newsBlock, customItems: [...(newsBlock.customItems || []), newItem] });
- };
-
- const handleAddMockCustomItems = () => {
- if (newsBlock.source !== 'custom') return;
- const mockItems: NewsItemFromUrl[] = [
- { id: `custom-news-mock-${Date.now()}`, title: 'Mock Article: The Future of AI', summary: 'A fascinating look into what comes next.', url: '#' },
- { id: `custom-news-mock-${Date.now()+1}`, title: 'Mock Post: 10 Design Trends for 2025', summary: 'Stay ahead of the curve with these new styles.', url: '#' },
- { id: `custom-news-mock-${Date.now()+2}`, title: 'Mock Update: Company Hits New Milestone', summary: 'Celebrating a new achievement in our journey.', url: '#' },
- ];
- onUpdate({ ...newsBlock, customItems: [...(newsBlock.customItems || []), ...mockItems] });
- };
-
- const handleRemoveCustomItem = (itemId: string) => {
- if(newsBlock.source !== 'custom') return;
- const updatedItems = newsBlock.customItems.filter(item => item.id !== itemId);
- onUpdate({ ...newsBlock, customItems: updatedItems });
- };
- return (
- <>
- {isNewsSelectorOpen && (
- <AIGCNewsLibraryModal
- articles={aigcArticles}
- selectedArticleIds={newsBlock.source === 'aigc' ? newsBlock.articleIds : []}
- onClose={() => setIsNewsSelectorOpen(false)}
- onSave={handleSaveSelection}
- />
- )}
- <div className="space-y-4">
- <LayoutToggle label="Layout" options={[{label: 'List', value: 'list'}, {label: 'Grid', value: 'grid'}]} value={newsBlock.layout} onLayoutChange={layout => onUpdate({ ...newsBlock, layout })} />
- <LayoutToggle label="Content Source" options={[{label: 'AIGC Library', value: 'aigc'}, {label: 'Custom URLs', value: 'custom'}]} value={newsBlock.source || 'aigc'} onLayoutChange={handleSourceChange} />
-
- {newsBlock.source === 'aigc' ? (
- <div>
- <h4 className="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">Selected Articles ({selectedArticles.length})</h4>
- <div className="space-y-2 p-3 bg-gray-100 dark:bg-gray-900 rounded-md max-h-48 overflow-y-auto">
- {selectedArticles.length > 0 ? selectedArticles.map(article => (
- <p key={article.id} className="text-sm truncate p-2 bg-white dark:bg-gray-800 rounded">{article.title}</p>
- )) : <p className="text-sm text-gray-400 text-center py-2">No articles selected.</p>}
- </div>
- <button onClick={() => setIsNewsSelectorOpen(true)} className="mt-2 w-full text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1 justify-center p-2 bg-brand-primary/10 rounded-md">
- <Icon className="h-4 w-4"><path d="M12 4v16m8-8H4" /></Icon>
- Select from AIGC Library
- </button>
- </div>
- ) : (
- <div className="space-y-3">
- {(newsBlock.customItems || []).map(item => (
- <div key={item.id} className="p-3 bg-gray-100 dark:bg-gray-900 rounded-md space-y-2 relative">
- <button onClick={() => handleRemoveCustomItem(item.id)} className="absolute top-2 right-2 text-gray-400 hover:text-red-500"><Icon className="h-4 w-4"><path d="M6 18L18 6M6 6l12 12" /></Icon></button>
- <div><label className="text-xs font-semibold text-gray-500">Title</label><input type="text" value={item.title} onChange={e => handleUpdateCustomItem(item.id, 'title', e.target.value)} className="w-full bg-white dark:bg-gray-700 p-1 text-sm rounded" /></div>
- <div><label className="text-xs font-semibold text-gray-500">Summary</label><input type="text" value={item.summary} onChange={e => handleUpdateCustomItem(item.id, 'summary', e.target.value)} className="w-full bg-white dark:bg-gray-700 p-1 text-sm rounded" /></div>
- <div><label className="text-xs font-semibold text-gray-500">URL</label><input type="url" value={item.url} onChange={e => handleUpdateCustomItem(item.id, 'url', e.target.value)} className="w-full bg-white dark:bg-gray-700 p-1 text-sm rounded" /></div>
- </div>
- ))}
- <div className="flex gap-2">
- <button onClick={handleAddCustomItem} className="flex-1 justify-center p-2 bg-brand-primary/10 rounded-md text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1">
- <Icon className="h-4 w-4"><path d="M12 4v16m8-8H4" /></Icon>Add URL
- </button>
- <button onClick={handleAddMockCustomItems} className="flex-1 justify-center p-2 bg-brand-primary/10 rounded-md text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1">
- <SparkleIcon />Add 3 Mock Items
- </button>
- </div>
- </div>
- )}
- </div>
- </>
- );
- }
- case 'product': {
- const productBlock = block as ProductBlock;
- const [newUrls, setNewUrls] = React.useState('');
- const [isFetching, setIsFetching] = React.useState(false);
- const [isMocking, setIsMocking] = React.useState(false);
- const handleAddProducts = async () => {
- const urls = newUrls.trim().split('\n').filter(url => url.trim().startsWith('http'));
- if (urls.length === 0) return;
- setIsFetching(true);
- try {
- const newItemsPromises = urls.map(async (url) => {
- const productData = await parseProductUrl(url);
- return {
- id: `prod-${Date.now()}-${Math.random()}`,
- url,
- ...productData,
- };
- });
- const newItems = await Promise.all(newItemsPromises);
- onUpdate({ ...productBlock, items: [...productBlock.items, ...newItems] });
- setNewUrls('');
- } catch (error) {
- alert("Could not fetch product data from one or more URLs.");
- console.error(error);
- } finally {
- setIsFetching(false);
- }
- };
-
- const handleAddMockProducts = () => {
- setIsMocking(true);
- setTimeout(() => {
- const newItems: ProductItem[] = [];
- for (let i = 0; i < 3; i++) {
- const randomProduct = mockProductDatabase[Math.floor(Math.random() * mockProductDatabase.length)];
- const randomId = `prod-mock-${Date.now()}-${Math.random()}`;
- newItems.push({
- id: randomId,
- url: '#',
- title: randomProduct.title,
- price: randomProduct.price,
- imageUrl: `https://picsum.photos/seed/${randomId}/400/400`,
- });
- }
- onUpdate({ ...productBlock, items: [...productBlock.items, ...newItems] });
- setIsMocking(false);
- }, 500);
- };
- const handleRemoveProduct = (itemId: string) => {
- onUpdate({ ...productBlock, items: productBlock.items.filter(item => item.id !== itemId) });
- };
- return (
- <div className="space-y-4">
- <LayoutToggle label="Layout" options={[{label: 'Grid', value: 'grid'}, {label: 'List', value: 'list'}]} value={productBlock.layout} onLayoutChange={(layout: 'grid' | 'list') => onUpdate({ ...productBlock, layout })} />
- <div>
- <label className={labelClasses}>Add Products from URL</label>
- <div className="flex flex-col items-stretch gap-2 mt-1">
- <textarea
- rows={3}
- value={newUrls}
- onChange={e => setNewUrls(e.target.value)}
- placeholder="Paste one URL per line... https://amazon.com/product... https://shopee.com/item..."
- className={inputClasses}
- disabled={isFetching || isMocking}
- />
- <button onClick={handleAddProducts} disabled={isFetching || isMocking} className="w-full px-4 py-2 bg-gray-200 dark:bg-gray-600 text-sm font-semibold rounded-md hover:bg-gray-300 dark:hover:bg-gray-500 disabled:opacity-50 whitespace-nowrap">
- {isFetching ? 'Fetching...' : `Add from URLs`}
- </button>
- </div>
- </div>
- <div>
- <label className={labelClasses}>Or Add Sample Products</label>
- <button onClick={handleAddMockProducts} disabled={isFetching || isMocking} className="mt-1 w-full flex items-center justify-center gap-2 px-4 py-2 bg-brand-primary/10 text-brand-primary text-sm font-semibold rounded-md hover:bg-brand-primary/20 disabled:opacity-50 whitespace-nowrap">
- <SparkleIcon />
- {isMocking ? 'Adding...' : `Add 3 Mock Items`}
- </button>
- </div>
- <div className="space-y-3 max-h-64 overflow-y-auto pr-2 -mr-2">
- {productBlock.items.map(item => (
- <div key={item.id} className="flex items-center gap-3 p-2 bg-gray-100 dark:bg-gray-900 rounded-md">
- <img src={item.imageUrl} alt={item.title} className="w-12 h-12 object-cover rounded flex-shrink-0" />
- <div className="flex-1 min-w-0">
- <p className="font-semibold truncate text-gray-900 dark:text-white">{item.title}</p>
- <p className="text-sm text-gray-500 dark:text-gray-400">{item.price}</p>
- </div>
- <button onClick={() => handleRemoveProduct(item.id)} className="text-gray-400 hover:text-red-500"><Icon className="h-5 w-5"><path d="M6 18L18 6M6 6l12 12" /></Icon></button>
- </div>
- ))}
- </div>
- </div>
- );
- }
- case 'award': {
- const awardBlock = block as AwardBlock;
- const handleUpdateItem = (id: string, field: keyof Omit<AwardItem, 'id'>, value: any) => {
- const updatedItems = awardBlock.items.map(item =>
- item.id === id ? { ...item, [field]: value } : item
- );
- onUpdate({ ...awardBlock, items: updatedItems });
- };
- const handleAddItem = () => {
- const newItem: AwardItem = {
- id: `award-${Date.now()}`,
- title: 'New Award',
- subtitle: 'Awarding Body',
- year: new Date().getFullYear().toString(),
- imageSource: { type: 'url', value: `https://picsum.photos/seed/award-${Date.now()}/100` }
- };
- onUpdate({ ...awardBlock, items: [...awardBlock.items, newItem] });
- };
- const handleRemoveItem = (id: string) => {
- onUpdate({ ...awardBlock, items: awardBlock.items.filter(item => item.id !== id) });
- };
- return (
- <div className="space-y-4">
- <LayoutToggle
- label="Layout"
- options={[{ label: 'Grid', value: 'grid' }, { label: 'Single', value: 'single' }]}
- value={awardBlock.layout}
- onLayoutChange={(layout: 'grid' | 'single') => onUpdate({ ...awardBlock, layout })}
- />
- <div className="space-y-3">
- {awardBlock.items.map(item => (
- <div key={item.id} className="p-3 bg-gray-100 dark:bg-gray-900 rounded-md space-y-2 relative">
- <button onClick={() => handleRemoveItem(item.id)} className="absolute top-2 right-2 text-gray-400 hover:text-red-500"><Icon className="h-4 w-4"><path d="M6 18L18 6M6 6l12 12" /></Icon></button>
- <div><label className="text-xs font-semibold text-gray-500">Title</label><input type="text" value={item.title} onChange={e => handleUpdateItem(item.id, 'title', e.target.value)} className="w-full bg-white dark:bg-gray-700 p-1 text-sm rounded" /></div>
- <div><label className="text-xs font-semibold text-gray-500">Subtitle (e.g., Awarded by)</label><input type="text" value={item.subtitle || ''} onChange={e => handleUpdateItem(item.id, 'subtitle', e.target.value)} className="w-full bg-white dark:bg-gray-700 p-1 text-sm rounded" /></div>
- <div className="grid grid-cols-2 gap-2">
- <div><label className="text-xs font-semibold text-gray-500">Year</label><input type="text" value={item.year || ''} onChange={e => handleUpdateItem(item.id, 'year', e.target.value)} className="w-full bg-white dark:bg-gray-700 p-1 text-sm rounded" /></div>
- <div><label className="text-xs font-semibold text-gray-500">Image URL</label><input type="url" value={item.imageSource?.type === 'url' ? item.imageSource.value : ''} onChange={e => handleUpdateItem(item.id, 'imageSource', { type: 'url', value: e.target.value })} className="w-full bg-white dark:bg-gray-700 p-1 text-sm rounded" /></div>
- </div>
- </div>
- ))}
- </div>
- <button onClick={handleAddItem} className="w-full justify-center p-2 bg-brand-primary/10 rounded-md text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1">
- <Icon className="h-4 w-4"><path d="M12 4v16m8-8H4" /></Icon>Add Award
- </button>
- </div>
- );
- }
- case 'form': {
- const formBlock = block as FormBlock;
- const handleFieldChange = (fieldId: FormFieldId, prop: keyof FormField, value: any) => {
- const updatedFields = formBlock.fields.map(f => f.id === fieldId ? { ...f, [prop]: value } : f);
- onUpdate({ ...formBlock, fields: updatedFields });
- };
- const handlePurposeChange = (optionId: string, newLabel: string) => {
- const updatedOptions = formBlock.purposeOptions.map(o => o.id === optionId ? { ...o, label: newLabel } : o);
- onUpdate({ ...formBlock, purposeOptions: updatedOptions });
- };
- const handleAddPurpose = () => {
- const newOption: FormPurposeOption = { id: `po-${Date.now()}`, label: 'New Option' };
- onUpdate({ ...formBlock, purposeOptions: [...formBlock.purposeOptions, newOption] });
- };
- const handleRemovePurpose = (optionId: string) => {
- const updatedOptions = formBlock.purposeOptions.filter(o => o.id !== optionId);
- onUpdate({ ...formBlock, purposeOptions: updatedOptions });
- };
- return (
- <div className="space-y-4">
- <div><label className={labelClasses}>Title</label><input type="text" value={formBlock.title} onChange={e => onUpdate({ ...block, title: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- <div><label className={labelClasses}>Description</label><textarea value={formBlock.description} onChange={e => onUpdate({ ...block, description: e.target.value })} rows={3} className={`${inputClasses} mt-1`} /></div>
-
- <div>
- <label className={labelClasses}>Form Fields</label>
- <div className="space-y-2 mt-2 p-3 bg-gray-100 dark:bg-gray-900 rounded-md">
- {formBlock.fields.map(field => (
- <div key={field.id} className="flex items-center justify-between p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-800/50">
- <label htmlFor={`field-enabled-${field.id}`} className="flex items-center gap-3 cursor-pointer text-sm font-medium">
- <input type="checkbox" id={`field-enabled-${field.id}`} checked={field.enabled} onChange={e => handleFieldChange(field.id, 'enabled', e.target.checked)} className="h-4 w-4 rounded bg-gray-200 dark:bg-gray-900 border-gray-300 dark:border-gray-600 text-brand-primary focus:ring-brand-secondary" />
- <span className="capitalize text-gray-800 dark:text-gray-200">{field.label}</span>
- </label>
- {field.enabled && (
- <label htmlFor={`field-required-${field.id}`} className="flex items-center gap-1.5 cursor-pointer text-xs text-gray-500 dark:text-gray-400">
- <input type="checkbox" id={`field-required-${field.id}`} checked={field.required} onChange={e => handleFieldChange(field.id, 'required', e.target.checked)} className="h-3 w-3 rounded-sm bg-gray-200 dark:bg-gray-900 border-gray-300 dark:border-gray-600 text-brand-primary focus:ring-brand-secondary" />
- <span>Required</span>
- </label>
- )}
- </div>
- ))}
- </div>
- </div>
-
- <div>
- <label className={labelClasses}>Purpose Options</label>
- <div className="space-y-2 mt-2">
- {formBlock.purposeOptions.map(option => (
- <div key={option.id} className="flex items-center gap-2">
- <input type="text" value={option.label} onChange={e => handlePurposeChange(option.id, e.target.value)} className={`${inputClasses} flex-1`} />
- <button onClick={() => handleRemovePurpose(option.id)} className="text-gray-400 hover:text-red-500"><Icon className="h-5 w-5"><path d="M6 18L18 6M6 6l12 12" /></Icon></button>
- </div>
- ))}
- <button onClick={handleAddPurpose} className="text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1">
- <Icon className="h-4 w-4"><path d="M12 4v16m8-8H4" /></Icon> Add Option
- </button>
- </div>
- </div>
-
- <div><label className={labelClasses}>Submit Button Text</label><input type="text" value={formBlock.submitButtonText} onChange={e => onUpdate({ ...block, submitButtonText: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- </div>
- );
- }
- case 'footer': {
- const footerBlock = block as FooterBlock;
- const handleUpdateLink = (list: 'navLinks' | 'otherLinks', id: string, field: 'title' | 'url', value: string) => {
- const updatedLinks = footerBlock[list].map(link => link.id === id ? { ...link, [field]: value } : link);
- onUpdate({ ...footerBlock, [list]: updatedLinks });
- };
-
- const handleAddLink = (list: 'navLinks' | 'otherLinks') => {
- const newLink: FooterLink = { id: `fl-${Date.now()}`, title: 'New Link', url: '#' };
- onUpdate({ ...footerBlock, [list]: [...footerBlock[list], newLink] });
- };
-
- const handleRemoveLink = (list: 'navLinks' | 'otherLinks', id: string) => {
- onUpdate({ ...footerBlock, [list]: footerBlock[list].filter(link => link.id !== id) });
- };
- const LinkEditorList: React.FC<{ list: FooterLink[], listKey: 'navLinks' | 'otherLinks', title: string }> = ({ list, listKey, title }) => (
- <div>
- <h4 className="text-base font-semibold text-gray-700 dark:text-gray-200 mb-2">{title}</h4>
- <div className="space-y-2 p-3 bg-gray-100 dark:bg-gray-900 rounded-md">
- {list.map(link => (
- <div key={link.id} className="flex items-center gap-2 p-2 rounded-md bg-white dark:bg-gray-800/50">
- <input type="text" placeholder="Title" value={link.title} onChange={e => handleUpdateLink(listKey, link.id, 'title', e.target.value)} className={`${inputClasses} text-sm`} />
- <input type="url" placeholder="URL" value={link.url} onChange={e => handleUpdateLink(listKey, link.id, 'url', e.target.value)} className={`${inputClasses} text-sm`} />
- <button onClick={() => handleRemoveLink(listKey, link.id)} className="text-gray-400 hover:text-red-500"><Icon className="h-4 w-4"><path d="M6 18L18 6M6 6l12 12" /></Icon></button>
- </div>
- ))}
- <button onClick={() => handleAddLink(listKey)} className="w-full text-sm text-brand-primary hover:underline font-semibold flex items-center gap-1 justify-center p-2 bg-brand-primary/10 rounded-md">
- <Icon className="h-4 w-4"><path d="M12 4v16m8-8H4" /></Icon> Add Link
- </button>
- </div>
- </div>
- );
- return (
- <div className="space-y-4">
- <LayoutToggle label="Layout" options={[{label: 'Standard', value: 'standard'}, {label: 'Centered', value: 'centered'}]} value={footerBlock.layout} onLayoutChange={(layout: 'standard' | 'centered') => onUpdate({ ...footerBlock, layout })} />
- <div><label className={labelClasses}>Copyright Text</label><input type="text" value={footerBlock.copyrightText} onChange={e => onUpdate({ ...block, copyrightText: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- <div><label className={labelClasses}>Legal / Filing Number (Optional)</label><input type="text" value={footerBlock.legalText || ''} onChange={e => onUpdate({ ...block, legalText: e.target.value })} className={`${inputClasses} mt-1`} /></div>
- <div><label className={labelClasses}>Special Statement (Optional)</label><textarea value={footerBlock.statement || ''} onChange={e => onUpdate({ ...block, statement: e.target.value })} rows={4} className={`${inputClasses} mt-1`} /></div>
- <LinkEditorList list={footerBlock.navLinks} listKey="navLinks" title="Navigation Links" />
- <LinkEditorList list={footerBlock.otherLinks} listKey="otherLinks" title="Other Hyperlinks" />
- </div>
- );
- }
- default: return null
- }
- };
- const AIGCLibraryModal: React.FC<{
- videos: AIGCVideo[];
- onClose: () => void;
- onSelect: (videoId: string) => void;
- }> = ({ videos, onClose, onSelect }) => {
- return (
- <div className="fixed inset-0 bg-black/70 z-50 flex items-center justify-center backdrop-blur-sm" onClick={onClose}>
- <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-4xl h-[80vh] p-8 border border-gray-200 dark:border-gray-700 flex flex-col" onClick={e => e.stopPropagation()}>
- <h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-6 flex-shrink-0">Select a Video from AIGC Library</h2>
- <div className="flex-1 overflow-y-auto pr-4 -mr-4">
- <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
- {videos.map(video => (
- <button key={video.id} onMouseDown={(e) => e.preventDefault()} onClick={() => onSelect(video.id)} className="group text-left p-2 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-brand-primary transition-colors">
- <img src={video.thumbnailUrl} alt={video.title} className="w-full h-24 object-cover rounded-md mb-2" />
- <p className="text-sm font-semibold truncate group-hover:text-brand-primary">{video.title}</p>
- </button>
- ))}
- </div>
- {videos.length === 0 && <p className="text-center text-gray-400 dark:text-gray-500 py-12">No videos in your AIGC library. Create some in the AIGC Creator!</p>}
- </div>
- </div>
- </div>
- );
- };
- const LinkEditor: React.FC<{ blocks: Block[]; setBlocks: (newBlocks: Block[]) => void; aigcVideos: AIGCVideo[]; aigcArticles: AIGCArticle[] }> = ({ blocks, setBlocks, aigcVideos, aigcArticles }) => {
- const [expandedBlockId, setExpandedBlockId] = React.useState<string | null>(null);
- const [draggedItemId, setDraggedItemId] = React.useState<string | null>(null);
- const [isAIGCModalOpen, setIsAIGCModalOpen] = React.useState(false);
- const [activeBlockForAIGC, setActiveBlockForAIGC] = React.useState<VideoBlock | null>(null);
- const [isAddBlockModalOpen, setIsAddBlockModalOpen] = React.useState(false);
-
- const hasChatBlock = React.useMemo(() => blocks.some(b => b.type === 'chat'), [blocks]);
- const hasFooterBlock = React.useMemo(() => blocks.some(b => b.type === 'footer'), [blocks]);
-
- const addBlock = (type: BlockType) => {
- const newBlock: Block = {
- id: `${type}-${Date.now()}`, type, visible: true,
- ...(type === 'link' && { title: 'New Link', url: '', iconUrl: '', thumbnailUrl: '' }),
- ...(type === 'header' && { text: 'New Header', titleAlignment: 'left' }),
- ...(type === 'social' && { links: [] }),
- ...(type === 'chat' && { layout: 'under_avatar' }),
- ...(type === 'enterprise_info' && { items: [
- { id: `ei-${Date.now()}-1`, icon: 'building', label: 'Company Name', value: 'Innovatech Inc.' },
- { id: `ei-${Date.now()}-2`, icon: 'location', label: 'Address', value: '123 Tech Avenue, Silicon Valley, CA' },
- { id: `ei-${Date.now()}-3`, icon: 'calendar', label: 'Founded', value: '2021' },
- ], alignment: 'left' }),
- ...(type === 'video' && { sources: [], layout: 'single' }),
- ...(type === 'image' && { sources: [], layout: 'grid' }),
- ...(type === 'text' && { content: 'This is a new text block. You can edit this content.', textAlign: 'left', fontSize: '16px', fontColor: '#E5E7EB', isBold: false, isItalic: false }),
- ...(type === 'map' && { address: 'New York, NY', displayStyle: 'interactiveMap', backgroundImageSource: { type: 'url', value: `https://picsum.photos/seed/new-map-${Date.now()}/600/400` } }),
- ...(type === 'pdf' && { source: { type: 'url', value: '' } }),
- ...(type === 'email' && { email: 'contact@example.com', label: 'Email Me', displayMode: 'labelAndValue' }),
- ...(type === 'phone' && { phone: '+1234567890', label: 'Call Me', displayMode: 'labelAndValue' }),
- ...(type === 'news' && { layout: 'list', source: 'aigc', articleIds: [] }),
- ...(type === 'product' && { items: [], layout: 'grid' }),
- ...(type === 'award' && {
- items: [
- { id: `award-${Date.now()}-1`, title: 'Design of the Year', subtitle: 'Global Design Awards', year: '2023', imageSource: { type: 'url', value: 'https://picsum.photos/seed/award1/100' }},
- { id: `award-${Date.now()}-2`, title: 'Top Innovator', subtitle: 'Tech Weekly Magazine', year: '2022', imageSource: { type: 'url', value: 'https://picsum.photos/seed/award2/100' }}
- ],
- layout: 'grid'
- }),
- ...(type === 'form' && {
- title: 'New Form',
- description: 'Collect information from your visitors.',
- submitButtonText: 'Submit',
- fields: [
- { id: 'name', label: 'Name', enabled: true, required: true },
- { id: 'email', label: 'Email', enabled: true, required: true },
- { id: 'company', label: 'Company', enabled: false, required: false },
- { id: 'phone', label: 'Phone', enabled: false, required: false },
- { id: 'industry', label: 'Industry', enabled: false, required: false },
- { id: 'position', label: 'Position', enabled: false, required: false },
- { id: 'country', label: 'Country', enabled: false, required: false },
- ],
- purposeOptions: [
- { id: `po-new-${Date.now()}`, label: 'General Inquiry' },
- ]
- }),
- ...(type === 'footer' && {
- layout: 'standard',
- copyrightText: `© ${new Date().getFullYear()} Your Company Name. All Rights Reserved.`,
- legalText: 'Your Legal ID / Filing Number',
- statement: 'This is a special statement or disclaimer area for your website footer.',
- navLinks: [
- { id: 'fl-1', title: 'Home', url: '#' },
- { id: 'fl-2', title: 'About Us', url: '#' },
- { id: 'fl-3', title: 'Services', url: '#' },
- { id: 'fl-4', title: 'Contact', url: '#' },
- ],
- otherLinks: [
- { id: 'fl-5', title: 'Privacy Policy', url: '#' },
- { id: 'fl-6', title: 'Terms of Service', url: '#' },
- ]
- }),
- } as Block;
- setBlocks([...blocks, newBlock]);
- setExpandedBlockId(newBlock.id);
- };
- const handleAddBlockFromModal = (type: BlockType) => {
- addBlock(type);
- setIsAddBlockModalOpen(false);
- };
- const updateBlock = (updatedBlock: Block) => setBlocks(blocks.map(b => b.id === updatedBlock.id ? updatedBlock : b));
- const deleteBlock = (id: string) => setBlocks(blocks.filter(b => b.id !== id));
- const toggleVisibility = (id: string) => setBlocks(blocks.map(b => b.id === id ? { ...b, visible: !b.visible } : b));
- const handleDragStart = (e: React.DragEvent<HTMLElement>, id: string) => { setDraggedItemId(id); e.dataTransfer.effectAllowed = 'move'; };
- const handleDragOver = (e: React.DragEvent<HTMLElement>) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; };
- const handleDrop = (e: React.DragEvent<HTMLElement>, targetId: string) => {
- e.preventDefault();
- if (!draggedItemId || draggedItemId === targetId) return;
- const draggedIndex = blocks.findIndex(b => b.id === draggedItemId);
- const targetIndex = blocks.findIndex(b => b.id === targetId);
- if(draggedIndex === -1 || targetIndex === -1) return;
- const newBlocks = [...blocks];
- const [draggedItem] = newBlocks.splice(draggedIndex, 1);
- newBlocks.splice(targetIndex, 0, draggedItem);
- setBlocks(newBlocks);
- setDraggedItemId(null);
- };
- const handleOpenAIGCModal = (block: VideoBlock) => {
- setActiveBlockForAIGC(block);
- setIsAIGCModalOpen(true);
- };
- const handleSelectAIGCVideo = (videoId: string) => {
- if (!activeBlockForAIGC) return;
- const newSource: MediaSource = { type: 'aigc', videoId };
- updateBlock({
- ...activeBlockForAIGC,
- sources: [...activeBlockForAIGC.sources, newSource],
- });
- setIsAIGCModalOpen(false);
- setActiveBlockForAIGC(null);
- };
- const getBlockTitle = (block: Block) => {
- switch (block.type) {
- case 'header': return block.text;
- case 'link': return block.title;
- case 'image': return `Image Gallery (${block.sources.length})`;
- case 'video': return `Video Gallery (${block.sources.length})`;
- case 'text': return `${block.content.substring(0, 30)}...`;
- case 'email': return block.label;
- case 'phone': return block.label;
- case 'chat': return 'Chat Button';
- case 'enterprise_info': return 'Enterprise Information';
- case 'social': return "Social Media Icons";
- case 'map': return block.address || "Map Location";
- case 'news': return `News Feed (${block.source === 'aigc' ? (block.articleIds || []).length : (block.customItems || []).length} items)`;
- case 'product': return `Product Showcase (${block.items.length} items)`;
- case 'form': return block.title;
- case 'award': return `Awards (${block.items.length} items)`;
- case 'footer': return 'Page Footer';
- case 'pdf': {
- const source = block.source;
- if (source.type === 'file') {
- return source.value.name;
- }
- if (source.type === 'url' && source.value) {
- const urlParts = source.value.split('/');
- return urlParts[urlParts.length - 1] || "PDF Document";
- }
- return "PDF Document";
- }
- default:
- const exhaustiveCheck = block as Block;
- return blockTypeMetadata[exhaustiveCheck.type]?.name || 'Unknown Block';
- }
- };
- return (
- <div>
- {isAIGCModalOpen && <AIGCLibraryModal videos={aigcVideos} onClose={() => setIsAIGCModalOpen(false)} onSelect={handleSelectAIGCVideo} />}
- {isAddBlockModalOpen && <AddBlockModal hasChatBlock={hasChatBlock} hasFooterBlock={hasFooterBlock} onSelect={handleAddBlockFromModal} onClose={() => setIsAddBlockModalOpen(false)} />}
-
- <button
- onClick={() => setIsAddBlockModalOpen(true)}
- className="w-full flex items-center justify-center gap-2 p-3 mb-8 bg-brand-primary text-white font-semibold rounded-lg hover:bg-brand-secondary transition-colors shadow-lg"
- >
- <Icon className="h-5 w-5"><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /></Icon>
- Add Block
- </button>
-
- <div>
- {blocks.map(block => {
- const isHeader = block.type === 'header';
- const containerClass = `${isHeader ? 'mt-8 mb-2 border-b-2 border-gray-200 dark:border-gray-700' : 'bg-white dark:bg-gray-800 rounded-lg mt-3'} transition-shadow ${draggedItemId === block.id ? 'opacity-50 shadow-2xl' : ''}`;
- const titleClass = isHeader ? 'font-bold text-gray-400 dark:text-gray-500 uppercase tracking-wider' : 'font-semibold text-gray-900 dark:text-white';
- return <div key={block.id} draggable onDragStart={e => handleDragStart(e, block.id)} onDragOver={handleDragOver} onDrop={e => handleDrop(e, block.id)} className={containerClass}>
- <div className="p-3 flex items-center gap-3 cursor-pointer" onClick={() => setExpandedBlockId(expandedBlockId === block.id ? null : block.id)}>
- <div onClick={e => e.stopPropagation()} className="cursor-grab text-gray-400 dark:text-gray-500"><Icon className="h-5 w-5"><path d="M4 8h16M4 16h16" /></Icon></div>
- <div className="flex-1 truncate"><p className={`${titleClass} truncate`}>{getBlockTitle(block)}</p></div>
- <button onMouseDown={(e) => e.preventDefault()} onClick={e => { e.stopPropagation(); toggleVisibility(block.id); }} className="text-gray-400 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white"><Icon className="h-5 w-5">{block.visible ? <><path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /><path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" /></> : <path d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />}</Icon></button>
- <button onMouseDown={(e) => e.preventDefault()} onClick={e => { e.stopPropagation(); deleteBlock(block.id); }} className="text-gray-400 dark:text-gray-500 hover:text-red-500"><Icon className="h-5 w-5"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></Icon></button>
- <div className="text-gray-400 dark:text-gray-400">
- <Icon className={`h-5 w-5 transition-transform ${expandedBlockId === block.id ? 'rotate-180' : ''}`}><path d="M19 9l-7 7-7-7" /></Icon>
- </div>
- </div>
- {expandedBlockId === block.id && (<div className={`p-4 ${isHeader ? '' : 'border-t border-gray-200 dark:border-gray-700'}`}><BlockItemEditor block={block} onUpdate={updateBlock} onAIGCOpen={() => handleOpenAIGCModal(block as VideoBlock)} aigcVideos={aigcVideos} aigcArticles={aigcArticles} /></div>)}
- </div>
- })}
- </div>
- </div>
- );
- };
- export default LinkEditor;
|