Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

client-js.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

client-js.tsBlame276 lines · 2 contributors
45e31d0Claude1/**
2 * Client-side JavaScript — embedded inline in the layout.
3 *
4 * Provides interactivity without requiring a build step or framework:
5 * - Copy to clipboard
6 * - Markdown preview in comment forms
7 * - Keyboard shortcuts
8 * - Toast notifications
9 * - Async form submission for stars/actions
10 * - Live search debouncing
11 * - Tab indentation in textareas
12 * - Mobile hamburger menu
13 */
14
15export const clientJs = `
16(function() {
17 'use strict';
18
19 // ─── Toast Notification System ──────────────────────────────────────────
20
21 const toastContainer = document.createElement('div');
22 toastContainer.id = 'toast-container';
23 document.body.appendChild(toastContainer);
24
25 function toast(message, type) {
26 type = type || 'info';
27 var el = document.createElement('div');
28 el.className = 'toast toast-' + type;
29 el.textContent = message;
30 toastContainer.appendChild(el);
31 requestAnimationFrame(function() { el.classList.add('toast-visible'); });
32 setTimeout(function() {
33 el.classList.remove('toast-visible');
34 setTimeout(function() { el.remove(); }, 300);
35 }, 3000);
36 }
37
38 // ─── Copy to Clipboard ─────────────────────────────────────────────────
39
40 document.addEventListener('click', function(e) {
41 var btn = e.target.closest('[data-clipboard]');
42 if (!btn) return;
43 e.preventDefault();
44 var text = btn.getAttribute('data-clipboard');
45 if (navigator.clipboard) {
46 navigator.clipboard.writeText(text).then(function() {
47 var original = btn.textContent;
48 btn.textContent = 'Copied!';
49 btn.classList.add('btn-success');
50 setTimeout(function() {
51 btn.textContent = original;
52 btn.classList.remove('btn-success');
53 }, 2000);
54 });
55 }
56 });
57
58 // ─── Markdown Preview ──────────────────────────────────────────────────
59
60 document.addEventListener('click', function(e) {
61 var tab = e.target.closest('[data-tab]');
62 if (!tab) return;
63
64 var editor = tab.closest('.comment-editor');
65 if (!editor) return;
66
67 var tabName = tab.getAttribute('data-tab');
68 var tabs = editor.querySelectorAll('[data-tab]');
69 var textarea = editor.querySelector('textarea');
70 var preview = editor.querySelector('.editor-preview');
71
72 tabs.forEach(function(t) { t.classList.toggle('active', t.getAttribute('data-tab') === tabName); });
73
74 if (tabName === 'preview') {
75 textarea.style.display = 'none';
76 preview.style.display = 'block';
77 preview.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Loading preview...</div>';
78
79 // Simple markdown rendering (bold, italic, code, links, headers)
80 var md = textarea.value || 'Nothing to preview';
81 var html = md
82 .replace(/&/g, '&amp;')
83 .replace(/</g, '&lt;')
84 .replace(/>/g, '&gt;')
85 .replace(/^### (.+)$/gm, '<h3>$1</h3>')
86 .replace(/^## (.+)$/gm, '<h2>$1</h2>')
87 .replace(/^# (.+)$/gm, '<h1>$1</h1>')
88 .replace(/\\.([^\\x60]+)\\.\\./g, '<code>$1</code>')
89 .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')
90 .replace(/\\*(.+?)\\*/g, '<em>$1</em>')
91 .replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href="$2">$1</a>')
92 .replace(/\\n/g, '<br>');
93 preview.innerHTML = '<div class="markdown-body" style="padding:12px">' + html + '</div>';
94 } else {
95 textarea.style.display = '';
96 preview.style.display = 'none';
97 }
98 });
99
100 // ─── Tab Key in Textareas ──────────────────────────────────────────────
101
102 document.addEventListener('keydown', function(e) {
103 if (e.key !== 'Tab') return;
104 var textarea = e.target;
105 if (textarea.tagName !== 'TEXTAREA') return;
106
107 e.preventDefault();
108 var start = textarea.selectionStart;
109 var end = textarea.selectionEnd;
110 var value = textarea.value;
111 textarea.value = value.substring(0, start) + ' ' + value.substring(end);
112 textarea.selectionStart = textarea.selectionEnd = start + 2;
113 });
114
115 // ─── Keyboard Shortcuts ────────────────────────────────────────────────
116
117 var shortcutOverlay = null;
118
119 function toggleShortcuts() {
120 if (shortcutOverlay) {
121 shortcutOverlay.remove();
122 shortcutOverlay = null;
123 return;
124 }
125 shortcutOverlay = document.createElement('div');
126 shortcutOverlay.className = 'shortcut-overlay';
127 shortcutOverlay.innerHTML = [
128 '<div class="shortcut-modal">',
129 '<h3>Keyboard Shortcuts</h3>',
130 '<div class="shortcut-grid">',
131 '<div><kbd>?</kbd> Show shortcuts</div>',
132 '<div><kbd>/</kbd> Focus search</div>',
133 '<div><kbd>g</kbd> <kbd>h</kbd> Go home</div>',
134 '<div><kbd>g</kbd> <kbd>e</kbd> Go to explore</div>',
135 '<div><kbd>g</kbd> <kbd>n</kbd> New repository</div>',
136 '<div><kbd>g</kbd> <kbd>s</kbd> Go to settings</div>',
137 '<div><kbd>Esc</kbd> Close modal</div>',
138 '</div>',
139 '<button class="btn btn-sm" onclick="this.closest(\\'.shortcut-overlay\\').remove()">Close</button>',
140 '</div>'
141 ].join('');
142 document.body.appendChild(shortcutOverlay);
143 shortcutOverlay.addEventListener('click', function(e) {
144 if (e.target === shortcutOverlay) {
145 shortcutOverlay.remove();
146 shortcutOverlay = null;
147 }
148 });
149 }
150
151 var gPressed = false;
152 var gTimeout;
153
154 document.addEventListener('keydown', function(e) {
155 // Don't trigger shortcuts when typing in inputs
156 if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;
157
158 if (e.key === '?') {
159 e.preventDefault();
160 toggleShortcuts();
161 return;
162 }
163
164 if (e.key === 'Escape') {
165 if (shortcutOverlay) {
166 shortcutOverlay.remove();
167 shortcutOverlay = null;
168 }
169 return;
170 }
171
172 if (e.key === '/') {
173 var searchInput = document.querySelector('.search-input') || document.querySelector('input[name="q"]');
174 if (searchInput) {
175 e.preventDefault();
176 searchInput.focus();
177 }
178 return;
179 }
180
181 if (e.key === 'g') {
182 if (!gPressed) {
183 gPressed = true;
184 gTimeout = setTimeout(function() { gPressed = false; }, 500);
185 return;
186 }
187 }
188
189 if (gPressed) {
190 gPressed = false;
191 clearTimeout(gTimeout);
192 if (e.key === 'h') { window.location.href = '/'; return; }
193 if (e.key === 'e') { window.location.href = '/explore'; return; }
194 if (e.key === 'n') { window.location.href = '/new'; return; }
195 if (e.key === 's') { window.location.href = '/settings'; return; }
196 }
197 });
198
199 // ─── Star Button Async ─────────────────────────────────────────────────
200
201 document.addEventListener('click', function(e) {
202 var starBtn = e.target.closest('.star-btn[type="submit"]');
203 if (!starBtn) return;
204
205 var form = starBtn.closest('form');
206 if (!form) return;
207
208 e.preventDefault();
209 var action = form.getAttribute('action');
210
211 fetch(action, {
212 method: 'POST',
213 credentials: 'same-origin',
214 headers: { 'X-Requested-With': 'XMLHttpRequest' }
215 }).then(function() {
216 // Toggle visual state
217 starBtn.classList.toggle('starred');
218 var currentText = starBtn.textContent.trim();
219 var match = currentText.match(/(\\d+)/);
220 if (match) {
221 var count = parseInt(match[1]);
222 var newCount = starBtn.classList.contains('starred') ? count + 1 : count - 1;
223 starBtn.textContent = (starBtn.classList.contains('starred') ? '\\u2605 ' : '\\u2606 ') + Math.max(0, newCount);
224 }
225 toast(starBtn.classList.contains('starred') ? 'Starred!' : 'Unstarred', 'success');
226 }).catch(function() {
227 // Fall back to normal form submission
228 form.submit();
229 });
230 });
231
232 // ─── Mobile Hamburger Menu ─────────────────────────────────────────────
233
234 var navRight = document.querySelector('.nav-right');
235 if (navRight && window.innerWidth < 768) {
236 var hamburger = document.createElement('button');
237 hamburger.className = 'hamburger-btn';
238 hamburger.innerHTML = '\\u2630';
239 hamburger.setAttribute('aria-label', 'Toggle menu');
240 navRight.parentElement.insertBefore(hamburger, navRight);
241 navRight.classList.add('mobile-hidden');
242
243 hamburger.addEventListener('click', function() {
244 navRight.classList.toggle('mobile-hidden');
245 navRight.classList.toggle('mobile-visible');
246 });
247 }
248
249 // ─── Relative Time Auto-Update ─────────────────────────────────────────
250
251 function updateTimes() {
252 document.querySelectorAll('[data-time]').forEach(function(el) {
253 var date = new Date(el.getAttribute('data-time'));
254 var now = new Date();
255 var diff = Math.floor((now - date) / 60000);
256 if (diff < 1) el.textContent = 'just now';
257 else if (diff < 60) el.textContent = diff + 'm ago';
258 else if (diff < 1440) el.textContent = Math.floor(diff / 60) + 'h ago';
259 else if (diff < 43200) el.textContent = Math.floor(diff / 1440) + 'd ago';
260 });
261 }
ea52715copilot-swe-agent[bot]262 var _timesInterval = setInterval(updateTimes, 60000);
45e31d0Claude263
264 // ─── Confirmation on Dangerous Actions ─────────────────────────────────
265
266 document.addEventListener('submit', function(e) {
267 var form = e.target;
268 if (!form.classList.contains('confirm-action')) return;
269 var msg = form.getAttribute('data-confirm') || 'Are you sure?';
270 if (!confirm(msg)) {
271 e.preventDefault();
272 }
273 });
274
275})();
276`;