Merge pull request #6 from comatory/feature-use-svelte-framework
Feature use svelte framework
This commit is contained in:
commit
1b0bb1c2a2
|
@ -0,0 +1,22 @@
|
|||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on merge
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: 'npm ci && npm run build:firebase:hosting'
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_FB2ICAL_3051B }}'
|
||||
channelId: live
|
||||
projectId: fb2ical-3051b
|
||||
env:
|
||||
FIREBASE_CLI_PREVIEWS: hostingchannels
|
|
@ -0,0 +1,18 @@
|
|||
# This file was auto-generated by the Firebase CLI
|
||||
# https://github.com/firebase/firebase-tools
|
||||
|
||||
name: Deploy to Firebase Hosting on PR
|
||||
'on': pull_request
|
||||
jobs:
|
||||
build_and_preview:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: 'npm ci && npm run build:firebase:hosting'
|
||||
- uses: FirebaseExtended/action-hosting-deploy@v0
|
||||
with:
|
||||
repoToken: '${{ secrets.GITHUB_TOKEN }}'
|
||||
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_FB2ICAL_3051B }}'
|
||||
projectId: fb2ical-3051b
|
||||
env:
|
||||
FIREBASE_CLI_PREVIEWS: hostingchannels
|
|
@ -0,0 +1,25 @@
|
|||
name: base
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2-beta
|
||||
with:
|
||||
node-version: 12.7
|
||||
- name: Install, build and test
|
||||
timeout-minutes: 10
|
||||
run: |
|
||||
npm ci --no-color --no-progress
|
||||
npm run build
|
||||
npm run test
|
||||
env:
|
||||
CI: true
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
language: node_js
|
||||
node_js:
|
||||
- 10.15.0
|
|
@ -14,8 +14,8 @@ class FirebaseTransport extends Transport {
|
|||
callback(null, info)
|
||||
this.emit('logged', info)
|
||||
} catch (err) {
|
||||
callback(error)
|
||||
this.emit('error', error)
|
||||
callback(err)
|
||||
this.emit('error', err)
|
||||
}
|
||||
|
||||
return info
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { postURL } from '../services'
|
||||
import { eventStore, parseStatusStore, requestStore } from '../stores'
|
||||
import { Request } from '../records'
|
||||
import { uuidv4, parseStartTimeFromiCalString, promptDownload } 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(),
|
||||
url,
|
||||
})
|
||||
requestStore.set(request)
|
||||
const response = await postURL(formData)
|
||||
const text = await response.text()
|
||||
|
||||
requestStore.set(null)
|
||||
return text
|
||||
} catch (error) {
|
||||
requestStore.update((prevRequest) => {
|
||||
prevRequest.error = error
|
||||
return prevRequest
|
||||
})
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const createICS = async (html, url, { logger }) => {
|
||||
try {
|
||||
parseStatusStore.set('Parsing event data...')
|
||||
|
||||
const eventData = extractEventDataFromHTML(html, url, { logger })
|
||||
const text = await generateICS(eventData)
|
||||
const dataUri = encodeURIComponent(text)
|
||||
const uri = `data:text/calendar;charset=utf-8,${dataUri}`
|
||||
|
||||
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)
|
||||
|
||||
eventStore.setCalculation({
|
||||
id: uuidv4(),
|
||||
link: uri,
|
||||
createdAt: new Date(),
|
||||
startTime,
|
||||
title: summary,
|
||||
})
|
||||
|
||||
parseStatusStore.set(null)
|
||||
|
||||
promptDownload(uri)
|
||||
} catch (err) {
|
||||
parseStatusStore.set(err)
|
||||
}
|
||||
}
|
||||
|
||||
export const createEvent = async (url, { logger }) => {
|
||||
const html = await getEventHTML(url)
|
||||
const ics = await createICS(html, url, { logger })
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { createEvent } from './events'
|
||||
|
||||
export {
|
||||
createEvent,
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<script>
|
||||
import AppContainer from './AppContainer.svelte'
|
||||
</script>
|
||||
|
||||
<AppContainer />
|
|
@ -0,0 +1,21 @@
|
|||
<script>
|
||||
import InputContainer from './InputContainer.svelte'
|
||||
import EventList from './EventList.svelte'
|
||||
import Status from './Status.svelte'
|
||||
import TrackingPanel from './TrackingPanel.svelte'
|
||||
|
||||
import { configStore, eventStore } from '../stores'
|
||||
|
||||
$: showTrackingPanel = $configStore.track === undefined
|
||||
$: showEventList = $eventStore.events.length > 0
|
||||
</script>
|
||||
|
||||
{#if showTrackingPanel}
|
||||
<TrackingPanel />
|
||||
{/if}
|
||||
|
||||
<InputContainer />
|
||||
{#if showEventList}
|
||||
<EventList />
|
||||
{/if}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<style>
|
||||
.list-wrapper {
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(odd) {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
td.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
td.actions div {
|
||||
margin: 2px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.delete-record {
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { eventStore } from '../stores'
|
||||
|
||||
const handleRecordDelete = (id) => {
|
||||
eventStore.clearCalculation(id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='list-wrapper'>
|
||||
<table id="list">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Name</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each $eventStore.events as event (event.id)}
|
||||
<tr>
|
||||
<td>
|
||||
{event.startTime
|
||||
? new Date(event.startTime).toLocaleString()
|
||||
: 'N/A\xa0\xa0\xa0\xa0\xa0'
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<a href={event.link}>{event.title}</a>
|
||||
</td>
|
||||
<td class='actions'>
|
||||
<div
|
||||
class='delete-record'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
on:click={() => handleRecordDelete(event.id)}
|
||||
>
|
||||
✖︎
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,78 @@
|
|||
<style>
|
||||
#form {
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
#form input {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
input#url {
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.input--error {
|
||||
border: 1px solid firebrick;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { createEvent } from '../actions'
|
||||
import logger from '../services/logger'
|
||||
|
||||
export let error
|
||||
export let pending
|
||||
export let pendingRequest
|
||||
|
||||
let value = ''
|
||||
$: value = error && pendingRequest && pendingRequest.url || ''
|
||||
let form
|
||||
|
||||
let inputClasses = ''
|
||||
$: inputClasses = [
|
||||
error ? 'input--error' : '',
|
||||
pending ? 'input--pending': '',
|
||||
].join(' ')
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
if (!form.reportValidity()) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
createEvent(value, { logger })
|
||||
value = ''
|
||||
}
|
||||
|
||||
const handleChange = (e) => {
|
||||
value = e.currentTarget.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<form id="form" on:submit={handleSubmit} bind:this={form}>
|
||||
<input
|
||||
required
|
||||
pattern={String.raw`^(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"
|
||||
class={inputClasses}
|
||||
disabled={pending}
|
||||
placeholder="Paste / type FB event URL or event number..."
|
||||
title="Please insert Facebook Event URL / Number"
|
||||
on:change={handleChange}
|
||||
value={value}
|
||||
/>
|
||||
<input
|
||||
id="submit"
|
||||
class={inputClasses}
|
||||
type='submit'
|
||||
value='Submit'
|
||||
disabled={pending}
|
||||
on:click={handleSubmit}
|
||||
/>
|
||||
</form>
|
|
@ -0,0 +1,26 @@
|
|||
<script>
|
||||
import { parseStatusStore, requestStore, swStatusStore } from '../stores'
|
||||
import Input from './Input.svelte'
|
||||
import Status from './Status.svelte'
|
||||
|
||||
$: error = ($requestStore && $requestStore.error) ? $requestStore.error : null
|
||||
$: pending = Boolean($requestStore && !$requestStore.error)
|
||||
$: pendingRequest = $requestStore
|
||||
$: status = $parseStatusStore
|
||||
$: swStatus = $swStatusStore
|
||||
</script>
|
||||
|
||||
<div class="input-container">
|
||||
<Input
|
||||
{pending}
|
||||
{pendingRequest}
|
||||
{error}
|
||||
/>
|
||||
<Status
|
||||
{error}
|
||||
{pending}
|
||||
{pendingRequest}
|
||||
{status}
|
||||
{swStatus}
|
||||
/>
|
||||
</div>
|
|
@ -0,0 +1,54 @@
|
|||
<style>
|
||||
#status {
|
||||
flex: 1;
|
||||
min-height: 1rem;
|
||||
max-height: 3rem;
|
||||
overflow: auto;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
min-width: 200px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-item--error {
|
||||
box-sizing: border-box;
|
||||
border: 1px solid firebrick;
|
||||
background-color: salmon;
|
||||
color: darkred;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export let error
|
||||
export let pending
|
||||
export let pendingRequest
|
||||
export let status
|
||||
export let swStatus
|
||||
</script>
|
||||
|
||||
<div id='status'>
|
||||
{#if error}
|
||||
<div class='status-item status-item--error'>
|
||||
{error.toString()}
|
||||
</div>
|
||||
{/if}
|
||||
{#if pending && pendingRequest}
|
||||
<div class='status-item'>
|
||||
Fetching event {pendingRequest.url}
|
||||
</div>
|
||||
{/if}
|
||||
{#if status}
|
||||
<div class='status-item'>
|
||||
{status}
|
||||
</div>
|
||||
{/if}
|
||||
{#if swStatus}
|
||||
<div class='status-item'>
|
||||
{swStatus}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -0,0 +1,46 @@
|
|||
<style>
|
||||
#tracking-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
border: 3px solid navy;
|
||||
background-color: lightyellow;
|
||||
max-width: 600px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#tracking-panel__yes-button,
|
||||
#tracking-panel__no-button {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
#tracking-panel__yes-button {
|
||||
font-weight: 600;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { configStore } from '../stores'
|
||||
|
||||
const handleYesButtonClick = () => {
|
||||
configStore.setValue('track', true)
|
||||
}
|
||||
|
||||
const handleNoButtonClick = () => {
|
||||
configStore.setValue('track', false)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="tracking-panel">
|
||||
<p>
|
||||
Can we store anonymous logs? This data is only saved to our internal database and is used to debug the parsing of the web pages. We'll ask you only this time and won't bother you again.
|
||||
</p>
|
||||
|
||||
<button on:click={handleYesButtonClick} id='tracking-panel__yes-button'>
|
||||
Ok
|
||||
</button>
|
||||
<button on:click={handleNoButtonClick} id='tracking-panel__no-button'>
|
||||
Nope
|
||||
</button>
|
||||
</div>
|
|
@ -0,0 +1,5 @@
|
|||
export const STORAGE_KEYS = {
|
||||
CONFIG: 'fb-to-ical-config',
|
||||
EVENTS: 'fb-to-ical-events',
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import App from './components/App.svelte'
|
||||
|
||||
import * as stores from './stores'
|
||||
import * as services from './services'
|
||||
import serviceWorkerBoot from './sw-boot'
|
||||
import loggerBoot from './logger-boot'
|
||||
|
||||
const boot = () => {
|
||||
|
||||
services.storageListener.register()
|
||||
|
||||
serviceWorkerBoot()
|
||||
loggerBoot()
|
||||
|
||||
new App({
|
||||
target: document.querySelector('#root'),
|
||||
})
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window._fb2ical = {
|
||||
...stores,
|
||||
...services,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boot()
|
|
@ -0,0 +1,8 @@
|
|||
import logger from './services/logger'
|
||||
import { configStore } from './stores'
|
||||
|
||||
export default () => {
|
||||
const enableTracking = configStore.track
|
||||
|
||||
logger.setRemoteLogging(enableTracking)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import Request from './request'
|
||||
|
||||
export {
|
||||
Request,
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
export default class Request {
|
||||
constructor({
|
||||
id,
|
||||
url,
|
||||
error,
|
||||
}) {
|
||||
this.id = id
|
||||
this.url = url
|
||||
this.error = error
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { postURL } from './network'
|
||||
import storageListener from './storageListener'
|
||||
|
||||
export {
|
||||
postURL,
|
||||
storageListener,
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import { STORAGE_KEYS } from '../constants'
|
||||
import { configStore, eventStore } from '../stores'
|
||||
|
||||
class StorageListener {
|
||||
constructor() {
|
||||
this._storeSubscribers = new Set()
|
||||
}
|
||||
|
||||
register() {
|
||||
window.addEventListener('storage', this._handleStorageChange)
|
||||
|
||||
const unsubscribeConfigStore = configStore.subscribe(this._handleConfigStoreChange)
|
||||
const unsubscribeEventStore = eventStore.subscribe(this._handleEventStoreChange)
|
||||
|
||||
this._storeSubscribers = new Set([
|
||||
...this._storeSubscribers,
|
||||
unsubscribeConfigStore,
|
||||
unsubscribeEventStore,
|
||||
])
|
||||
}
|
||||
|
||||
unregister() {
|
||||
window.removeEventListener('storage', this._handleStorageChange)
|
||||
|
||||
this._storeSubscribers.forEach((unsubscribe) => unsubscribe())
|
||||
this._storeSubscribers.clear()
|
||||
}
|
||||
|
||||
_handleStorageChange(event) {
|
||||
switch (event.key) {
|
||||
case STORAGE_KEYS.CONFIG:
|
||||
configStore.set(JSON.parse(event.newValue))
|
||||
break
|
||||
case STORAGE_KEYS.EVENTS:
|
||||
eventStore.set({ events: JSON.parse(event.newValue) })
|
||||
break
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_handleConfigStoreChange(value) {
|
||||
localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(value))
|
||||
}
|
||||
|
||||
_handleEventStoreChange(value) {
|
||||
localStorage.setItem(STORAGE_KEYS.EVENTS, JSON.stringify(value.events || []))
|
||||
}
|
||||
}
|
||||
|
||||
export default new StorageListener()
|
|
@ -0,0 +1,26 @@
|
|||
import { writable } from 'svelte/store'
|
||||
|
||||
import { STORAGE_KEYS } from '../constants'
|
||||
|
||||
const createConfigStore = () => {
|
||||
const state = JSON.parse(localStorage.getItem(STORAGE_KEYS.CONFIG) || '{}')
|
||||
|
||||
const { subscribe, set, update } = writable(state)
|
||||
|
||||
const setValue = (key, value) => {
|
||||
update((prevState) => ({ ...prevState, [key]: value }))
|
||||
}
|
||||
|
||||
const getState = () => state
|
||||
|
||||
return {
|
||||
...state,
|
||||
subscribe,
|
||||
set,
|
||||
update,
|
||||
setValue,
|
||||
getState,
|
||||
}
|
||||
}
|
||||
|
||||
export default createConfigStore()
|
|
@ -0,0 +1,65 @@
|
|||
import { writable } from 'svelte/store'
|
||||
|
||||
import { STORAGE_KEYS } from '../constants'
|
||||
import { migrateRecord, sortRecord } from '../utils'
|
||||
|
||||
const createEventStore = () => {
|
||||
const storedState = JSON.parse(localStorage.getItem(STORAGE_KEYS.EVENTS) || '[]')
|
||||
.map(migrateRecord)
|
||||
.sort(sortRecord)
|
||||
|
||||
let state = { events: storedState }
|
||||
|
||||
const getState = () => state
|
||||
|
||||
const { subscribe, set, update } = writable(state)
|
||||
|
||||
const setCalculation = ({ id, link, createdAt, startTime, title }) => {
|
||||
update((prevState) => {
|
||||
const nextState = {
|
||||
...prevState,
|
||||
events: [
|
||||
...prevState.events,
|
||||
{
|
||||
id,
|
||||
link,
|
||||
createdAt: createdAt.toString(),
|
||||
startTime: startTime.toString(),
|
||||
title,
|
||||
},
|
||||
].sort(sortRecord)
|
||||
}
|
||||
|
||||
state = nextState
|
||||
return nextState
|
||||
})
|
||||
}
|
||||
|
||||
const clearCalculation = (id) => {
|
||||
const calculationIndex = getState().events.findIndex((event) => event.id === id)
|
||||
|
||||
if (calculationIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextEvents = [ ...getState().events ]
|
||||
nextEvents.splice(calculationIndex, 1)
|
||||
|
||||
const nextState = { ...getState(), events: nextEvents }
|
||||
state = nextState
|
||||
|
||||
set(nextState)
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
subscribe,
|
||||
set,
|
||||
update,
|
||||
setCalculation,
|
||||
clearCalculation,
|
||||
getState,
|
||||
}
|
||||
}
|
||||
|
||||
export default createEventStore()
|
|
@ -0,0 +1,13 @@
|
|||
import configStore from './configStore'
|
||||
import eventStore from './eventStore'
|
||||
import parseStatusStore from './parseStatusStore'
|
||||
import requestStore from './requestStore'
|
||||
import swStatusStore from './swStatusStore'
|
||||
|
||||
export {
|
||||
configStore,
|
||||
eventStore,
|
||||
parseStatusStore,
|
||||
requestStore,
|
||||
swStatusStore,
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
import { writable } from 'svelte/store'
|
||||
|
||||
export default writable(null)
|
|
@ -0,0 +1,3 @@
|
|||
import { writable } from 'svelte/store'
|
||||
|
||||
export default writable(null)
|
|
@ -0,0 +1,3 @@
|
|||
import { writable } from 'svelte/store'
|
||||
|
||||
export default writable(null)
|
|
@ -0,0 +1,40 @@
|
|||
import { swStatusStore } from './stores'
|
||||
|
||||
export default () => {
|
||||
if (!window.navigator || !window.navigator.serviceWorker) {
|
||||
return
|
||||
}
|
||||
|
||||
const { serviceWorker } = window.navigator
|
||||
|
||||
serviceWorker.register('sw.js', {
|
||||
scope: './',
|
||||
}).then((registration) => {
|
||||
swStatusStore.set(`Service worker registered with scope ${registration.scope}`)
|
||||
setTimeout(() => {
|
||||
swStatusStore.set(null)
|
||||
}, 4500)
|
||||
|
||||
registration.addEventListener('updatefound', () => {
|
||||
console.info('Service worker will be updated...')
|
||||
const newWorker = registration.installing
|
||||
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed') {
|
||||
newWorker.postMessage({ action: 'skipWaiting' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}).catch((err) => {
|
||||
swStatusStore.set(`Service worker error: ${err.toString()}`)
|
||||
})
|
||||
|
||||
let refreshing
|
||||
serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (refreshing) {
|
||||
return
|
||||
}
|
||||
window.location.reload()
|
||||
refreshing = true
|
||||
})
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
export const migrateRecord = (record) => {
|
||||
// NOTE: v3 records
|
||||
const id = record.id || record.order
|
||||
const startTime = record.startTime || null
|
||||
|
||||
return {
|
||||
...record,
|
||||
id,
|
||||
startTime,
|
||||
}
|
||||
}
|
||||
|
||||
export const sortRecord = (a, b) => {
|
||||
const aDate = new Date(a.createdAt)
|
||||
const bDate = new Date(b.createdAt)
|
||||
|
||||
if (aDate < bDate) {
|
||||
return 1
|
||||
}
|
||||
if (aDate > bDate) {
|
||||
return -1
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
export const promptDownload = (uri) => {
|
||||
const link = document.getElementById('current-download')
|
||||
|
||||
link.setAttribute('href', uri)
|
||||
link.setAttribute('download', 'download.ics')
|
||||
link.click()
|
||||
|
||||
link.setAttribute('href', '')
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
const crawl = async (url, { logger }) => {
|
||||
if (logger) {
|
||||
logger.log({
|
||||
message: `Crawl started for url: ${url}`,
|
||||
level: 'info',
|
||||
service: 'parser',
|
||||
})
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(url, {
|
||||
method: 'GET',
|
||||
}).then((response) => {
|
||||
console.log(response)
|
||||
resolve()
|
||||
}).catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
export default crawl
|
|
@ -1,113 +0,0 @@
|
|||
import { useStorage } from './utils'
|
||||
|
||||
const migrateRecord = (record) => {
|
||||
// NOTE: v3 records
|
||||
const id = record.id || record.order
|
||||
const startTime = record.startTime || null
|
||||
|
||||
return {
|
||||
...record,
|
||||
id,
|
||||
startTime,
|
||||
}
|
||||
}
|
||||
|
||||
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 getConfigStorage = () => {
|
||||
if (!useStorage()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const storage = localStorage.getItem('fb-to-ical-config')
|
||||
|
||||
if (!storage) {
|
||||
localStorage.setItem('fb-to-ical-config', 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 updateConfigStorage = (storageContents, key, value) => {
|
||||
const encodedStorage = JSON.stringify({
|
||||
...storageContents,
|
||||
[key]: value,
|
||||
})
|
||||
|
||||
localStorage.setItem('fb-to-ical-config', encodedStorage)
|
||||
}
|
||||
|
||||
const saveRecord = ({ id, link, createdAt, startTime, title }) => {
|
||||
if (!useStorage()) {
|
||||
return
|
||||
}
|
||||
|
||||
const storage = getStorage()
|
||||
const storageContents = getStorageContents(storage)
|
||||
|
||||
const record = {
|
||||
id,
|
||||
link,
|
||||
createdAt: createdAt.toString(),
|
||||
startTime: startTime.toString(),
|
||||
title,
|
||||
}
|
||||
|
||||
updateStorage([ ...storageContents, record ])
|
||||
}
|
||||
|
||||
const deleteRecord = (id) => {
|
||||
if (!useStorage()) {
|
||||
return
|
||||
}
|
||||
|
||||
const storage = getStorage()
|
||||
const storageContents = getStorageContents(storage)
|
||||
const index = storageContents.findIndex((record) => {
|
||||
return record.id === id
|
||||
})
|
||||
|
||||
if (!Number.isFinite(index)) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextStorage = [ ...storageContents ]
|
||||
nextStorage.splice(index, 1)
|
||||
|
||||
updateStorage(nextStorage)
|
||||
}
|
||||
|
||||
export {
|
||||
migrateRecord,
|
||||
getStorage,
|
||||
getConfigStorage,
|
||||
getStorageContents,
|
||||
updateStorage,
|
||||
updateConfigStorage,
|
||||
saveRecord,
|
||||
deleteRecord,
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
const useStorage = () => Boolean(window.localStorage)
|
||||
|
||||
// NOTE: Generate random IDs: https://stackoverflow.com/a/2117523/3056783
|
||||
const uuidv4 = () => {
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
export {
|
||||
uuidv4,
|
||||
parseStartTimeFromiCalString,
|
||||
useStorage,
|
||||
}
|
|
@ -23,59 +23,32 @@
|
|||
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">
|
||||
<div class="status-item" id="network">
|
||||
Fetching file...
|
||||
</div>
|
||||
<div class="status-item" id="parsing">
|
||||
Parsing data...
|
||||
</div>
|
||||
<div class="status-item" id="error">
|
||||
</div>
|
||||
<div class="status-item" id="service-worker">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
<div class="list-wrapper">
|
||||
<table id="list" class="hidden">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Name</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="root"></div>
|
||||
</article>
|
||||
|
||||
|
||||
<footer>
|
||||
v.<%= htmlWebpackPlugin.options.version %> ◆
|
||||
<a href="https://ondrejsynacek.com" target="_blank">Ondrej Synacek</a> (2019) ◆
|
||||
<a href="https://github.com/comatory/fb2iCal" target="_blank" title="Github">Source</a> ◆
|
||||
<a href="/about" target="_blank" title="About the project">About</about>
|
||||
<a href="/about" target="_blank" title="About the project">About</a>
|
||||
</footer>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,365 +0,0 @@
|
|||
import { uuidv4, parseStartTimeFromiCalString, useStorage } from './app/utils'
|
||||
import {
|
||||
migrateRecord,
|
||||
getStorage,
|
||||
getConfigStorage,
|
||||
getStorageContents,
|
||||
updateStorage,
|
||||
updateConfigStorage,
|
||||
saveRecord,
|
||||
deleteRecord,
|
||||
} from './app/storage'
|
||||
import logger from './app/logger'
|
||||
import { extractEventDataFromHTML } from '../../lib/services/ics-retriever'
|
||||
import generateICS from '../../lib/services/ics-generator'
|
||||
|
||||
(() => {
|
||||
if (!window.fetch || !window.Promise || !window.URLSearchParams || !window.crypto) {
|
||||
console.warn('JS features not available.')
|
||||
return
|
||||
}
|
||||
|
||||
const showTable = () => {
|
||||
list.classList.remove('hidden')
|
||||
}
|
||||
|
||||
const deleteTableRow = (id, row) => {
|
||||
deleteRecord(id)
|
||||
|
||||
if (!useStorage()) {
|
||||
return
|
||||
}
|
||||
|
||||
row.remove()
|
||||
}
|
||||
|
||||
const renderTrackingPanel = (storageContents) => {
|
||||
const trackingPanel = document.createElement('div')
|
||||
trackingPanel.id = 'tracking-panel'
|
||||
|
||||
const text = document.createElement('p')
|
||||
text.innerText = `Can we store anonymous logs? This data is only saved to \
|
||||
our internal database and is used to debug the parsing of the web pages.\n\
|
||||
We'll ask you only this time and won't bother you again.`
|
||||
|
||||
const yesButton = document.createElement('button')
|
||||
yesButton.id = 'tracking-panel__yes-button'
|
||||
yesButton.innerText = 'Ok'
|
||||
yesButton.addEventListener('click', () => {
|
||||
updateConfigStorage(storageContents, 'track', true)
|
||||
logger.setRemoteLogging(true)
|
||||
trackingPanel.remove()
|
||||
})
|
||||
|
||||
const noButton = document.createElement('button')
|
||||
noButton.id = 'tracking-panel__no-button'
|
||||
noButton.innerText = 'Nope'
|
||||
noButton.addEventListener('click', () => {
|
||||
updateConfigStorage(storageContents, 'track', false)
|
||||
logger.setRemoteLogging(false)
|
||||
trackingPanel.remove()
|
||||
})
|
||||
|
||||
trackingPanel.appendChild(text)
|
||||
trackingPanel.appendChild(yesButton)
|
||||
trackingPanel.appendChild(noButton)
|
||||
|
||||
document.body.appendChild(trackingPanel)
|
||||
}
|
||||
|
||||
const insertTableRow = ({ id, link, createdAt, startTime, title }) => {
|
||||
showTable()
|
||||
|
||||
const newRow = document.createElement('tr')
|
||||
|
||||
const startTimeCol = document.createElement('td')
|
||||
startTimeCol.innerText = startTime ?
|
||||
new Date(startTime).toLocaleString() :
|
||||
'N/A\xa0\xa0\xa0\xa0\xa0'
|
||||
|
||||
const downloadEl = document.createElement('a')
|
||||
downloadEl.setAttribute('href', link)
|
||||
downloadEl.innerText = title
|
||||
|
||||
const titleCol = document.createElement('td')
|
||||
titleCol.appendChild(downloadEl)
|
||||
|
||||
const deleteEl = document.createElement('a')
|
||||
deleteEl.setAttribute('href', 'javascript:void(0)')
|
||||
deleteEl.innerText = '✖︎'
|
||||
deleteEl.classList.add('delete-record')
|
||||
deleteEl.addEventListener('click', (event) => {
|
||||
event.preventDefault()
|
||||
deleteTableRow(id, newRow)
|
||||
})
|
||||
|
||||
const actionCol = document.createElement('td')
|
||||
actionCol.classList.add('actions')
|
||||
actionCol.appendChild(deleteEl)
|
||||
|
||||
newRow.appendChild(startTimeCol)
|
||||
newRow.appendChild(titleCol)
|
||||
newRow.appendChild(actionCol)
|
||||
|
||||
tableBody.prepend(newRow)
|
||||
}
|
||||
|
||||
const createRecord = (uri, summary, startTime) => {
|
||||
const id = uuidv4()
|
||||
const createdAt = new Date()
|
||||
|
||||
saveRecord({
|
||||
id,
|
||||
link: uri,
|
||||
createdAt,
|
||||
startTime,
|
||||
title: summary,
|
||||
})
|
||||
insertTableRow({
|
||||
id,
|
||||
link: uri,
|
||||
createdAt,
|
||||
title: summary,
|
||||
startTime
|
||||
})
|
||||
}
|
||||
|
||||
const hydrateList = () => {
|
||||
if (!useStorage()) {
|
||||
return
|
||||
}
|
||||
|
||||
const prevStorage = getStorage()
|
||||
const migratedStorageContents = getStorageContents(prevStorage).map((record) => {
|
||||
return migrateRecord(record)
|
||||
})
|
||||
updateStorage(migratedStorageContents)
|
||||
const storage = getStorage()
|
||||
const storageContents = getStorageContents(storage)
|
||||
|
||||
if (storageContents.length > 0) {
|
||||
showTable()
|
||||
}
|
||||
|
||||
storageContents
|
||||
.sort((a, b) => {
|
||||
const aDate = new Date(a.createdAt)
|
||||
const bDate = new Date(b.createdAt)
|
||||
if (aDate < bDate) {
|
||||
return -1
|
||||
}
|
||||
if (aDate > bDate) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
.forEach((record) => {
|
||||
insertTableRow(record)
|
||||
})
|
||||
}
|
||||
|
||||
const hydrateConfig = () => {
|
||||
if (!useStorage()) {
|
||||
return
|
||||
}
|
||||
|
||||
const prevStorage = getConfigStorage()
|
||||
const storageContents = getStorageContents(prevStorage)
|
||||
|
||||
const useTrackingSet = storageContents.track !== undefined
|
||||
|
||||
if (!useTrackingSet) {
|
||||
renderTrackingPanel(storageContents)
|
||||
}
|
||||
}
|
||||
|
||||
const configureLogger = (logger) => {
|
||||
if (!logger || !useStorage()) {
|
||||
return
|
||||
}
|
||||
|
||||
const prevStorage = getConfigStorage()
|
||||
const storageContents = getStorageContents(prevStorage)
|
||||
|
||||
const shouldTrack = Boolean(storageContents.track)
|
||||
|
||||
logger.setRemoteLogging(shouldTrack)
|
||||
}
|
||||
|
||||
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) => {
|
||||
finishedRequest()
|
||||
setStatusError(error)
|
||||
}
|
||||
|
||||
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")
|
||||
const link = document.querySelector("#current-download")
|
||||
const table = document.querySelector('#list')
|
||||
const tableBody = table.querySelector('tbody')
|
||||
|
||||
if (window.navigator && window.navigator.serviceWorker) {
|
||||
const serviceWorker = window.navigator.serviceWorker
|
||||
serviceWorker.register('sw.js', {
|
||||
scope: './',
|
||||
}).then((registration) => {
|
||||
setServiceWorkerStatus(`Service worker registered with scope ${registration.scope}`)
|
||||
setTimeout(() => {
|
||||
setServiceWorkerStatus('')
|
||||
}, 4500)
|
||||
|
||||
registration.addEventListener('updatefound', () => {
|
||||
console.info('Service worker will be updated...')
|
||||
const newWorker = registration.installing
|
||||
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
if (newWorker.state === 'installed') {
|
||||
newWorker.postMessage({ action: 'skipWaiting' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}).catch((err) => {
|
||||
setServiceWorkerStatus(`Service worker error: ${err.toString()}`)
|
||||
})
|
||||
|
||||
let refreshing
|
||||
serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (refreshing) {
|
||||
return
|
||||
}
|
||||
window.location.reload()
|
||||
refreshing = true
|
||||
})
|
||||
}
|
||||
|
||||
hydrateList()
|
||||
hydrateConfig()
|
||||
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)
|
||||
})
|
||||
})
|
||||
})()
|
|
@ -1,31 +0,0 @@
|
|||
// Worker v14
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open('fb-to-ical').then((cache) => {
|
||||
return cache.addAll([
|
||||
'/',
|
||||
'/favicon.ico',
|
||||
'/scripts.js?6',
|
||||
'/style.css?9',
|
||||
'/about?3',
|
||||
'/icon-512.png',
|
||||
])
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(
|
||||
caches.match(event.request)
|
||||
.then((response) => {
|
||||
return response || fetch(event.request)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
if (event.data.action === 'skipWaiting') {
|
||||
self.skipWaiting()
|
||||
}
|
||||
})
|
|
@ -73,116 +73,7 @@ img#logo {
|
|||
margin: 5px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#current-download {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.show {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
thead {
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(odd) {
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) {
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
td.actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
td.actions a {
|
||||
margin: 2px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.delete-record {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.notice {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
#nojs {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
#tracking-panel {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
border: 3px solid navy;
|
||||
background-color: lightyellow;
|
||||
max-width: 600px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
#tracking-panel__yes-button,
|
||||
#tracking-panel__no-button {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
#tracking-panel__yes-button {
|
||||
font-weight: 600;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"name": "facebook-events-ical-converter",
|
||||
"version": "1.3.2",
|
||||
"version": "1.4.0",
|
||||
"private": true,
|
||||
"description": "App that converts events on Facebook event page to iCal file.",
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
|
@ -8,10 +9,10 @@
|
|||
"node": "10.15.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run clean:build && webpack --mode=production",
|
||||
"build:dev": "npm run clean:build && webpack --mode=development",
|
||||
"build:firebase:hosting": "npm run clean:build && NODE_ENV=production NODE_APP=firebase webpack --mode=production",
|
||||
"build:firebase:hosting:dev": "npm run clean:build && NODE_ENV=development NODE_APP=firebase webpack --mode=development",
|
||||
"build": "npm run clean:build && NODE_ENV=production webpack --config=./webpack.prod.js",
|
||||
"build:dev": "npm run clean:build && NODE_ENV=development webpack --config=./webpack.dev.js",
|
||||
"build:firebase:hosting": "npm run clean:build && NODE_ENV=production NODE_APP=firebase webpack --config=./webpack.prod.js",
|
||||
"build:firebase:hosting:dev": "npm run clean:build && NODE_ENV=development NODE_APP=firebase webpack --config=./webpack.dev.js",
|
||||
"clean:build": "rm dist/** || true",
|
||||
"deploy:firebase": "npm run build:firebase:hosting && firebase deploy",
|
||||
"start": "npm run build && node lib/index.js",
|
||||
|
@ -31,6 +32,7 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"body-parser": "^1.19.0",
|
||||
"buffer": "^6.0.3",
|
||||
"cheerio": "^1.0.0-rc.3",
|
||||
"cors": "^2.8.5",
|
||||
"dayjs": "^1.8.16",
|
||||
|
@ -39,8 +41,12 @@
|
|||
"express-rate-limit": "^5.0.0",
|
||||
"express-winston": "^4.0.1",
|
||||
"ics": "^2.22.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"request": "^2.88.0",
|
||||
"serve-favicon": "^2.5.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"svelte": "^3.31.0",
|
||||
"util": "^0.12.3",
|
||||
"winston": "^3.2.1",
|
||||
"winston-daily-rotate-file": "^4.2.1",
|
||||
"yargs": "^15.4.1"
|
||||
|
@ -49,13 +55,19 @@
|
|||
"chai": "^4.2.0",
|
||||
"chai-sinon": "^2.8.1",
|
||||
"concurrently": "^5.2.0",
|
||||
"copy-webpack-plugin": "^6.0.3",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"copy-webpack-plugin": "^7.0.0",
|
||||
"css-loader": "^5.0.1",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"jest": "^26.1.0",
|
||||
"mini-css-extract-plugin": "^1.3.3",
|
||||
"nodemon": "^1.19.3",
|
||||
"sinon": "^9.0.2",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"workbox-webpack-plugin": "^5.1.3"
|
||||
"svelte-loader": "^2.13.6",
|
||||
"webpack": "^5.11.0",
|
||||
"webpack-cli": "^4.2.0",
|
||||
"webpack-merge": "^5.7.2",
|
||||
"workbox-core": "^6.0.2",
|
||||
"workbox-precaching": "^6.0.2",
|
||||
"workbox-webpack-plugin": "^6.0.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
const webpack = require('webpack')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const pkg = require('./package.json')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const { GenerateSW } = require('workbox-webpack-plugin')
|
||||
|
||||
const destination = path.join(__dirname, 'dist')
|
||||
const isFirebaseEnv = process.env.NODE_APP === 'firebase'
|
||||
const firebaseConfigFilePath = path.join(__dirname, '.firebaserc')
|
||||
const hasFirebaseConfig = fs.existsSync(firebaseConfigFilePath)
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
console.log(`Detected dev mode? ${isDev}`)
|
||||
|
||||
if (isFirebaseEnv && hasFirebaseConfig) {
|
||||
console.info('Prepare build for Firebase hosting')
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: path.join(__dirname, 'lib', 'frontend', 'index.js'),
|
||||
output: {
|
||||
filename: '[name].[fullhash].js',
|
||||
path: destination,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
svelte: path.resolve('node_modules', 'svelte'),
|
||||
},
|
||||
extensions: [ '.mjs', '.js', '.svelte' ],
|
||||
mainFields: [ 'svelte', 'browser', 'module', 'main' ],
|
||||
fallback: {
|
||||
util: require.resolve('util/'),
|
||||
stream: require.resolve('stream-browserify'),
|
||||
path: require.resolve('path-browserify'),
|
||||
buffer: require.resolve('buffer/'),
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(svelte)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'svelte-loader',
|
||||
options: {
|
||||
emitCss: false,
|
||||
hotReload: isDev,
|
||||
dev: isDev,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: [ MiniCssExtractPlugin.loader, 'css-loader' ],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(__dirname, 'lib', 'static', 'index.html'),
|
||||
version: pkg.version,
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||
'process.env.NODE_DEBUG': JSON.stringify(process.env.NODE_DEBUG),
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: path.join(__dirname, 'lib', 'static', 'favicon.ico'), to: destination },
|
||||
{ from: path.join(__dirname, 'lib', 'static', 'manifest.json'), to: destination },
|
||||
{ from: path.join(__dirname, 'lib', 'static', 'style.css'), to: destination },
|
||||
{ from: path.join(__dirname, 'lib', 'static', 'icon-180.png'), to: destination },
|
||||
{ from: path.join(__dirname, 'lib', 'static', 'icon-192.png'), to: destination },
|
||||
{ from: path.join(__dirname, 'lib', 'static', 'icon-512.png'), to: destination },
|
||||
],
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: '[name].[fullhash].css',
|
||||
}),
|
||||
new GenerateSW({
|
||||
swDest: 'sw.js',
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
})
|
||||
],
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const pkg = require('./package.json')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const { GenerateSW } = require('workbox-webpack-plugin')
|
||||
|
||||
const destination = path.join(__dirname, 'dist')
|
||||
const isDevelopment = Boolean(process.argv[2] && process.argv[2].includes('mode=development'))
|
||||
const isFirebaseEnv = process.env.NODE_APP === 'firebase'
|
||||
const firebaseConfigFilePath = path.join(__dirname, '.firebaserc')
|
||||
const hasFirebaseConfig = fs.existsSync(firebaseConfigFilePath)
|
||||
|
||||
if (isFirebaseEnv && hasFirebaseConfig) {
|
||||
console.info('Prepare build for Firebase hosting')
|
||||
}
|
||||
|
||||
const getFirebaseUrl = () => {
|
||||
const contents = fs.readFileSync(firebaseConfigFilePath)
|
||||
const rawContents = contents.toString()
|
||||
const json = JSON.parse(rawContents)
|
||||
const projectName = json.projects ? json.projects.default : null
|
||||
|
||||
if (isDevelopment) {
|
||||
return `http://localhost:5001/${projectName}/uscentral-1/app`
|
||||
}
|
||||
|
||||
return `${projectName}.web.app/app`
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
entry: path.join(__dirname, 'lib', 'static', 'index.js'),
|
||||
watch: isDevelopment,
|
||||
output: {
|
||||
filename: '[name].[hash].js',
|
||||
path: destination,
|
||||
},
|
||||
plugins: [
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: path.join(__dirname, 'lib', 'static', '{*.ico,*.json,*.png,*.css}'), to: destination, flatten: true },
|
||||
],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.join(__dirname, 'lib', 'static', 'index.html'),
|
||||
version: pkg.version,
|
||||
serverURL: (isFirebaseEnv && hasFirebaseConfig) ? getFirebaseUrl() : '',
|
||||
inject: 'body',
|
||||
}),
|
||||
new GenerateSW({
|
||||
swDest: 'sw.js',
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
}),
|
||||
],
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
const { merge } = require('webpack-merge')
|
||||
|
||||
const baseConfig = require('./webpack.common')
|
||||
|
||||
const isDevelopment = Boolean(process.argv[2] && process.argv[2].includes('mode=development'))
|
||||
|
||||
module.exports = merge(baseConfig, {
|
||||
mode: 'development',
|
||||
devtool: 'eval-source-map',
|
||||
watch: true,
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
const { merge } = require('webpack-merge')
|
||||
|
||||
const baseConfig = require('./webpack.common')
|
||||
|
||||
module.exports = merge(baseConfig, {
|
||||
mode: 'production',
|
||||
})
|
Loading…
Reference in New Issue