From d6ada9a222b9d34f292cf9f67234e552ca916a6f Mon Sep 17 00:00:00 2001 From: garrettmills Date: Sat, 4 Jan 2025 05:02:05 -0500 Subject: [PATCH] Implement basic Chorus comment rendering for blog posts --- src/app/configs/server.config.ts | 4 + src/app/http/controllers/Blog.controller.ts | 10 +- src/app/resources/assets/chorus.js | 106 ++++++++++++++++++++ src/app/resources/assets/main-70s.css | 47 +++++++++ src/app/resources/views/blog/post.pug | 17 ++++ 5 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/app/resources/assets/chorus.js diff --git a/src/app/configs/server.config.ts b/src/app/configs/server.config.ts index bbf4d9c..8708a73 100644 --- a/src/app/configs/server.config.ts +++ b/src/app/configs/server.config.ts @@ -15,6 +15,10 @@ export default { forceSsl: env('SERVER_FORCE_SSL', false), + chorus: { + baseUrl: env('CHORUS_BASE_URL'), + }, + session: { /* The implementation of @extollo/lib.Session that serves as the session backend. */ driver: ORMSession, diff --git a/src/app/http/controllers/Blog.controller.ts b/src/app/http/controllers/Blog.controller.ts index f31a81a..172a6d6 100644 --- a/src/app/http/controllers/Blog.controller.ts +++ b/src/app/http/controllers/Blog.controller.ts @@ -1,4 +1,4 @@ -import {Controller, view, Inject, Injectable, collect, plaintext} from '@extollo/lib' +import {Controller, view, Inject, Injectable, collect, plaintext, Config} from '@extollo/lib' import {Home} from './Home.controller' import {Blog as BlogService} from '../../services/Blog.service' import {BlogPost} from '../../services/blog/AbstractBlog.service' @@ -13,6 +13,9 @@ export class Blog extends Controller { @Inject() protected readonly blog!: BlogService + @Inject() + protected readonly config!: Config + public async index() { const home = this.make(Home) const posts = await this.blog.getAllPosts() @@ -37,6 +40,7 @@ export class Blog extends Controller { }) } + // FIXME: set chorusThread here return view('blog:post', { ...home.getThemeCSS(), ...this.getBlogData(), @@ -128,7 +132,11 @@ export class Blog extends Controller { } public getBlogData(): any { + let chorusUrl = String(this.config.get('server.chorus.baseUrl') || '') + if ( chorusUrl && !chorusUrl.endsWith('/') ) chorusUrl += '/' + return { + chorusUrl, blogUrl: (post: BlogPost): string => this.blog.getUrl(post), blogDate: (date: Date): string => { const year = date.getFullYear() diff --git a/src/app/resources/assets/chorus.js b/src/app/resources/assets/chorus.js new file mode 100644 index 0000000..7ac0bc0 --- /dev/null +++ b/src/app/resources/assets/chorus.js @@ -0,0 +1,106 @@ + +if ( !window.Chorus ) window.Chorus = {} + +Chorus.processThreadData = function(threadData) { + threadData.refresh.date = new Date(threadData.refresh.date) + for ( const comment of threadData.comments ) { + comment.date = new Date(comment.date) + } + + threadData.comments = threadData.comments + .sort((a, b) => b.date.getTime() - a.date.getTime()) + + return threadData +}; + +Chorus.init = async function(selector) { + const el = document.querySelector(selector) + if ( !el ) { + console.warn('[Chorus] Could not init: could not find element with selector: ' + selector) + return + } + + let baseUrl = el.getAttribute('data-chorus-url') + if ( !baseUrl ) { + console.warn('[Chorus] Could not init: host element is missing data-chorus-url attribute') + return + } + if ( !baseUrl.endsWith('/') ) { + baseUrl += '/' + } + + const threadId = el.getAttribute('data-chorus-thread') + if ( !threadId ) { + console.warn('[Chorus] Could not init: host element is missing data-chorus-thread attribute') + return + } + + const threadDataUrl = `${baseUrl}threads/${threadId}.json` + const response = await fetch(threadDataUrl) + + if ( response.status === 404 ) { + // fixme + } + + if ( !response.ok ) { + console.error('[Chorus] Failed to load thread data') + return + } + + const threadData = Chorus.processThreadData(await response.json()) + + Chorus.renderSummary(el, threadData) + + const commentsDiv = document.createElement('div') + commentsDiv.classList.add('chorus-comments-list') + for ( const comment of threadData.comments ) { + Chorus.renderComment(commentsDiv, comment) + } + + el.appendChild(commentsDiv) + console.log(threadData) +}; + +Chorus.renderComment = function(commentsDiv, comment) { + const commentDiv = document.createElement('div') + commentDiv.classList.add('chorus-comment') + commentsDiv.appendChild(commentDiv) + + const authorEl = document.createElement('h2') + authorEl.classList.add('chorus-byline') + authorEl.innerText = comment.user.name + authorEl.title = 'User ID: ' + comment.user.mailId + '\nDomain ID: ' + comment.user.domainId + commentDiv.appendChild(authorEl) + + const statusEl = document.createElement('p') + statusEl.classList.add('chorus-status') + statusEl.innerText = comment.date.toLocaleString() + commentDiv.appendChild(statusEl) + + if ( comment.subject ) { + const subjectEl = document.createElement('h3') + subjectEl.innerText = comment.subject + commentDiv.appendChild(subjectEl) + } + + const contentEl = document.createElement('p') + contentEl.innerHTML = comment.rendered + commentDiv.appendChild(contentEl) +}; + +Chorus.renderSummary = function(el, threadData) { + const summaryDiv = document.createElement('div') + summaryDiv.classList.add('chorus-summary') + el.appendChild(summaryDiv) + + const summaryUl = document.createElement('ul') + summaryDiv.appendChild(summaryUl) + + const countLi = document.createElement('li') + countLi.innerText = `${threadData.comments.length} comment${threadData.comments.length === 1 ? '' : 's'}` + summaryUl.appendChild(countLi) + + const lastRefreshLi = document.createElement('li') + lastRefreshLi.innerText = `Last refreshed: ${threadData.refresh.date.toLocaleString()}` + summaryUl.appendChild(lastRefreshLi) +}; diff --git a/src/app/resources/assets/main-70s.css b/src/app/resources/assets/main-70s.css index 5154257..65e4d64 100644 --- a/src/app/resources/assets/main-70s.css +++ b/src/app/resources/assets/main-70s.css @@ -893,3 +893,50 @@ section#auth h3 { padding: 3px 7px; font-size: 1.2em; } + +.comments-container { + margin-top: 100px; +} + +.comments-container > h1 { + font-size: 3em; +} + +.comments .chorus-summary ul { + list-style-type: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: row; +} + +.comments .chorus-summary ul li:first-of-type { + flex: 1; +} + +.comments .chorus-comment { + background-color: var(--c-background-offset); + border-left: 3px solid var(--c-hero); + padding: 1px 20px; + margin: 50px 20px; +} + +.comments .chorus-comment .chorus-byline { + font-size: 1.5em; + padding-top: 10px; +} + +.comments .chorus-comment .chorus-status { + font-size: 1.0em; + margin-top: 0; +} + +.comments .chorus-comment > h3 { + font-family: "Lora", serif; + font-weight: bold; + font-size: 1.5em; + margin: 0; + padding: 10px 0 0; + color: var(--c-font); + background-color: var(--c-font); +} diff --git a/src/app/resources/views/blog/post.pug b/src/app/resources/views/blog/post.pug index 828b207..cfff423 100644 --- a/src/app/resources/views/blog/post.pug +++ b/src/app/resources/views/blog/post.pug @@ -29,6 +29,10 @@ block append script script(src=asset('highlight/highlight.min.js')) script. hljs.highlightAll(); + if chorusUrl && chorusThread + script(src=asset('chorus.js')) + script. + Chorus?.init('#chorus-container') block blog_content h2.post-title #{post.title} @@ -40,3 +44,16 @@ block blog_content a.button(href=named('blog')+'/tag/'+tag) ##{tag} .post-content !{renderedPost} + + if chorusUrl && chorusThread + .section-border + .section-border-inner-1 + .section-border-inner-2 + + .comments-container + h1 Comments + p Thanks for reading! I'd love to hear your thoughts and questions. + p My blog uses an email-based comments system: Submit a Comment + p You can also email me directly. + hr + div.comments#chorus-container(data-chorus-url=chorusUrl data-chorus-thread=chorusThread)