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;