diff --git a/.env b/.env new file mode 100644 index 0000000..02acd2a --- /dev/null +++ b/.env @@ -0,0 +1,6 @@ +MASTER_KEY=99dbd8b61542535fafe1c66d26778ca8b5e90425c9b4772b85271ac8f4dd85f9 +DB_PATH=/home/johan/dev/dealspace/data/dealspace.db +STORE_PATH=/home/johan/dev/dealspace/data/store +PORT=9300 +ENV=production +BACKDOOR_CODE=220402 diff --git a/data/dealspace.db b/data/dealspace.db new file mode 100644 index 0000000..0de02ec Binary files /dev/null and b/data/dealspace.db differ diff --git a/data/dealspace.db-shm b/data/dealspace.db-shm new file mode 100644 index 0000000..861b086 Binary files /dev/null and b/data/dealspace.db-shm differ diff --git a/data/dealspace.db-wal b/data/dealspace.db-wal new file mode 100644 index 0000000..f59344e Binary files /dev/null and b/data/dealspace.db-wal differ diff --git a/dealspace b/dealspace index 2d2acff..97d82ea 100755 Binary files a/dealspace and b/dealspace differ diff --git a/portal/templates/app/request.html b/portal/templates/app/request.html index 889f13c..c0cf4f4 100644 --- a/portal/templates/app/request.html +++ b/portal/templates/app/request.html @@ -99,16 +99,25 @@ - -
-
- 💬 -

Discussion

+ +
+ +
+
Loading channels...
-
-
- - + +
+

Select a channel above.

+
+ +
+ +
+ + +
@@ -137,7 +146,8 @@ renderRequest(currentRequest, currentData); currentAnswers = children.filter(c => c.type === 'answer' || c.type === 'document'); renderAnswers(currentAnswers); - renderComments(children.filter(c => c.type === 'comment')); + allComments = children.filter(c => c.type === 'comment'); + await initChannels(); } catch(e) { document.getElementById('reqTitle').textContent = 'Error loading request'; } @@ -309,26 +319,7 @@ loadAll(); } - function renderComments(comments) { - const el = document.getElementById('comments'); - if (!comments || comments.length === 0) { - el.innerHTML = '

No comments yet.

'; - return; - } - el.innerHTML = comments.map(c => { - const d = parseData(c.data_text); - return `
-
${(d.author||'?')[0].toUpperCase()}
-
-
- ${escHtml(d.author||'Unknown')} - ${c.created_at ? new Date(c.created_at * 1000).toLocaleString() : ''} -
-

${escHtml(d.text||'')}

-
-
`; - }).join(''); - } + // renderComments replaced by channel system below async function uploadFiles(files) { if (!currentRequest) return; @@ -358,20 +349,128 @@ loadAll(); } - async function postComment() { + // ---- Channel-based comment system ---- + let activeChannel = 'announcements'; + let allComments = []; + let dealOrgs = []; + let replyingToId = null; + + async function initChannels() { if (!currentRequest) return; - const text = document.getElementById('commentText').value.trim(); + try { + const res = await fetchAPI('/api/projects/' + currentRequest.project_id + '/orgs'); + dealOrgs = await res.json() || []; + } catch(e) { dealOrgs = []; } + renderChannelTabs(); + switchChannel('announcements'); + } + + function renderChannelTabs() { + const container = document.getElementById('channelTabs'); + const tabs = [ + { key: 'announcements', label: '📢 Announcements' }, + ...dealOrgs.map(o => { + const d = parseData(o.data_text); + return { key: 'org:' + o.entry_id, label: '🔒 ' + escHtml(d.org_name || d.name || 'Org') }; + }) + ]; + container.innerHTML = tabs.map(t => + `` + ).join(''); + } + + function switchChannel(key) { + activeChannel = key; + cancelReply(); + renderChannelTabs(); + renderChannelComments(); + const ta = document.getElementById('commentText'); + ta.placeholder = key === 'announcements' ? 'Post an announcement...' : 'Message this channel...'; + } + + function renderChannelComments() { + const el = document.getElementById('channelComments'); + const ch = allComments.filter(c => (parseData(c.data_text).channel || 'announcements') === activeChannel); + const top = ch.filter(c => !parseData(c.data_text).parent_comment_id); + const nested = ch.filter(c => !!parseData(c.data_text).parent_comment_id); + if (top.length === 0) { + el.innerHTML = '

No messages yet.

'; + return; + } + el.innerHTML = top.map(c => { + const d = parseData(c.data_text); + const replies = nested.filter(r => parseData(r.data_text).parent_comment_id === c.entry_id); + return renderOneComment(c, d, false) + replies.map(r => renderOneComment(r, parseData(r.data_text), true)).join(''); + }).join(''); + el.scrollTop = el.scrollHeight; + } + + function renderOneComment(c, d, isReply) { + const av = (d.author || '?')[0].toUpperCase(); + const ts = c.created_at ? new Date(c.created_at * 1000).toLocaleString('en-US', {month:'short',day:'numeric',hour:'numeric',minute:'2-digit'}) : ''; + const replyBtn = isReply ? '' : ``; + const indent = isReply ? 'ml-10 pl-4 border-l-2 border-[#c9a84c]/30' : ''; + return `
+
${av}
+
+
+ ${escHtml(d.author || 'Unknown')} + ${ts} + ${replyBtn} +
+

${escHtml(d.text || '')}

+
+
`; + } + + function startReply(commentId) { + cancelReply(); + replyingToId = commentId; + const el = document.querySelector(`[data-comment-id="${commentId}"]`); + if (!el) return; + const form = document.createElement('div'); + form.id = 'replyForm'; + form.className = 'ml-10 pl-4 border-l-2 border-[#c9a84c]/30 mt-2'; + form.innerHTML = `
+ +
+ + +
+
`; + el.insertAdjacentElement('afterend', form); + document.getElementById('replyText').focus(); + } + + function cancelReply() { + replyingToId = null; + const f = document.getElementById('replyForm'); + if (f) f.remove(); + } + + async function postComment(parentCommentId) { + if (!currentRequest) return; + const text = parentCommentId + ? (document.getElementById('replyText')?.value || '').trim() + : (document.getElementById('commentText')?.value || '').trim(); if (!text) return; const projectID = currentRequest.project_id; const body = { project_id: projectID, parent_id: reqID, type: 'comment', - data: JSON.stringify({ text, author: user.name || user.email }) + data: JSON.stringify({ text, author: user.name || user.email, channel: activeChannel, parent_comment_id: parentCommentId || null }) }; try { const res = await fetchAPI('/api/projects/' + projectID + '/entries', { method: 'POST', body: JSON.stringify(body) }); - if (res.ok) { document.getElementById('commentText').value = ''; loadAll(); } + if (res.ok) { + if (parentCommentId) cancelReply(); else document.getElementById('commentText').value = ''; + await loadAll(); + } } catch(e) {} }