From 924b3a9350a4616d5bc137fa73cf13f115a29940 Mon Sep 17 00:00:00 2001 From: Antal Orcsik Date: Fri, 2 Feb 2024 15:31:43 +0100 Subject: [PATCH 1/5] list page first version with worker --- .eslintrc.json | 2 + index.js | 4 + src/js/changelog.js | 159 +++++++++++++++++++++++++++++++++++++ src/js/changelog/worker.js | 17 ++++ webpack.common.js | 1 + 5 files changed, 183 insertions(+) create mode 100644 src/js/changelog.js create mode 100644 src/js/changelog/worker.js diff --git a/.eslintrc.json b/.eslintrc.json index 683ba6d..e8768a2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,9 +22,11 @@ "HTMLElement", "HTMLHeadingElement", "HTMLImageElement", + "HTMLLIElement", "HTMLMetaElement", "HTMLParagraphElement", "HTMLSpanElement", + "HTMLUListElement", "Promise", "URL" ] diff --git a/index.js b/index.js index dba3724..41d2db9 100644 --- a/index.js +++ b/index.js @@ -41,6 +41,7 @@ addEventListener('common', event => { }); includeWorkerScript("integrations", "./src/js/integrations/worker.js"); +includeWorkerScript("changelog", "./src/js/changelog/worker.js"); /** * @param {URL} urlObject @@ -50,6 +51,9 @@ function getFetchEventHandler(urlObject) { if (urlObject.pathname.match(/^\/integrations/)) { return fetchEventHandlers['integrations']; } + if (urlObject.pathname.match(/^\/changelog/)) { + return fetchEventHandlers['changelog']; + } return fetchEventHandlers['common']; } diff --git a/src/js/changelog.js b/src/js/changelog.js new file mode 100644 index 0000000..6732920 --- /dev/null +++ b/src/js/changelog.js @@ -0,0 +1,159 @@ + + +class TopicListItem +{ + constructor(data) { + this.data = data; + } + + /** @returns {number} */ + get id() { + return this.data.id; + } + + /** @returns {string} */ + get fancyTitle() { + return this.data.fancy_title.replace(/:([^\s]+):/ig, '$1'); + } + + /** @returns {Date} */ + get createdAt() { + return new Date(this.data.created_at); + } + + /** @returns {string} */ + get slug() { + return this.data.slug; + } + + /** @returns {string} */ + get webflowUrl() { + return `/changelog/${this.slug}/${this.id}`; + } + + /** @returns {boolean} */ + get isPinned() { + return !!this.data.pinned; + } +} + +class ChangelogService +{ + constructor() { + this.changelogUrl = "/changelog.json"; + + /** @type {TopicListItem[]} */ + this.topics = []; + + this.nextPage = 0; + this.currentPage = -1; + } + + /** @returns {Promise} */ + async loadMore() { + if (this.isMore) { + const url = this.changelogUrl + "?page=" + this.nextPage; + const response = await fetch(url); + const json = await response.json(); + + this.currentPage = this.nextPage; + + if (json.topic_list.more_topics_url) { + const moreTopicsUrlMatch = json.topic_list.more_topics_url.match(/page=(\d+)/); + this.nextPage = parseInt(moreTopicsUrlMatch[1]); + } + + json.topic_list.topics.forEach(data => { + this.topics.push(new TopicListItem(data)); + }); + } + + return this.topics; + } + + /** @returns {boolean} */ + get isMore() { + return this.nextPage > this.currentPage; + } +} + +class ChangelogList +{ + constructor(list, loadMoreButton) { + /** @type {HTMLUListElement} */ + this.list = list; + + /** @type {HTMLAnchorElement} */ + this.loadMoreButton = loadMoreButton; + + /** @type {HTMLLIElement[]} */ + this.listItems = []; + + /** @type {HTMLLIElement} */ + this.unreadListItemTemplate = this.list.querySelector("#changelog-unread-template"); + this.unreadListItemTemplate.id = ""; + this.unreadListItemTemplate.remove(); + + /** @type {HTMLLIElement} */ + this.readListItemTemplate = this.list.querySelector("#changelog-read-template"); + this.readListItemTemplate.id = ""; + this.readListItemTemplate.remove(); + } + + + /** + * @param {TopicListItem} topic + * @param {boolean} isUnread + * @returns {HTMLLIElement} + */ + renderListItem(topic, isUnread = false) { + const listItem = (isUnread ? this.unreadListItemTemplate : this.readListItemTemplate).cloneNode(true); + listItem.querySelector(".changelog-timestamp").innerHTML = `[${topic.createdAt.toLocaleDateString()}]`; + listItem.querySelector(".changelog-title").innerHTML = topic.fancyTitle; + listItem.querySelector("a").href = topic.webflowUrl; + return listItem; + } + + /** + * @param {TopicListItem[]} topics + * @param {boolean} isMore + */ + render(topics, isMore) { + for (let topic of topics) { + if (!this.listItems[topic.id]) { + const listItem = this.renderListItem(topic); + this.listItems[topic.id] = listItem; + this.list.append(listItem); + } + } + if (!isMore) { + this.loadMoreButton.remove(); + } + } +} + +const changelogService = new ChangelogService(); + +const changelogList = new ChangelogList( + document.getElementById("changelog-list"), + document.getElementById("changelog-load-more-button") +); +changelogList.loadMoreButton.addEventListener('click', () => { + changelogList.loadMoreButton.disabled = true; + changelogList.loadMoreButton.innerHTML = "Loading..."; + changelogService.loadMore().then(topics => { + changelogList.render(topics, changelogService.isMore); + changelogList.loadMoreButton.disabled = false; + changelogList.loadMoreButton.innerHTML = "Load more..."; + }); +}); + +changelogList.loadMoreButton.disabled = true; +changelogList.loadMoreButton.innerHTML = "Loading..."; +changelogService.loadMore().then(topics => { + changelogList.render(topics, changelogService.isMore); + changelogList.loadMoreButton.disabled = false; + changelogList.loadMoreButton.innerHTML = "Load more..."; +}); + + diff --git a/src/js/changelog/worker.js b/src/js/changelog/worker.js new file mode 100644 index 0000000..af35475 --- /dev/null +++ b/src/js/changelog/worker.js @@ -0,0 +1,17 @@ +addEventListener('fetch', event => { + let urlObject = new URL(event.request.url); + + urlObject.hostname = 'webflow.bitrise.io'; + + if (urlObject.pathname.match(/^\/changelog\/(.+\.json)$/)) { + urlObject.hostname = 'discuss.bitrise.io'; + urlObject.pathname = urlObject.pathname.replace(/^\/changelog\/(.+\.json)$/, '/t/$1'); + } else if (urlObject.pathname.match(/^\/changelog\.json$/)) { + urlObject.hostname = 'discuss.bitrise.io'; + urlObject.pathname = '/c/product-updates/42.json'; + } else if (urlObject.pathname.match(/^\/changelog\/.+/)) { + urlObject.pathname = '/changelog/topic'; + } + + event.respondWith(fetch(urlObject)); +}) \ No newline at end of file diff --git a/webpack.common.js b/webpack.common.js index d8918ba..a2992f6 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -11,6 +11,7 @@ module.exports = { integrations: "js/integrations.js", steps: "js/steps.js", careers: "js/careers.js", + changelog: "js/changelog.js", }, output: { path: path.resolve(__dirname, "dist"), From c50316228d041eae9c9f8b71536aa278ea788580 Mon Sep 17 00:00:00 2001 From: Antal Orcsik Date: Fri, 2 Feb 2024 18:05:20 +0100 Subject: [PATCH 2/5] changelog topics --- src/css/changelog.css | 17 +++ src/js/changelog-topic.js | 28 +++++ src/js/changelog.js | 175 ++++----------------------- src/js/changelog/ChangelogList.js | 51 ++++++++ src/js/changelog/ChangelogService.js | 64 ++++++++++ src/js/changelog/ChangelogTopic.js | 45 +++++++ webpack.common.js | 9 +- 7 files changed, 236 insertions(+), 153 deletions(-) create mode 100644 src/css/changelog.css create mode 100644 src/js/changelog-topic.js create mode 100644 src/js/changelog/ChangelogList.js create mode 100644 src/js/changelog/ChangelogService.js create mode 100644 src/js/changelog/ChangelogTopic.js diff --git a/src/css/changelog.css b/src/css/changelog.css new file mode 100644 index 0000000..355f329 --- /dev/null +++ b/src/css/changelog.css @@ -0,0 +1,17 @@ +.changelog-title img.emoji, +#changelog-topic-title img.emoji, +.changelog-topic-content img.emoji { + vertical-align: sub; + width: 1.25em; + height: 1.25em; + display: inline; +} + +.changelog-topic-content .meta { + display: none; +} + +.changelog-topic-content h2 { + font-size: 2rem; + margin-top: 1rem; +} \ No newline at end of file diff --git a/src/js/changelog-topic.js b/src/js/changelog-topic.js new file mode 100644 index 0000000..be2d64d --- /dev/null +++ b/src/js/changelog-topic.js @@ -0,0 +1,28 @@ +import ChangelogService from "./changelog/ChangelogService"; + +const style = document.createElement("style"); +document.getElementsByTagName("head")[0].appendChild(style); +style.appendChild(document.createTextNode(require("../css/changelog.css"))); + +/** + * @param {URL} url + * @returns {?string} + */ +function detectTopicFromUrl(url) { + let path = url.pathname; + const match = path.match(/changelog\/(.+)$/); + if (match) { + return match[1]; + } + return null; +} + +const url = new URL(document.location.href); +const topicSlugId = detectTopicFromUrl(url); + +ChangelogService.loadTopic(topicSlugId).then(topic => { + + document.getElementById("changelog-topic-title").innerHTML = topic.fancyTitle; + document.getElementById("changelog-topic-meta").innerHTML = topic.createdAt.toLocaleDateString(); + document.getElementById("changelog-topic-content").innerHTML = topic.posts[0].cooked; +}); \ No newline at end of file diff --git a/src/js/changelog.js b/src/js/changelog.js index 6732920..3ececf0 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,159 +1,36 @@ +import ChangelogList from "./changelog/ChangelogList"; +import ChangelogService from "./changelog/ChangelogService"; +const style = document.createElement("style"); +document.getElementsByTagName("head")[0].appendChild(style); +style.appendChild(document.createTextNode(require("../css/changelog.css"))); -class TopicListItem -{ - constructor(data) { - this.data = data; - } +/** @type {string} */ +const apiBase = document.location.hostname.match(/(localhost|127\.0\.0\.1)/) ? "" : "https://bitrise.io"; +const changelogService = new ChangelogService(apiBase); - /** @returns {number} */ - get id() { - return this.data.id; - } +const changelogList = new ChangelogList(document.getElementById("changelog-list")); +/** @type {HTMLAnchorElement} */ +const changelogLoadMoreButton = document.getElementById("changelog-load-more-button"); - /** @returns {string} */ - get fancyTitle() { - return this.data.fancy_title.replace(/:([^\s]+):/ig, '$1'); - } - - /** @returns {Date} */ - get createdAt() { - return new Date(this.data.created_at); - } - - /** @returns {string} */ - get slug() { - return this.data.slug; - } - - /** @returns {string} */ - get webflowUrl() { - return `/changelog/${this.slug}/${this.id}`; - } - - /** @returns {boolean} */ - get isPinned() { - return !!this.data.pinned; - } -} - -class ChangelogService -{ - constructor() { - this.changelogUrl = "/changelog.json"; - - /** @type {TopicListItem[]} */ - this.topics = []; - - this.nextPage = 0; - this.currentPage = -1; - } - - /** @returns {Promise} */ - async loadMore() { - if (this.isMore) { - const url = this.changelogUrl + "?page=" + this.nextPage; - const response = await fetch(url); - const json = await response.json(); - - this.currentPage = this.nextPage; - - if (json.topic_list.more_topics_url) { - const moreTopicsUrlMatch = json.topic_list.more_topics_url.match(/page=(\d+)/); - this.nextPage = parseInt(moreTopicsUrlMatch[1]); - } - - json.topic_list.topics.forEach(data => { - this.topics.push(new TopicListItem(data)); - }); - } - - return this.topics; - } - - /** @returns {boolean} */ - get isMore() { - return this.nextPage > this.currentPage; - } -} - -class ChangelogList -{ - constructor(list, loadMoreButton) { - /** @type {HTMLUListElement} */ - this.list = list; - - /** @type {HTMLAnchorElement} */ - this.loadMoreButton = loadMoreButton; - - /** @type {HTMLLIElement[]} */ - this.listItems = []; - - /** @type {HTMLLIElement} */ - this.unreadListItemTemplate = this.list.querySelector("#changelog-unread-template"); - this.unreadListItemTemplate.id = ""; - this.unreadListItemTemplate.remove(); - - /** @type {HTMLLIElement} */ - this.readListItemTemplate = this.list.querySelector("#changelog-read-template"); - this.readListItemTemplate.id = ""; - this.readListItemTemplate.remove(); - } - - - /** - * @param {TopicListItem} topic - * @param {boolean} isUnread - * @returns {HTMLLIElement} - */ - renderListItem(topic, isUnread = false) { - const listItem = (isUnread ? this.unreadListItemTemplate : this.readListItemTemplate).cloneNode(true); - listItem.querySelector(".changelog-timestamp").innerHTML = `[${topic.createdAt.toLocaleDateString()}]`; - listItem.querySelector(".changelog-title").innerHTML = topic.fancyTitle; - listItem.querySelector("a").href = topic.webflowUrl; - return listItem; - } +/** */ +function loadMore() { + changelogLoadMoreButton.disabled = true; + changelogLoadMoreButton.innerHTML = "Loading..."; + changelogService.loadMore().then(topics => { + changelogList.render(topics); + changelogLoadMoreButton.disabled = false; + changelogLoadMoreButton.innerHTML = "Load more..."; - /** - * @param {TopicListItem[]} topics - * @param {boolean} isMore - */ - render(topics, isMore) { - for (let topic of topics) { - if (!this.listItems[topic.id]) { - const listItem = this.renderListItem(topic); - this.listItems[topic.id] = listItem; - this.list.append(listItem); - } - } - if (!isMore) { - this.loadMoreButton.remove(); + if (!changelogService.isMore) { + changelogLoadMoreButton.remove(); } - } -} - -const changelogService = new ChangelogService(); - -const changelogList = new ChangelogList( - document.getElementById("changelog-list"), - document.getElementById("changelog-load-more-button") -); -changelogList.loadMoreButton.addEventListener('click', () => { - changelogList.loadMoreButton.disabled = true; - changelogList.loadMoreButton.innerHTML = "Loading..."; - changelogService.loadMore().then(topics => { - changelogList.render(topics, changelogService.isMore); - changelogList.loadMoreButton.disabled = false; - changelogList.loadMoreButton.innerHTML = "Load more..."; }); -}); +} -changelogList.loadMoreButton.disabled = true; -changelogList.loadMoreButton.innerHTML = "Loading..."; -changelogService.loadMore().then(topics => { - changelogList.render(topics, changelogService.isMore); - changelogList.loadMoreButton.disabled = false; - changelogList.loadMoreButton.innerHTML = "Load more..."; +changelogLoadMoreButton.addEventListener('click', event => { + event.preventDefault(); + loadMore(); }); - +loadMore(); diff --git a/src/js/changelog/ChangelogList.js b/src/js/changelog/ChangelogList.js new file mode 100644 index 0000000..15897c8 --- /dev/null +++ b/src/js/changelog/ChangelogList.js @@ -0,0 +1,51 @@ +import ChangelogTopic from "./ChangelogTopic"; + +class ChangelogList +{ + constructor(list) { + /** @type {HTMLUListElement} */ + this.list = list; + + /** @type {HTMLLIElement[]} */ + this.listItems = []; + + /** @type {HTMLLIElement} */ + this.unreadListItemTemplate = this.list.querySelector("#changelog-unread-template"); + this.unreadListItemTemplate.id = ""; + this.unreadListItemTemplate.remove(); + + /** @type {HTMLLIElement} */ + this.readListItemTemplate = this.list.querySelector("#changelog-read-template"); + this.readListItemTemplate.id = ""; + this.readListItemTemplate.remove(); + } + + + /** + * @param {ChangelogTopic} topic + * @param {boolean} isUnread + * @returns {HTMLLIElement} + */ + renderListItem(topic, isUnread = false) { + const listItem = (isUnread ? this.unreadListItemTemplate : this.readListItemTemplate).cloneNode(true); + listItem.querySelector(".changelog-timestamp").innerHTML = `[${topic.createdAt.toLocaleDateString()}]`; + listItem.querySelector(".changelog-title").innerHTML = topic.fancyTitle; + listItem.querySelector("a").href = topic.webflowUrl; + return listItem; + } + + /** + * @param {ChangelogTopic[]} topics + */ + render(topics) { + for (let topic of topics) { + if (!this.listItems[topic.id]) { + const listItem = this.renderListItem(topic); + this.listItems[topic.id] = listItem; + this.list.append(listItem); + } + } + } +} + +export default ChangelogList; \ No newline at end of file diff --git a/src/js/changelog/ChangelogService.js b/src/js/changelog/ChangelogService.js new file mode 100644 index 0000000..c77e4f4 --- /dev/null +++ b/src/js/changelog/ChangelogService.js @@ -0,0 +1,64 @@ +import ChangelogTopic from "./ChangelogTopic"; + +class ChangelogService +{ + /** + * @param {string} apiBase + */ + constructor(apiBase) { + /** @type {string} */ + this.apiBase = apiBase; + + /** @type {string} */ + this.changelogUrl = "/changelog.json"; + + /** @type {ChangelogTopic[]} */ + this.topics = []; + + /** @type {number} */ + this.nextPage = 0; + /** @type {number} */ + this.currentPage = -1; + } + + /** @returns {Promise} */ + async loadMore() { + if (this.isMore) { + const url = this.apiBase + this.changelogUrl + "?page=" + this.nextPage; + const response = await fetch(url); + const json = await response.json(); + + this.currentPage = this.nextPage; + + if (json.topic_list.more_topics_url) { + const moreTopicsUrlMatch = json.topic_list.more_topics_url.match(/page=(\d+)/); + this.nextPage = parseInt(moreTopicsUrlMatch[1]); + } + + json.topic_list.topics.forEach(data => { + this.topics.push(new ChangelogTopic(data)); + }); + } + + return this.topics; + } + + /** @returns {boolean} */ + get isMore() { + return this.nextPage > this.currentPage; + } + + /** + * @param {string} topicSlugId + * @returns {Promise} + */ + async loadTopic(topicSlugId) { + const url = this.apiBase + `/changelog/${topicSlugId}.json`; + const response = await fetch(url); + const data = await response.json(); + console.log(data); + return new ChangelogTopic(data); + } +} + +export default ChangelogService; \ No newline at end of file diff --git a/src/js/changelog/ChangelogTopic.js b/src/js/changelog/ChangelogTopic.js new file mode 100644 index 0000000..1956f93 --- /dev/null +++ b/src/js/changelog/ChangelogTopic.js @@ -0,0 +1,45 @@ +class ChangelogTopic +{ + constructor(data) { + this.data = data; + } + + /** @returns {number} */ + get id() { + return this.data.id; + } + + /** @returns {string} */ + get fancyTitle() { + return this.data.fancy_title.replace(/:([^\s]+):/ig, '$1'); + } + + /** @returns {Date} */ + get createdAt() { + return new Date(this.data.created_at); + } + + /** @returns {string} */ + get slug() { + return this.data.slug; + } + + /** @returns {string} */ + get webflowUrl() { + return `/changelog/${this.slug}/${this.id}`; + } + + /** @returns {boolean} */ + get isPinned() { + return !!this.data.pinned; + } + + get posts() { + if (this.data.post_stream) { + return this.data.post_stream.posts; + } + return []; + } +} + +export default ChangelogTopic; \ No newline at end of file diff --git a/webpack.common.js b/webpack.common.js index a2992f6..be3e338 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -8,10 +8,11 @@ module.exports = { ] }, entry: { - integrations: "js/integrations.js", - steps: "js/steps.js", - careers: "js/careers.js", - changelog: "js/changelog.js", + integrations: "js/integrations.js", + steps: "js/steps.js", + careers: "js/careers.js", + changelog: "js/changelog.js", + "changelog-topic": "js/changelog-topic.js", }, output: { path: path.resolve(__dirname, "dist"), From 2f9ed14c8c17138dc8a6204f28c46e687c3713b9 Mon Sep 17 00:00:00 2001 From: Antal Orcsik Date: Fri, 2 Feb 2024 15:31:43 +0100 Subject: [PATCH 3/5] list page first version with worker --- .eslintrc.json | 2 + index.js | 4 + src/js/changelog.js | 159 +++++++++++++++++++++++++++++++++++++ src/js/changelog/worker.js | 17 ++++ webpack.common.js | 1 + 5 files changed, 183 insertions(+) create mode 100644 src/js/changelog.js create mode 100644 src/js/changelog/worker.js diff --git a/.eslintrc.json b/.eslintrc.json index 683ba6d..e8768a2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,9 +22,11 @@ "HTMLElement", "HTMLHeadingElement", "HTMLImageElement", + "HTMLLIElement", "HTMLMetaElement", "HTMLParagraphElement", "HTMLSpanElement", + "HTMLUListElement", "Promise", "URL" ] diff --git a/index.js b/index.js index dba3724..41d2db9 100644 --- a/index.js +++ b/index.js @@ -41,6 +41,7 @@ addEventListener('common', event => { }); includeWorkerScript("integrations", "./src/js/integrations/worker.js"); +includeWorkerScript("changelog", "./src/js/changelog/worker.js"); /** * @param {URL} urlObject @@ -50,6 +51,9 @@ function getFetchEventHandler(urlObject) { if (urlObject.pathname.match(/^\/integrations/)) { return fetchEventHandlers['integrations']; } + if (urlObject.pathname.match(/^\/changelog/)) { + return fetchEventHandlers['changelog']; + } return fetchEventHandlers['common']; } diff --git a/src/js/changelog.js b/src/js/changelog.js new file mode 100644 index 0000000..6732920 --- /dev/null +++ b/src/js/changelog.js @@ -0,0 +1,159 @@ + + +class TopicListItem +{ + constructor(data) { + this.data = data; + } + + /** @returns {number} */ + get id() { + return this.data.id; + } + + /** @returns {string} */ + get fancyTitle() { + return this.data.fancy_title.replace(/:([^\s]+):/ig, '$1'); + } + + /** @returns {Date} */ + get createdAt() { + return new Date(this.data.created_at); + } + + /** @returns {string} */ + get slug() { + return this.data.slug; + } + + /** @returns {string} */ + get webflowUrl() { + return `/changelog/${this.slug}/${this.id}`; + } + + /** @returns {boolean} */ + get isPinned() { + return !!this.data.pinned; + } +} + +class ChangelogService +{ + constructor() { + this.changelogUrl = "/changelog.json"; + + /** @type {TopicListItem[]} */ + this.topics = []; + + this.nextPage = 0; + this.currentPage = -1; + } + + /** @returns {Promise} */ + async loadMore() { + if (this.isMore) { + const url = this.changelogUrl + "?page=" + this.nextPage; + const response = await fetch(url); + const json = await response.json(); + + this.currentPage = this.nextPage; + + if (json.topic_list.more_topics_url) { + const moreTopicsUrlMatch = json.topic_list.more_topics_url.match(/page=(\d+)/); + this.nextPage = parseInt(moreTopicsUrlMatch[1]); + } + + json.topic_list.topics.forEach(data => { + this.topics.push(new TopicListItem(data)); + }); + } + + return this.topics; + } + + /** @returns {boolean} */ + get isMore() { + return this.nextPage > this.currentPage; + } +} + +class ChangelogList +{ + constructor(list, loadMoreButton) { + /** @type {HTMLUListElement} */ + this.list = list; + + /** @type {HTMLAnchorElement} */ + this.loadMoreButton = loadMoreButton; + + /** @type {HTMLLIElement[]} */ + this.listItems = []; + + /** @type {HTMLLIElement} */ + this.unreadListItemTemplate = this.list.querySelector("#changelog-unread-template"); + this.unreadListItemTemplate.id = ""; + this.unreadListItemTemplate.remove(); + + /** @type {HTMLLIElement} */ + this.readListItemTemplate = this.list.querySelector("#changelog-read-template"); + this.readListItemTemplate.id = ""; + this.readListItemTemplate.remove(); + } + + + /** + * @param {TopicListItem} topic + * @param {boolean} isUnread + * @returns {HTMLLIElement} + */ + renderListItem(topic, isUnread = false) { + const listItem = (isUnread ? this.unreadListItemTemplate : this.readListItemTemplate).cloneNode(true); + listItem.querySelector(".changelog-timestamp").innerHTML = `[${topic.createdAt.toLocaleDateString()}]`; + listItem.querySelector(".changelog-title").innerHTML = topic.fancyTitle; + listItem.querySelector("a").href = topic.webflowUrl; + return listItem; + } + + /** + * @param {TopicListItem[]} topics + * @param {boolean} isMore + */ + render(topics, isMore) { + for (let topic of topics) { + if (!this.listItems[topic.id]) { + const listItem = this.renderListItem(topic); + this.listItems[topic.id] = listItem; + this.list.append(listItem); + } + } + if (!isMore) { + this.loadMoreButton.remove(); + } + } +} + +const changelogService = new ChangelogService(); + +const changelogList = new ChangelogList( + document.getElementById("changelog-list"), + document.getElementById("changelog-load-more-button") +); +changelogList.loadMoreButton.addEventListener('click', () => { + changelogList.loadMoreButton.disabled = true; + changelogList.loadMoreButton.innerHTML = "Loading..."; + changelogService.loadMore().then(topics => { + changelogList.render(topics, changelogService.isMore); + changelogList.loadMoreButton.disabled = false; + changelogList.loadMoreButton.innerHTML = "Load more..."; + }); +}); + +changelogList.loadMoreButton.disabled = true; +changelogList.loadMoreButton.innerHTML = "Loading..."; +changelogService.loadMore().then(topics => { + changelogList.render(topics, changelogService.isMore); + changelogList.loadMoreButton.disabled = false; + changelogList.loadMoreButton.innerHTML = "Load more..."; +}); + + diff --git a/src/js/changelog/worker.js b/src/js/changelog/worker.js new file mode 100644 index 0000000..af35475 --- /dev/null +++ b/src/js/changelog/worker.js @@ -0,0 +1,17 @@ +addEventListener('fetch', event => { + let urlObject = new URL(event.request.url); + + urlObject.hostname = 'webflow.bitrise.io'; + + if (urlObject.pathname.match(/^\/changelog\/(.+\.json)$/)) { + urlObject.hostname = 'discuss.bitrise.io'; + urlObject.pathname = urlObject.pathname.replace(/^\/changelog\/(.+\.json)$/, '/t/$1'); + } else if (urlObject.pathname.match(/^\/changelog\.json$/)) { + urlObject.hostname = 'discuss.bitrise.io'; + urlObject.pathname = '/c/product-updates/42.json'; + } else if (urlObject.pathname.match(/^\/changelog\/.+/)) { + urlObject.pathname = '/changelog/topic'; + } + + event.respondWith(fetch(urlObject)); +}) \ No newline at end of file diff --git a/webpack.common.js b/webpack.common.js index d8918ba..a2992f6 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -11,6 +11,7 @@ module.exports = { integrations: "js/integrations.js", steps: "js/steps.js", careers: "js/careers.js", + changelog: "js/changelog.js", }, output: { path: path.resolve(__dirname, "dist"), From 02dd871630d589b6688fd54348a1032889faf0e0 Mon Sep 17 00:00:00 2001 From: Antal Orcsik Date: Fri, 2 Feb 2024 18:05:20 +0100 Subject: [PATCH 4/5] changelog topics --- src/css/changelog.css | 17 +++ src/js/changelog-topic.js | 28 +++++ src/js/changelog.js | 175 ++++----------------------- src/js/changelog/ChangelogList.js | 51 ++++++++ src/js/changelog/ChangelogService.js | 64 ++++++++++ src/js/changelog/ChangelogTopic.js | 45 +++++++ webpack.common.js | 9 +- 7 files changed, 236 insertions(+), 153 deletions(-) create mode 100644 src/css/changelog.css create mode 100644 src/js/changelog-topic.js create mode 100644 src/js/changelog/ChangelogList.js create mode 100644 src/js/changelog/ChangelogService.js create mode 100644 src/js/changelog/ChangelogTopic.js diff --git a/src/css/changelog.css b/src/css/changelog.css new file mode 100644 index 0000000..355f329 --- /dev/null +++ b/src/css/changelog.css @@ -0,0 +1,17 @@ +.changelog-title img.emoji, +#changelog-topic-title img.emoji, +.changelog-topic-content img.emoji { + vertical-align: sub; + width: 1.25em; + height: 1.25em; + display: inline; +} + +.changelog-topic-content .meta { + display: none; +} + +.changelog-topic-content h2 { + font-size: 2rem; + margin-top: 1rem; +} \ No newline at end of file diff --git a/src/js/changelog-topic.js b/src/js/changelog-topic.js new file mode 100644 index 0000000..be2d64d --- /dev/null +++ b/src/js/changelog-topic.js @@ -0,0 +1,28 @@ +import ChangelogService from "./changelog/ChangelogService"; + +const style = document.createElement("style"); +document.getElementsByTagName("head")[0].appendChild(style); +style.appendChild(document.createTextNode(require("../css/changelog.css"))); + +/** + * @param {URL} url + * @returns {?string} + */ +function detectTopicFromUrl(url) { + let path = url.pathname; + const match = path.match(/changelog\/(.+)$/); + if (match) { + return match[1]; + } + return null; +} + +const url = new URL(document.location.href); +const topicSlugId = detectTopicFromUrl(url); + +ChangelogService.loadTopic(topicSlugId).then(topic => { + + document.getElementById("changelog-topic-title").innerHTML = topic.fancyTitle; + document.getElementById("changelog-topic-meta").innerHTML = topic.createdAt.toLocaleDateString(); + document.getElementById("changelog-topic-content").innerHTML = topic.posts[0].cooked; +}); \ No newline at end of file diff --git a/src/js/changelog.js b/src/js/changelog.js index 6732920..3ececf0 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -1,159 +1,36 @@ +import ChangelogList from "./changelog/ChangelogList"; +import ChangelogService from "./changelog/ChangelogService"; +const style = document.createElement("style"); +document.getElementsByTagName("head")[0].appendChild(style); +style.appendChild(document.createTextNode(require("../css/changelog.css"))); -class TopicListItem -{ - constructor(data) { - this.data = data; - } +/** @type {string} */ +const apiBase = document.location.hostname.match(/(localhost|127\.0\.0\.1)/) ? "" : "https://bitrise.io"; +const changelogService = new ChangelogService(apiBase); - /** @returns {number} */ - get id() { - return this.data.id; - } +const changelogList = new ChangelogList(document.getElementById("changelog-list")); +/** @type {HTMLAnchorElement} */ +const changelogLoadMoreButton = document.getElementById("changelog-load-more-button"); - /** @returns {string} */ - get fancyTitle() { - return this.data.fancy_title.replace(/:([^\s]+):/ig, '$1'); - } - - /** @returns {Date} */ - get createdAt() { - return new Date(this.data.created_at); - } - - /** @returns {string} */ - get slug() { - return this.data.slug; - } - - /** @returns {string} */ - get webflowUrl() { - return `/changelog/${this.slug}/${this.id}`; - } - - /** @returns {boolean} */ - get isPinned() { - return !!this.data.pinned; - } -} - -class ChangelogService -{ - constructor() { - this.changelogUrl = "/changelog.json"; - - /** @type {TopicListItem[]} */ - this.topics = []; - - this.nextPage = 0; - this.currentPage = -1; - } - - /** @returns {Promise} */ - async loadMore() { - if (this.isMore) { - const url = this.changelogUrl + "?page=" + this.nextPage; - const response = await fetch(url); - const json = await response.json(); - - this.currentPage = this.nextPage; - - if (json.topic_list.more_topics_url) { - const moreTopicsUrlMatch = json.topic_list.more_topics_url.match(/page=(\d+)/); - this.nextPage = parseInt(moreTopicsUrlMatch[1]); - } - - json.topic_list.topics.forEach(data => { - this.topics.push(new TopicListItem(data)); - }); - } - - return this.topics; - } - - /** @returns {boolean} */ - get isMore() { - return this.nextPage > this.currentPage; - } -} - -class ChangelogList -{ - constructor(list, loadMoreButton) { - /** @type {HTMLUListElement} */ - this.list = list; - - /** @type {HTMLAnchorElement} */ - this.loadMoreButton = loadMoreButton; - - /** @type {HTMLLIElement[]} */ - this.listItems = []; - - /** @type {HTMLLIElement} */ - this.unreadListItemTemplate = this.list.querySelector("#changelog-unread-template"); - this.unreadListItemTemplate.id = ""; - this.unreadListItemTemplate.remove(); - - /** @type {HTMLLIElement} */ - this.readListItemTemplate = this.list.querySelector("#changelog-read-template"); - this.readListItemTemplate.id = ""; - this.readListItemTemplate.remove(); - } - - - /** - * @param {TopicListItem} topic - * @param {boolean} isUnread - * @returns {HTMLLIElement} - */ - renderListItem(topic, isUnread = false) { - const listItem = (isUnread ? this.unreadListItemTemplate : this.readListItemTemplate).cloneNode(true); - listItem.querySelector(".changelog-timestamp").innerHTML = `[${topic.createdAt.toLocaleDateString()}]`; - listItem.querySelector(".changelog-title").innerHTML = topic.fancyTitle; - listItem.querySelector("a").href = topic.webflowUrl; - return listItem; - } +/** */ +function loadMore() { + changelogLoadMoreButton.disabled = true; + changelogLoadMoreButton.innerHTML = "Loading..."; + changelogService.loadMore().then(topics => { + changelogList.render(topics); + changelogLoadMoreButton.disabled = false; + changelogLoadMoreButton.innerHTML = "Load more..."; - /** - * @param {TopicListItem[]} topics - * @param {boolean} isMore - */ - render(topics, isMore) { - for (let topic of topics) { - if (!this.listItems[topic.id]) { - const listItem = this.renderListItem(topic); - this.listItems[topic.id] = listItem; - this.list.append(listItem); - } - } - if (!isMore) { - this.loadMoreButton.remove(); + if (!changelogService.isMore) { + changelogLoadMoreButton.remove(); } - } -} - -const changelogService = new ChangelogService(); - -const changelogList = new ChangelogList( - document.getElementById("changelog-list"), - document.getElementById("changelog-load-more-button") -); -changelogList.loadMoreButton.addEventListener('click', () => { - changelogList.loadMoreButton.disabled = true; - changelogList.loadMoreButton.innerHTML = "Loading..."; - changelogService.loadMore().then(topics => { - changelogList.render(topics, changelogService.isMore); - changelogList.loadMoreButton.disabled = false; - changelogList.loadMoreButton.innerHTML = "Load more..."; }); -}); +} -changelogList.loadMoreButton.disabled = true; -changelogList.loadMoreButton.innerHTML = "Loading..."; -changelogService.loadMore().then(topics => { - changelogList.render(topics, changelogService.isMore); - changelogList.loadMoreButton.disabled = false; - changelogList.loadMoreButton.innerHTML = "Load more..."; +changelogLoadMoreButton.addEventListener('click', event => { + event.preventDefault(); + loadMore(); }); - +loadMore(); diff --git a/src/js/changelog/ChangelogList.js b/src/js/changelog/ChangelogList.js new file mode 100644 index 0000000..15897c8 --- /dev/null +++ b/src/js/changelog/ChangelogList.js @@ -0,0 +1,51 @@ +import ChangelogTopic from "./ChangelogTopic"; + +class ChangelogList +{ + constructor(list) { + /** @type {HTMLUListElement} */ + this.list = list; + + /** @type {HTMLLIElement[]} */ + this.listItems = []; + + /** @type {HTMLLIElement} */ + this.unreadListItemTemplate = this.list.querySelector("#changelog-unread-template"); + this.unreadListItemTemplate.id = ""; + this.unreadListItemTemplate.remove(); + + /** @type {HTMLLIElement} */ + this.readListItemTemplate = this.list.querySelector("#changelog-read-template"); + this.readListItemTemplate.id = ""; + this.readListItemTemplate.remove(); + } + + + /** + * @param {ChangelogTopic} topic + * @param {boolean} isUnread + * @returns {HTMLLIElement} + */ + renderListItem(topic, isUnread = false) { + const listItem = (isUnread ? this.unreadListItemTemplate : this.readListItemTemplate).cloneNode(true); + listItem.querySelector(".changelog-timestamp").innerHTML = `[${topic.createdAt.toLocaleDateString()}]`; + listItem.querySelector(".changelog-title").innerHTML = topic.fancyTitle; + listItem.querySelector("a").href = topic.webflowUrl; + return listItem; + } + + /** + * @param {ChangelogTopic[]} topics + */ + render(topics) { + for (let topic of topics) { + if (!this.listItems[topic.id]) { + const listItem = this.renderListItem(topic); + this.listItems[topic.id] = listItem; + this.list.append(listItem); + } + } + } +} + +export default ChangelogList; \ No newline at end of file diff --git a/src/js/changelog/ChangelogService.js b/src/js/changelog/ChangelogService.js new file mode 100644 index 0000000..c77e4f4 --- /dev/null +++ b/src/js/changelog/ChangelogService.js @@ -0,0 +1,64 @@ +import ChangelogTopic from "./ChangelogTopic"; + +class ChangelogService +{ + /** + * @param {string} apiBase + */ + constructor(apiBase) { + /** @type {string} */ + this.apiBase = apiBase; + + /** @type {string} */ + this.changelogUrl = "/changelog.json"; + + /** @type {ChangelogTopic[]} */ + this.topics = []; + + /** @type {number} */ + this.nextPage = 0; + /** @type {number} */ + this.currentPage = -1; + } + + /** @returns {Promise} */ + async loadMore() { + if (this.isMore) { + const url = this.apiBase + this.changelogUrl + "?page=" + this.nextPage; + const response = await fetch(url); + const json = await response.json(); + + this.currentPage = this.nextPage; + + if (json.topic_list.more_topics_url) { + const moreTopicsUrlMatch = json.topic_list.more_topics_url.match(/page=(\d+)/); + this.nextPage = parseInt(moreTopicsUrlMatch[1]); + } + + json.topic_list.topics.forEach(data => { + this.topics.push(new ChangelogTopic(data)); + }); + } + + return this.topics; + } + + /** @returns {boolean} */ + get isMore() { + return this.nextPage > this.currentPage; + } + + /** + * @param {string} topicSlugId + * @returns {Promise} + */ + async loadTopic(topicSlugId) { + const url = this.apiBase + `/changelog/${topicSlugId}.json`; + const response = await fetch(url); + const data = await response.json(); + console.log(data); + return new ChangelogTopic(data); + } +} + +export default ChangelogService; \ No newline at end of file diff --git a/src/js/changelog/ChangelogTopic.js b/src/js/changelog/ChangelogTopic.js new file mode 100644 index 0000000..1956f93 --- /dev/null +++ b/src/js/changelog/ChangelogTopic.js @@ -0,0 +1,45 @@ +class ChangelogTopic +{ + constructor(data) { + this.data = data; + } + + /** @returns {number} */ + get id() { + return this.data.id; + } + + /** @returns {string} */ + get fancyTitle() { + return this.data.fancy_title.replace(/:([^\s]+):/ig, '$1'); + } + + /** @returns {Date} */ + get createdAt() { + return new Date(this.data.created_at); + } + + /** @returns {string} */ + get slug() { + return this.data.slug; + } + + /** @returns {string} */ + get webflowUrl() { + return `/changelog/${this.slug}/${this.id}`; + } + + /** @returns {boolean} */ + get isPinned() { + return !!this.data.pinned; + } + + get posts() { + if (this.data.post_stream) { + return this.data.post_stream.posts; + } + return []; + } +} + +export default ChangelogTopic; \ No newline at end of file diff --git a/webpack.common.js b/webpack.common.js index a2992f6..be3e338 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -8,10 +8,11 @@ module.exports = { ] }, entry: { - integrations: "js/integrations.js", - steps: "js/steps.js", - careers: "js/careers.js", - changelog: "js/changelog.js", + integrations: "js/integrations.js", + steps: "js/steps.js", + careers: "js/careers.js", + changelog: "js/changelog.js", + "changelog-topic": "js/changelog-topic.js", }, output: { path: path.resolve(__dirname, "dist"), From a5aa7a52554320358cc69ed6cc392c4ff80c74d8 Mon Sep 17 00:00:00 2001 From: Antal Orcsik Date: Fri, 2 Feb 2024 18:12:32 +0100 Subject: [PATCH 5/5] remove timestamp formatting --- src/js/changelog/ChangelogList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/changelog/ChangelogList.js b/src/js/changelog/ChangelogList.js index 15897c8..a6303ab 100644 --- a/src/js/changelog/ChangelogList.js +++ b/src/js/changelog/ChangelogList.js @@ -28,7 +28,7 @@ class ChangelogList */ renderListItem(topic, isUnread = false) { const listItem = (isUnread ? this.unreadListItemTemplate : this.readListItemTemplate).cloneNode(true); - listItem.querySelector(".changelog-timestamp").innerHTML = `[${topic.createdAt.toLocaleDateString()}]`; + listItem.querySelector(".changelog-timestamp").innerHTML = topic.createdAt.toLocaleDateString(); listItem.querySelector(".changelog-title").innerHTML = topic.fancyTitle; listItem.querySelector("a").href = topic.webflowUrl; return listItem;