diff options
| author | Owen Jacobson <owen@grimoire.ca> | 2024-10-10 21:51:10 -0400 |
|---|---|---|
| committer | Owen Jacobson <owen@grimoire.ca> | 2024-10-10 21:52:26 -0400 |
| commit | 215b0c5cb2ff0ef0b2c7b5549704e23d651a4df9 (patch) | |
| tree | 356484ce699539f2937b768d1a1c9b83f0c7a402 /ui/src | |
| parent | 4401dce2b5545ce8117818812d8e3c8919f5f7fd (diff) | |
Hoist the UI one step up further
Diffstat (limited to 'ui/src')
| -rw-r--r-- | ui/src/app.css | 3 | ||||
| -rw-r--r-- | ui/src/app.html | 12 | ||||
| -rw-r--r-- | ui/src/lib/apiServer.js | 101 | ||||
| -rw-r--r-- | ui/src/lib/components/ActiveChannel.svelte | 27 | ||||
| -rw-r--r-- | ui/src/lib/components/Channel.svelte | 21 | ||||
| -rw-r--r-- | ui/src/lib/components/ChannelList.svelte | 18 | ||||
| -rw-r--r-- | ui/src/lib/components/CreateChannelForm.svelte | 23 | ||||
| -rw-r--r-- | ui/src/lib/components/LogIn.svelte | 35 | ||||
| -rw-r--r-- | ui/src/lib/components/LogOut.svelte | 22 | ||||
| -rw-r--r-- | ui/src/lib/components/Message.svelte | 33 | ||||
| -rw-r--r-- | ui/src/lib/components/MessageInput.svelte | 30 | ||||
| -rw-r--r-- | ui/src/lib/index.js | 1 | ||||
| -rw-r--r-- | ui/src/lib/store.js | 10 | ||||
| -rw-r--r-- | ui/src/lib/store/channels.js | 71 | ||||
| -rw-r--r-- | ui/src/lib/store/logins.js | 22 | ||||
| -rw-r--r-- | ui/src/lib/store/messages.js | 44 | ||||
| -rw-r--r-- | ui/src/routes/(app)/+layout.svelte | 89 | ||||
| -rw-r--r-- | ui/src/routes/(app)/+page.svelte | 0 | ||||
| -rw-r--r-- | ui/src/routes/(app)/ch/[channel]/+page.svelte | 17 | ||||
| -rw-r--r-- | ui/src/routes/+layout.js | 1 | ||||
| -rw-r--r-- | ui/src/routes/+layout.svelte | 30 |
21 files changed, 0 insertions, 610 deletions
diff --git a/ui/src/app.css b/ui/src/app.css deleted file mode 100644 index b5c61c9..0000000 --- a/ui/src/app.css +++ /dev/null @@ -1,3 +0,0 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; diff --git a/ui/src/app.html b/ui/src/app.html deleted file mode 100644 index 63eb917..0000000 --- a/ui/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ -<!doctype html> -<html lang="en" class="dark"> - <head> - <meta charset="utf-8" /> - <link rel="icon" href="%sveltekit.assets%/favicon.png" /> - <meta name="viewport" content="width=device-width, initial-scale=1" /> - %sveltekit.head% - </head> - <body data-sveltekit-preload-data="hover" data-theme="skeleton"> - <div style="display: contents">%sveltekit.body%</div> - </body> -</html> diff --git a/ui/src/lib/apiServer.js b/ui/src/lib/apiServer.js deleted file mode 100644 index f6d6148..0000000 --- a/ui/src/lib/apiServer.js +++ /dev/null @@ -1,101 +0,0 @@ -import axios from 'axios'; -import { activeChannel, channelsList, logins, messages } from '$lib/store'; - -export const apiServer = axios.create({ - baseURL: '/api/', -}); - -export async function boot() { - return apiServer.get('/boot'); -} - -export async function logIn(username, password) { - const data = { - name: username, - password, - }; - return apiServer.post('/auth/login', data); -} - -export async function logOut() { - return apiServer.post('/auth/logout', {}); -} - -export async function createChannel(name) { - return apiServer.post('/channels', { name }); -} - -export async function postToChannel(channelId, body) { - return apiServer.post(`/channels/${channelId}`, { body }); -} - -export async function deleteMessage(messageId) { - // TODO -} - -export function subscribeToEvents(resume_point) { - const eventsUrl = new URL('/api/events', window.location); - eventsUrl.searchParams.append('resume_point', resume_point); - const evtSource = new EventSource(eventsUrl.toString()); - // TODO: this should process all incoming events and store them. - // TODO: eventually we'll need to handle expiring old info, so as not to use - // infinite browser memory. - /* - * Known message types as of now: - * - created: a channel is created. - * - action: ignore. - * - message: a message is created. - * - action: display message in channel. - * - message_deleted: a message is deleted. - * - action: replace message with <...>. - * - deleted: a channel is deleted. - * - action: remove channel from sidebar. - */ - evtSource.onmessage = (evt) => { - const data = JSON.parse(evt.data); - - switch (data.type) { - case 'login': - onLoginEvent(data); - break; - case 'channel': - onChannelEvent(data); - break; - case 'message': - onMessageEvent(data); - break; - } - } -} - -function onLoginEvent(data) { - switch (data.event) { - case 'created': - logins.update((value) => value.addLogin(data.id, data.name)) - break; - } -} - -function onChannelEvent(data) { - switch (data.event) { - case 'created': - channelsList.update((value) => value.addChannel(data.id, data.name)) - break; - case 'deleted': - activeChannel.update((value) => value.deleteChannel(data.id)); - channelsList.update((value) => value.deleteChannel(data.id)); - messages.update((value) => value.deleteChannel(data.id)); - break; - } -} - -function onMessageEvent(data) { - switch (data.event) { - case 'sent': - messages.update((value) => value.addMessage(data.channel, data.id, data.at, data.sender, data.body)); - break; - case 'deleted': - messages.update((value) => value.deleteMessage(data.id)); - break; - } -} diff --git a/ui/src/lib/components/ActiveChannel.svelte b/ui/src/lib/components/ActiveChannel.svelte deleted file mode 100644 index 978e952..0000000 --- a/ui/src/lib/components/ActiveChannel.svelte +++ /dev/null @@ -1,27 +0,0 @@ -<script> - import { activeChannel, messages } from '$lib/store'; - import Message from './Message.svelte'; - - let container; - $: messageList = $activeChannel.isSet() ? $messages.inChannel($activeChannel.get()) : []; - - // TODO: eventually, store scroll height/last unread in channel? scroll there? - - let scroll = (message) => { - message.scrollIntoView(); - } -</script> - -<div class="container" bind:this={container}> - {#each messageList as message} - <div use:scroll> - <Message {...message} /> - </div> - {/each} -</div> - -<style> - .container { - overflow: scroll; - } -</style> diff --git a/ui/src/lib/components/Channel.svelte b/ui/src/lib/components/Channel.svelte deleted file mode 100644 index 97fea1f..0000000 --- a/ui/src/lib/components/Channel.svelte +++ /dev/null @@ -1,21 +0,0 @@ -<script> - import { activeChannel } from '$lib/store'; - - export let id; - export let name; - let active = false; - - activeChannel.subscribe((value) => { - active = value.is(id); - }); -</script> - -<li - class="rounded-full" - class:bg-slate-400={active} -> -<a href="/ch/{id}"> - <span class="badge bg-primary-500">#</span> - <span class="flex-auto">{name}</span> -</a> -</li> diff --git a/ui/src/lib/components/ChannelList.svelte b/ui/src/lib/components/ChannelList.svelte deleted file mode 100644 index e0e5f06..0000000 --- a/ui/src/lib/components/ChannelList.svelte +++ /dev/null @@ -1,18 +0,0 @@ -<script> - import { channelsList } from '$lib/store'; - import Channel from './Channel.svelte'; - - let channels; - - channelsList.subscribe((value) => { - channels = value.channels; - }); -</script> - -<nav class="list-nav"> - <ul> - {#each channels as channel} - <Channel {...channel} /> - {/each} - </ul> -</nav> diff --git a/ui/src/lib/components/CreateChannelForm.svelte b/ui/src/lib/components/CreateChannelForm.svelte deleted file mode 100644 index ddcf486..0000000 --- a/ui/src/lib/components/CreateChannelForm.svelte +++ /dev/null @@ -1,23 +0,0 @@ -<script> - import { createChannel } from '$lib/apiServer'; - - let name = ''; - let disabled = false; - - async function handleSubmit(event) { - disabled = true; - const response = await createChannel(name); - if (200 <= response.status && response.status < 300) { - name = ''; - } - disabled = false; - } -</script> - -<form on:submit|preventDefault={handleSubmit} class="form form-row flex-nowrap"> - <input type="text" placeholder="create channel" bind:value={name} disabled={disabled} class="input flex-auto h-6 w-9/12" /> - <button type="submit" class="flex-none w-6 h-6">➕</button> -</form> - -<style> -</style> diff --git a/ui/src/lib/components/LogIn.svelte b/ui/src/lib/components/LogIn.svelte deleted file mode 100644 index 2836e6d..0000000 --- a/ui/src/lib/components/LogIn.svelte +++ /dev/null @@ -1,35 +0,0 @@ -<script> - import { logIn } from '$lib/apiServer'; - import { currentUser } from '$lib/store'; - - let disabled = false; - let username = ''; - let password = ''; - - async function handleLogin(event) { - disabled = true; - const response = await logIn(username, password); - if (200 <= response.status && response.status < 300) { - currentUser.update(() => ({ username })); - username = ''; - password = ''; - } - disabled = false; - } -</script> - -<div class="card m-4 p-4"> - <form on:submit|preventDefault={handleLogin}> - <label class="label" for="username"> - username - <input class="input" name="username" type="text" placeholder="username" bind:value={username} disabled={disabled}> - </label> - <label class="label" for="password"> - password - <input class="input" name="password" type="password" placeholder="password" bind:value={password} disabled={disabled}> - </label> - <button class="btn variant-filled" type="submit"> - sign in or up - </button> - </form> -</div> diff --git a/ui/src/lib/components/LogOut.svelte b/ui/src/lib/components/LogOut.svelte deleted file mode 100644 index 01bef1b..0000000 --- a/ui/src/lib/components/LogOut.svelte +++ /dev/null @@ -1,22 +0,0 @@ -<script> - import { logOut} from '$lib/apiServer'; - import { currentUser } from '$lib/store'; - - async function handleLogout(event) { - const response = await logOut(); - if (200 <= response.status && response.status < 300) { - currentUser.update(() => null); - } - } -</script> - -<form on:submit|preventDefault={handleLogout}> - @{$currentUser.username} - <button - class="border-slate-500 border-solid border-2 font-bold p-1 rounded" - type="submit" - >log out</button> -</form> - -<style> -</style> diff --git a/ui/src/lib/components/Message.svelte b/ui/src/lib/components/Message.svelte deleted file mode 100644 index d040433..0000000 --- a/ui/src/lib/components/Message.svelte +++ /dev/null @@ -1,33 +0,0 @@ -<script> - import SvelteMarkdown from 'svelte-markdown'; - import { currentUser, logins } from '$lib/store'; - import { deleteMessage } from '$lib/apiServer'; - - export let at; // XXX: Omitted for now. - export let sender; - export let body; - - let timestamp = new Date(at).toTimeString(); - let name; - $: name = $logins.get(sender); -</script> - -<div class="card card-hover m-4 relative"> - <span class="chip variant-soft sticky top-o left-0"> - <!-- TODO: should this show up for only the first of a run? --> - @{name}: - </span> - <span class="timestamp chip variant-soft absolute top-0 right-0">{at}</span> - <section class="p-4"> - <SvelteMarkdown source={body} /> - </section> -</div> - -<style> - .card .timestamp { - display: none; - } - .card:hover .timestamp { - display: flex; - } -</style> diff --git a/ui/src/lib/components/MessageInput.svelte b/ui/src/lib/components/MessageInput.svelte deleted file mode 100644 index b33574b..0000000 --- a/ui/src/lib/components/MessageInput.svelte +++ /dev/null @@ -1,30 +0,0 @@ -<script> - import { tick } from 'svelte'; - import { postToChannel } from '$lib/apiServer'; - import { activeChannel } from '$lib/store'; - - let input; - let value; - let disabled; - activeChannel.subscribe((value) => { - disabled = !value.isSet(); - if (input && !disabled) { - input.focus(); - } - }); - - async function handleSubmit(event) { - disabled = true; - // TODO try/catch: - await postToChannel($activeChannel.get(), value); - value = ''; - disabled = false; - await tick(); - input.focus(); - } -</script> - -<form on:submit|preventDefault={handleSubmit} class="flex flex-row flex-nowrap"> - <input bind:this={input} bind:value={value} disabled={disabled} type="search" class="flex-auto h-6 input rounded-r-none" /> - <button color="primary variant-filled-secondary" type="submit" class="flex-none w-6 h-6 btn-icon variant-filled rounded-l-none">»</button> -</form> diff --git a/ui/src/lib/index.js b/ui/src/lib/index.js deleted file mode 100644 index 856f2b6..0000000 --- a/ui/src/lib/index.js +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/ui/src/lib/store.js b/ui/src/lib/store.js deleted file mode 100644 index b964b4b..0000000 --- a/ui/src/lib/store.js +++ /dev/null @@ -1,10 +0,0 @@ -import { writable } from 'svelte/store'; -import { ActiveChannel, Channels } from '$lib/store/channels'; -import { Messages } from '$lib/store/messages'; -import { Logins } from '$lib/store/logins'; - -export const currentUser = writable(null); -export const activeChannel = writable(new ActiveChannel()); -export const logins = writable(new Logins()); -export const channelsList = writable(new Channels()); -export const messages = writable(new Messages()); diff --git a/ui/src/lib/store/channels.js b/ui/src/lib/store/channels.js deleted file mode 100644 index bb6c86c..0000000 --- a/ui/src/lib/store/channels.js +++ /dev/null @@ -1,71 +0,0 @@ -export class Channels { - constructor() { - this.channels = []; - } - - setChannels(channels) { - this.channels = [...channels]; - this.sort(); - return this; - } - - addChannel(id, name) { - this.channels = [...this.channels, { id, name }]; - this.sort(); - return this; - } - - deleteChannel(id) { - const channelIndex = this.channels.map((e) => e.id).indexOf(id); - if (channelIndex !== -1) { - this.channels.splice(channelIndex, 1); - } - return this; - } - - sort() { - this.channels.sort((a, b) => { - if (a.name < b.name) { - return -1; - } else if (a.name > b.name) { - return 1; - } - return 0; - }); - } -} - -export class ActiveChannel { - constructor() { - this.channel = null; - } - - isSet() { - return this.channel !== null; - } - - get() { - return this.channel; - } - - is(id) { - return this.channel === id; - } - - set(id) { - this.channel = id; - return this; - } - - deleteChannel(id) { - if (this.is(id)) { - return this.clear(); - } - return this; - } - - clear() { - this.channel = null; - return this; - } -} diff --git a/ui/src/lib/store/logins.js b/ui/src/lib/store/logins.js deleted file mode 100644 index 5b45206..0000000 --- a/ui/src/lib/store/logins.js +++ /dev/null @@ -1,22 +0,0 @@ -export class Logins { - constructor() { - this.logins = {}; - } - - addLogin(id, name) { - this.logins[id] = name; - return this; - } - - setLogins(logins) { - this.logins = {}; - for (let { id, name } of logins) { - this.addLogin(id, name); - } - return this; - } - - get(id) { - return this.logins[id]; - } -} diff --git a/ui/src/lib/store/messages.js b/ui/src/lib/store/messages.js deleted file mode 100644 index 931b8fb..0000000 --- a/ui/src/lib/store/messages.js +++ /dev/null @@ -1,44 +0,0 @@ -export class Messages { - constructor() { - this.channels = {}; - } - - inChannel(channel) { - return this.channels[channel] = (this.channels[channel] || []); - } - - addMessage(channel, id, at, sender, body) { - this.updateChannel(channel, (messages) => [...messages, { id, at, sender, body }]); - return this; - } - - setMessages(messages) { - this.channels = {}; - for (let { channel, id, at, sender, body } of messages) { - this.inChannel(channel).push({ id, at, sender, body, }); - } - for (let channel in this.channels) { - this.channels[channel].sort((a, b) => a.at - b.at); - } - return this; - } - - - deleteMessage(message) { - for (let channel in this.channels) { - this.updateChannel(channel, (messages) => messages.filter((msg) => msg.id != message)); - } - return this; - } - - deleteChannel(id) { - delete this.channels[id]; - return this; - } - - updateChannel(channel, callback) { - let messages = callback(this.inChannel(channel)); - messages.sort((a, b) => a.at - b.at); - this.channels[channel] = messages; - } -} diff --git a/ui/src/routes/(app)/+layout.svelte b/ui/src/routes/(app)/+layout.svelte deleted file mode 100644 index f8744c1..0000000 --- a/ui/src/routes/(app)/+layout.svelte +++ /dev/null @@ -1,89 +0,0 @@ -<script> - import { onMount } from 'svelte'; - - import { boot, subscribeToEvents } from '$lib/apiServer'; - import { currentUser, logins, channelsList, messages } from '$lib/store'; - - import ChannelList from '$lib/components/ChannelList.svelte'; - import CreateChannelForm from '$lib/components/CreateChannelForm.svelte'; - import LogIn from '$lib/components/LogIn.svelte'; - import MessageInput from '$lib/components/MessageInput.svelte'; - - let user; - let loading = true; - - currentUser.subscribe((value) => { - user = value; - }); - - function onBooted(boot) { - currentUser.update(() => ({ - id: boot.login.id, - username: boot.login.name, - })); - logins.update((value) => value.setLogins(boot.logins)); - channelsList.update((value) => value.setChannels(boot.channels)); - messages.update((value) => value.setMessages(boot.messages)); - } - - onMount(async () => { - try { - let response = await boot(); - switch (response.status) { - case 200: - onBooted(response.data); - subscribeToEvents(response.data.resume_point); - break; - case 401: - currentUser.update(() => null); - break; - default: - // TODO: display error. - break; - } - } catch (_) { - // I don't want exceptions on non-200 series responses, dammit. - } - loading = false; - }); -</script> - -{#if loading} - <h2>Loading…</h2> -{:else if user != null} - <div id="interface"> - <div class="channel-list"> - <ChannelList /> - </div> - <div class="active-channel"> - <slot /> - </div> - <div class="create-channel"> - <CreateChannelForm /> - </div> - <div class="create-message"> - <MessageInput /> - </div> - </div> -{:else} - <LogIn /> -{/if} - -<style> - #interface { - height: 88vh; - margin: 1rem; - display: grid; - grid-template-columns: 18rem auto; - grid-template-rows: auto 2rem; - grid-gap: 0.25rem; - } - #interface div { - max-height: 100%; - overflow: scroll; - } - #interface .active-channel { - border: 1px solid grey; - border-radius: 1.25rem; - } -</style> diff --git a/ui/src/routes/(app)/+page.svelte b/ui/src/routes/(app)/+page.svelte deleted file mode 100644 index e69de29..0000000 --- a/ui/src/routes/(app)/+page.svelte +++ /dev/null diff --git a/ui/src/routes/(app)/ch/[channel]/+page.svelte b/ui/src/routes/(app)/ch/[channel]/+page.svelte deleted file mode 100644 index ef439d0..0000000 --- a/ui/src/routes/(app)/ch/[channel]/+page.svelte +++ /dev/null @@ -1,17 +0,0 @@ -<script> - import { afterNavigate } from '$app/navigation'; - import { page } from '$app/stores'; - - import { activeChannel } from '$lib/store'; - import ActiveChannel from '$lib/components/ActiveChannel.svelte'; - - afterNavigate(async () => { - let { channel } = $page.params; - activeChannel.update((value) => { - value.set(channel) - return value; - }); - }); -</script> - -<ActiveChannel /> diff --git a/ui/src/routes/+layout.js b/ui/src/routes/+layout.js deleted file mode 100644 index a3d1578..0000000 --- a/ui/src/routes/+layout.js +++ /dev/null @@ -1 +0,0 @@ -export const ssr = false; diff --git a/ui/src/routes/+layout.svelte b/ui/src/routes/+layout.svelte deleted file mode 100644 index 7b99d62..0000000 --- a/ui/src/routes/+layout.svelte +++ /dev/null @@ -1,30 +0,0 @@ -<script> - import { AppBar } from '@skeletonlabs/skeleton'; - import "../app.css"; - - import { currentUser } from '$lib/store'; - import LogOut from '$lib/components/LogOut.svelte'; -</script> - -<div id="app"> - <AppBar> - <svelte:fragment slot="lead">🌳</svelte:fragment> - <a href="/">understory</a> - <svelte:fragment slot="trail"> - {#if $currentUser} - <LogOut /> - {/if} - </svelte:fragment> - </AppBar> - - <slot /> -</div> - -<style> - #app { - margin: 0; - padding: 1rem; - height: 100vh; - width: 100%; - } -</style> |
