From b28995aa1e668dd5ceb0805ff99e3cb9a962db3e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ond=C5=99ej=20Syn=C3=A1=C4=8Dek?=
Date: Sun, 20 Dec 2020 13:50:12 +0100
Subject: [PATCH] feature: convert form input to Svelte component and create
module for fetching event data & parsing it
---
lib/frontend/actions/events.js | 70 ++++++++++
lib/frontend/actions/index.js | 5 +
lib/frontend/components/AppContainer.svelte | 2 +
lib/frontend/components/Input.svelte | 45 ++++++
lib/frontend/services/index.js | 2 +
lib/frontend/services/network.js | 23 ++++
lib/frontend/utils.js | 23 ++++
lib/static/index.html | 24 ++--
lib/static/index.js | 144 --------------------
lib/static/style.css | 10 --
10 files changed, 182 insertions(+), 166 deletions(-)
create mode 100644 lib/frontend/actions/events.js
create mode 100644 lib/frontend/actions/index.js
create mode 100644 lib/frontend/components/Input.svelte
create mode 100644 lib/frontend/services/network.js
diff --git a/lib/frontend/actions/events.js b/lib/frontend/actions/events.js
new file mode 100644
index 0000000..554deb0
--- /dev/null
+++ b/lib/frontend/actions/events.js
@@ -0,0 +1,70 @@
+import { postURL } from '../services'
+import { requestStore } from '../stores'
+import { Request } from '../records'
+import { uuidv4, parseStartTimeFromiCalString } from '../utils'
+import { extractEventDataFromHTML } from '../../../lib/services/ics-retriever'
+import generateICS from '../../../lib/services/ics-generator'
+
+const getEventHTML = async (url) => {
+ const formData = new URLSearchParams()
+ formData.set('url', url)
+
+ try {
+ const request = new Request({ id: uuidv4() })
+ requestStore.set(request)
+ const response = await postURL(formData)
+ const text = await response.text()
+ return text
+ } catch (error) {
+ requestStore.update((prevRequest) => {
+ prevRequest.error = error
+ return prevRequest
+ })
+ return null
+ }
+}
+
+const createICS = (html, url, { logger }) => {
+ try {
+ // TODO: set parsing status in UI
+
+ const eventData = extractEventDataFromHTML(html, url, { logger })
+ generateICS(eventData)
+ .then((text) => {
+ const dataUri = encodeURIComponent(text)
+ const uri = `data:text/calendar;charset=utf-8,${dataUri}`
+ console.log(`SUCCESS - uri: ${uri}`)
+
+ // TODO: create download link
+ // 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:/, '') : ''
+ const startTimeMatches = text.match(/DTSTART:.*/)
+ const startTimeMatch = text.length > 0 ?
+ (startTimeMatches[0] || '').replace(/DTSTART:/, '') :
+ ''
+ const startTime = parseStartTimeFromiCalString(startTimeMatch)
+
+ // TODO: save record to a store
+ // createRecord(uri, summary, startTime)
+
+ // TODO: clear UI status
+ // clearStatuses()
+ })
+ // TODO: catch errors
+ .catch(alert)
+ } catch (err) {
+ // TODO: catch errors
+ alert(err)
+ }
+}
+
+export const createEvent = async (url, { logger }) => {
+ const html = await getEventHTML(url)
+ const ics = await createICS(html, url, { logger })
+}
diff --git a/lib/frontend/actions/index.js b/lib/frontend/actions/index.js
new file mode 100644
index 0000000..66b41ed
--- /dev/null
+++ b/lib/frontend/actions/index.js
@@ -0,0 +1,5 @@
+import { createEvent } from './events'
+
+export {
+ createEvent,
+}
diff --git a/lib/frontend/components/AppContainer.svelte b/lib/frontend/components/AppContainer.svelte
index cfb6551..7cee4e5 100644
--- a/lib/frontend/components/AppContainer.svelte
+++ b/lib/frontend/components/AppContainer.svelte
@@ -1,4 +1,5 @@
+
+
diff --git a/lib/frontend/services/index.js b/lib/frontend/services/index.js
index d05033b..46dfa35 100644
--- a/lib/frontend/services/index.js
+++ b/lib/frontend/services/index.js
@@ -1,5 +1,7 @@
+import { postURL } from './network'
import storageListener from './storageListener'
export {
+ postURL,
storageListener,
}
diff --git a/lib/frontend/services/network.js b/lib/frontend/services/network.js
new file mode 100644
index 0000000..8a4b814
--- /dev/null
+++ b/lib/frontend/services/network.js
@@ -0,0 +1,23 @@
+export const postURL = (data) => {
+ return new Promise((resolve, reject) => {
+ fetch('/download/html/', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'text/html, application/json',
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: data,
+ }).then((response) => {
+ if (response.status !== 200) {
+ if (response.body.constructor === ReadableStream) {
+ response.json().then((json) => reject(json.error || response.statusText))
+ return
+ }
+ reject(response.statusText)
+ return
+ }
+
+ resolve(response)
+ }).catch(reject)
+ })
+}
diff --git a/lib/frontend/utils.js b/lib/frontend/utils.js
index 1412440..c9de08a 100644
--- a/lib/frontend/utils.js
+++ b/lib/frontend/utils.js
@@ -23,3 +23,26 @@ export const sortRecord = (a, b) => {
return 0
}
+// NOTE: Generate random IDs: https://stackoverflow.com/a/2117523/3056783
+export const uuidv4 = () => {
+ return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
+ (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
+ )
+}
+
+export const parseStartTimeFromiCalString = (text = '') => {
+ const [ dateStr, timeStr ] = text.split('T')
+ const rawDate = dateStr || ''
+ const rawTime = timeStr || ''
+
+ const year = Number(rawDate.slice(0, 4))
+ const month = Number(Math.max(rawDate.slice(4, 6) - 1), 0)
+ const date = Number(rawDate.slice(6, 8))
+ const hour = Number(rawTime.slice(0, 2))
+ const minutes = Number(rawTime.slice(2, 4))
+ const seconds = Number(rawTime.slice(4, 6))
+
+ const parsedDate = new Date(year, month, date, hour, minutes, seconds)
+ return parsedDate.toString()
+}
+
diff --git a/lib/static/index.html b/lib/static/index.html
index 397121d..1a7cd76 100644
--- a/lib/static/index.html
+++ b/lib/static/index.html
@@ -23,22 +23,22 @@
file that you can import into your calendar. Only public events are supported.
-
-
diff --git a/lib/static/index.js b/lib/static/index.js
index 73c44e6..d353a84 100644
--- a/lib/static/index.js
+++ b/lib/static/index.js
@@ -21,19 +21,6 @@ import { Request } from '../frontend/records'
document.addEventListener('DOMContentLoaded', boot)
- const createRecord = (uri, summary, startTime) => {
- const id = uuidv4()
- const createdAt = new Date()
-
- saveRecord({
- id,
- link: uri,
- createdAt,
- startTime,
- title: summary,
- })
- }
-
const configureLogger = (logger) => {
if (!logger) {
return
@@ -53,23 +40,6 @@ import { Request } from '../frontend/records'
})
}
- 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')
@@ -77,59 +47,6 @@ import { Request } from '../frontend/records'
status ? sw.classList.add('show') : sw.classList.remove('show')
}
- const pendingRequest = () => {
- input.disabled = true
- submitButton.disabled = true
- setStatusDownloading()
-
- requestStore.set(new Request({
- id: uuidv4(),
- error: null,
- }))
- }
-
- const finishedRequest = () => {
- input.disabled = false
- submitButton.disabled = false
- clearStatuses()
-
- requestStore.set(null)
- }
-
- const handleError = (error) => {
- finishedRequest()
- setStatusError(error)
-
- requestStore.update((prevRequest) => {
- prevRequest.error = error
- return prevRequest
- })
- }
-
- const postURL = (data) => {
- return new Promise((resolve, reject) => {
- fetch('/download/html/', {
- method: 'POST',
- headers: {
- 'Accept': 'text/html, application/json',
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- body: data,
- }).then((response) => {
- if (response.status !== 200) {
- if (response.body.constructor === ReadableStream) {
- response.json().then((json) => reject(json.error || response.statusText))
- return
- }
- reject(response.statusText)
- return
- }
- finishedRequest()
- resolve(response)
- }).catch(reject)
- })
- }
-
const form = document.querySelector('form')
const submitButton = document.querySelector("#submit")
const input = document.querySelector("#url")
@@ -170,65 +87,4 @@ import { Request } from '../frontend/records'
}
configureLogger(logger)
-
- const handleHTMLResponse = (html, url) => {
- try {
- setStatusParsing()
-
- const eventData = extractEventDataFromHTML(html, url, { logger })
- generateICS(eventData)
- .then((text) => {
- 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:/, '') : ''
- const startTimeMatches = text.match(/DTSTART:.*/)
- const startTimeMatch = text.length > 0 ?
- (startTimeMatches[0] || '').replace(/DTSTART:/, '') :
- ''
- const startTime = parseStartTimeFromiCalString(startTimeMatch)
-
- createRecord(uri, summary, startTime)
-
- clearStatuses()
- })
- .catch((err) => {
- handleError(err)
- })
- } catch (err) {
- handleError(err)
- }
- }
-
- submitButton.addEventListener('click', (event) => {
- if (!form.reportValidity()) {
- return
- }
-
- event.preventDefault()
-
- const formData = new URLSearchParams()
- formData.set('url', input.value)
-
- pendingRequest()
-
- postURL(formData)
- .then((res) => {
- res.text()
- .then((response) => handleHTMLResponse(response, input.value))
- .catch((err) => {
- handleError(err)
- })
- })
- .catch((err) => {
- handleError(err)
- })
- })
})()
diff --git a/lib/static/style.css b/lib/static/style.css
index 717b387..336dcc3 100644
--- a/lib/static/style.css
+++ b/lib/static/style.css
@@ -91,16 +91,6 @@ input#url {
flex: 1;
}
-#form {
- flex: 1;
- display: flex;
- min-width: 300px;
-}
-
-#form input {
- margin: 5px;
-}
-
#status {
flex: 1;
height: 1rem;