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
Files changed (1) hide show
  1. client/src/pages/TutorialTasks.tsx +37 -1
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
 
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: https://example.com
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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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}`}