diff --git a/app/globals.css b/app/globals.css index 449c304..6ed7459 100644 --- a/app/globals.css +++ b/app/globals.css @@ -175,8 +175,8 @@ -webkit-transform-style: preserve-3d; backface-visibility: hidden; -webkit-backface-visibility: hidden; - transition: transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); - -webkit-transition: -webkit-transform 0.6s cubic-bezier(0.4, 0.0, 0.2, 1); + transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); + -webkit-transition: -webkit-transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); } .flashcard-face { @@ -315,3 +315,169 @@ display: none; } */ + +/* Enhanced Dropdown Styles */ +.dropdown-enhanced { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + background-image: none; + background-color: white; + border: 1px solid #d1d5db; + border-radius: 0.5rem; + padding: 0.625rem 2.5rem 0.625rem 1rem; + font-size: 0.875rem; + line-height: 1.25rem; + color: #111827; + cursor: pointer; + transition: all 0.2s ease-in-out; + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05); + min-width: 140px; + position: relative; +} + +.dropdown-enhanced:hover { + border-color: #9ca3af; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), + 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.dropdown-enhanced:focus { + outline: none; + border-color: transparent; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); +} + +.dropdown-enhanced:disabled { + opacity: 0.5; + cursor: not-allowed; + background-color: #f9fafb; +} + +/* Dark mode dropdown styles */ +.dark .dropdown-enhanced { + background-color: #1f2937; + border-color: #4b5563; + color: #f3f4f6; +} + +.dark .dropdown-enhanced:hover { + border-color: #6b7280; +} + +.dark .dropdown-enhanced:focus { + border-color: transparent; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.5); +} + +.dark .dropdown-enhanced:disabled { + background-color: #374151; + color: #9ca3af; +} + +/* Custom dropdown arrow */ +.dropdown-wrapper { + position: relative; + display: inline-block; +} + +.dropdown-wrapper::after { + content: ""; + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #6b7280; + pointer-events: none; +} + +.dark .dropdown-wrapper::after { + border-top-color: #9ca3af; +} + +/* Option styling for better visibility */ +.dropdown-enhanced option { + background-color: white; + color: #111827; + padding: 0.5rem; +} + +.dark .dropdown-enhanced option { + background-color: #1f2937; + color: #f3f4f6; +} + +/* Firefox specific styles */ +@-moz-document url-prefix() { + .dropdown-enhanced { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%236b7280'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 1rem; + padding-right: 2.5rem; + } + + .dark .dropdown-enhanced { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%239ca3af'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E"); + } +} + +/* Edge/IE specific styles */ +.dropdown-enhanced::-ms-expand { + display: none; +} + +/* Safari specific fixes */ +@supports (-webkit-appearance: none) { + .dropdown-enhanced { + -webkit-appearance: none; + appearance: none; + } +} + +/* Responsive dropdown styling */ +@media (max-width: 640px) { + .dropdown-enhanced { + min-width: 120px; + font-size: 0.8rem; + padding: 0.5rem 2rem 0.5rem 0.75rem; + } +} + +/* Accessibility improvements */ +.dropdown-enhanced:focus-visible { + outline: 2px solid #3b82f6; + outline-offset: 2px; +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + .dropdown-enhanced { + border-width: 2px; + border-color: #000; + } + + .dropdown-enhanced:focus { + border-color: #0066cc; + box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.5); + } + + .dark .dropdown-enhanced { + border-color: #fff; + } + + .dark .dropdown-enhanced:focus { + border-color: #66b3ff; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + .dropdown-enhanced { + transition: none; + } +} diff --git a/app/interview-experiences/(routes)/share-experience/page.tsx b/app/interview-experiences/(routes)/share-experience/page.tsx index ee21e28..5610080 100644 --- a/app/interview-experiences/(routes)/share-experience/page.tsx +++ b/app/interview-experiences/(routes)/share-experience/page.tsx @@ -66,61 +66,65 @@ const Page = () => { }; const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + e.preventDefault(); - const processedRounds = interviewRounds.map((round) => ({ - round: round.id, - type: round.type, - duration: parseInt(round.duration.toString()), - questions: round.questions.split("\n").filter((q) => q.trim() !== ""), - experience: round.experience, - })); + const processedRounds = interviewRounds.map((round) => ({ + round: round.id, + type: round.type, + duration: parseInt(round.duration.toString()), + questions: round.questions.split("\n").filter((q) => q.trim() !== ""), + experience: round.experience, + })); - const finalData = { - company: formData.company, - position: formData.position, - author: formData.author, - date: formData.date, - duration: parseInt(formData.duration.toString()), - rounds: parseInt(formData.rounds.toString()), - level: formData.difficulty.toLowerCase(), - result: formData.outcome.toLowerCase(), - likes: 0, - comments: 0, - tags: formData.tags - .split(",") - .map((tag) => tag.trim()) - .filter((tag) => tag !== ""), - preview: formData.overallExperience.substring(0, 100) + "...", - interview: { - rounds: processedRounds, - overallExperience: formData.overallExperience, - tips: formData.tips.split("\n").filter((tip) => tip.trim() !== ""), - finalOutcome: formData.outcome.toLowerCase(), // fix - }, - }; + const finalData = { + company: formData.company, + position: formData.position, + author: formData.author, + date: formData.date, + duration: parseInt(formData.duration.toString()), + rounds: parseInt(formData.rounds.toString()), + level: formData.difficulty.toLowerCase(), + result: formData.outcome.toLowerCase(), + likes: 0, + comments: 0, + tags: formData.tags + .split(",") + .map((tag) => tag.trim()) + .filter((tag) => tag !== ""), + preview: formData.overallExperience.substring(0, 100) + "...", + interview: { + rounds: processedRounds, + overallExperience: formData.overallExperience, + tips: formData.tips.split("\n").filter((tip) => tip.trim() !== ""), + finalOutcome: formData.outcome.toLowerCase(), // fix + }, + }; - try { - const res = await fetch("/api/interview-experiences", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(finalData), - }); + try { + const res = await fetch("/api/interview-experiences", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(finalData), + }); - const result = await res.json(); - if (result.success) { - showMessage("Interview Experience shared successfully!", "success"); - } else { - showMessage("Error sharing experience!", "error"); + const result = await res.json(); + if (result.success) { + showMessage("Interview Experience shared successfully!", "success"); + } else { + showMessage("Error sharing experience!", "error"); + } + } catch (error) { + showMessage("Something went wrong!", "error"); } - } catch (error) { - showMessage("Something went wrong!", "error"); - } -}; + }; return (
- } pageTitle="Share Experience" onBack="/interview-experiences" /> + } + pageTitle="Share Experience" + onBack="/interview-experiences" + />

Interview Feedback @@ -248,20 +252,34 @@ const Page = () => { > Difficulty - +
+ + + + +

- - +
+ + + + +
diff --git a/app/sheet/page.tsx b/app/sheet/page.tsx index 3e5b2f9..12cfde0 100644 --- a/app/sheet/page.tsx +++ b/app/sheet/page.tsx @@ -126,89 +126,114 @@ export default function SheetPage() { {/* FILTERS */}
{/* Difficulty Filter */} - +
+ + + + +
{/* Solved Status Filter */} - +
+ + + + +
{/* Revision Filter */} - +
+ + + + +
{/* Platform Filter */} - +
+ + + + +
{/* Company Filter */} - +
+ + + + +
{/* Reset Button */} - -

+ +

{question.title}

- +
- + {question.description && (

{question.description} @@ -233,14 +344,22 @@ export default function CompanyQuestionsList({ questions, companyName }: Company

- + {question.difficulty} - - + + {question.frequency} frequency - + {question.lastAsked && ( @@ -262,21 +381,22 @@ export default function CompanyQuestionsList({ questions, companyName }: Company
- {Object.entries(question.links).map(([platform, url]) => ( - url && ( - - - {platform} - - ) - ))} - + {Object.entries(question.links).map( + ([platform, url]) => + url && ( + + + {platform} + + ) + )} + {question.solutionLink && ( { // Ensure we're on the client side - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { setMounted(true); } }, []); // Set initial content when component mounts or value changes externally useEffect(() => { - if (mounted && editorRef.current && typeof window !== 'undefined') { + if (mounted && editorRef.current && typeof window !== "undefined") { // Only update if the content is different and we're not currently editing const selection = window.getSelection(); - const isEditing = selection && selection.rangeCount > 0 && - editorRef.current.contains(selection.anchorNode); - + const isEditing = + selection && + selection.rangeCount > 0 && + editorRef.current.contains(selection.anchorNode); + if (!isEditing && editorRef.current.innerHTML !== value) { - editorRef.current.innerHTML = value || ''; + editorRef.current.innerHTML = value || ""; } } }, [value, mounted]); @@ -82,7 +84,7 @@ export default function RichTextEditor({ // Handle content changes (define this first) const handleContentChange = useCallback(() => { - if (editorRef.current && mounted && typeof window !== 'undefined') { + if (editorRef.current && mounted && typeof window !== "undefined") { const newContent = editorRef.current.innerHTML; if (newContent !== value) { onChange(newContent); @@ -92,17 +94,17 @@ export default function RichTextEditor({ // Auto-save on Ctrl+S useEffect(() => { - if (typeof window === 'undefined') return; - + if (typeof window === "undefined") return; + const handleKeyDown = (e: KeyboardEvent) => { - if (e.ctrlKey && e.key === 's') { + if (e.ctrlKey && e.key === "s") { e.preventDefault(); handleSave(); } }; - document.addEventListener('keydown', handleKeyDown); - return () => document.removeEventListener('keydown', handleKeyDown); + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); }, [handleSave]); // Helper functions for manual formatting @@ -113,7 +115,7 @@ export default function RichTextEditor({ element.textContent = selectedText; range.deleteContents(); range.insertNode(element); - + // Restore selection const newRange = document.createRange(); newRange.selectNodeContents(element); @@ -125,9 +127,9 @@ export default function RichTextEditor({ } else { // If no text selected, insert placeholder text with formatting const element = document.createElement(tagName); - element.textContent = 'Formatted text'; + element.textContent = "Formatted text"; range.insertNode(element); - + // Select the inserted text const newRange = document.createRange(); newRange.selectNodeContents(element); @@ -139,37 +141,37 @@ export default function RichTextEditor({ } }, []); - const createList = useCallback((range: Range, listType: 'ul' | 'ol') => { + const createList = useCallback((range: Range, listType: "ul" | "ol") => { const selectedText = range.toString(); - + // Create the list and list item const list = document.createElement(listType); - const listItem = document.createElement('li'); - + const listItem = document.createElement("li"); + if (selectedText) { // If text is selected, use it as list item content listItem.textContent = selectedText; } else { // If no text selected, create empty list item with placeholder - listItem.textContent = 'List item'; + listItem.textContent = "List item"; } - + list.appendChild(listItem); - + // Insert the list range.deleteContents(); range.insertNode(list); - + // Add a line break after the list for better formatting - const br = document.createElement('br'); + const br = document.createElement("br"); range.setStartAfter(list); range.insertNode(br); - + // Position cursor at the end of the list item content const newRange = document.createRange(); newRange.selectNodeContents(listItem); newRange.collapse(false); - + const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); @@ -180,21 +182,21 @@ export default function RichTextEditor({ const alignText = useCallback((range: Range, alignment: string) => { // Get the selected text or current position const selectedText = range.toString(); - + if (selectedText) { // If text is selected, wrap it in a div with alignment - const alignDiv = document.createElement('div'); + const alignDiv = document.createElement("div"); alignDiv.style.textAlign = alignment; alignDiv.textContent = selectedText; - + range.deleteContents(); range.insertNode(alignDiv); - + // Position cursor at the end of the aligned text const newRange = document.createRange(); newRange.setStartAfter(alignDiv); newRange.collapse(true); - + const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); @@ -203,14 +205,22 @@ export default function RichTextEditor({ } else { // If no selection, find the current block element or create one const container = range.commonAncestorContainer; - let targetElement = container.nodeType === Node.TEXT_NODE ? container.parentElement : container as Element; - + let targetElement = + container.nodeType === Node.TEXT_NODE + ? container.parentElement + : (container as Element); + // Find the closest block element or the editor itself - while (targetElement && targetElement !== editorRef.current && - !['DIV', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI'].includes(targetElement.tagName)) { + while ( + targetElement && + targetElement !== editorRef.current && + !["DIV", "P", "H1", "H2", "H3", "H4", "H5", "H6", "LI"].includes( + targetElement.tagName + ) + ) { targetElement = targetElement.parentElement; } - + if (targetElement && targetElement !== editorRef.current) { (targetElement as HTMLElement).style.textAlign = alignment; } else if (editorRef.current) { @@ -222,25 +232,25 @@ export default function RichTextEditor({ const formatBlock = useCallback((range: Range, tagName: string) => { const selectedText = range.toString(); - + if (selectedText) { // If text is selected, wrap it in the specified tag const element = document.createElement(tagName); element.textContent = selectedText; - + range.deleteContents(); range.insertNode(element); - + // Add a line break after the element for better formatting - const br = document.createElement('br'); + const br = document.createElement("br"); range.setStartAfter(element); range.insertNode(br); - + // Position cursor after the element const newRange = document.createRange(); newRange.setStartAfter(br); newRange.collapse(true); - + const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); @@ -249,19 +259,22 @@ export default function RichTextEditor({ } else { // If no text selected, create a new heading with placeholder text const element = document.createElement(tagName); - element.textContent = tagName.toUpperCase() === 'P' ? 'Paragraph' : `Heading ${tagName.slice(1)}`; - + element.textContent = + tagName.toUpperCase() === "P" + ? "Paragraph" + : `Heading ${tagName.slice(1)}`; + range.insertNode(element); - + // Add a line break after for better formatting - const br = document.createElement('br'); + const br = document.createElement("br"); range.setStartAfter(element); range.insertNode(br); - + // Select the placeholder text so user can type over it const newRange = document.createRange(); newRange.selectNodeContents(element); - + const selection = window.getSelection(); if (selection) { selection.removeAllRanges(); @@ -273,12 +286,12 @@ export default function RichTextEditor({ const colorText = useCallback((range: Range, color: string) => { const selectedText = range.toString(); if (selectedText) { - const span = document.createElement('span'); + const span = document.createElement("span"); span.style.color = color; span.textContent = selectedText; range.deleteContents(); range.insertNode(span); - + // Restore selection const newRange = document.createRange(); newRange.selectNodeContents(span); @@ -291,157 +304,175 @@ export default function RichTextEditor({ }, []); // Execute formatting commands with better handling - const executeCommand = useCallback((command: string, value?: string) => { - if (!mounted || !editorRef.current || typeof window === 'undefined') return; - - // Focus the editor first - editorRef.current.focus(); - - try { - let success = false; - - // Try document.execCommand first - if (document.execCommand) { - if (command === 'formatBlock') { - success = document.execCommand('formatBlock', false, value || 'div'); - } else { - success = document.execCommand(command, false, value); - } - } - - // If execCommand failed or isn't available, use manual implementation - if (!success) { - const selection = window.getSelection(); - let range: Range; - - if (selection && selection.rangeCount > 0) { - range = selection.getRangeAt(0); - } else { - // Create a new range at the end of the editor if no selection - range = document.createRange(); - range.selectNodeContents(editorRef.current); - range.collapse(false); + const executeCommand = useCallback( + (command: string, value?: string) => { + if (!mounted || !editorRef.current || typeof window === "undefined") + return; + + // Focus the editor first + editorRef.current.focus(); + + try { + let success = false; + + // Try document.execCommand first + if (document.execCommand) { + if (command === "formatBlock") { + success = document.execCommand( + "formatBlock", + false, + value || "div" + ); + } else { + success = document.execCommand(command, false, value); + } } - - switch (command) { - case 'bold': - wrapSelection(range, 'strong'); - break; - case 'italic': - wrapSelection(range, 'em'); - break; - case 'underline': - wrapSelection(range, 'u'); - break; - case 'strikeThrough': - wrapSelection(range, 's'); - break; - case 'insertUnorderedList': - createList(range, 'ul'); - break; - case 'insertOrderedList': - createList(range, 'ol'); - break; - case 'justifyLeft': - alignText(range, 'left'); - break; - case 'justifyCenter': - alignText(range, 'center'); - break; - case 'justifyRight': - alignText(range, 'right'); - break; - case 'formatBlock': - formatBlock(range, value || 'p'); - break; - case 'foreColor': - colorText(range, value || '#000000'); - break; - case 'undo': - if (document.execCommand) { - document.execCommand('undo', false); - } - break; - case 'redo': - if (document.execCommand) { - document.execCommand('redo', false); - } - break; - case 'indent': - if (document.execCommand) { - document.execCommand('indent', false); - } - break; - case 'outdent': - if (document.execCommand) { - document.execCommand('outdent', false); - } - break; + + // If execCommand failed or isn't available, use manual implementation + if (!success) { + const selection = window.getSelection(); + let range: Range; + + if (selection && selection.rangeCount > 0) { + range = selection.getRangeAt(0); + } else { + // Create a new range at the end of the editor if no selection + range = document.createRange(); + range.selectNodeContents(editorRef.current); + range.collapse(false); + } + + switch (command) { + case "bold": + wrapSelection(range, "strong"); + break; + case "italic": + wrapSelection(range, "em"); + break; + case "underline": + wrapSelection(range, "u"); + break; + case "strikeThrough": + wrapSelection(range, "s"); + break; + case "insertUnorderedList": + createList(range, "ul"); + break; + case "insertOrderedList": + createList(range, "ol"); + break; + case "justifyLeft": + alignText(range, "left"); + break; + case "justifyCenter": + alignText(range, "center"); + break; + case "justifyRight": + alignText(range, "right"); + break; + case "formatBlock": + formatBlock(range, value || "p"); + break; + case "foreColor": + colorText(range, value || "#000000"); + break; + case "undo": + if (document.execCommand) { + document.execCommand("undo", false); + } + break; + case "redo": + if (document.execCommand) { + document.execCommand("redo", false); + } + break; + case "indent": + if (document.execCommand) { + document.execCommand("indent", false); + } + break; + case "outdent": + if (document.execCommand) { + document.execCommand("outdent", false); + } + break; + } } + + // Update content + setTimeout(() => handleContentChange(), 10); + } catch (error) { + console.warn("Command execution failed:", command, error); } - - // Update content - setTimeout(() => handleContentChange(), 10); - - } catch (error) { - console.warn('Command execution failed:', command, error); - } - }, [handleContentChange, mounted, wrapSelection, createList, alignText, formatBlock, colorText]); + }, + [ + handleContentChange, + mounted, + wrapSelection, + createList, + alignText, + formatBlock, + colorText, + ] + ); // Handle paste to clean up formatting - const handlePaste = useCallback((e: React.ClipboardEvent) => { - if (!mounted || typeof window === 'undefined') return; - - e.preventDefault(); - const text = e.clipboardData.getData('text/plain'); - try { - document.execCommand('insertText', false, text); - handleContentChange(); - } catch (error) { - // Fallback for browsers that don't support execCommand - if (editorRef.current) { - const selection = window.getSelection(); - if (selection && selection.rangeCount > 0) { - const range = selection.getRangeAt(0); - range.deleteContents(); - range.insertNode(document.createTextNode(text)); - selection.collapseToEnd(); - handleContentChange(); + const handlePaste = useCallback( + (e: React.ClipboardEvent) => { + if (!mounted || typeof window === "undefined") return; + + e.preventDefault(); + const text = e.clipboardData.getData("text/plain"); + try { + document.execCommand("insertText", false, text); + handleContentChange(); + } catch (error) { + // Fallback for browsers that don't support execCommand + if (editorRef.current) { + const selection = window.getSelection(); + if (selection && selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + range.deleteContents(); + range.insertNode(document.createTextNode(text)); + selection.collapseToEnd(); + handleContentChange(); + } } } - } - }, [handleContentChange, mounted]); + }, + [handleContentChange, mounted] + ); // Insert link const insertLink = useCallback(() => { if (linkUrl && linkText && mounted && editorRef.current) { editorRef.current.focus(); - + try { // Try using execCommand first const linkHtml = `${linkText}`; let success = false; - + if (document.execCommand) { - success = document.execCommand('insertHTML', false, linkHtml); + success = document.execCommand("insertHTML", false, linkHtml); } - + // Fallback method if (!success) { const selection = window.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); - const link = document.createElement('a'); + const link = document.createElement("a"); link.href = linkUrl; - link.target = '_blank'; - link.rel = 'noopener noreferrer'; - link.style.color = '#2563eb'; - link.style.textDecoration = 'underline'; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.style.color = "#2563eb"; + link.style.textDecoration = "underline"; link.textContent = linkText; - + range.deleteContents(); range.insertNode(link); - + // Position cursor after the link const newRange = document.createRange(); newRange.setStartAfter(link); @@ -450,92 +481,146 @@ export default function RichTextEditor({ selection.addRange(newRange); } else { // If no selection, just append to the end - const link = document.createElement('a'); + const link = document.createElement("a"); link.href = linkUrl; - link.target = '_blank'; - link.rel = 'noopener noreferrer'; - link.style.color = '#2563eb'; - link.style.textDecoration = 'underline'; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.style.color = "#2563eb"; + link.style.textDecoration = "underline"; link.textContent = linkText; editorRef.current.appendChild(link); } } - + handleContentChange(); setIsLinkModalOpen(false); setLinkUrl(""); setLinkText(""); } catch (error) { - console.warn('Link insertion failed:', error); + console.warn("Link insertion failed:", error); } } }, [linkUrl, linkText, handleContentChange, mounted]); // Apply text color - const applyTextColor = useCallback((color: string) => { - executeCommand('foreColor', color); - setShowColorPicker(false); - }, [executeCommand]); + const applyTextColor = useCallback( + (color: string) => { + executeCommand("foreColor", color); + setShowColorPicker(false); + }, + [executeCommand] + ); // Heading options const headingOptions = [ - { label: 'Normal', value: 'p' }, - { label: 'Heading 1', value: 'h1' }, - { label: 'Heading 2', value: 'h2' }, - { label: 'Heading 3', value: 'h3' }, + { label: "Normal", value: "p" }, + { label: "Heading 1", value: "h1" }, + { label: "Heading 2", value: "h2" }, + { label: "Heading 3", value: "h3" }, ]; // Format buttons configuration const formatButtons = [ - { icon: Bold, command: 'bold', title: 'Bold (Ctrl+B)' }, - { icon: Italic, command: 'italic', title: 'Italic (Ctrl+I)' }, - { icon: Strikethrough, command: 'strikeThrough', title: 'Strikethrough' }, - { icon: Link, command: 'link', title: 'Insert Link', special: true }, - { icon: List, command: 'insertUnorderedList', title: 'Bullet List' }, - { icon: ListOrdered, command: 'insertOrderedList', title: 'Numbered List' }, - { icon: Indent, command: 'indent', title: 'Increase Indent' }, - { icon: Outdent, command: 'outdent', title: 'Decrease Indent' }, - { icon: AlignLeft, command: 'justifyLeft', title: 'Align Left' }, - { icon: AlignCenter, command: 'justifyCenter', title: 'Align Center' }, - { icon: AlignRight, command: 'justifyRight', title: 'Align Right' }, - { icon: Undo, command: 'undo', title: 'Undo (Ctrl+Z)' }, - { icon: Redo, command: 'redo', title: 'Redo (Ctrl+Y)' }, + { icon: Bold, command: "bold", title: "Bold (Ctrl+B)" }, + { icon: Italic, command: "italic", title: "Italic (Ctrl+I)" }, + { icon: Strikethrough, command: "strikeThrough", title: "Strikethrough" }, + { icon: Link, command: "link", title: "Insert Link", special: true }, + { icon: List, command: "insertUnorderedList", title: "Bullet List" }, + { icon: ListOrdered, command: "insertOrderedList", title: "Numbered List" }, + { icon: Indent, command: "indent", title: "Increase Indent" }, + { icon: Outdent, command: "outdent", title: "Decrease Indent" }, + { icon: AlignLeft, command: "justifyLeft", title: "Align Left" }, + { icon: AlignCenter, command: "justifyCenter", title: "Align Center" }, + { icon: AlignRight, command: "justifyRight", title: "Align Right" }, + { icon: Undo, command: "undo", title: "Undo (Ctrl+Z)" }, + { icon: Redo, command: "redo", title: "Redo (Ctrl+Y)" }, ]; const colors = [ - '#000000', '#e60000', '#ff9900', '#ffff00', '#008a00', '#0066cc', '#9933ff', - '#ffffff', '#facccc', '#ffebcc', '#ffffcc', '#cce8cc', '#cce0f5', '#ebd6ff', - '#bbbbbb', '#f06666', '#ffc266', '#ffff66', '#66b266', '#66a3e0', '#c285ff', - '#888888', '#a10000', '#b26b00', '#b2b200', '#006100', '#0047b2', '#6b24b2', - '#444444', '#5c0000', '#663d00', '#666600', '#003700', '#002966', '#3d1466' + "#000000", + "#e60000", + "#ff9900", + "#ffff00", + "#008a00", + "#0066cc", + "#9933ff", + "#ffffff", + "#facccc", + "#ffebcc", + "#ffffcc", + "#cce8cc", + "#cce0f5", + "#ebd6ff", + "#bbbbbb", + "#f06666", + "#ffc266", + "#ffff66", + "#66b266", + "#66a3e0", + "#c285ff", + "#888888", + "#a10000", + "#b26b00", + "#b2b200", + "#006100", + "#0047b2", + "#6b24b2", + "#444444", + "#5c0000", + "#663d00", + "#666600", + "#003700", + "#002966", + "#3d1466", ]; if (!mounted) { return ( -
+
-
Loading editor...
+
+ Loading editor... +
); } return ( -
+
{/* Toolbar */}
{/* Heading Dropdown */} - +
+ + + + +
{/* Separator */}
@@ -545,7 +630,7 @@ export default function RichTextEditor({ key={command} type="button" onClick={() => { - if (special && command === 'link') { + if (special && command === "link") { setIsLinkModalOpen(true); } else { executeCommand(command); @@ -557,7 +642,7 @@ export default function RichTextEditor({ ))} - + {/* Color picker */}
- + {showColorPicker && (
@@ -598,13 +683,17 @@ export default function RichTextEditor({ disabled={isSaved} className={`flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-200 shadow-md hover:shadow-lg ${ isSaved - ? 'bg-gradient-to-r from-green-100 to-emerald-100 dark:from-green-900/30 dark:to-emerald-900/30 text-green-700 dark:text-green-300 cursor-default border-2 border-green-200 dark:border-green-600' - : 'bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white border-2 border-blue-500 hover:border-blue-600 hover:scale-105 active:scale-95' + ? "bg-gradient-to-r from-green-100 to-emerald-100 dark:from-green-900/30 dark:to-emerald-900/30 text-green-700 dark:text-green-300 cursor-default border-2 border-green-200 dark:border-green-600" + : "bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white border-2 border-blue-500 hover:border-blue-600 hover:scale-105 active:scale-95" }`} - title={isSaved ? 'All changes saved' : 'Save changes (Ctrl+S)'} + title={isSaved ? "All changes saved" : "Save changes (Ctrl+S)"} > - {isSaved ? : } - {isSaved ? 'Saved' : 'Save'} + {isSaved ? ( + + ) : ( + + )} + {isSaved ? "Saved" : "Save"} )}
@@ -614,9 +703,9 @@ export default function RichTextEditor({ ref={editorRef} contentEditable className="min-h-[200px] p-6 bg-gradient-to-br from-white via-blue-50/30 to-white dark:from-gray-900 dark:via-blue-900/10 dark:to-gray-900 text-gray-900 dark:text-white focus:outline-none focus:ring-0 transition-all duration-200" - style={{ - wordWrap: 'break-word', - overflowWrap: 'break-word' + style={{ + wordWrap: "break-word", + overflowWrap: "break-word", }} onInput={handleContentChange} onPaste={handlePaste} @@ -636,7 +725,7 @@ export default function RichTextEditor({ Insert Link
- +
- +