gohorsejobs/frontend/src/components/rich-text-editor.tsx
Tiago Yamamoto cca951ca23 feat: add currency, salary period, and rich text description
Frontend:
- Added currency selector (BRL, USD, EUR, JPY, GBP, CNY, AED, CAD, AUD, CHF)
- Added salary period dropdown (hourly, daily, weekly, monthly, yearly)
- Created RichTextEditor component for job descriptions (Bold, Lists, Alignment)
- Updated confirmation step to display currency symbol and period label

Backend:
- JobService now persists currency in job creation
- Extended currency validation in DTOs

Seeder:
- Already includes currency in job insertion
2025-12-26 15:37:54 -03:00

99 lines
3.7 KiB
TypeScript

"use client";
import { useState, useRef, useCallback } from "react";
import { Bold, List, ListOrdered, AlignLeft, AlignRight } from "lucide-react";
interface RichTextEditorProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
minHeight?: string;
}
export function RichTextEditor({ value, onChange, placeholder, minHeight = "200px" }: RichTextEditorProps) {
const editorRef = useRef<HTMLDivElement>(null);
const [isFocused, setIsFocused] = useState(false);
const execCommand = useCallback((command: string, value?: string) => {
document.execCommand(command, false, value);
if (editorRef.current) {
onChange(editorRef.current.innerHTML);
}
}, [onChange]);
const handleInput = () => {
if (editorRef.current) {
onChange(editorRef.current.innerHTML);
}
};
const handlePaste = (e: React.ClipboardEvent) => {
e.preventDefault();
const text = e.clipboardData.getData('text/plain');
document.execCommand('insertText', false, text);
};
const ToolbarButton = ({ onClick, active, children, title }: {
onClick: () => void;
active?: boolean;
children: React.ReactNode;
title: string;
}) => (
<button
type="button"
onClick={onClick}
title={title}
className={`p-1.5 rounded hover:bg-muted transition-colors ${active ? 'bg-muted text-primary' : 'text-muted-foreground'}`}
>
{children}
</button>
);
return (
<div className={`border rounded-lg overflow-hidden ${isFocused ? 'ring-2 ring-ring ring-offset-2' : ''}`}>
{/* Toolbar */}
<div className="flex items-center gap-1 p-2 border-b bg-muted/30">
<ToolbarButton onClick={() => execCommand('bold')} title="Negrito">
<Bold className="h-4 w-4" />
</ToolbarButton>
<div className="w-px h-4 bg-border mx-1" />
<ToolbarButton onClick={() => execCommand('insertUnorderedList')} title="Lista com marcadores">
<List className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton onClick={() => execCommand('insertOrderedList')} title="Lista numerada">
<ListOrdered className="h-4 w-4" />
</ToolbarButton>
<div className="w-px h-4 bg-border mx-1" />
<ToolbarButton onClick={() => execCommand('justifyLeft')} title="Alinhar à esquerda">
<AlignLeft className="h-4 w-4" />
</ToolbarButton>
<ToolbarButton onClick={() => execCommand('justifyRight')} title="Alinhar à direita">
<AlignRight className="h-4 w-4" />
</ToolbarButton>
</div>
{/* Editor Area */}
<div
ref={editorRef}
contentEditable
onInput={handleInput}
onPaste={handlePaste}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
className="p-3 outline-none bg-background"
style={{ minHeight }}
data-placeholder={placeholder}
dangerouslySetInnerHTML={{ __html: value }}
suppressContentEditableWarning
/>
<style jsx>{`
[contenteditable]:empty:before {
content: attr(data-placeholder);
color: hsl(var(--muted-foreground));
pointer-events: none;
}
`}</style>
</div>
);
}