koro

an event time scheduler
git clone https://tilde.team/~marisa/repo/koro.git
Log | Files | Refs | README | LICENSE

commit 236d8fe42552a48d6df8111ace06716ec493f31b
parent ab1eaf62053e72c356247d38d0b06a7324e6496d
Author: mokou <mokou@posteo.de>
Date:   Thu,  1 Oct 2020 20:52:32 +0200

feat: Rename event, add half of times

Diffstat:
Msrc/App.svelte | 4++++
Asrc/pages/CreateEvent.svelte | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/pages/CreateTimes.svelte | 151++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/pages/Index.svelte | 171++++++-------------------------------------------------------------------------
4 files changed, 321 insertions(+), 158 deletions(-)

diff --git a/src/App.svelte b/src/App.svelte @@ -2,6 +2,8 @@ import navaid from 'navaid' import { is24Hrs, isDarkMode } from './stores' import Index from './pages/Index.svelte' + import CreateEvent from './pages/CreateEvent.svelte' + import CreateTimes from './pages/CreateTimes.svelte' import Event from './pages/Event.svelte' import CreatePoll from './pages/CreatePoll.svelte' import Poll from './pages/Poll.svelte' @@ -12,6 +14,8 @@ let routeParams router.on('/', setRoute(Index)) + router.on('/event', setRoute(CreateEvent)) + router.on('/times', setRoute(CreateTimes)) router.on('/poll', setRoute(CreatePoll)) router.on('/updates', setRoute(Log)) router.on('/p/:id', setRoute(Poll)) diff --git a/src/pages/CreateEvent.svelte b/src/pages/CreateEvent.svelte @@ -0,0 +1,153 @@ +<script> + import autocomplete from 'accessible-autocomplete' + import search from 'fuzzysearch' + import day from 'dayjs' + import utc from 'dayjs/plugin/utc' + import isOnline from 'is-online' + import { nanoid } from 'nanoid' + import { onMount } from 'svelte' + import { isDarkMode } from '../stores' + import { pouch, replicateToCouch } from '../couch' + import DateTimeGrid from '../components/DateTimeGrid.svelte' + day.extend(utc) + + document.title = 'Create Event - Koro' + let submitting = false + let success = false + let errorFlash = '' + let durationInput = null + let durationsList = [ + '15 minutes', + '30 minutes', + '45 minutes', + '1 hour', + '2 hours', + '3 hours', + '4 hours', + '5 hours' + ] + let event = { + _id: nanoid(), + name: '', + duration: '', + times: [] + } + let eventLink = '' + + async function submitForm() { + errorFlash = '' + submitting = true + if (!event.name) { + errorFlash = 'Please set a name for your event!' + submitting = false + return + } + + if (!event.duration) { + errorFlash = 'Please set a duration for your event!' + submitting = false + return + } + + if (!durationsList.includes(event.duration)) { + errorFlash = 'The duration must be picked from the provided list!' + submitting = false + return + } + + if (event.times.length === 0) { + errorFlash = 'Please pick at least one event time!' + submitting = false + return + } + + const db = await pouch() + try { + const res = await db.put({ + times: event.times.map((e) => day(e).utc().format()), + ...event + }) + await replicateToCouch() + success = true + eventLink = `https://koro.moe/${res.id}` + } catch (e) { + console.error(e) + } + } + + function durationSource(query, cb) { + if (durationsList.includes(query)) cb([]) + const filtered = durationsList.filter((d) => search(query, d)) + cb(filtered) + } + + onMount(async () => { + if (!(await isOnline())) { + errorFlash = `You're offline! This means you can still create an event, but you'll have to be online before you can share the link with others.` + } + autocomplete({ + element: document.querySelector('#duration-mount'), + id: 'duration', + source: durationSource, + confirmOnBlur: false, + onConfirm: (opt) => { + event.duration = opt + } + }) + }) +</script> + +<h1 class="text-5xl font-serif font-extrabold">Create Event</h1> +{#if success} + <p class="text-2xl font-medium max-w-xl leading-normal"> + Your event has successfully been created! You can share the link below with + anyone you want to invite, and they can set their available times from the + ones you picked out. Don't forget to fill it out yourself! + </p> + + <div + class="text-2xl font-mono bg-black text-white max-w-xl w-full px-3 py-2 mt-5 + text-center"> + {eventLink} + </div> +{:else} + <form class="max-w-xl mt-6" on:submit|preventDefault={submitForm}> + {#if errorFlash} + <div + class="w-full bg-red-100 px-3 py-2 border rounded border-red-400 + text-red-600 my-5"> + {errorFlash} + </div> + {/if} + <label + for="name" + class="font-bold text-gray-300" + class:text-gray-600={!$isDarkMode}> + Your event's name + </label> + <input + id="name" + type="text" + bind:value={event.name} + class:dark={$isDarkMode} + placeholder="Raid with the boys" /> + <label + for="duration" + class="font-bold text-gray-300 inline-block mt-4" + class:text-gray-600={!$isDarkMode}> + Duration of the event + </label> + <div id="duration-mount" class="mb-5" class:dark={$isDarkMode} /> + <label class="font-bold text-gray-300" class:text-gray-600={!$isDarkMode}> + Event start times + </label> + </form> + <DateTimeGrid startDate={new Date()} bind:selected={event.times} /> + <button + class="btn mt-5" + type="submit" + on:click|preventDefault={submitForm} + disabled={submitting}> + Create event + </button> +{/if} diff --git a/src/pages/CreateTimes.svelte b/src/pages/CreateTimes.svelte @@ -0,0 +1,151 @@ +<script> + import autocomplete from 'accessible-autocomplete' + import search from 'fuzzysearch' + import { nanoid } from 'nanoid' + import isOnline from 'is-online' + import { rawTimeZones } from '@vvo/tzdb' + import day from 'dayjs' + import customParseFormat from 'dayjs/plugin/customParseFormat' + import { onMount } from 'svelte' + import { pouch, replicateToCouch } from '../couch' + import { isDarkMode, is24Hrs } from '../stores' + + document.title = 'Create Times - Koro' + day.extend(customParseFormat) + let times = { + _id: `t:${nanoid()}`, + times: [ + + ] + } + let currentTime = { + time: null, + timezone: '' + } + + let timeInput = '' + let timezoneInput = '' + let errorFlash = '' + let success = false + let isValidTime = false + const tzs = rawTimeZones + const tzlist = tzs.map(tz => { + return { str: `${tz.name} (${tz.abbreviation})`, name: tz.name } + }) + + function parseTime (evt) { + const time = evt.target.value + const match = day(time, ['YYYY MM DD HH:mm', 'MM DD HH:mm', 'YYYY MM DD hh:mm', 'MM DD hh:mm'], true) + if (match.isValid()) { + currentTime.time = match.toISOString() + isValidTime = true + } else { + isValidTime = false + } + } + + function addTime () { + const copiedCurrentTime = Object.assign({}, currentTime) + times.times = [...times.times, copiedCurrentTime] + } + + function removeTime (i) { + return () => { + times.times.splice(i, 1) + times = times + } + } + + function submitForm () { + + } + + function timezoneSource (query, cb) { + if (tzlist.includes(query)) db([]) + const filtered = tzlist.filter(d => search(query.toLowerCase(), d.str.toLowerCase())) + cb(filtered) + } + + onMount(async () => { + if (!(await isOnline())) { + errorFlash = `You're offline! This means you can still create times, but you'll have to be online before you can share the link with others.` + } + + autocomplete({ + element: document.querySelector('#timezone-mount'), + id: 'timezone', + source: timezoneSource, + confirmOnBlur: false, + onConfirm: (opt) => { + currentTime.timezone = opt.name + }, + templates: { + inputValue (opt) { + return opt && opt.str + }, + suggestion (s) { + return s.str + } + } + }) + }) +</script> + + +<h1 class="text-5xl font-serif font-extrabold">Create Times</h1> +{#if success} + blah +{:else} + <form class="max-w-xl mt-6" on:submit|preventDefault={submitForm}> + {#if errorFlash} + <div + class="w-full bg-red-100 px-3 py-2 border rounded border-red-400 + text-red-600 my-5"> + {errorFlash} + </div> + {/if} + + <label + for="time" + class="font-bold text-gray-300 inline-block mt-4" + class:text-gray-600={!$isDarkMode}> + Time and Date + </label> + <input + id="time" + on:keyup={parseTime} + class:dark={$isDarkMode} + placeholder="(YYYY) MM DD hh:mm" + type="text"> + <div class:text-red-400={!isValidTime} class:text-green-400={isValidTime}> + {#if isValidTime} + Looks good! + {:else} + Check that you entered the date correctly! + {/if} + </div> + + <label + for="timezone" + class="font-bold text-gray-300 inline-block mt-4" + class:text-gray-600={!$isDarkMode}> + Source Timezone (type to see options) + </label> + <div id="timezone-mount" class="mb-5" class:dark={$isDarkMode} /> + <button + class="btn mt-5" + on:click|preventDefault={addTime} + type="submit"> + Add time + </button> + </form> + + <ul class="list-disc my-3"> + {#each times.times as time, i} + <li> + <span class="hover:cursor-pointer" on:click={removeTime(i)}>❌</span> + <span class="font-bold">{day(time.time).format(`DD/MM/YYYY ${$is24Hrs ? 'HH:mm' : 'hh:mma'}`)}</span> in <span class="font-bold">{tzs.find(tz => tz.name === time.timezone).abbreviation}</span> + </li> + {/each} + </ul> +{/if} diff --git a/src/pages/Index.svelte b/src/pages/Index.svelte @@ -1,164 +1,19 @@ <script> - import autocomplete from 'accessible-autocomplete' - import search from 'fuzzysearch' - import day from 'dayjs' - import utc from 'dayjs/plugin/utc' - import isOnline from 'is-online' - import { nanoid } from 'nanoid' - import { onMount } from 'svelte' import { isDarkMode } from '../stores' - import { pouch, replicateToCouch } from '../couch' - import DateTimeGrid from '../components/DateTimeGrid.svelte' - day.extend(utc) document.title = 'Koro' - let submitting = false - let success = false - let errorFlash = '' - let durationInput = null - let durationsList = [ - '15 minutes', - '30 minutes', - '45 minutes', - '1 hour', - '2 hours', - '3 hours', - '4 hours', - '5 hours' - ] - let event = { - _id: nanoid(), - name: '', - duration: '', - times: [] - } - let eventLink = '' - - async function submitForm() { - errorFlash = '' - submitting = true - if (!event.name) { - errorFlash = 'Please set a name for your event!' - submitting = false - return - } - - if (!event.duration) { - errorFlash = 'Please set a duration for your event!' - submitting = false - return - } - - if (!durationsList.includes(event.duration)) { - errorFlash = 'The duration must be picked from the provided list!' - submitting = false - return - } - - if (event.times.length === 0) { - errorFlash = 'Please pick at least one event time!' - submitting = false - return - } - - const db = await pouch() - try { - const res = await db.put({ - times: event.times.map((e) => day(e).utc().format()), - ...event - }) - await replicateToCouch() - success = true - eventLink = `https://koro.moe/${res.id}` - } catch (e) { - console.error(e) - } - } - - function durationSource(query, cb) { - if (durationsList.includes(query)) cb([]) - const filtered = durationsList.filter((d) => search(query, d)) - cb(filtered) - } - - onMount(async () => { - if (!(await isOnline())) { - errorFlash = `You're offline! This means you can still create an event, but you'll have to be online before you can share the link with others.` - } - autocomplete({ - element: document.querySelector('#duration-mount'), - id: 'duration', - source: durationSource, - confirmOnBlur: false, - onConfirm: (opt) => { - event.duration = opt - } - }) - }) </script> -<h1 class="text-5xl font-serif font-extrabold">koro</h1> -{#if success} - <p class="text-2xl font-medium max-w-xl leading-normal"> - Your event has successfully been created! You can share the link below with - anyone you want to invite, and they can set their available times from the - ones you picked out. Don't forget to fill it out yourself! - </p> - - <div - class="text-2xl font-mono bg-black text-white max-w-xl w-full px-3 py-2 mt-5 - text-center"> - {eventLink} - </div> -{:else} - <p class="text-2xl font-medium max-w-xl leading-normal"> - An event time planning site for the rest of us. Use this to find a timeslot - for your next meeting, party, raid, whatever you want. It automatically - supports the user's timezone and works locally, too. - </p> - - <p class="text-2xl my-4 font-medium max-w-xl leading-normal"> - You can also create a - <a class="text-purple-400" href="/poll">regular poll</a>. - </p> - - <form class="max-w-xl mt-6" on:submit|preventDefault={submitForm}> - {#if errorFlash} - <div - class="w-full bg-red-100 px-3 py-2 border rounded border-red-400 - text-red-600 my-5"> - {errorFlash} - </div> - {/if} - <label - for="name" - class="font-bold text-gray-300" - class:text-gray-600={!$isDarkMode}> - Your event's name - </label> - <input - id="name" - type="text" - bind:value={event.name} - class:dark={$isDarkMode} - placeholder="Raid with the boys" /> - <label - for="duration" - class="font-bold text-gray-300 inline-block mt-4" - class:text-gray-600={!$isDarkMode}> - Duration of the event - </label> - <div id="duration-mount" class="mb-5" class:dark={$isDarkMode} /> - <label class="font-bold text-gray-300" class:text-gray-600={!$isDarkMode}> - Event start times - </label> - </form> - <DateTimeGrid startDate={new Date()} bind:selected={event.times} /> - <button - class="btn mt-5" - type="submit" - on:click|preventDefault={submitForm} - disabled={submitting}> - Create event - </button> -{/if} +<h1 class="text-5xl font-serif font-extrabold">Koro</h1> +<p class="text-2xl font-medium max-w-xl leading-normal"> + An event time planning site for the rest of us. You can plan event + times with as many people as you like, run polls, or simply display a bunch + of times in the user's timezone. It also (kind of) works when you're offline, + and it's free! +</p> + +<ul class="list-disc text-2xl mt-4"> + <li><a class="text-purple-400" href="/event">/event</a> - Flexibly schedule an event time with other people</li> + <li><a class="text-purple-400" href="/poll">/poll</a> - Create a poll</li> + <li><a class="text-purple-400" href="/times">/times</a> - Display times in the user's timezone</li> +</ul>