Spaces:
Sleeping
Sleeping
Tristan Yu commited on
Commit ·
f58d9bc
1
Parent(s): b2e992a
Add Link button to brief and source editors; improve auto-linking (Markdown, URLs, www)
Browse files
client/src/pages/TutorialTasks.tsx
CHANGED
|
@@ -115,17 +115,47 @@ const TutorialTasks: React.FC = () => {
|
|
| 115 |
// Basic inline formatting helpers (bold/italic via simple markdown) for weeks 4–6
|
| 116 |
const renderFormatted = (text: string) => {
|
| 117 |
const escape = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
|
|
| 118 |
const html = escape(text)
|
| 119 |
// Markdown-style links: [label](https://example.com)
|
| 120 |
.replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
| 121 |
-
// Plain URLs
|
| 122 |
.replace(/(https?:\/\/[^\s<]+[^\s<\.)])/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>')
|
|
|
|
|
|
|
| 123 |
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
| 124 |
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
| 125 |
.replace(/\n/g, '<br/>');
|
| 126 |
return <span dangerouslySetInnerHTML={{ __html: html }} />;
|
| 127 |
};
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
const applyInlineFormat = (
|
| 130 |
elementId: string,
|
| 131 |
current: string,
|
|
@@ -987,6 +1017,7 @@ const TutorialTasks: React.FC = () => {
|
|
| 987 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 988 |
<button onClick={() => applyInlineFormat('tutorial-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 989 |
<button onClick={() => applyInlineFormat('tutorial-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
|
|
|
| 990 |
</div>
|
| 991 |
<textarea id="tutorial-brief-input"
|
| 992 |
value={editForm.translationBrief}
|
|
@@ -1029,6 +1060,7 @@ const TutorialTasks: React.FC = () => {
|
|
| 1029 |
<div className="flex items-center justify-end space-x-2">
|
| 1030 |
<button onClick={() => applyInlineFormat('tutorial-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1031 |
<button onClick={() => applyInlineFormat('tutorial-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
|
|
|
| 1032 |
</div>
|
| 1033 |
<textarea
|
| 1034 |
id="tutorial-brief-input"
|
|
@@ -1088,6 +1120,7 @@ const TutorialTasks: React.FC = () => {
|
|
| 1088 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 1089 |
<button onClick={() => applyInlineFormat('tutorial-newtask-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1090 |
<button onClick={() => applyInlineFormat('tutorial-newtask-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
|
|
|
| 1091 |
</div>
|
| 1092 |
<textarea
|
| 1093 |
id="tutorial-newtask-input"
|
|
@@ -1441,6 +1474,7 @@ const TutorialTasks: React.FC = () => {
|
|
| 1441 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 1442 |
<button onClick={() => applyInlineFormat('tutorial-newtask-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1443 |
<button onClick={() => applyInlineFormat('tutorial-newtask-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
|
|
|
| 1444 |
</div>
|
| 1445 |
<textarea
|
| 1446 |
id="tutorial-newtask-input"
|
|
@@ -1576,6 +1610,7 @@ const TutorialTasks: React.FC = () => {
|
|
| 1576 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 1577 |
<button onClick={() => applyInlineFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1578 |
<button onClick={() => applyInlineFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
|
|
|
| 1579 |
</div>
|
| 1580 |
<textarea
|
| 1581 |
id={`tutorial-translation-${task._id}`}
|
|
@@ -1769,6 +1804,7 @@ const TutorialTasks: React.FC = () => {
|
|
| 1769 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 1770 |
<button onClick={() => applyInlineFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1771 |
<button onClick={() => applyInlineFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
|
|
|
| 1772 |
</div>
|
| 1773 |
<textarea
|
| 1774 |
id={`tutorial-translation-${task._id}`}
|
|
|
|
| 115 |
// Basic inline formatting helpers (bold/italic via simple markdown) for weeks 4–6
|
| 116 |
const renderFormatted = (text: string) => {
|
| 117 |
const escape = (s: string) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
| 118 |
+
// Auto-linker: supports [label](url), plain URLs, and www.*
|
| 119 |
const html = escape(text)
|
| 120 |
// Markdown-style links: [label](https://example.com)
|
| 121 |
.replace(/\[([^\]]+?)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>')
|
| 122 |
+
// Plain URLs with protocol
|
| 123 |
.replace(/(https?:\/\/[^\s<]+[^\s<\.)])/g, '<a href="$1" target="_blank" rel="noopener noreferrer">$1</a>')
|
| 124 |
+
// www.* domains (prepend https://)
|
| 125 |
+
.replace(/(^|\s)(www\.[^\s<]+[^\s<\.)])/g, (_, p1, p2) => `${p1}<a href="https://${p2}" target="_blank" rel="noopener noreferrer">${p2}</a>`)
|
| 126 |
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
| 127 |
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
| 128 |
.replace(/\n/g, '<br/>');
|
| 129 |
return <span dangerouslySetInnerHTML={{ __html: html }} />;
|
| 130 |
};
|
| 131 |
|
| 132 |
+
const applyLinkFormat = (
|
| 133 |
+
elementId: string,
|
| 134 |
+
current: string,
|
| 135 |
+
setValue: (v: string) => void
|
| 136 |
+
) => {
|
| 137 |
+
const urlInput = window.prompt('Enter URL (e.g., https://example.com):');
|
| 138 |
+
if (!urlInput) return;
|
| 139 |
+
const url = /^https?:\/\//i.test(urlInput) ? urlInput : `https://${urlInput}`;
|
| 140 |
+
const el = document.getElementById(elementId) as HTMLTextAreaElement | null;
|
| 141 |
+
if (!el) {
|
| 142 |
+
setValue(`${current}[link](${url})`);
|
| 143 |
+
return;
|
| 144 |
+
}
|
| 145 |
+
const start = el.selectionStart ?? current.length;
|
| 146 |
+
const end = el.selectionEnd ?? current.length;
|
| 147 |
+
const before = current.slice(0, start);
|
| 148 |
+
const selection = current.slice(start, end) || 'link';
|
| 149 |
+
const after = current.slice(end);
|
| 150 |
+
setValue(`${before}[${selection}](${url})${after}`);
|
| 151 |
+
// Restore focus and selection
|
| 152 |
+
setTimeout(() => {
|
| 153 |
+
el.focus();
|
| 154 |
+
const newPos = before.length + selection.length + 4 + url.length + 2; // rough caret placement
|
| 155 |
+
try { el.setSelectionRange(newPos, newPos); } catch {}
|
| 156 |
+
}, 0);
|
| 157 |
+
};
|
| 158 |
+
|
| 159 |
const applyInlineFormat = (
|
| 160 |
elementId: string,
|
| 161 |
current: string,
|
|
|
|
| 1017 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 1018 |
<button onClick={() => applyInlineFormat('tutorial-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1019 |
<button onClick={() => applyInlineFormat('tutorial-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
| 1020 |
+
<button onClick={() => applyLinkFormat('tutorial-brief-input', editForm.translationBrief || '', v => setEditForm({ ...editForm, translationBrief: v }))} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">Link</button>
|
| 1021 |
</div>
|
| 1022 |
<textarea id="tutorial-brief-input"
|
| 1023 |
value={editForm.translationBrief}
|
|
|
|
| 1060 |
<div className="flex items-center justify-end space-x-2">
|
| 1061 |
<button onClick={() => applyInlineFormat('tutorial-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1062 |
<button onClick={() => applyInlineFormat('tutorial-brief-input', editForm.translationBrief, v => setEditForm({ ...editForm, translationBrief: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
| 1063 |
+
<button onClick={() => applyLinkFormat('tutorial-brief-input', editForm.translationBrief || '', v => setEditForm({ ...editForm, translationBrief: v }))} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">Link</button>
|
| 1064 |
</div>
|
| 1065 |
<textarea
|
| 1066 |
id="tutorial-brief-input"
|
|
|
|
| 1120 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 1121 |
<button onClick={() => applyInlineFormat('tutorial-newtask-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1122 |
<button onClick={() => applyInlineFormat('tutorial-newtask-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
| 1123 |
+
<button onClick={() => applyLinkFormat('tutorial-newtask-input', editForm.content || '', v => setEditForm({ ...editForm, content: v }))} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">Link</button>
|
| 1124 |
</div>
|
| 1125 |
<textarea
|
| 1126 |
id="tutorial-newtask-input"
|
|
|
|
| 1474 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 1475 |
<button onClick={() => applyInlineFormat('tutorial-newtask-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1476 |
<button onClick={() => applyInlineFormat('tutorial-newtask-input', editForm.content, v => setEditForm({ ...editForm, content: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
| 1477 |
+
<button onClick={() => applyLinkFormat('tutorial-newtask-input', editForm.content || '', v => setEditForm({ ...editForm, content: v }))} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">Link</button>
|
| 1478 |
</div>
|
| 1479 |
<textarea
|
| 1480 |
id="tutorial-newtask-input"
|
|
|
|
| 1610 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 1611 |
<button onClick={() => applyInlineFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1612 |
<button onClick={() => applyInlineFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
| 1613 |
+
<button onClick={() => applyLinkFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }))} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">Link</button>
|
| 1614 |
</div>
|
| 1615 |
<textarea
|
| 1616 |
id={`tutorial-translation-${task._id}`}
|
|
|
|
| 1804 |
<div className="flex items-center justify-end space-x-2 mb-2">
|
| 1805 |
<button onClick={() => applyInlineFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }), '**')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">B</button>
|
| 1806 |
<button onClick={() => applyInlineFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }), '*')} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded italic">I</button>
|
| 1807 |
+
<button onClick={() => applyLinkFormat(`tutorial-translation-${task._id}`, translationText[task._id] || '', v => setTranslationText({ ...translationText, [task._id]: v }))} className="px-2 py-1 text-xs bg-indigo-100 text-indigo-900 rounded">Link</button>
|
| 1808 |
</div>
|
| 1809 |
<textarea
|
| 1810 |
id={`tutorial-translation-${task._id}`}
|