diff --git a/lib/public/scripts.js b/lib/public/scripts.js new file mode 100644 index 0000000..5226248 --- /dev/null +++ b/lib/public/scripts.js @@ -0,0 +1,283 @@ +(() => { + const noJS = () => { + return Boolean(document.querySelector("#nojs").checked) + } + + const useStorage = Boolean(window.localStorage) + + if (!window.fetch || !window.Promise || !window.URLSearchParams) { + console.info('JS features not available.') + return + } + + const getStorage = () => { + if (!useStorage) { + return null + } + + const storage = localStorage.getItem('fb-to-ical-events') + + if (!storage) { + localStorage.setItem('fb-to-ical-events', JSON.stringify([])) + return "[]" + } + + return storage + } + + const getStorageContents = (storage) => { + return JSON.parse(storage) + } + + const updateStorage = (storageContents) => { + const encodedStorage = JSON.stringify(storageContents) + + localStorage.setItem('fb-to-ical-events', encodedStorage) + } + + const saveRecord = (order, link, createdAt, title) => { + if (!useStorage) { + return + } + + const storage = getStorage() + const storageContents = getStorageContents(storage) + + const record = { + order, + link, + createdAt: createdAt.toString(), + title, + } + + updateStorage([ ...storageContents, record ]) + } + + const showTable = () => { + list.classList.remove('hidden') + } + + const insertTableRow = ({ order, link, createdAt, title }) => { + showTable() + + const row = document.createElement('tr') + + const orderCol = document.createElement('td') + orderCol.innerText = order + + const linkElement = document.createElement('a') + linkElement.setAttribute('href', link) + linkElement.innerText = 'Download' + + const linkCol = document.createElement('td') + linkCol.appendChild(linkElement) + + const createdAtCol = document.createElement('td') + const parsedCreatedAt = new Date(createdAt) + createdAtCol.innerText = (parsedCreatedAt || new Date()).toLocaleString() + + const titleCol = document.createElement('td') + titleCol.innerText = title + + const newRow = document.createElement('tr') + + newRow.appendChild(orderCol) + newRow.appendChild(linkCol) + newRow.appendChild(createdAtCol) + newRow.appendChild(titleCol) + + tableBody.prepend(newRow) + } + + const createRecord = (uri, summary) => { + const order = tableBody.querySelectorAll('tr').length + 1 + const createdAt = new Date() + + saveRecord(order, uri, createdAt, summary) + insertTableRow({ + order, + link: uri, + createdAt, + title: summary, + }) + } + + const hydrateList = () => { + if (!useStorage) { + return + } + + const storage = getStorage() + const storageContents = getStorageContents(storage) + + if (storageContents.length > 0) { + showTable() + } + + storageContents + .sort((a, b) => { + if (a.order < b.order) { + return -1 + } + if (a.order > b.order) { + return 1 + } + return 0 + }) + .forEach((record) => { + insertTableRow(record) + }) + } + + const clearStatuses = () => { + document.querySelectorAll('.status-item').forEach((item) => { + item.classList.remove('show') + }) + } + + const setStatusDownloading = () => { + clearStatuses() + document.querySelector('#network').classList.add('show') + } + + const setStatusParsing = () => { + clearStatuses() + document.querySelector('#parsing').classList.add('show') + } + + const setStatusError = (err) => { + clearStatuses() + const error = document.querySelector('#error') + error.innerText = err.toString() + error.classList.add('show') + } + + const setServiceWorkerStatus = (status) => { + clearStatuses() + const sw = document.querySelector('#service-worker') + sw.innerText = status + status ? sw.classList.add('show') : sw.classList.remove('show') + } + + const pendingRequest = () => { + input.disabled = true + submitButton.disabled = true + setStatusDownloading() + } + + const finishedRequest = () => { + input.disabled = false + submitButton.disabled = false + clearStatuses() + } + + const handleError = (error) => { + console.error(error) + finishedRequest() + setStatusError(error) + } + + const postURL = (data) => { + return new Promise((resolve, reject) => { + fetch('/download', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: data, + }).then((response) => { + if (response.status === 500) { + reject(response.statusText) + return + } + finishedRequest() + resolve(response) + }).catch(reject) + }) + } + + const form = document.querySelector('form') + const submitButton = document.querySelector("#submit") + const input = document.querySelector("#url") + const link = document.querySelector("#current-download") + const table = document.querySelector('#list') + const tableBody = table.querySelector('tbody') + const noJScheckbox = document.querySelector('#nojs') + + const loadNoJS = () => { + if (!useStorage) { + return + } + const value = localStorage.getItem('fb-to-ical-nojs') + noJScheckbox.checked = value ? JSON.parse(value) : false + } + + loadNoJS() + + if (window.navigator && window.navigator.serviceWorker && !noJS()) { + const serviceWorker = window.navigator.serviceWorker + serviceWorker.register('service-worker.js', { + scope: './', + }).then((registration) => { + setServiceWorkerStatus(`Service worker registered with scope ${registration.scope}`) + setTimeout(() => { + setServiceWorkerStatus('') + }, 4500) + }).catch((err) => { + setServiceWorkerStatus(`Service worker error: ${err.toString()}`) + }) + } + + if (!noJS()) { + hydrateList() + } + + noJScheckbox.addEventListener('click', (event) => { + if (!useStorage) { + return + } + + localStorage.setItem('fb-to-ical-nojs', event.target.checked) + }) + + submitButton.addEventListener('click', (event) => { + if (noJS()) { + return + } + + event.preventDefault() + + const formData = new URLSearchParams() + formData.set('url', input.value) + + pendingRequest() + + postURL(formData) + .then((res) => { + res.text() + .then((text) => { + setStatusParsing() + const dataUri = encodeURIComponent(text) + const uri = `data:text/calendar;charset=utf-8,${dataUri}` + + link.setAttribute('href', uri) + link.setAttribute('download', 'download.ics') + link.click() + + input.value = '' + + const summaryMatch = text.match(/SUMMARY:.*/)[0] + const summary = summaryMatch ? summaryMatch.replace(/SUMMARY:/, '') : '' + + createRecord(uri, summary) + clearStatuses() + }) + .catch((err) => { + handleError(err) + }) + }) + .catch((err) => { + handleError(err) + }) + }) +})() diff --git a/lib/public/style.css b/lib/public/style.css new file mode 100644 index 0000000..fea86b2 --- /dev/null +++ b/lib/public/style.css @@ -0,0 +1,79 @@ +body { + display: flex; + flex-direction: column; + min-height: 100vh; + font-size: 16px; + margin: 0px; +} + +header, footer, article { + padding: 5px; +} + +footer { + position: absolute; + bottom: 0; + left: 0; + right: 0; + background-color: #fff; +} + +article#main { + flex: 1; +} + +p, input, table, button, label { + font-size: 1rem; +} + +#current-download { + display: none; +} + +.list-wrapper { + max-height: 500px; + overflow: auto; +} + +#list { +} + +.row { + display: flex; + align-items: center; + flex-wrap: wrap; +} + +input#url { + flex: 1; +} + +#form { + flex: 1; + display: flex; + min-width: 300px; +} + +#form input { + margin: 5px; +} + +#status { + flex: 1; + height: 1rem; + margin: 5px; +} + +.status-item { + display: none; + min-width: 200px; +} + +.show { + display: block; +} + +.hidden { + display: none; +} + diff --git a/lib/views/index.ejs b/lib/views/index.ejs index e29e6c8..47e2fe7 100644 --- a/lib/views/index.ejs +++ b/lib/views/index.ejs @@ -5,87 +5,8 @@ + Facebook Event to iCal Converter - @@ -150,291 +71,6 @@ Created by Ondrej Synacek - - +