11import React , { useEffect , useState , useRef , useCallback } from 'react' ;
22import { BlockData , BlockType } from '../types' ;
3- import { Youtube , MoveVertical , Play , Loader2 , Pencil , Move , Check , X , Trash2 } from 'lucide-react' ;
3+ import {
4+ Youtube ,
5+ MoveVertical ,
6+ Play ,
7+ Loader2 ,
8+ Pencil ,
9+ Move ,
10+ Check ,
11+ X ,
12+ Trash2 ,
13+ CopyPlus ,
14+ } from 'lucide-react' ;
415import { motion } from 'framer-motion' ;
516import { getSocialPlatformOption , inferSocialPlatformFromUrl } from '../socialPlatforms' ;
6- import {
7- openSafeUrl ,
8- isValidYouTubeChannelId ,
9- isValidLocationString ,
10- sanitizeUrl ,
11- } from '../utils/security' ;
17+ import { openSafeUrl , isValidYouTubeChannelId , isValidLocationString } from '../utils/security' ;
1218
1319// Apple TV style 3D tilt effect hook
1420const useTiltEffect = ( isEnabled : boolean = true ) => {
@@ -78,6 +84,7 @@ interface BlockProps {
7884 onDragEnter : ( id : string ) => void ;
7985 onDragEnd : ( ) => void ;
8086 onDrop : ( id : string ) => void ;
87+ onDuplicate ?: ( id : string ) => void ;
8188 enableResize ?: boolean ;
8289 isResizing ?: boolean ;
8390 onResizeStart ?: ( block : BlockData , e : React . PointerEvent < HTMLButtonElement > ) => void ;
@@ -97,6 +104,7 @@ const Block: React.FC<BlockProps> = ({
97104 onDragEnter,
98105 onDragEnd,
99106 onDrop,
107+ onDuplicate,
100108 enableResize,
101109 isResizing,
102110 onResizeStart,
@@ -402,6 +410,9 @@ const Block: React.FC<BlockProps> = ({
402410 </ button >
403411 ) : null ;
404412
413+ const showActionButtons = ! previewMode && ( ! ! onDuplicate || ! ! onDelete ) ;
414+ const repositionButtonOffsetClass = showActionButtons ? 'top-12' : 'top-2' ;
415+
405416 // Explicit grid positioning (if defined)
406417 const gridPositionStyle : React . CSSProperties = { } ;
407418 if ( block . gridColumn !== undefined ) {
@@ -550,19 +561,34 @@ const Block: React.FC<BlockProps> = ({
550561 </ span >
551562 ) : null }
552563
553- { /* Delete button - appears on hover (not in preview mode) */ }
554- { ! previewMode && (
555- < button
556- onClick = { ( e ) => {
557- e . preventDefault ( ) ;
558- e . stopPropagation ( ) ;
559- onDelete ( block . id ) ;
560- } }
561- className = "absolute top-1 left-1 p-1 bg-red-500/80 hover:bg-red-600 text-white rounded-md opacity-0 group-hover:opacity-100 transition-opacity z-20"
562- title = "Delete"
563- >
564- < Trash2 size = { 12 } />
565- </ button >
564+ { /* Action buttons */ }
565+ { showActionButtons && (
566+ < div className = "absolute top-1 right-1 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20" >
567+ { onDuplicate && (
568+ < button
569+ onClick = { ( e ) => {
570+ e . preventDefault ( ) ;
571+ e . stopPropagation ( ) ;
572+ onDuplicate ( block . id ) ;
573+ } }
574+ className = "p-1 bg-white/90 text-gray-800 rounded-md shadow-sm hover:bg-white"
575+ title = "Duplicate block"
576+ >
577+ < CopyPlus size = { 12 } />
578+ </ button >
579+ ) }
580+ < button
581+ onClick = { ( e ) => {
582+ e . preventDefault ( ) ;
583+ e . stopPropagation ( ) ;
584+ onDelete ( block . id ) ;
585+ } }
586+ className = "p-1 bg-red-500/80 hover:bg-red-600 text-white rounded-md shadow-sm"
587+ title = "Delete block"
588+ >
589+ < Trash2 size = { 12 } />
590+ </ button >
591+ </ div >
566592 ) }
567593
568594 { resizeHandle }
@@ -608,24 +634,13 @@ const Block: React.FC<BlockProps> = ({
608634 // ===== YOUTUBE GRID/LIST LAYOUT (ADAPTIVE) =====
609635 if ( isYoutubeGrid || isYoutubeList ) {
610636 // Adaptive layout based on block size
611- const isLargeBlock = block . colSpan >= 2 && block . rowSpan >= 2 ; // 2x2 or larger
612637 const isWideBlock = block . colSpan >= 2 && block . rowSpan === 1 ; // 2x1
613- const isTallBlock = block . colSpan === 1 && block . rowSpan >= 2 ; // 1x2
614638 const isSmallBlock = block . colSpan === 1 && block . rowSpan === 1 ; // 1x1
615639
616640 // Determine display mode based on size
617- const showTitles = isLargeBlock || isTallBlock ;
618641 const videosToShow = isSmallBlock ? 2 : isWideBlock ? 2 : 4 ;
619642 const displayVideos = activeVideos . slice ( 0 , videosToShow ) ;
620643
621- // Grid configuration
622- const getGridClass = ( ) => {
623- if ( isSmallBlock ) return 'grid grid-cols-2 gap-1.5' ;
624- if ( isWideBlock ) return 'grid grid-cols-2 gap-2' ;
625- if ( isTallBlock ) return 'flex flex-col gap-2' ;
626- return 'grid grid-cols-2 gap-2' ; // Large block
627- } ;
628-
629644 return (
630645 < motion . div
631646 layoutId = { block . id }
@@ -824,19 +839,34 @@ const Block: React.FC<BlockProps> = ({
824839 } }
825840 />
826841 ) }
827- { /* Delete button - appears on hover (not in preview mode) */ }
828- { ! previewMode && (
829- < button
830- onClick = { ( e ) => {
831- e . preventDefault ( ) ;
832- e . stopPropagation ( ) ;
833- onDelete ( block . id ) ;
834- } }
835- className = "absolute top-2 left-2 p-1.5 bg-red-500/80 hover:bg-red-600 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity z-20 backdrop-blur-sm"
836- title = "Delete block"
837- >
838- < Trash2 size = { 14 } />
839- </ button >
842+ { /* Action buttons */ }
843+ { showActionButtons && (
844+ < div className = "absolute top-2 right-2 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity z-30 pointer-events-auto" >
845+ { onDuplicate && (
846+ < button
847+ onClick = { ( e ) => {
848+ e . preventDefault ( ) ;
849+ e . stopPropagation ( ) ;
850+ onDuplicate ( block . id ) ;
851+ } }
852+ className = "p-2 bg-white/80 hover:bg-white text-gray-800 rounded-lg shadow-sm backdrop-blur-sm"
853+ title = "Duplicate block"
854+ >
855+ < CopyPlus size = { 14 } />
856+ </ button >
857+ ) }
858+ < button
859+ onClick = { ( e ) => {
860+ e . preventDefault ( ) ;
861+ e . stopPropagation ( ) ;
862+ onDelete ( block . id ) ;
863+ } }
864+ className = "p-2 bg-red-500/80 hover:bg-red-600 text-white rounded-lg backdrop-blur-sm shadow-sm"
865+ title = "Delete block"
866+ >
867+ < Trash2 size = { 14 } />
868+ </ button >
869+ </ div >
840870 ) }
841871
842872 { resizeHandle }
@@ -859,7 +889,7 @@ const Block: React.FC<BlockProps> = ({
859889 e . stopPropagation ( ) ;
860890 setIsRepositioning ( true ) ;
861891 } }
862- className = " absolute top-2 right-2 p-2 bg-black/60 hover:bg-black/80 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity z-20 backdrop-blur-sm"
892+ className = { ` absolute ${ repositionButtonOffsetClass } right-2 p-2 bg-black/60 hover:bg-black/80 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity z-20 backdrop-blur-sm` }
863893 title = "Reposition image"
864894 >
865895 < Move size = { 16 } />
@@ -945,7 +975,7 @@ const Block: React.FC<BlockProps> = ({
945975 e . stopPropagation ( ) ;
946976 setIsRepositioning ( true ) ;
947977 } }
948- className = " absolute top-2 right-2 p-2 bg-black/60 hover:bg-black/80 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-auto z-20 backdrop-blur-sm"
978+ className = { ` absolute ${ repositionButtonOffsetClass } right-2 p-2 bg-black/60 hover:bg-black/80 text-white rounded-lg opacity-0 group-hover:opacity-100 transition-opacity pointer-events-auto z-20 backdrop-blur-sm` }
949979 title = "Reposition media"
950980 >
951981 < Move size = { 16 } />
0 commit comments