mirror of
https://github.com/comatory/fb2iCal
synced 2025-02-21 14: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:
parent
128b03344b
commit
b28995aa1e
70
lib/frontend/actions/events.js
Normal file
70
lib/frontend/actions/events.js
Normal 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 })
|
||||||
|
}
|
5
lib/frontend/actions/index.js
Normal file
5
lib/frontend/actions/index.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createEvent } from './events'
|
||||||
|
|
||||||
|
export {
|
||||||
|
createEvent,
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import Input from './Input.svelte'
|
||||||
import TrackingPanel from './TrackingPanel.svelte'
|
import TrackingPanel from './TrackingPanel.svelte'
|
||||||
import EventList from './EventList.svelte'
|
import EventList from './EventList.svelte'
|
||||||
|
|
||||||
@ -12,6 +13,7 @@
|
|||||||
<TrackingPanel />
|
<TrackingPanel />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<Input />
|
||||||
{#if showEventList}
|
{#if showEventList}
|
||||||
<EventList />
|
<EventList />
|
||||||
{/if}
|
{/if}
|
||||||
|
45
lib/frontend/components/Input.svelte
Normal file
45
lib/frontend/components/Input.svelte
Normal 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>
|
@ -1,5 +1,7 @@
|
|||||||
|
import { postURL } from './network'
|
||||||
import storageListener from './storageListener'
|
import storageListener from './storageListener'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
postURL,
|
||||||
storageListener,
|
storageListener,
|
||||||
}
|
}
|
||||||
|
23
lib/frontend/services/network.js
Normal file
23
lib/frontend/services/network.js
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
@ -23,3 +23,26 @@ export const sortRecord = (a, b) => {
|
|||||||
return 0
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
@ -23,22 +23,22 @@
|
|||||||
file that you can import into your calendar. <strong>Only public events are supported.</strong>
|
file that you can import into your calendar. <strong>Only public events are supported.</strong>
|
||||||
</p>
|
</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>
|
<noscript>
|
||||||
<div id="nojs" class="notice">
|
<div id="nojs" class="notice">
|
||||||
🤚 JavaScript is <em>disabled</em>. Enable to get full experience and offline capabilities.
|
🤚 JavaScript is <em>disabled</em>. Enable to get full experience and offline capabilities.
|
||||||
</div>
|
</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>
|
</noscript>
|
||||||
|
|
||||||
<div id="status">
|
<div id="status">
|
||||||
|
@ -21,19 +21,6 @@ import { Request } from '../frontend/records'
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', boot)
|
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) => {
|
const configureLogger = (logger) => {
|
||||||
if (!logger) {
|
if (!logger) {
|
||||||
return
|
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) => {
|
const setServiceWorkerStatus = (status) => {
|
||||||
clearStatuses()
|
clearStatuses()
|
||||||
const sw = document.querySelector('#service-worker')
|
const sw = document.querySelector('#service-worker')
|
||||||
@ -77,59 +47,6 @@ import { Request } from '../frontend/records'
|
|||||||
status ? sw.classList.add('show') : sw.classList.remove('show')
|
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 form = document.querySelector('form')
|
||||||
const submitButton = document.querySelector("#submit")
|
const submitButton = document.querySelector("#submit")
|
||||||
const input = document.querySelector("#url")
|
const input = document.querySelector("#url")
|
||||||
@ -170,65 +87,4 @@ import { Request } from '../frontend/records'
|
|||||||
}
|
}
|
||||||
|
|
||||||
configureLogger(logger)
|
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})()
|
})()
|
||||||
|
@ -91,16 +91,6 @@ input#url {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#form {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
min-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#form input {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#status {
|
#status {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user