aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRuben Beltran del Rio <ruben@unlimited.pizza>2021-03-09 22:43:12 +0100
committerRuben Beltran del Rio <ruben@unlimited.pizza>2021-03-09 22:43:12 +0100
commitbd8e98d7e24c4dbaee7db6ec7955f7c2f6d396a6 (patch)
treee5f0dcbc14d3009c17e5f562404fe19f6aceab73 /src
parent862a5f9cdbbda522c608ea63c1e296e81f44de10 (diff)
Update to SvelteKit
Diffstat (limited to 'src')
-rw-r--r--src/app.html23
-rw-r--r--src/components/author/author.svelte0
-rw-r--r--src/components/error_block/error_block.svelte31
-rw-r--r--src/components/footer/footer.svelte36
-rw-r--r--src/components/forum_list/forum_list.svelte52
-rw-r--r--src/components/glyph/glyph.svelte39
-rw-r--r--src/components/header/header.svelte48
-rw-r--r--src/components/home/home.svelte6
-rw-r--r--src/components/invalid_route/invalid_route.svelte6
-rw-r--r--src/components/language_selector/language_selector.svelte28
-rw-r--r--src/components/post/post.svelte27
-rw-r--r--src/components/topic/topic.svelte59
-rw-r--r--src/components/topic_index/topic_index.svelte8
-rw-r--r--src/config/config.js11
-rw-r--r--src/config/env.dist1
-rw-r--r--src/config/i18n.js12
-rw-r--r--src/config/translations/en.json91
-rw-r--r--src/config/translations/es.json91
-rw-r--r--src/forum.svelte63
-rw-r--r--src/routes/$error.svelte5
-rw-r--r--src/routes/$layout.svelte39
-rw-r--r--src/routes/a/[id].svelte10
-rw-r--r--src/routes/f/[id].svelte9
-rw-r--r--src/routes/g/[id].svelte9
-rw-r--r--src/routes/index.svelte6
-rw-r--r--src/routes/p/[id].svelte11
-rw-r--r--src/routes/t/[id].svelte10
-rw-r--r--src/socket_coordinator.js63
-rw-r--r--src/stores/forums.js36
-rw-r--r--src/utils/glyph_hash.js38
30 files changed, 868 insertions, 0 deletions
diff --git a/src/app.html b/src/app.html
new file mode 100644
index 0000000..26d754f
--- /dev/null
+++ b/src/app.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="description" content="A forum for the year 3000">
+ <meta name="robots" content="noindex, nofollow" />
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="theme-color" content="#ffffff">
+
+ <title>Forum.</title>
+
+ <link rel="manifest" href="/manifest.webmanifest">
+
+ %svelte.head%
+ </head>
+ <body>
+ <div id="forum">%svelte.body%</div>
+ <noscript>
+ <h1>Javascript Required.</h1>
+ <p>Please enable Javascript to use this forum.</p>
+ </noscript>
+ </body>
+</html>
diff --git a/src/components/author/author.svelte b/src/components/author/author.svelte
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/components/author/author.svelte
diff --git a/src/components/error_block/error_block.svelte b/src/components/error_block/error_block.svelte
new file mode 100644
index 0000000..e46afa1
--- /dev/null
+++ b/src/components/error_block/error_block.svelte
@@ -0,0 +1,31 @@
+<script>
+ import { _ } from 'svelte-i18n';
+ export let message;
+</script>
+
+<div>
+ <h2>{$_('error.generic.title')}</h2>
+ <p>{message || $_('error.generic.message')}</p>
+</div>
+
+<style>
+ div {
+ background: repeating-linear-gradient( -45deg, red, red 5px, black 5px, black 10px );
+ border: 5px solid red;
+ color: yellow;
+ font-family: 'ヒラギノ角ゴ ProN' , 'Hiragino Kaku Gothic ProN' , '游ゴシック' , '游ゴシック体' , YuGothic , 'Yu Gothic' , 'メイリオ' , Meiryo , 'MS ゴシック' , 'MS Gothic' , HiraKakuProN-W3 , 'TakaoExゴシック' , TakaoExGothic , 'MotoyaLCedar' , 'Droid Sans Japanese' , sans-serif;
+ margin: 0 10px 0 0;
+ padding: 100px 5px;
+ text-align: center;
+ }
+
+ h2, p {
+ background-color: black;
+ font-size: 1em;
+ }
+
+ h2 {
+ text-transform: uppercase;
+ }
+</style>
+
diff --git a/src/components/footer/footer.svelte b/src/components/footer/footer.svelte
new file mode 100644
index 0000000..3587aab
--- /dev/null
+++ b/src/components/footer/footer.svelte
@@ -0,0 +1,36 @@
+<script>
+ import { _ } from 'svelte-i18n';
+
+ import LanguageSelector from '../language_selector/language_selector.svelte';
+
+ const licenseUrl = 'https://gitlab.com/rbdr/forum/';
+</script>
+
+<footer title="{$_('footer.title')}">
+ <ul>
+ <li>{@html $_('footer.license', { values: { licenseUrl } })}</li>
+ <li>{$_('footer.choose_language')}: <LanguageSelector /></li>
+ </ul>
+</footer>
+
+<style>
+ footer {
+ grid-column: col-start 1 / span 12;
+ border-top: 1px solid black;
+ }
+
+ ul {
+ padding: 0;
+ }
+
+ li {
+ display: inline;
+ margin: 5px;
+ }
+
+ a {
+ text-decoration: none;
+ line-height: 3em;
+ display: inline-block;
+ }
+</style>
diff --git a/src/components/forum_list/forum_list.svelte b/src/components/forum_list/forum_list.svelte
new file mode 100644
index 0000000..174ae25
--- /dev/null
+++ b/src/components/forum_list/forum_list.svelte
@@ -0,0 +1,52 @@
+<script>
+ import { _ } from 'svelte-i18n';
+ import { forums } from '../../stores/forums.js';
+ import ErrorBlock from '../error_block/error_block.svelte';
+</script>
+
+<nav title="{$_('forum_list.title')}">
+ {#if !$forums.length}
+ <ErrorBlock message={$_('forum_list.error.unavailable')} />
+ {/if}
+ <ul>
+ {#each $forums as forum}
+ <li>
+ <a href="/f/{forum.id}">
+ <span aria-hidden="true" class="navigation-glyph {forum.glyph}">{forum.glyph}</span>
+ <span class="navigation-label">{$_(forum.label)}</span>
+ </a>
+ </li>
+ {/each}
+ </ul>
+</nav>
+
+<style>
+ nav {
+ grid-column: col-start 1;
+ grid-row: 2;
+ border-right: 1px solid black;
+ }
+
+ ul {
+ padding: 0;
+ }
+
+ li {
+ display: block;
+ text-align: left;
+ margin-bottom: 20px;
+ }
+
+ .navigation-glyph {
+ font-size: 1.5rem;
+ display: block;
+ }
+
+ .☽ {
+ font-size: 2rem;
+ }
+
+ a {
+ text-decoration: none;
+ }
+</style>
diff --git a/src/components/glyph/glyph.svelte b/src/components/glyph/glyph.svelte
new file mode 100644
index 0000000..e4b9166
--- /dev/null
+++ b/src/components/glyph/glyph.svelte
@@ -0,0 +1,39 @@
+<script>
+ import { _ } from 'svelte-i18n';
+ import { getGlyphHash } from '../../utils/glyph_hash';
+
+ export let uuid;
+</script>
+
+<div class="glyphicon" aria-hidden="true" title="{$_('glyph.title')}">
+ {#each getGlyphHash(uuid) as fragment}
+ <span class="{fragment.glyph}" style="color: {fragment.color} ">
+ {fragment.glyph}
+ </span>
+ {/each}
+</div>
+
+<style>
+ .glyphicon {
+ border: 1px solid black;
+ display: inline-block;
+ font-size: 1.4rem;
+ height: 48px;
+ margin-top: 5px;
+ width: 48px;
+ }
+
+ span {
+ display: block;
+ float: left;
+ width: 24px;
+ height: 24px;
+ text-align: center;
+ line-height: 24px;
+ }
+
+ .☽ {
+ font-size: 2rem;
+ line-height: 19px;
+ }
+</style>
diff --git a/src/components/header/header.svelte b/src/components/header/header.svelte
new file mode 100644
index 0000000..36d4d50
--- /dev/null
+++ b/src/components/header/header.svelte
@@ -0,0 +1,48 @@
+<script>
+ import { _ } from 'svelte-i18n';
+
+ import { version } from '$config/config';
+</script>
+
+<header title="{$_('header.title')}">
+ <ul>
+ <li><strong><a href="/" aria-label="{$_('header.long_version', { values: { version } })}">{$_('header.short_version', { values: { version } })}</a></strong></li>
+ <li><a href="/new" aria-label="{$_('header.action.new.title')}">{@html $_('header.action.new.display')}</a></li>
+ <li><a href="/reply" aria-label="{$_('header.action.reply.title')}">{@html $_('header.action.reply.display')}</a></li>
+ <li><a href="/search" aria-label="{$_('header.action.search.title')}">{@html $_('header.action.search.display')}</a></li>
+ <li><a href="/logout" aria-label="{$_('header.action.log_out.title')}">{@html $_('header.action.log_out.display')}</a></li>
+ </ul>
+</header>
+
+<style>
+ header {
+ grid-column: col-start 1 / span 12;
+ border-bottom: 1px solid black;
+ }
+
+ ul {
+ padding: 0;
+ }
+
+ .action-key {
+ font-weight: bold;
+ text-decoration: underline;
+ }
+
+ li {
+ display: inline;
+ margin: 5px;
+ }
+
+ a {
+ text-decoration: none;
+ line-height: 3em;
+ display: inline-block;
+ }
+
+ strong a {
+ color: blue;
+ text-decoration: underline;
+ }
+
+</style>
diff --git a/src/components/home/home.svelte b/src/components/home/home.svelte
new file mode 100644
index 0000000..7000137
--- /dev/null
+++ b/src/components/home/home.svelte
@@ -0,0 +1,6 @@
+<script>
+ import { _ } from 'svelte-i18n';
+</script>
+
+<h1>{$_('home.title')}</h1>
+<p>{$_('home.content')}</p>
diff --git a/src/components/invalid_route/invalid_route.svelte b/src/components/invalid_route/invalid_route.svelte
new file mode 100644
index 0000000..aaf7927
--- /dev/null
+++ b/src/components/invalid_route/invalid_route.svelte
@@ -0,0 +1,6 @@
+<script>
+ import { _ } from 'svelte-i18n';
+</script>
+
+<h1>{$_('error.invalid_url.title')}</h1>
+<p>{$_('error.invalid_url.message')}</p>
diff --git a/src/components/language_selector/language_selector.svelte b/src/components/language_selector/language_selector.svelte
new file mode 100644
index 0000000..67c0814
--- /dev/null
+++ b/src/components/language_selector/language_selector.svelte
@@ -0,0 +1,28 @@
+<script>
+ import { locale, locales } from 'svelte-i18n';
+ import { getLangNameFromCode } from 'language-name-map';
+
+ $: namedLocales = $locales
+ .map((locale) => ({
+ code: locale,
+ ...getLangNameFromCode(locale)
+ }))
+ .sort((a, b) => a.native - b.native);
+
+ let selected = $locale;
+
+ $: {
+ console.log(`the current locale is ${selected}`);
+ locale.set(selected);
+ }
+</script>
+
+<select bind:value={selected}>
+ {#each namedLocales as namedLocale}
+ <option value="{ namedLocale.code }">{ namedLocale.native }</option>
+ {/each}
+</select>
+
+<style>
+</style>
+
diff --git a/src/components/post/post.svelte b/src/components/post/post.svelte
new file mode 100644
index 0000000..4720533
--- /dev/null
+++ b/src/components/post/post.svelte
@@ -0,0 +1,27 @@
+<script>
+ import { _ } from 'svelte-i18n';
+
+ import Glyph from '../glyph/glyph.svelte';
+</script>
+
+<aside class="post-meta" title="{$_('post.metadata_title', { values: { count: 2, total: 2 } })}">
+ <Glyph uuid="b33f0339f7d64d1ca27f1c0aefb7d753" />
+ <span class="h-card">
+ {$_('post.author_credit')} <a href="/a/time4carrots" class="p-nickname u-url">time4carrots</a>.
+ </span>
+ <time role="presentation" class="dt-published" datetime="2018-08-15T04:10:00.929Z">
+ <a href="/p/da9910f3febde91948000ce1535ea">
+ 2018-08-15 04:10:00
+ </a>
+ </time>
+</aside>
+<article class="e-content" title="{$_('post.title', { values: { count: 1, total: 2, author: 'time4carrots' } })}">
+ <p>It's just how it is...</p>
+</article>
+<hr/>
+
+<style>
+ .post-meta * {
+ vertical-align: top;
+ }
+</style>
diff --git a/src/components/topic/topic.svelte b/src/components/topic/topic.svelte
new file mode 100644
index 0000000..2aff79a
--- /dev/null
+++ b/src/components/topic/topic.svelte
@@ -0,0 +1,59 @@
+<script>
+ import { _ } from 'svelte-i18n';
+
+ import Glyph from '../glyph/glyph.svelte';
+</script>
+
+<div class="h-entry" title="{$_('topic.title')}">
+ <h1 class="p-name">This is a post in the forum.</h1>
+ <aside class="topic-meta" title="{$_('topic.metadata_title')}">
+ <span class="topic-location">{$_('topic.category_location')} <a href="/f/interaction"
+ class="p-category">{$_('forum.name.interaction')}</a>.</span>
+ <span class="topic-ttl"><a class="u-url u-uid" title="{$_('topic.permalink_title')}" href="/t/2a3fc567af8c897ca6f55fb5fj">{$_('topic.remaining_time', { values: { remaining: $_('time.days', { values: { count: 3 } }) } })}</a>.</span>
+ </aside>
+ <aside class="topic-tags" title="{$_('topic.tags_title')}">
+ {$_('topic.tags_location')}
+ <a href="/g/question" class="p-category">question<span class="tag-weight">(5)</span></a>
+ <a href="/g/meta" class="p-category">meta<span class="tag-weight">(34)</span></a>
+ <a href="/g/carrots" class="p-category">carrots<span class="tag-weight">(1)</span></a>
+ <a href="/g/tpbo" class="p-category">tpbo<span class="tag-weight">(2)</span></a>
+ </aside>
+ <aside class="post-meta" title="{$_('post.metadata_title', { values: { count: 1, total: 2 } })}">
+ <Glyph uuid="3cd8c84e18144a2da71f6bace9392abc" />
+ <span class="h-card">
+ {$_('post.author_credit')} <a href="/a/rbdr" class="p-nickname u-url">rbdr</a>.
+ </span>
+ <time role="presentation" class="dt-published" datetime="2018-08-14T03:32:10.929Z">
+ <a title="{$_('post.permalink_title')}" href="/p/a80c70ea0120387123097ce1907ff">
+ 2018-08-14 03:32:10
+ </a>
+ </time>
+ </aside>
+ <article class="e-content" title="{$_('post.title', { values: { count: 1, total: 2, author: 'rbdr' } })}">
+ <p>This is a main topic in the forum. Does that abstraction still even make sense?</p>
+ <p>Is this really it??</p>
+ <p>This might all be fake but at least the links look purple when visited</p>
+ </article>
+ <hr/>
+ <aside class="post-meta" title="{$_('post.metadata_title', { values: { count: 2, total: 2 } })}">
+ <Glyph uuid="b33f0339f7d64d1ca27f1c0aefb7d753" />
+ <span class="h-card">
+ {$_('post.author_credit')} <a href="/a/time4carrots" class="p-nickname u-url">time4carrots</a>.
+ </span>
+ <time role="presentation" class="dt-published" datetime="2018-08-15T04:10:00.929Z">
+ <a href="/p/da9910f3febde91948000ce1535ea">
+ 2018-08-15 04:10:00
+ </a>
+ </time>
+ </aside>
+ <article class="e-content" title="{$_('post.title', { values: { count: 1, total: 2, author: 'time4carrots' } })}">
+ <p>It's just how it is...</p>
+ </article>
+ <hr/>
+</div>
+
+<style>
+ .post-meta * {
+ vertical-align: top;
+ }
+</style>
diff --git a/src/components/topic_index/topic_index.svelte b/src/components/topic_index/topic_index.svelte
new file mode 100644
index 0000000..3de83bb
--- /dev/null
+++ b/src/components/topic_index/topic_index.svelte
@@ -0,0 +1,8 @@
+<script>
+ export let params;
+ import ErrorBlock from '../error_block/error_block.svelte';
+</script>
+
+<ErrorBlock />
+<h1>Topic Index.</h1>
+<p>This component lists topics for category or tag with id: {params.id}</p>
diff --git a/src/config/config.js b/src/config/config.js
new file mode 100644
index 0000000..6d819a3
--- /dev/null
+++ b/src/config/config.js
@@ -0,0 +1,11 @@
+import { version as packageVersion } from '../../package.json';
+
+/*
+ * The main configuration object for the Forum frontend. These values
+ * are calculated during compile time and need to be set in a .env
+ * file, otherwise it won't work.
+ */
+
+export const socketServer = import.meta.env.FORUM_SOCKET_SERVER;
+
+export const version = packageVersion;
diff --git a/src/config/env.dist b/src/config/env.dist
new file mode 100644
index 0000000..0bcd782
--- /dev/null
+++ b/src/config/env.dist
@@ -0,0 +1 @@
+FORUM_SOCKET_SERVER=ws://location_of_socket_server
diff --git a/src/config/i18n.js b/src/config/i18n.js
new file mode 100644
index 0000000..25ffc45
--- /dev/null
+++ b/src/config/i18n.js
@@ -0,0 +1,12 @@
+import { addMessages, getLocaleFromNavigator, init } from 'svelte-i18n';
+
+import en from './translations/en.json';
+import es from './translations/es.json';
+
+addMessages('en', en);
+addMessages('es', es);
+
+init({
+ fallbackLocale: 'en',
+ initialLocale: getLocaleFromNavigator()
+});
diff --git a/src/config/translations/en.json b/src/config/translations/en.json
new file mode 100644
index 0000000..5b6a4b6
--- /dev/null
+++ b/src/config/translations/en.json
@@ -0,0 +1,91 @@
+{
+ "error": {
+ "generic": {
+ "title": "Error!",
+ "message": "Unknown error has occurred. Panic!"
+ },
+ "invalid_url": {
+ "title": "This is not right.",
+ "message": "This URL is not valid. Try another one maybe?"
+ }
+ },
+ "footer": {
+ "choose_language": "Choose your language",
+ "license": "Forum is <a href=\"{licenseUrl}\">open source.</a>",
+ "title": "Footer"
+ },
+ "forum": {
+ "name": {
+ "everything": "Everything",
+ "us": "Us",
+ "words": "Words",
+ "sound": "Sounds",
+ "structure": "Structure",
+ "interaction": "Interaction",
+ "emotion": "Emotion",
+ "movement": "Movement",
+ "belief": "Belief",
+ "experience": "Experience",
+ "online": "Online",
+ "the_world": "The World",
+ "life": "Life"
+ }
+ },
+ "forum_list": {
+ "title": "List of forums",
+ "error": {
+ "unavailable": "Forum list unavailable."
+ }
+ },
+ "glyph": {
+ "title": "User avatar"
+ },
+ "header": {
+ "action": {
+ "new": {
+ "title": "New",
+ "display": "<u>N</u>ew"
+ },
+ "reply": {
+ "title": "Reply",
+ "display": "<u>R</u>eply"
+ },
+ "search": {
+ "title": "Search",
+ "display": "<u>S</u>earch"
+ },
+ "log_out": {
+ "title": "Log Out",
+ "display": "Log <u>O</u>ut"
+ }
+ },
+ "long_version": "Forum version {version}",
+ "title": "Toolbar",
+ "short_version": "Forum v{version}"
+ },
+ "home": {
+ "title": "Hello.",
+ "content": "You are now in a forum. Select a category from the left or input a valid URL."
+ },
+ "post": {
+ "author_credit": "By:",
+ "metadata_title": "Post {count} of {total} metadata",
+ "permalink_title": "Permalink to post",
+ "title": "Post {count} of {total} by {author}"
+ },
+ "topic": {
+ "category_location": "Posted on",
+ "metadata_title": "Topic metadata",
+ "permalink_title": "Permalink to topic",
+ "remaining_time": "{remaining} remaining",
+ "tags_location": "Tags:",
+ "tags_title": "Topic tags",
+ "title": "Topic"
+ },
+ "time": {
+ "days": "{count, plural, =1 {# day} other {# days}}",
+ "hours": "{count, plural, =1 {# hour} other {# hours}}",
+ "minutes": "{count, plural, =1 {# minute} other {# minutes}}",
+ "seconds": "{count, plural, =1 {# second} other {# seconds}}"
+ }
+}
diff --git a/src/config/translations/es.json b/src/config/translations/es.json
new file mode 100644
index 0000000..be8f96b
--- /dev/null
+++ b/src/config/translations/es.json
@@ -0,0 +1,91 @@
+{
+ "error": {
+ "generic": {
+ "title": "Error!",
+ "message": "Hubo un error desconocido. ¡Entra en pánico!"
+ },
+ "invalid_url": {
+ "title": "Esto no está bien.",
+ "message": "Este URL no es válido. Intenta otro, ¿Tal vez?"
+ }
+ },
+ "footer": {
+ "choose_language": "Escoge un lenguaje",
+ "license": "Forum es <a href=\"{licenseUrl}\">software libre.</a>",
+ "title": "Pie de página"
+ },
+ "forum": {
+ "name": {
+ "everything": "Todo",
+ "us": "Nosotros",
+ "words": "Palabras",
+ "sound": "Sonidos",
+ "structure": "Estructura",
+ "interaction": "Interacción",
+ "emotion": "Emoción",
+ "movement": "Movimiento",
+ "belief": "Creencia",
+ "experience": "Experiencia",
+ "online": "En Línea",
+ "the_world": "El Mundo",
+ "life": "Vida"
+ }
+ },
+ "forum_list": {
+ "title": "Lista de foros",
+ "error": {
+ "unavailable": "Lista de foros no disponible."
+ }
+ },
+ "glyph": {
+ "title": "Avatar del usuario"
+ },
+ "header": {
+ "action": {
+ "new": {
+ "title": "Nuevo",
+ "display": "<u>N</u>uevo"
+ },
+ "reply": {
+ "title": "Responder",
+ "display": "<u>R</u>esponder"
+ },
+ "search": {
+ "title": "Buscar",
+ "display": "Bu<u>s</u>car"
+ },
+ "log_out": {
+ "title": "Cerrar Sesión",
+ "display": "Cerrar Sesi<u>ó</u>n"
+ }
+ },
+ "long_version": "Forum versión {version}",
+ "title": "Barra de herramientas",
+ "short_version": "Forum v{version}"
+ },
+ "home": {
+ "title": "Hola.",
+ "content": "Ahora estás en un foro. Elige una categoría de la izquierda o escribe un URL válido."
+ },
+ "post": {
+ "author_credit": "Por:",
+ "metadata_title": "Metadatos de entrada {count} de {total}",
+ "permalink_title": "Permalink a entrada",
+ "title": "Entrada {count} de {total}, por {author}"
+ },
+ "topic": {
+ "category_location": "Agregado a",
+ "metadata_title": "Metadatos del tema",
+ "permalink_title": "Permalink al tema",
+ "remaining_time": "Quedan {remaining}",
+ "tags_location": "Etiquetas:",
+ "tags_title": "Etiquetas del tema",
+ "title": "Tema"
+ },
+ "time": {
+ "days": "{count, plural, =1 {# día} other {# días}}",
+ "hours": "{count, plural, =1 {# hora} other {# horas}}",
+ "minutes": "{count, plural, =1 {# minuto} other {# minutos}}",
+ "seconds": "{count, plural, =1 {# segundo} other {# segundos}}"
+ }
+}
diff --git a/src/forum.svelte b/src/forum.svelte
new file mode 100644
index 0000000..d2a6e1b
--- /dev/null
+++ b/src/forum.svelte
@@ -0,0 +1,63 @@
+<script>
+ import LightRouter from 'lightrouter';
+
+ // Initialize localization
+
+ import './config/i18n';
+
+ // Global components
+
+ import ForumList from './components/forum_list/forum_list.svelte';
+ import Header from './components/header/header.svelte';
+ import Footer from './components/footer/footer.svelte';
+
+ // Routed Components
+ import Author from './components/author/author.svelte';
+ import Home from './components/home/home.svelte';
+ import InvalidRoute from './components/invalid_route/invalid_route.svelte';
+ import Post from './components/post/post.svelte';
+ import Topic from './components/topic/topic.svelte';
+ import TopicIndex from './components/topic_index/topic_index.svelte';
+
+ let page;
+ let params;
+
+ // sets the route params and current page.
+
+ const setRoute = function setRoute(targetPage) {
+
+ return function (routerParams) {
+
+ params = routerParams;
+ page = targetPage;
+ };
+ };
+
+ const router = new LightRouter({
+ routes: {
+ '': () => (page = Home) && true,
+ 'f/{id}': setRoute(TopicIndex),
+ 'g/{id}': setRoute(TopicIndex),
+ 'a/{id}': setRoute(Author),
+ 't/{id}': setRoute(Topic),
+ 'p/{id}': setRoute(Post),
+ '.*': setRoute(InvalidRoute)
+ }
+ });
+
+ router.run();
+
+</script>
+
+<Header />
+<main>
+ <svelte:component this={ page } params={ params } />
+</main>
+<ForumList />
+<Footer />
+
+<style>
+ main {
+ grid-column: col-start 2 / span 11;
+ }
+</style>
diff --git a/src/routes/$error.svelte b/src/routes/$error.svelte
new file mode 100644
index 0000000..df7da3c
--- /dev/null
+++ b/src/routes/$error.svelte
@@ -0,0 +1,5 @@
+<script>
+ import InvalidRoute from '$components/invalid_route/invalid_route.svelte';
+</script>
+
+<InvalidRoute />
diff --git a/src/routes/$layout.svelte b/src/routes/$layout.svelte
new file mode 100644
index 0000000..a848a59
--- /dev/null
+++ b/src/routes/$layout.svelte
@@ -0,0 +1,39 @@
+<script>
+ import '$config/i18n';
+ import { isLoading, waitLocale } from 'svelte-i18n';
+
+ export const load = function load() {
+
+ return waitLocale;
+ };
+
+ import ForumList from '$components/forum_list/forum_list.svelte';
+ import Header from '$components/header/header.svelte';
+ import Footer from '$components/footer/footer.svelte';
+</script>
+
+{#if $isLoading}
+ <h1>Loading.</h1>
+ <p>Please wait.</p>
+{:else}
+ <Header />
+ <main>
+ <slot></slot>
+ </main>
+ <ForumList />
+ <Footer />
+{/if}
+
+<style>
+ main {
+ grid-column: col-start 2 / span 11;
+ }
+
+ :global(#forum) {
+ display: grid;
+ font-family : 'ヒラギノ明朝 ProN' , 'Hiragino Mincho ProN' , '游明朝','游明朝体',YuMincho,'Yu Mincho' , 'MS 明朝' , 'MS Mincho' , HiraMinProN-W3 , 'TakaoEx明朝' , TakaoExMincho , 'MotoyaLCedar' , 'Droid Sans Japanese' , serif;
+ grid-template-columns: repeat(12, [col-start] 1fr);
+ grid-gap: 20px;
+ grid-auto-rows: minmax(24px, auto);
+ }
+</style>
diff --git a/src/routes/a/[id].svelte b/src/routes/a/[id].svelte
new file mode 100644
index 0000000..1b6c705
--- /dev/null
+++ b/src/routes/a/[id].svelte
@@ -0,0 +1,10 @@
+<script>
+ export async function load(ctx) {
+ let id = ctx.page.params.id
+ return { props: { id }}
+ }
+
+ import Author from '$components/author/author.svelte';
+</script>
+
+<Author/>
diff --git a/src/routes/f/[id].svelte b/src/routes/f/[id].svelte
new file mode 100644
index 0000000..d1fae4c
--- /dev/null
+++ b/src/routes/f/[id].svelte
@@ -0,0 +1,9 @@
+<script>
+ export async function load(ctx) {
+ let id = ctx.page.params.id
+ return { props: { id }}
+ }
+</script>
+
+<h1>Forum Index.</h1>
+<p>This component lists topics for forum with id: {id}</p>
diff --git a/src/routes/g/[id].svelte b/src/routes/g/[id].svelte
new file mode 100644
index 0000000..d036c36
--- /dev/null
+++ b/src/routes/g/[id].svelte
@@ -0,0 +1,9 @@
+<script>
+ export async function load(ctx) {
+ let id = ctx.page.params.id
+ return { props: { id }}
+ }
+</script>
+
+<h1>Tag Index.</h1>
+<p>This component lists topics for tag with id: {id}</p>
diff --git a/src/routes/index.svelte b/src/routes/index.svelte
new file mode 100644
index 0000000..7000137
--- /dev/null
+++ b/src/routes/index.svelte
@@ -0,0 +1,6 @@
+<script>
+ import { _ } from 'svelte-i18n';
+</script>
+
+<h1>{$_('home.title')}</h1>
+<p>{$_('home.content')}</p>
diff --git a/src/routes/p/[id].svelte b/src/routes/p/[id].svelte
new file mode 100644
index 0000000..b8a8165
--- /dev/null
+++ b/src/routes/p/[id].svelte
@@ -0,0 +1,11 @@
+<script>
+ export const load = function load(ctx) {
+
+ const id = ctx.page.params.id;
+ return { props: { id } };
+ };
+
+ import Post from '$components/post/post.svelte';
+</script>
+
+<Post/>
diff --git a/src/routes/t/[id].svelte b/src/routes/t/[id].svelte
new file mode 100644
index 0000000..8775cec
--- /dev/null
+++ b/src/routes/t/[id].svelte
@@ -0,0 +1,10 @@
+<script>
+ export async function load(ctx) {
+ let id = ctx.page.params.id
+ return { props: { id }}
+ }
+
+ import Topic from '$components/topic/topic.svelte';
+</script>
+
+<Topic/>
diff --git a/src/socket_coordinator.js b/src/socket_coordinator.js
new file mode 100644
index 0000000..f8fb525
--- /dev/null
+++ b/src/socket_coordinator.js
@@ -0,0 +1,63 @@
+import EventEmitter from 'eventemitter3';
+import { socketServer } from './config/config';
+
+const internals = {
+
+ kReconnectInterval: 3000, // How often we attempt to reconnect
+
+ eventEmitter: new EventEmitter(), // internal event emitter
+ socket: null, // stores the socket connection
+ retry: null, // stores the retry operation
+
+ connect() {
+
+ console.debug('Connecting socket.');
+ internals.socket = new WebSocket(socketServer);
+
+ internals.socket.addEventListener('message', internals.onMessage);
+ internals.socket.addEventListener('error', internals.onError);
+ internals.socket.addEventListener('close', internals.onClose);
+ },
+
+ // Handles socket errors.
+
+ onError(event) {
+
+ console.error('Socket error. Closing connection');
+ internals.socket.close();
+ },
+
+ // Handles socket errors.
+
+ onClose(event) {
+
+ console.debug(`Connection closed: ${event.reason || 'Unknown reason'}. Retrying in ${internals.kReconnectInterval}ms`);
+
+ internals.retry && clearTimeout(internals.retry);
+ internals.retry = setTimeout(() => {
+
+ console.debug('Reconnecting socket.');
+ internals.retry = null;
+ internals.connect();
+ }, internals.kReconnectInterval);
+ },
+
+ // Forwards events from the socket to our internal event emitter.
+
+ onMessage(event) {
+
+ internals.eventEmitter.emit('message', event);
+ }
+};
+
+export const onMessage = function (listener) {
+
+ if (!internals.socket) {
+ internals.connect();
+ }
+
+ internals.eventEmitter.on('message', (message) => {
+
+ listener(JSON.parse(message.data));
+ });
+};
diff --git a/src/stores/forums.js b/src/stores/forums.js
new file mode 100644
index 0000000..e7acb60
--- /dev/null
+++ b/src/stores/forums.js
@@ -0,0 +1,36 @@
+import { readable } from 'svelte/store';
+
+const internals = {
+
+ // Constants
+
+ kChangeFeedEventName: 'changefeed:forums',
+
+ // The exported data structure
+
+ forums: [],
+
+ // Handles messages from the event
+
+ handleChangeFeed(data) {
+
+ // No old value == add
+ if (!data.old_val) {
+ return internals.forums.push(data.new_val);
+ }
+
+ // We have an old value, let's find it.
+ const index = internals.forums.findIndex((element) => element.id === data.old_val.id);
+
+ if (index > -1) {
+ if (data.new_val) {
+ return internals.forums.splice(index, 1, data.new_val || undefined);
+ }
+
+ return internals.forums.splice(index, 1);
+ }
+ }
+};
+
+export const forums = readable(internals.forums, (set) => {
+});
diff --git a/src/utils/glyph_hash.js b/src/utils/glyph_hash.js
new file mode 100644
index 0000000..120c02a
--- /dev/null
+++ b/src/utils/glyph_hash.js
@@ -0,0 +1,38 @@
+const internals = {
+ kSplitterRegex: /.{1,8}/g,
+ kGlyphs: [
+ '☽',
+ '☆',
+ '♢',
+ '♡',
+ '╱',
+ '╲',
+ '╳',
+ '〰',
+ '▷',
+ '⏊',
+ '〒',
+ '▢',
+ '◯',
+ '⏃',
+ '⏀',
+ '⏆'
+ ]
+};
+
+// Return a glyph with color based on a 4 byte fragment of a UUIDv4
+const getGlyphHashFragment = function (uuidFragment) {
+
+ const glyphIndex = parseInt(uuidFragment.substring(0,2), 16) % 16;
+ return {
+ glyph: internals.kGlyphs[glyphIndex],
+ color: `#${uuidFragment.substring(2,8)}`
+ };
+};
+
+// Return an array of glyphs based on a UUIDv4
+export const getGlyphHash = function (uuid) {
+
+ const hashFragments = uuid.match(internals.kSplitterRegex);
+ return hashFragments.map(getGlyphHashFragment);
+};