Skip to content

Commit ede4e12

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat(image-editor): implement advanced text styles (bold, italic, underline, alignment, size) with floating toolbar
1 parent d8d37df commit ede4e12

1 file changed

Lines changed: 93 additions & 24 deletions

File tree

src/components/dashboard/ImageEditorModal.tsx

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {
33
X, Crop, Pencil, Type, Save,
44
ChevronRight, Undo2, Redo2,
55
Square, Circle, ArrowUpRight, Eraser,
6-
SlidersHorizontal, Check, Maximize2
6+
SlidersHorizontal, Check, Maximize2,
7+
Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight,
8+
Type as TypeIcon
79
} from 'lucide-react';
810

911
interface ImageEditorModalProps {
@@ -35,6 +37,11 @@ export const ImageEditorModal: React.FC<ImageEditorModalProps> = ({ src, onSave,
3537
const [isAddingText, setIsAddingText] = useState(false);
3638
const [textPos, setTextPos] = useState({ x: 0, y: 0 });
3739
const [fontFamily, setFontFamily] = useState('Inter, sans-serif');
40+
const [fontSize, setFontSize] = useState(24);
41+
const [isBold, setIsBold] = useState(false);
42+
const [isItalic, setIsItalic] = useState(false);
43+
const [isUnderline, setIsUnderline] = useState(false);
44+
const [textAlign, setTextAlign] = useState<CanvasTextAlign>('left');
3845

3946
// Adjustments State
4047
const [brightness, setBrightness] = useState(100);
@@ -308,14 +315,38 @@ export const ImageEditorModal: React.FC<ImageEditorModalProps> = ({ src, onSave,
308315
};
309316

310317
const finalizeText = () => {
311-
if (!textInput.trim() || !contextRef.current) {
312-
setIsAddingText(false);
313-
return;
314-
}
315318
const ctx = contextRef.current;
316-
ctx.font = `bold ${brushSize * 4}px ${fontFamily}`;
319+
320+
// Construct font string: "bold italic 24px Arial"
321+
const styleString = [
322+
isBold ? 'bold' : '',
323+
isItalic ? 'italic' : '',
324+
`${fontSize}px`,
325+
fontFamily
326+
].filter(Boolean).join(' ');
327+
328+
ctx.font = styleString;
317329
ctx.fillStyle = brushColor;
330+
ctx.textAlign = textAlign;
331+
318332
ctx.fillText(textInput, textPos.x, textPos.y);
333+
334+
if (isUnderline) {
335+
const metrics = ctx.measureText(textInput);
336+
const textWidth = metrics.width;
337+
let startX = textPos.x;
338+
339+
if (textAlign === 'center') startX = textPos.x - textWidth / 2;
340+
else if (textAlign === 'right') startX = textPos.x - textWidth;
341+
342+
ctx.beginPath();
343+
ctx.lineWidth = Math.max(1, fontSize / 15);
344+
ctx.strokeStyle = brushColor;
345+
ctx.moveTo(startX, textPos.y + fontSize / 4);
346+
ctx.lineTo(startX + textWidth, textPos.y + fontSize / 4);
347+
ctx.stroke();
348+
}
349+
319350
setIsAddingText(false);
320351
setTextInput('');
321352
saveHistory();
@@ -515,24 +546,52 @@ export const ImageEditorModal: React.FC<ImageEditorModalProps> = ({ src, onSave,
515546
/>
516547

517548
{isAddingText && (
518-
<div className="absolute z-50 flex flex-col gap-2 p-3 bg-[#121212] border border-white/10 rounded-xl shadow-2xl" style={{ left: textPos.x, top: textPos.y }}>
519-
<input
520-
autoFocus
521-
type="text"
522-
value={textInput}
523-
onChange={(e) => setTextInput(e.target.value)}
524-
onKeyDown={(e) => {
525-
e.stopPropagation();
526-
if (e.key === 'Enter') finalizeText();
527-
}}
528-
onKeyUp={(e) => e.stopPropagation()}
529-
onKeyPress={(e) => e.stopPropagation()}
530-
className="bg-transparent border-b border-blue-500 text-white outline-none py-1 min-w-[150px]"
531-
placeholder="Type labels..."
532-
/>
533-
<div className="flex justify-end gap-2">
534-
<button onClick={() => setIsAddingText(false)} className="p-1 hover:text-red-400"><X className="w-3 h-3"/></button>
535-
<button onClick={finalizeText} className="p-1 hover:text-green-400"><Check className="w-3 h-3"/></button>
549+
<div className="absolute z-50 flex flex-col gap-3 p-4 bg-[#121212] border border-white/10 rounded-2xl shadow-2xl animate-in zoom-in duration-200" style={{ left: textPos.x, top: textPos.y - 120 }}>
550+
{/* Floating Style Toolbar */}
551+
<div className="flex items-center gap-1 border-b border-white/5 pb-2 mb-1">
552+
<TextStyleButton active={isBold} onClick={() => setIsBold(!isBold)} icon={<Bold className="w-3.5 h-3.5" />} />
553+
<TextStyleButton active={isItalic} onClick={() => setIsItalic(!isItalic)} icon={<Italic className="w-3.5 h-3.5" />} />
554+
<TextStyleButton active={isUnderline} onClick={() => setIsUnderline(!isUnderline)} icon={<Underline className="w-3.5 h-3.5" />} />
555+
<div className="w-px h-4 bg-white/10 mx-1" />
556+
<TextStyleButton active={textAlign === 'left'} onClick={() => setTextAlign('left')} icon={<AlignLeft className="w-3.5 h-3.5" />} />
557+
<TextStyleButton active={textAlign === 'center'} onClick={() => setTextAlign('center')} icon={<AlignCenter className="w-3.5 h-3.5" />} />
558+
<TextStyleButton active={textAlign === 'right'} onClick={() => setTextAlign('right')} icon={<AlignRight className="w-3.5 h-3.5" />} />
559+
<div className="w-px h-4 bg-white/10 mx-1" />
560+
<div className="flex items-center gap-2 px-2">
561+
<span className="text-[10px] font-bold text-gray-500">Size</span>
562+
<input
563+
type="number"
564+
value={fontSize}
565+
onChange={(e) => setFontSize(parseInt(e.target.value))}
566+
className="w-10 bg-white/5 border border-white/10 rounded px-1 py-0.5 text-[10px] text-white outline-none focus:border-blue-500"
567+
/>
568+
</div>
569+
</div>
570+
571+
<div className="flex flex-col gap-2">
572+
<input
573+
autoFocus
574+
type="text"
575+
value={textInput}
576+
onChange={(e) => setTextInput(e.target.value)}
577+
onKeyDown={(e) => {
578+
e.stopPropagation();
579+
if (e.key === 'Enter') finalizeText();
580+
if (e.key === 'Escape') setIsAddingText(false);
581+
}}
582+
onKeyUp={(e) => e.stopPropagation()}
583+
onKeyPress={(e) => e.stopPropagation()}
584+
className="bg-transparent border-b border-blue-500 text-white outline-none py-1 min-w-[200px] text-lg"
585+
placeholder="Type labels..."
586+
style={{ fontFamily, fontWeight: isBold ? 'bold' : 'normal', fontStyle: isItalic ? 'italic' : 'normal', textDecoration: isUnderline ? 'underline' : 'none', textAlign }}
587+
/>
588+
<div className="flex justify-between items-center gap-2">
589+
<span className="text-[10px] text-gray-500 font-medium">Hit Enter to place</span>
590+
<div className="flex gap-2">
591+
<button onClick={() => setIsAddingText(false)} className="px-3 py-1.5 bg-white/5 hover:bg-white/10 text-gray-400 rounded-lg text-xs transition-colors">Cancel</button>
592+
<button onClick={finalizeText} className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 text-white rounded-lg text-xs font-bold transition-colors">Place Text</button>
593+
</div>
594+
</div>
536595
</div>
537596
</div>
538597
)}
@@ -684,6 +743,16 @@ export const ImageEditorModal: React.FC<ImageEditorModalProps> = ({ src, onSave,
684743
);
685744
};
686745

746+
const TextStyleButton: React.FC<{ active: boolean; onClick: () => void; icon: React.ReactNode }> = ({ active, onClick, icon }) => (
747+
<button
748+
onClick={(e) => { e.stopPropagation(); onClick(); }}
749+
onMouseDown={(e) => e.stopPropagation()}
750+
className={`p-1.5 rounded-lg transition-all ${active ? 'bg-blue-600 text-white shadow-lg' : 'text-gray-500 hover:text-white hover:bg-white/5'}`}
751+
>
752+
{icon}
753+
</button>
754+
);
755+
687756
const ToolbarDivider = () => <div className="w-10 h-px bg-white/5 my-2 mx-auto" />;
688757

689758
const ToolButton: React.FC<{ active?: boolean; onClick: () => void; icon?: React.ReactNode; label?: string; disabled?: boolean; title?: string; children?: React.ReactNode }> = ({ active, onClick, icon, label, disabled, title, children }) => (

0 commit comments

Comments
 (0)