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
99 lines
3.7 KiB
TypeScript
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>
|
|
);
|
|
}
|