From dd362e3ebeb1c9f0f7c3bd6b2c844cbc6cde4864 Mon Sep 17 00:00:00 2001 From: Rendall Date: Fri, 17 Nov 2023 18:50:29 +0200 Subject: [PATCH] Add /icebreakers path to integrate with Icebreakers (#123) * Add topic path to local dev server * Add topic page * Fix threadComments when allComments is undefined * Update layout * Fix the create topic flow * Change /topic to /icebreakers * Add 'cancel' and 'load...' functions * Add Icebreakers styling * Add icebreakers functionality --- src/components/DiscussionDisplay.svelte | 31 ++- src/frontend-utilities.ts | 3 +- src/lib/discussion.xstate.ts | 2 +- src/scss/simple-comment-style.scss | 26 +++ src/simple-comment-icebreakers.ts | 78 +++++++ src/simple-comment.ts | 21 +- src/static/_redirects | 2 +- src/static/icebreakers/index.html | 263 ++++++++++++++++++++++++ src/static/topic/index.html | 11 - webpack.frontend.js | 9 + 10 files changed, 420 insertions(+), 26 deletions(-) create mode 100644 src/simple-comment-icebreakers.ts create mode 100644 src/static/icebreakers/index.html delete mode 100644 src/static/topic/index.html diff --git a/src/components/DiscussionDisplay.svelte b/src/components/DiscussionDisplay.svelte index 076c5c8..a40b1c0 100644 --- a/src/components/DiscussionDisplay.svelte +++ b/src/components/DiscussionDisplay.svelte @@ -16,6 +16,13 @@ export let discussionId: string export let title: string = "" export let currentUser: User | undefined + const emptyTopicMessages = [ + "Looks like this topic is waiting for its first thoughts. Be the pioneer and start the conversation!", + "This space is all yours! Kick off the discussion with your insights.", + "No comments yet. Share your perspective and spark the dialogue!", + "It's quiet here... Why not break the ice with your viewpoint?", + "A fresh topic awaits your input. Lead the way with your comment!", + ] let repliesFlatArray: (Comment & { isNew?: true })[] = [] let discussion: Discussion = { @@ -49,9 +56,9 @@ const updateDiscussionDisplay = ( topic: Discussion, - topicReplies: Comment[] + topicReplies: Comment[] | undefined ) => { - repliesFlatArray = topicReplies + repliesFlatArray = topicReplies ?? [] discussion = threadComments( topic, topicReplies, @@ -76,12 +83,13 @@ const { status, statusText, ok, body } = error as ServerResponse if (ok) console.warn("Error handler caught an OK response", error) - if ( - status === 404 && - statusText === "Not Found" && - body.startsWith("Topic") - ) { - send("CREATE") + const isUnknownTopic = + status === 404 && statusText === "Not Found" && body.startsWith("Topic") + + if (isUnknownTopic) { + setTimeout(() => { + send("CREATE") + }, 1) return } @@ -183,6 +191,13 @@
+ {#if !discussion?.replies} +

+ {emptyTopicMessages[ + Math.floor(Math.random() * emptyTopicMessages.length) + ]} +

+ {/if} {#if showReply === discussionId} ( comment: T, - allComments: U[], + allComments: U[] | undefined, sort: (a: U, b: U) => number = (a, b) => (b.id < a.id ? 0 : 1) ): T => { + if (!allComments) return { ...comment, replies: [] } // Make this robust but warn allComments = allComments.filter(reply => { if (reply && reply.id) return true diff --git a/src/lib/discussion.xstate.ts b/src/lib/discussion.xstate.ts index 4074a62..d97521b 100644 --- a/src/lib/discussion.xstate.ts +++ b/src/lib/discussion.xstate.ts @@ -34,7 +34,7 @@ export const discussionMachine = createMachine< DiscussionTypestate >( { - /** @xstate-layout N4IgpgJg5mDOIC5QQJawMYFdaxQewDsBaAMwBs8B3AOhQjLAGIBtABgF1FQAHPXAF3wEuIAB6IiAFgCcADmrSA7AFZFrSZNbTWi6coA0IAJ6IAjK1PVTANknKZagEzKlpgMwBfD4dQZsuQlIKGgoAQ1QCKEYAZQBVAGF4gFFo6LZOJBBeASERcQQiN1N5a0dbFxtpW2s3QxMER0US61ZZc2trPWLZRS8fNCwcISCqajCIqKSAJSmAeSn0kWyUQUI8iTdG6kVHWWULN1kepVrjREli6kk3SV1lTpt3U2U+kF9BgOJyUfQAJzBQoJIjEEslUotMstVsJMvlpJpqI5WG5WEjlLJHG4OpI6mZJI4FNIiXoUS5JEdrK93v5ht8aH8AUDJjN5hCeHwVrlYYh4axEcjUax0ZjsbiGrJpNR7o4ZG45Bcdo4qQMaYE6dQGYDICwOEsOdD1gVHGUpbdNJ09tcyWLTETqBLbmpZJ19sbZMq-EM1cExnhwtr4lMkgBBAAqSTZWX1XNA+Q0fJ02hRdnschlYqKBJ66k2jmKlVMHo+tJ9YF+vzwv0YQdDUwAmpGoTGxGZTIpEYpnnJpOZzLJWmL4dRrDY9hjTM9bPii6qvqXy5XGIGQ+HG9G1tyEOYdMOepo3PYdodTvVMZLWKUZLINLID4peq8CHgIHARNSvXOqHqchvYxJ7m41BuFieiKCOMhaDiZwFKUUoWLaeyND2SKUt4bwqh+Iw0HQDDfpyv4tgUyjPEBIFYmByJtKYYoyoSRysOoxqtA4M6Yeq4woJEeEGpuRDEcopEumBpgQfCNrSIBqLoioEEKqh-Sep8WEav8gKcVA3HNvkbRZtc1iqNojg9sBGYWAo1h7hKyL8SorFKeqmr8JAmkEfkE4XIiV63kUF6KIcNotMOSg9DKrTKAe8J2SWozjM5kLrjCf4FBo2wouo5pCsUEkGNBKKWFUllJjZUXeqMZYVr8LmJYRImAX2HQ6BZejSGKdgEneNSPK0VmeF4HhAA */ + /** @xstate-layout N4IgpgJg5mDOIC5QQJawMYFdaxQewDsBaAMwBs8B3AOhQjLAGIBtABgF1FQAHPXAF3wEuIAB6IALAE4AHNQkB2AIwTWKpawUBWCToA0IAJ6IATKwDM1TUq0yAbOYkmtCu7IC+7g6gzZchUgoaCgBDVAIoRgBlAFUAYTiAUSiotk4kEF4BIRFxBGk5RRU1CQ1tXQkDYwQTaWoZVxMZEyVzOzclGXNPbzQsHCFAqmpQ8MjEgCUJgHkJtJEslEFCXMQFR3qW83N7CS67bSrEGwVqFy0Tc0uGuxUpOx6QH37-YnJh9AAnMBDBCOj4kkUvMMotlsIMnkTJdqCYpAopI4FHDdBolEcEOZ1vUpKw8dCruZWDIpI9nn5Bu8aF8fn9xlNZiCeHwljlIZItEpYV0SeYVBIsQiMbpTiTrIpbHZat0vE8+hSAlTqDTfpAWBwFizwat8rJ5MpVOpNDp9EZEOZOdQ7HjlBc7IoZKwpBIyfKBoqgiM8GE1QAZaYAQQAIkzMlq2aA8gV9cUjeVTdU4VIrTJUyoTMo1LdXb53W9PWBPp88J9GBNEgAVCYATVDYIjYkQMi0WmoUiUiKKOxMDkqZsxrBMbZaEntWjxTk0OZelILRZLjDi5YDFcSdfDK3ZCFxooUqjxWjsDQRWgx22o63tLWbUlvzqUnllBDwEDgInJeaGlE12U3kcQRB6koCItPcUrKPadgYkQWiWJK458k0Ap4lIWjTgq+bDHQDA-qyf6NpiyjyFIBLrGBNgmBiZhcsi7bIessGqA8sofq8X5emEKARLh2pble8hKAcZgZvaFpSFR8JnGojilDe9wPixbpsUqKp0jxDZ5DBrDUMBJEdu0GaCaOwpStQOx7g47Ydm07ToZ+KnfKqEDqfheRYnYxHXrYCJgQm5rjrCthNESFpqLeMq9LmymeqMkAuRC-6EVy0ikeY5GwRJNFHgc0gSHlzoyHZ0XDIWxafPFOrwh5uIkmYDQ6PlZ54rC2yocFpSsIej7uEAA */ id: "discussion-flow", initial: "idle", context: {}, diff --git a/src/scss/simple-comment-style.scss b/src/scss/simple-comment-style.scss index cad0c0b..feaf306 100644 --- a/src/scss/simple-comment-style.scss +++ b/src/scss/simple-comment-style.scss @@ -563,3 +563,29 @@ $highlight-text-color: color.scale( background-color: inherit; } } + +.icebreakers { + header { + max-width: 62rem; + align-items: flex-start; + margin: 3rem auto; + + .headlines { + margin-left: 3rem; + h1, + h2 { + margin: 0; + padding: 0; + } + + h2.icebreakers-logotype { + font-size: 2.2rem; + margin-bottom: 2rem; + } + } + + p.instructions { + margin-bottom: 2rem; + } + } +} diff --git a/src/simple-comment-icebreakers.ts b/src/simple-comment-icebreakers.ts new file mode 100644 index 0000000..0202de4 --- /dev/null +++ b/src/simple-comment-icebreakers.ts @@ -0,0 +1,78 @@ +declare global { + interface Window { + getQuestion: (slug: string) => Promise + } +} +const storageKey = "icebreakerQuestions" +const timeStampKey = "icebreakerQuestionsTimeStamp" + +const toSlug = (str: string): string => + str + .toLowerCase() + .replace(/ /g, "-") + .replace(/[^\w-]+/g, "") + .replace(/_/g, "") + .replace(/-{2,}/g, "-") + .replace(/-$/, "") + .replace(/^-/, "") + +const isSlugMatch = (slug: string, question: string) => + slug === toSlug(question) + +const reverseSlug = (slug: string, questions: string[]) => + questions.find(question => isSlugMatch(slug, question)) + +const fetchAndStoreQuestions = () => + new Promise((resolve, reject) => { + const currentTimestamp = + document?.getElementById("questions-time-stamp")?.innerText || "0" + const storedTimestamp = localStorage.getItem(timeStampKey) + + const isStoredQuestionsValid = + storedTimestamp && parseInt(storedTimestamp) >= parseInt(currentTimestamp) + + const storedQuestions = localStorage.getItem(storageKey) + if (isStoredQuestionsValid && storedQuestions) { + resolve(JSON.parse(storedQuestions)) + return + } + + fetchQuestions(currentTimestamp, resolve, reject) + }) + +const fetchQuestions = async (currentTimestamp, resolve, reject) => { + try { + const questionFile = await fetch( + "https://raw.githubusercontent.com/rendall/icebreakers/master/QUESTIONS.md" + ) + const questionsText = await questionFile.text() + const questionLines = questionsText.split("\n") + const questions = questionLines + .filter(line => /^ {2}\* [A-Z]/.test(line)) + .map(line => line.slice(4)) + + localStorage.setItem(storageKey, JSON.stringify(questions)) + localStorage.setItem(timeStampKey, currentTimestamp) + resolve(questions) + } catch (error) { + reject(error) + } +} + +const getQuestion = (slug: string) => + new Promise((resolve, reject) => { + fetchAndStoreQuestions() + .then(questions => { + const question = reverseSlug(slug, questions) + if (question) { + resolve(question) + } else { + reject("No question found") + } + }) + .catch(reject) + }) + +window.getQuestion = getQuestion + +export default getQuestion diff --git a/src/simple-comment.ts b/src/simple-comment.ts index 7005ac3..8e2c081 100644 --- a/src/simple-comment.ts +++ b/src/simple-comment.ts @@ -4,19 +4,23 @@ import SimpleComment from "./components/SimpleComment.svelte" declare global { interface Window { - setSimpleCommentOptions: (setupOptions: { [key: string]: unknown }) => void + loadSimpleComment: (options: Options) => void setSimpleCommentDiscussion: (discussionId: string) => void + setSimpleCommentOptions: (setupOptions: { [key: string]: unknown }) => void } } let simpleComment let options = { + cancel: false, discussionId: getDefaultDiscussionId(), - title: document.title, target: document.getElementById("simple-comment") ?? document.body, + title: document.title, } +type Options = typeof options + /** * Sets the options for the SimpleComment component. * This function merges the provided options with the default options. @@ -51,16 +55,25 @@ window.setSimpleCommentOptions = setupOptions => window.setSimpleCommentDiscussion = (discussionId: string) => window.setSimpleCommentOptions({ discussionId }) -// Wait for DOMContentLoaded event before initializing SimpleComment -document.addEventListener("DOMContentLoaded", () => { +const loadSimpleComment = (setupOptions: Options) => { + options = { ...options, ...setupOptions } simpleComment = new SimpleComment({ target: options.target, props: { discussionId: options.discussionId, title: options.title }, }) +} + +window.loadSimpleComment = loadSimpleComment + +// Wait for DOMContentLoaded event before initializing SimpleComment +document.addEventListener("DOMContentLoaded", () => { + if (options.cancel) return + else loadSimpleComment(options) }) export default { simpleComment, setSimpleCommentOptions: window.setSimpleCommentOptions, setSimpleCommentDiscussion: window.setSimpleCommentDiscussion, + loadSimpleComment: window.loadSimpleComment, } diff --git a/src/static/_redirects b/src/static/_redirects index 06f3d16..6024817 100644 --- a/src/static/_redirects +++ b/src/static/_redirects @@ -1 +1 @@ -/topic/* /topic/index.html 200 +/icebreakers/* /icebreakers/index.html 200 diff --git a/src/static/icebreakers/index.html b/src/static/icebreakers/index.html new file mode 100644 index 0000000..3cb614d --- /dev/null +++ b/src/static/icebreakers/index.html @@ -0,0 +1,263 @@ + + + + Simple Comment + + + + + + + + + + + + + + +
+ +
+

Icebreakers

+

Questions to Break the Ice

+
+
+
+
+

+ What's your take? Share your answer or respond to someone else's. + Whether it's a thoughtful insight, a quick remark, or a deep + reflection, your voice adds to the mix. +

+
+
+ +
+ + + + + diff --git a/src/static/topic/index.html b/src/static/topic/index.html deleted file mode 100644 index 7813acf..0000000 --- a/src/static/topic/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Hello World - - -

Hello World

- - diff --git a/webpack.frontend.js b/webpack.frontend.js index 6b23d2c..ad07b34 100644 --- a/webpack.frontend.js +++ b/webpack.frontend.js @@ -36,10 +36,19 @@ module.exports = { compress: true, port: 5000, static: path.join(__dirname, "dist"), + historyApiFallback: { + rewrites: [ + { from: /^\/icebreakers\/.*$/, to: "/icebreakers/index.html" }, + ], + }, }, devtool: "source-map", entry: { "simple-comment": path.resolve(__dirname, "src/simple-comment.ts"), + "simple-comment-icebreakers": path.resolve( + __dirname, + "src/simple-comment-icebreakers.ts" + ), "simple-comment-style": path.resolve( __dirname, "src/scss/simple-comment-style.scss"