Skip to content

Commit 6014895

Browse files
committed
fix: resolve merge conflict with main (keep AI Generator + duplicate block features)
2 parents 0adc200 + e3bb064 commit 6014895

File tree

2 files changed

+153
-48
lines changed

2 files changed

+153
-48
lines changed

components/Block.tsx

Lines changed: 76 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import React, { useEffect, useState, useRef, useCallback } from 'react';
22
import { 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';
415
import { motion } from 'framer-motion';
516
import { 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
1420
const 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} />

components/Builder.tsx

Lines changed: 77 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useEffect, useCallback, useRef } from 'react';
2-
import { SiteData, UserProfile, BlockData, BlockType, SavedBento, AvatarStyle } from '../types';
2+
import { UserProfile, BlockData, BlockType, SavedBento, AvatarStyle } from '../types';
33
import Block from './Block';
44
import EditorSidebar from './EditorSidebar';
55
import ProfileDropdown from './ProfileDropdown';
@@ -12,7 +12,6 @@ import {
1212
initializeApp,
1313
updateBentoData,
1414
setActiveBentoId,
15-
getBento,
1615
downloadBentoJSON,
1716
loadBentoFromFile,
1817
renameBento,
@@ -656,6 +655,81 @@ const Builder: React.FC<BuilderProps> = ({ onBack }) => {
656655
if (editingBlockId === id) setEditingBlockId(null);
657656
};
658657

658+
const duplicateBlock = useCallback(
659+
(id: string) => {
660+
let duplicated: BlockData | null = null;
661+
662+
handleSetBlocks((prev) => {
663+
const source = prev.find((b) => b.id === id);
664+
if (!source) return prev;
665+
666+
const generateId = () => {
667+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
668+
return crypto.randomUUID();
669+
}
670+
return Math.random().toString(36).slice(2, 11);
671+
};
672+
673+
const clone: BlockData = {
674+
...source,
675+
id: generateId(),
676+
gridColumn: undefined,
677+
gridRow: undefined,
678+
zIndex: undefined,
679+
mediaPosition: source.mediaPosition ? { ...source.mediaPosition } : undefined,
680+
youtubeVideos: source.youtubeVideos
681+
? source.youtubeVideos.map((vid) => ({ ...vid }))
682+
: undefined,
683+
};
684+
685+
const occupiedCells = getOccupiedCells(prev);
686+
const startRow = source.gridRow ?? 1;
687+
const position = findNextAvailablePosition(clone, occupiedCells, startRow);
688+
689+
clone.gridColumn = position.col;
690+
clone.gridRow = position.row;
691+
692+
duplicated = clone;
693+
return [...prev, clone];
694+
});
695+
696+
if (duplicated) {
697+
setEditingBlockId(duplicated.id);
698+
if (!isSidebarOpen) setIsSidebarOpen(true);
699+
}
700+
},
701+
[handleSetBlocks, isSidebarOpen]
702+
);
703+
704+
useEffect(() => {
705+
const handleKeyDown = (event: KeyboardEvent) => {
706+
if (event.defaultPrevented) return;
707+
if (!(event.metaKey || event.ctrlKey)) return;
708+
if (event.key.toLowerCase() !== 'd') return;
709+
if (!editingBlockId) return;
710+
711+
const activeElement = (document.activeElement as HTMLElement) || null;
712+
const targetElement = (event.target as HTMLElement) || null;
713+
const shouldSkip =
714+
(activeElement &&
715+
(activeElement.tagName === 'INPUT' ||
716+
activeElement.tagName === 'TEXTAREA' ||
717+
activeElement.isContentEditable)) ||
718+
(targetElement &&
719+
(targetElement.tagName === 'INPUT' ||
720+
targetElement.tagName === 'TEXTAREA' ||
721+
targetElement.isContentEditable));
722+
723+
if (shouldSkip) return;
724+
725+
event.preventDefault();
726+
duplicateBlock(editingBlockId);
727+
};
728+
729+
window.addEventListener('keydown', handleKeyDown);
730+
return () => window.removeEventListener('keydown', handleKeyDown);
731+
}, [duplicateBlock, editingBlockId]);
732+
659733
const handleExport = () => {
660734
setHasDownloadedExport(false);
661735
setExportError(null);
@@ -1897,6 +1971,7 @@ const Builder: React.FC<BuilderProps> = ({ onBack }) => {
18971971
onDragEnter={handleDragEnter}
18981972
onDragEnd={handleDragEnd}
18991973
onDrop={handleDrop}
1974+
onDuplicate={duplicateBlock}
19001975
onInlineUpdate={updateBlock}
19011976
/>
19021977
))}

0 commit comments

Comments
 (0)