initial commit
This commit is contained in:
commit
155aa524f3
48 changed files with 3943 additions and 0 deletions
35
src/lib/AsideCard.svelte
Normal file
35
src/lib/AsideCard.svelte
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<script lang="ts">
|
||||
|
||||
let { children, title = null } = $props();
|
||||
|
||||
</script>
|
||||
|
||||
<aside class="card">
|
||||
{#if title}
|
||||
<h3>{title}</h3>
|
||||
{/if}
|
||||
{@render children()}
|
||||
</aside>
|
||||
|
||||
<style lang="scss">
|
||||
aside {
|
||||
overflow: hidden;
|
||||
border: var(--border) 1px solid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
background-color: var(--accent);
|
||||
color: var(--bg-main);
|
||||
padding: 6px 12px;
|
||||
margin: -12px -12px 0 -12px;
|
||||
position: relative;
|
||||
font: inherit;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
14
src/lib/Centered.svelte
Normal file
14
src/lib/Centered.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
|
||||
<script lang="ts">
|
||||
let { narrow = false, children } : { narrow?: boolean, children: any } = $props();
|
||||
</script>
|
||||
|
||||
<div class="centered" class:narrow={narrow}>
|
||||
{@render children () }
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.centered {text-align: center;}
|
||||
.centered.narrow { max-width: 400px; margin: auto; }
|
||||
</style>
|
||||
|
||||
55
src/lib/CheckboxLabel.svelte
Normal file
55
src/lib/CheckboxLabel.svelte
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
|
||||
<script lang="ts">
|
||||
|
||||
let { children, name, value = '1' } = $props();
|
||||
|
||||
let checked = $state(false);
|
||||
</script>
|
||||
|
||||
<label>
|
||||
<span class="toggleout" class:checked role="checkbox" aria-checked={checked}></span>
|
||||
<input type="checkbox" bind:checked {name} {value} />
|
||||
{@render children()}
|
||||
</label>
|
||||
|
||||
<style>
|
||||
[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toggleout {
|
||||
display: inline-block;
|
||||
height: 1em;
|
||||
width: 2em;
|
||||
vertical-align: middle;
|
||||
background-color: var(--bg-sharp);
|
||||
border-radius: 1em;
|
||||
position: relative;
|
||||
transition: ease .5s;
|
||||
}
|
||||
|
||||
.toggleout::before {
|
||||
display: inline-block;
|
||||
background-color: var(--text-primary);
|
||||
width: .8em;
|
||||
height: .8em;
|
||||
border-radius: 1em;
|
||||
content: '\00d7';
|
||||
color: var(--bg-sharp);
|
||||
position: absolute;
|
||||
left: .1em;
|
||||
top: .1em;
|
||||
line-height: .6;
|
||||
transition: ease .5s left;
|
||||
}
|
||||
|
||||
.toggleout.checked {
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
.toggleout.checked::before {
|
||||
content: '\2713';
|
||||
left: 1.1em;
|
||||
top: .1em;
|
||||
}
|
||||
</style>
|
||||
34
src/lib/Feed.svelte
Normal file
34
src/lib/Feed.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { RiChatOffLine } from "svelte-remixicon";
|
||||
import Centered from "./Centered.svelte";
|
||||
import FeedPost from "./FeedPost.svelte";
|
||||
|
||||
let { posts, emptymsg = "No posts, how empty" } = $props();
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#each posts as post}
|
||||
<li><FeedPost {post} /></li>
|
||||
{:else}
|
||||
<Centered>
|
||||
<p class="big"><RiChatOffLine /></p>
|
||||
{emptymsg}</Centered>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0 1em;
|
||||
}
|
||||
|
||||
ul > li {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
p.big {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
</style>
|
||||
25
src/lib/FeedPost.svelte
Normal file
25
src/lib/FeedPost.svelte
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import { DateTime } from "luxon";
|
||||
import { RiHashtag, RiHistoryLine, RiUserLine } from "svelte-remixicon";
|
||||
import type { PostEntry } from "./backend";
|
||||
import PostMeta from "./PostMeta.svelte";
|
||||
import { SvelteShowdown } from "svelte-showdown";
|
||||
|
||||
let { post }: { post: PostEntry } = $props();
|
||||
let { id, title, created_at, content } = post;
|
||||
</script>
|
||||
|
||||
<card class="post-frame">
|
||||
<h3 class="post-title">
|
||||
<a href="/={id}">{title}</a>
|
||||
</h3>
|
||||
<PostMeta {post} />
|
||||
<!-- TODO pist content -->
|
||||
<div class="post-content shorten">
|
||||
<SvelteShowdown content={ content || "" } />
|
||||
</div>
|
||||
</card>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
15
src/lib/FlashMessage.svelte
Normal file
15
src/lib/FlashMessage.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts"
|
||||
>
|
||||
let { message } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flash card">
|
||||
{message}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.flash {
|
||||
border-color: yellow;
|
||||
background-color: #fff00040;
|
||||
}
|
||||
</style>
|
||||
62
src/lib/FullPost.svelte
Normal file
62
src/lib/FullPost.svelte
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<script lang="ts">
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
import { RiHistoryLine, RiHome2Line, RiUserLine } from "svelte-remixicon";
|
||||
import type { PostEntry } from "./backend";
|
||||
import SLayout from "./SLayout.svelte";
|
||||
import GuildAbout from "./GuildAbout.svelte";
|
||||
import UserAbout from "./UserAbout.svelte";
|
||||
import UserMenu from "./UserMenu.svelte";
|
||||
import PostMeta from "./PostMeta.svelte";
|
||||
import GuildMenu from "./GuildMenu.svelte";
|
||||
import { SvelteShowdown } from "svelte-showdown";
|
||||
|
||||
|
||||
|
||||
let { post }: { post: PostEntry } = $props();
|
||||
let { title, created_at, id, content = '', to } = post;
|
||||
</script>
|
||||
|
||||
<SLayout title={to.display_name + (to.type === 'guild' ? ` (+${to.name})` : to.type === 'user' ? ` (@${to.username})` : '')}>
|
||||
<article class="card">
|
||||
<div class="post-frame" id={id}>
|
||||
<div class="post-body">
|
||||
<h1>{title}</h1>
|
||||
<PostMeta {post} />
|
||||
<!-- here go reports -->
|
||||
<!-- here goes removal message -->
|
||||
<div class="post-content">
|
||||
<SvelteShowdown content={ content || "" } />
|
||||
<!-- content, formatted as markdown -->
|
||||
</div>
|
||||
</div><!-- .post-body -->
|
||||
<div class="message-stats">
|
||||
<!-- upvotes / downvotes -->
|
||||
</div>
|
||||
<ul class="message-options row">
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
{#snippet left()}
|
||||
{#if to.type === 'guild'}
|
||||
<GuildMenu guild={to} />
|
||||
{:else if to.type == 'user'}
|
||||
<UserMenu user={to} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{#snippet right()}
|
||||
{#if to.type === 'guild'}
|
||||
<GuildAbout guild={to} />
|
||||
{:else if to.type === 'user'}
|
||||
<UserAbout user={to} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SLayout>
|
||||
|
||||
<style>
|
||||
.post-content {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
24
src/lib/GuildAbout.svelte
Normal file
24
src/lib/GuildAbout.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { RiCakeLine, RiInfoCardLine } from "svelte-remixicon";
|
||||
import AsideCard from "./AsideCard.svelte";
|
||||
import type { GuildEntry } from "./backend";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
let { guild }: { guild: GuildEntry } = $props();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
{#if guild}
|
||||
<AsideCard title={'About ' + (guild.display_name? `${guild.display_name} (+${guild.name})`: `+${guild.name}`)}>
|
||||
<ul>
|
||||
{#if guild.description}
|
||||
<li><RiInfoCardLine /> {guild.description}</li>
|
||||
{/if}
|
||||
{#if guild.created_at}
|
||||
<li><RiCakeLine /> Cake day: {DateTime.fromISO(guild.created_at).toFormat('d LLL yyyy')}</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</AsideCard>
|
||||
{/if}
|
||||
15
src/lib/GuildMenu.svelte
Normal file
15
src/lib/GuildMenu.svelte
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { RiHome2Line, RiUserLine } from "svelte-remixicon";
|
||||
import type { GuildEntry } from "./backend";
|
||||
|
||||
let { guild } : { guild: GuildEntry } = $props();
|
||||
</script>
|
||||
|
||||
|
||||
<ul class="column">
|
||||
<li>
|
||||
<a href="/+{guild.name}">
|
||||
<RiHome2Line />
|
||||
Home</a>
|
||||
</li>
|
||||
</ul>
|
||||
101
src/lib/MetaNav.svelte
Normal file
101
src/lib/MetaNav.svelte
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { RiLoginBoxLine, RiLogoutBoxLine, RiSearch2Line, RiSearchLine, RiSettings3Line } from "svelte-remixicon";
|
||||
import { activePostCount } from "./globals.svelte";
|
||||
import type { UserEntry } from "./backend";
|
||||
|
||||
|
||||
let { user } : {user: UserEntry} = $props();
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div class="metanav">
|
||||
<ul>
|
||||
<li>
|
||||
<form action="/search"
|
||||
method="POST"
|
||||
class="mini-search-bar nomobile">
|
||||
<!-- csrf_token() -->
|
||||
<input type="search" disabled={true} name="q" placeholder="Search among {activePostCount()} posts" />
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
<a href="/search" aria-label="Search" title="Search" class="mobileonly">
|
||||
<RiSearchLine />
|
||||
</a>
|
||||
</li>
|
||||
{#if user}
|
||||
<li>
|
||||
<div class="header-username nomobile">
|
||||
<span>@{user.username}</span>
|
||||
<span>{0} karma</span>
|
||||
</div>
|
||||
</li>
|
||||
<li><a href="/logout" aria-label="Log out" title="Log out"><RiLogoutBoxLine /></a></li>
|
||||
{:else}
|
||||
<li><a href="/login" aria-label="Log in" title="Log in"><RiLoginBoxLine /></a></li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.metanav {
|
||||
align-self: flex-end;
|
||||
font-size: 1.5em;
|
||||
margin: auto;
|
||||
margin-inline-start: 2em;
|
||||
}
|
||||
|
||||
.metanav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.metanav ul > li {
|
||||
margin: 0 6px;
|
||||
}
|
||||
.metanav ul,.metanav ul > li {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.metanav, .metanav > ul,.metanav > ul > li:has(.mini-search-bar) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.metanav .header-username {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text-primary);
|
||||
font-size: .6em;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.mini-search-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
font-size: 1.2rem;
|
||||
}.mini-search-bar [type="search"] {
|
||||
flex: 1;
|
||||
}
|
||||
.mini-search-bar [type="submit"]{
|
||||
height: 0;
|
||||
width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mini-search-bar + a {display: none}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
48
src/lib/MobileFooter.svelte
Normal file
48
src/lib/MobileFooter.svelte
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { RiAddCircleLine, RiChat2Line, RiCompassDiscoverLine, RiHome2Line, RiHomeLine, RiNotificationLine } from "svelte-remixicon";
|
||||
|
||||
function rickroll (){
|
||||
if (browser) {
|
||||
window.open( "https://youtu.be/dQw4w9WgXcQ" );
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<footer class="mobileonly">
|
||||
<ul class="row">
|
||||
<li><a href="/" title="Homepage"><RiHome2Line size="2em" /></a></li>
|
||||
<li><a href="/" title="Discover"><RiCompassDiscoverLine size="2em" /></a></li>
|
||||
<li><a href="/new" title="Create"><RiAddCircleLine size="2em" /></a></li>
|
||||
<li><a href="/user/yusur" title="Messages"><RiChat2Line size="2em" /></a></li>
|
||||
<li><a href="?" onclick={rickroll} title="Notifications"><RiNotificationLine size="2em" /></a></li>
|
||||
</ul>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
|
||||
footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: var(--background);
|
||||
box-shadow: 0 0 6px var(--border);
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
ul.row {
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
ul.row > li {
|
||||
flex: 1;
|
||||
padding: .5em;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
34
src/lib/PasswordInput.svelte
Normal file
34
src/lib/PasswordInput.svelte
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { RiEyeLine, RiEyeOffLine } from "svelte-remixicon";
|
||||
|
||||
let showPassword: boolean = $state(false);
|
||||
let { name } : { name: string } = $props();
|
||||
</script>
|
||||
|
||||
<div class="inset">
|
||||
<input type={showPassword? 'text': 'password'} {name} />
|
||||
<span>
|
||||
{#if showPassword}
|
||||
<button class="inline" onclick={() => { showPassword = false; }}><RiEyeOffLine /></button>
|
||||
{:else}
|
||||
<button class="inline" onclick={() => { showPassword = true; }}><RiEyeLine /></button>
|
||||
{/if}
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.inset {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.inset input {
|
||||
flex: 1;
|
||||
}
|
||||
.inset :has(button) {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
20
src/lib/PostMeta.svelte
Normal file
20
src/lib/PostMeta.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">
|
||||
import { RiHashtag, RiHistoryLine, RiUserLine } from "svelte-remixicon";
|
||||
import type { PostEntry } from "./backend";
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
let { post } : {post: PostEntry }= $props();
|
||||
</script>
|
||||
<ul class="post-meta row">
|
||||
<li><RiUserLine />
|
||||
{#if post.author}<a href="/@{post.author.username}">@{post.author.username}</a>
|
||||
{:else}<i>Someone</i>{/if}</li>
|
||||
{#if post.to.type == 'guild'}
|
||||
<li><RiHashtag /> <a href="/+{post.to.name}">+{post.to.name}</a></li>
|
||||
{/if}
|
||||
<li><RiHistoryLine /> <time datetime={post.created_at}>{ DateTime.fromISO(post.created_at).toFormat('d LLL yyyy') }</time></li>
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
ul.post-meta > li { margin-inline-end: 9px; }
|
||||
</style>
|
||||
76
src/lib/SLayout.svelte
Normal file
76
src/lib/SLayout.svelte
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<script lang="ts">
|
||||
import { RiInformationLine, RiMenu3Line, RiShieldLine } from "svelte-remixicon";
|
||||
|
||||
let { children, title, left, right } = $props();
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="layout">
|
||||
<div class="layout-title">{title}</div>
|
||||
<div class="layout-left">{@render left()}</div>
|
||||
<div class="layout-content">{@render children()}</div>
|
||||
<div class="layout-right">{@render right()}</div>
|
||||
|
||||
<div class="layout-licon" onclick={() => {}}><RiMenu3Line /></div>
|
||||
<div class="layout-ricon" onclick={() => {}}><RiInformationLine /></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 300px auto 300px;
|
||||
grid-template-rows: 2em auto;
|
||||
grid-template-areas:
|
||||
". title ."
|
||||
"left center right";
|
||||
margin: 1em 2em;
|
||||
}
|
||||
|
||||
.layout-left { grid-area: left; }
|
||||
.layout-right { grid-area: right; }
|
||||
.layout-content { grid-area: center; }
|
||||
.layout-title { grid-area: title; text-align: center; font-size: 1.4em; }
|
||||
|
||||
|
||||
@media (min-width: 800px) {
|
||||
.layout {
|
||||
grid-template-columns: 240px auto 240px;
|
||||
}
|
||||
|
||||
.layout-licon, .layout-ricon { display: none; }
|
||||
}
|
||||
|
||||
@media (min-width: 960px) {
|
||||
.layout {
|
||||
grid-template-columns: 270px auto 270px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1080px) {
|
||||
.layout {
|
||||
grid-template-columns: 300px auto 300px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.layout {
|
||||
grid-template-columns: 330px auto 330px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 799px) {
|
||||
.layout {
|
||||
grid-template-areas:
|
||||
"licon title ricon"
|
||||
"center center center";
|
||||
|
||||
grid-template-columns: 2em auto 2em;
|
||||
}
|
||||
|
||||
.layout-left, .layout-right { display: none; }
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
38
src/lib/UserAbout.svelte
Normal file
38
src/lib/UserAbout.svelte
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import AsideCard from "./AsideCard.svelte";
|
||||
import { RiCake2Line, RiCakeLine, RiInfoCardLine, RiShieldStarLine } from 'svelte-remixicon';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { UserEntry } from "./backend";
|
||||
|
||||
|
||||
let { user }: { user: UserEntry } = $props();
|
||||
|
||||
let { badges = [] } = user;
|
||||
|
||||
</script>
|
||||
|
||||
{#if user}
|
||||
<AsideCard title={'About ' + (user.display_name? `${user.display_name} (@${user.username})` : `@${user.username}`)}>
|
||||
<ul>
|
||||
{#if user.biography}
|
||||
<li><RiInfoCardLine /> {user.biography}</li>
|
||||
{/if}
|
||||
<li><RiCake2Line /> {user.age} years old
|
||||
{#if user.age < 18}<span class="error">(MINOR)</span>{/if}
|
||||
</li>
|
||||
{#if user.joined_at}
|
||||
<li><RiCakeLine /> Cake day: {DateTime.fromISO(user.joined_at).toFormat('d LLL yyyy')}</li>
|
||||
{/if}
|
||||
{#if badges.indexOf('administrator') >= 0}
|
||||
<li><RiShieldStarLine /> Administrator</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</AsideCard>
|
||||
|
||||
<button class="card">Follow</button>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
11
src/lib/UserMenu.svelte
Normal file
11
src/lib/UserMenu.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { RiUserLine } from "svelte-remixicon";
|
||||
import type { UserEntry } from "./backend";
|
||||
|
||||
let { user } : { user: UserEntry } = $props();
|
||||
</script>
|
||||
|
||||
|
||||
<ul class="column">
|
||||
<li><RiUserLine /> <a href="/@{user.username}">Profile</a></li>
|
||||
</ul>
|
||||
104
src/lib/backend.ts
Normal file
104
src/lib/backend.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
|
||||
|
||||
export type UserEntry = {
|
||||
id: string,
|
||||
username: string,
|
||||
display_name?: string,
|
||||
biography?: string,
|
||||
joined_at?: string,
|
||||
age: number, // actually not just that
|
||||
badges?: string[]
|
||||
type?: 'user',
|
||||
karma?: number
|
||||
};
|
||||
|
||||
export type GuildEntry = {
|
||||
type?: 'guild',
|
||||
id: string,
|
||||
name: string,
|
||||
display_name?: string,
|
||||
description?: string,
|
||||
created_at?: string
|
||||
};
|
||||
|
||||
export type PostEntry = {
|
||||
id: string,
|
||||
url: string,
|
||||
title: string,
|
||||
created_at: string,
|
||||
author?: UserEntry | null,
|
||||
content?: string | null,
|
||||
to: UserEntry | GuildEntry
|
||||
};
|
||||
|
||||
export type ServerHealth = {
|
||||
version: string,
|
||||
name: string,
|
||||
post_count: number,
|
||||
user_count: number,
|
||||
me: string | null,
|
||||
csrf_token?: string
|
||||
};
|
||||
|
||||
|
||||
export class Backend {
|
||||
static ENDPOINT_BASE = '/v1';
|
||||
|
||||
constructor () {
|
||||
}
|
||||
|
||||
async fetch(url: string, options?: RequestInit) {
|
||||
console.info(`fetch ${Backend.ENDPOINT_BASE}/${encodeURIComponent(url)}`)
|
||||
return await fetch(`${Backend.ENDPOINT_BASE}/${encodeURIComponent(url)}`, options);
|
||||
}
|
||||
|
||||
withEvent(event: any) {
|
||||
return new EventBackend(event);
|
||||
}
|
||||
}
|
||||
|
||||
class EventBackend extends Backend {
|
||||
event: any;
|
||||
|
||||
constructor (event: any) {
|
||||
super();
|
||||
this.event = event;
|
||||
}
|
||||
|
||||
async oath (): Promise<CsrfEventBackend> {
|
||||
const resp = await this.fetch("oath");
|
||||
if (resp.status !== 200) {
|
||||
throw new Error();
|
||||
}
|
||||
const respJ = await resp.json();
|
||||
const { csrfToken } = respJ;
|
||||
return new CsrfEventBackend(this.event, csrfToken);
|
||||
}
|
||||
|
||||
async fetch(url: string, options?: RequestInit): Promise<Response> {
|
||||
return await this.event.fetch(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options);
|
||||
}
|
||||
}
|
||||
|
||||
class CsrfEventBackend extends EventBackend {
|
||||
csrfToken: string;
|
||||
|
||||
constructor(event:EventBackend, csrfToken: string) {
|
||||
super(event);
|
||||
this.csrfToken = csrfToken;
|
||||
}
|
||||
|
||||
async submitJson(url: string, data: object) {
|
||||
return await this.fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'x-csrftoken': this.csrfToken
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const backend = new Backend();
|
||||
|
||||
49
src/lib/globals.svelte.ts
Normal file
49
src/lib/globals.svelte.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { ServerHealth, UserEntry } from "$lib/backend";
|
||||
|
||||
|
||||
let health : {
|
||||
app_name: string, version: string, post_count: number,
|
||||
user_count: number, me: null | UserEntry
|
||||
} = $state({
|
||||
app_name: 'app_name',
|
||||
version: "?.?",
|
||||
post_count: NaN,
|
||||
user_count: NaN,
|
||||
me: null
|
||||
});
|
||||
|
||||
|
||||
export function setHealth ({ name, version, post_count, user_count }: ServerHealth) {
|
||||
health.app_name = name;
|
||||
health.version = version;
|
||||
health.post_count = post_count;
|
||||
health.user_count = user_count;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function setMe (me: UserEntry | null) {
|
||||
health.me = me;
|
||||
}
|
||||
|
||||
export function getMe () {
|
||||
return health.me;
|
||||
}
|
||||
|
||||
export function appName(): string {
|
||||
return health.app_name;
|
||||
}
|
||||
|
||||
export function version () {
|
||||
return health.version;
|
||||
}
|
||||
|
||||
export function activePostCount (): number{
|
||||
return health.post_count;
|
||||
}
|
||||
|
||||
|
||||
export function activeUserCount (): number{
|
||||
return health.user_count || 0;
|
||||
}
|
||||
|
||||
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
// place files you want to import through the `$lib` alias in this folder.
|
||||
1
src/lib/snowflake.ts
Normal file
1
src/lib/snowflake.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
Loading…
Add table
Add a link
Reference in a new issue