1
0
mirror of https://github.com/comatory/fb2iCal synced 2025-02-17 12:10:36 +01:00

feature: convert form input to Svelte component and create module for

fetching event data & parsing it
This commit is contained in:
Ondřej Synáček 2020-12-20 13:50:12 +01:00
parent 128b03344b
commit b28995aa1e
10 changed files with 182 additions and 166 deletions

View File

@ -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 })
}

View File

@ -0,0 +1,5 @@
import { createEvent } from './events'
export {
createEvent,
}

View File

@ -1,4 +1,5 @@
<script>
import Input from './Input.svelte'
import TrackingPanel from './TrackingPanel.svelte'
import EventList from './EventList.svelte'
@ -12,6 +13,7 @@
<TrackingPanel />
{/if}
<Input />
{#if showEventList}
<EventList />
{/if}

View File

@ -0,0 +1,45 @@
<style>
#form {
flex: 1;
display: flex;
min-width: 300px;
}
#form input {
margin: 5px;
}
</style>
<script>
import { createEvent } from '../actions'
import logger from '../../static/app/logger'
let value
const onChange = (e) => {
value = e.currentTarget.value
}
const handleSubmit = async (e) => {
e.preventDefault()
createEvent(value, { logger })
}
</script>
<form id="form">
<input
required
pattern="^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|\d+$"
id="url"
name="url"
bind:value={value}
placeholder="Paste / type FB event URL or event number..."
title="Please insert Facebook Event URL / Number"
/>
<input
id="submit"
type='submit'
value='Submit'
on:click={handleSubmit}
/>
</form>

View File

@ -1,5 +1,7 @@
import { postURL } from './network'
import storageListener from './storageListener'
export {
postURL,
storageListener,
}

View File

@ -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)
})
}

View File

@ -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()
}

View File

@ -23,22 +23,22 @@
file that you can import into your calendar. <strong>Only public events are supported.</strong>
</p>
<form action='/download' method='POST' id="form">
<input
required
pattern="^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|\d+$"
id="url"
name="url"
placeholder="Paste / type FB event URL or event number..."
title="Please insert Facebook Event URL / Number"
/>
<input id="submit" type='submit' value='Submit' />
</form>
<noscript>
<div id="nojs" class="notice">
🤚 JavaScript is <em>disabled</em>. Enable to get full experience and offline capabilities.
</div>
<form action='/download' method='POST' id="form">
<input
required
pattern="^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?|\d+$"
id="url"
name="url"
placeholder="Paste / type FB event URL or event number..."
title="Please insert Facebook Event URL / Number"
/>
<input id="submit" type='submit' value='Submit' />
</form>
</noscript>
<div id="status">

View File

@ -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)
})
})
})()

View File

@ -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;