Compare commits
13 commits
155aa524f3
...
54cafaa1da
| Author | SHA1 | Date | |
|---|---|---|---|
| 54cafaa1da | |||
| 58d6a248c7 | |||
| 4f816c9066 | |||
| 792cce7973 | |||
| f6cb4fed12 | |||
| c0b9ad57db | |||
| 1dc5768640 | |||
| ddb07a651e | |||
| c33ab48225 | |||
| cd92a4b12e | |||
| 55455c1693 | |||
| a13c7cbc22 | |||
| 244f7e40ce |
37 changed files with 775 additions and 102 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "@yusurko/vigil",
|
"name": "@yusurko/vigil",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0-dev36",
|
"version": "0.1.0-dev42",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
|
|
|
||||||
15
src/app.d.ts
vendored
15
src/app.d.ts
vendored
|
|
@ -1,10 +1,21 @@
|
||||||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||||
|
|
||||||
|
|
||||||
// for information about these interfaces
|
// for information about these interfaces
|
||||||
declare global {
|
declare global {
|
||||||
namespace App {
|
namespace App {
|
||||||
// interface Error {}
|
// interface Error {}
|
||||||
// interface Locals {}
|
interface Locals {
|
||||||
// interface PageData {}
|
results?: object[],
|
||||||
|
query?: string,
|
||||||
|
me: UserEntry | null
|
||||||
|
}
|
||||||
|
interface PageData {
|
||||||
|
site: ServerHealth | null,
|
||||||
|
me: UserEntry | null,
|
||||||
|
results?: object[],
|
||||||
|
query?: string
|
||||||
|
}
|
||||||
// interface PageState {}
|
// interface PageState {}
|
||||||
// interface Platform {}
|
// interface Platform {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/app.html
24
src/app.html
|
|
@ -187,6 +187,26 @@ ul.column {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.grid {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.grid > li {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: .5em;
|
||||||
|
padding: .5em;
|
||||||
|
margin: 1em .5em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.grid > li small {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
a svg {
|
a svg {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +254,10 @@ button.card {
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
article h1, article h2 {
|
article h1, article h2 {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
9
src/hooks.server.ts
Normal file
9
src/hooks.server.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { getMe } from "$lib/globals.svelte";
|
||||||
|
import type { Handle } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
|
||||||
|
export const handle: Handle = async ({event, resolve }) => {
|
||||||
|
event.locals.me = getMe();
|
||||||
|
|
||||||
|
return resolve(event);
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ let { children, title = null } = $props();
|
||||||
position: relative;
|
position: relative;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
|
||||||
a {
|
:global(a) {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
src/lib/BigSearchInput.svelte
Normal file
13
src/lib/BigSearchInput.svelte
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
let { query = $bindable<string>() }: {query?: string} = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input value={query} type="search" name="query" class="bigsearch" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bigsearch {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
import { RiHistoryLine, RiHome2Line, RiUserLine } from "svelte-remixicon";
|
import { RiEditLine, RiFlagLine, RiHistoryLine, RiHome2Line, RiUserLine } from "svelte-remixicon";
|
||||||
import type { PostEntry } from "./backend";
|
import type { PostEntry } from "./backend";
|
||||||
import SLayout from "./SLayout.svelte";
|
import SLayout from "./SLayout.svelte";
|
||||||
import GuildAbout from "./GuildAbout.svelte";
|
import GuildAbout from "./GuildAbout.svelte";
|
||||||
|
|
@ -10,10 +10,14 @@
|
||||||
import PostMeta from "./PostMeta.svelte";
|
import PostMeta from "./PostMeta.svelte";
|
||||||
import GuildMenu from "./GuildMenu.svelte";
|
import GuildMenu from "./GuildMenu.svelte";
|
||||||
import { SvelteShowdown } from "svelte-showdown";
|
import { SvelteShowdown } from "svelte-showdown";
|
||||||
|
import VoteButton from "./VoteButton.svelte";
|
||||||
|
import { getMe } from "./globals.svelte";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let { post }: { post: PostEntry } = $props();
|
let { post }: { post: PostEntry } = $props();
|
||||||
|
|
||||||
|
let me = getMe();
|
||||||
let { title, created_at, id, content = '', to } = post;
|
let { title, created_at, id, content = '', to } = post;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -32,8 +36,15 @@ let { title, created_at, id, content = '', to } = post;
|
||||||
</div><!-- .post-body -->
|
</div><!-- .post-body -->
|
||||||
<div class="message-stats">
|
<div class="message-stats">
|
||||||
<!-- upvotes / downvotes -->
|
<!-- upvotes / downvotes -->
|
||||||
|
<VoteButton />
|
||||||
</div>
|
</div>
|
||||||
<ul class="message-options row">
|
<ul class="message-options row">
|
||||||
|
{#if me && me.id !== post.author?.id}
|
||||||
|
<li><a href="/report/post/{id}"><RiFlagLine/> Report</a></li>
|
||||||
|
{/if}
|
||||||
|
{#if me && me.id === post.author?.id }
|
||||||
|
<li><a href="/edit/post/{id}"><RiEditLine/> Edit</a></li>
|
||||||
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
@ -56,7 +67,23 @@ let { title, created_at, id, content = '', to } = post;
|
||||||
</SLayout>
|
</SLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.post-frame {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.post-content {
|
.post-content {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
.post-body {
|
||||||
|
margin-inline-start: 3em;
|
||||||
|
}
|
||||||
|
.message-stats {
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-start: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 3em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { RiHome2Line, RiUserLine } from "svelte-remixicon";
|
import { RiHome2Line, RiUserLine } from "svelte-remixicon";
|
||||||
import type { GuildEntry } from "./backend";
|
import type { GuildEntry } from "./backend";
|
||||||
|
import MenuLink from "./MenuLink.svelte";
|
||||||
|
|
||||||
let { guild } : { guild: GuildEntry } = $props();
|
let { guild } : { guild: GuildEntry } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<ul class="column">
|
<ul class="column">
|
||||||
<li>
|
<MenuLink href="/+{guild.name}" icon={RiHome2Line} label="Home" />
|
||||||
<a href="/+{guild.name}">
|
|
||||||
<RiHome2Line />
|
|
||||||
Home</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
71
src/lib/GuildSelect.svelte
Normal file
71
src/lib/GuildSelect.svelte
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { backend, type GuildEntry } from "./backend";
|
||||||
|
|
||||||
|
|
||||||
|
let value = $state("");
|
||||||
|
|
||||||
|
let suggestions: Promise<GuildEntry[]> = $derived(getSuggestions(value));
|
||||||
|
|
||||||
|
async function getSuggestions (query: string): Promise<GuildEntry[]> {
|
||||||
|
if (!query.trim()) return [];
|
||||||
|
let readyBackend = await backend.withEvent(null).oath();
|
||||||
|
let result = await readyBackend.submitJson("suggest/guild", { query });
|
||||||
|
if (result.status !== 200) return [];
|
||||||
|
let resultJ = await result.json();
|
||||||
|
return resultJ?.has || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="inset-suggestion">
|
||||||
|
<input type="text" name="to" bind:value placeholder="your user page or +guild name" autocomplete="off" />
|
||||||
|
<ul class="column">
|
||||||
|
{#await suggestions}
|
||||||
|
<!-- loading... -->
|
||||||
|
{:then suggs}
|
||||||
|
{#each suggs as sug}
|
||||||
|
<li><button class="inline" onclick={(ev)=>{
|
||||||
|
ev.preventDefault();
|
||||||
|
value = sug.name;
|
||||||
|
}}>
|
||||||
|
<faint>+</faint>{sug.name}
|
||||||
|
</button></li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<faint>+{value}</faint>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
{:catch ex}
|
||||||
|
<li>
|
||||||
|
<faint>+{value} ({ex})</faint>
|
||||||
|
</li>
|
||||||
|
{/await}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.inset-suggestion {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
position: absolute;
|
||||||
|
top: 2em;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background-color: var(--background);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul > li {
|
||||||
|
padding: 6px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
:not(:focus) + ul:not(:active) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
10
src/lib/HomeMenu.svelte
Normal file
10
src/lib/HomeMenu.svelte
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { RiHome2Line, RiSettings2Line } from 'svelte-remixicon';
|
||||||
|
import MenuLink from './MenuLink.svelte';
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="column">
|
||||||
|
<MenuLink href="/" icon={RiHome2Line} label="Home" />
|
||||||
|
<MenuLink href="/settings" icon={RiSettings2Line} label="Settings" />
|
||||||
|
</ul>
|
||||||
33
src/lib/MenuLink.svelte
Normal file
33
src/lib/MenuLink.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
let { icon, label, href } = $props();
|
||||||
|
let Icon = $derived(icon);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<a {href}>
|
||||||
|
<Icon />
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
li {
|
||||||
|
min-height: 2em;
|
||||||
|
padding: .5em;
|
||||||
|
border-radius: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
li:has(a:hover) {
|
||||||
|
background-color: var(--background);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
|
|
||||||
let { user } : {user: UserEntry} = $props();
|
let { user } : {user: UserEntry} = $props();
|
||||||
|
let enable_search = $derived(user !== null);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -14,22 +14,26 @@ let { user } : {user: UserEntry} = $props();
|
||||||
<div class="metanav">
|
<div class="metanav">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
|
{#if enable_search}
|
||||||
<form action="/search"
|
<form action="/search"
|
||||||
method="POST"
|
method="POST"
|
||||||
class="mini-search-bar nomobile">
|
class="mini-search-bar nomobile">
|
||||||
<!-- csrf_token() -->
|
<!-- csrf_token() -->
|
||||||
<input type="search" disabled={true} name="q" placeholder="Search among {activePostCount()} posts" />
|
<input type="search" name="q" placeholder="Search among {activePostCount()} posts" />
|
||||||
<button type="submit">Search</button>
|
<button type="submit">Search</button>
|
||||||
</form>
|
</form>
|
||||||
<a href="/search" aria-label="Search" title="Search" class="mobileonly">
|
<a href="/search" aria-label="Search" title="Search" class="mobileonly">
|
||||||
<RiSearchLine />
|
<RiSearchLine />
|
||||||
</a>
|
</a>
|
||||||
|
{:else}
|
||||||
|
<div style="flex:1"></div>
|
||||||
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{#if user}
|
{#if user}
|
||||||
<li>
|
<li>
|
||||||
<div class="header-username nomobile">
|
<div class="header-username nomobile">
|
||||||
<span>@{user.username}</span>
|
<span><a href="/@{user.username}">@{user.username}</a></span>
|
||||||
<span>{0} karma</span>
|
<span>{user.karma || 0} karma</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li><a href="/logout" aria-label="Log out" title="Log out"><RiLogoutBoxLine /></a></li>
|
<li><a href="/logout" aria-label="Log out" title="Log out"><RiLogoutBoxLine /></a></li>
|
||||||
|
|
@ -94,7 +98,7 @@ let { user } : {user: UserEntry} = $props();
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mini-search-bar + a {display: none}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { RiHashtag, RiHistoryLine, RiUserLine } from "svelte-remixicon";
|
import { RiHistoryLine, RiShieldCrossLine, RiUserLine } from "svelte-remixicon";
|
||||||
import type { PostEntry } from "./backend";
|
import type { PostEntry } from "./backend";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ let { post } : {post: PostEntry }= $props();
|
||||||
{#if post.author}<a href="/@{post.author.username}">@{post.author.username}</a>
|
{#if post.author}<a href="/@{post.author.username}">@{post.author.username}</a>
|
||||||
{:else}<i>Someone</i>{/if}</li>
|
{:else}<i>Someone</i>{/if}</li>
|
||||||
{#if post.to.type == 'guild'}
|
{#if post.to.type == 'guild'}
|
||||||
<li><RiHashtag /> <a href="/+{post.to.name}">+{post.to.name}</a></li>
|
<li><RiShieldCrossLine /> <a href="/+{post.to.name}">+{post.to.name}</a></li>
|
||||||
{/if}
|
{/if}
|
||||||
<li><RiHistoryLine /> <time datetime={post.created_at}>{ DateTime.fromISO(post.created_at).toFormat('d LLL yyyy') }</time></li>
|
<li><RiHistoryLine /> <time datetime={post.created_at}>{ DateTime.fromISO(post.created_at).toFormat('d LLL yyyy') }</time></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
59
src/lib/PrivacySelect.svelte
Normal file
59
src/lib/PrivacySelect.svelte
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { RiLink, RiLock2Line, RiPlaneLine, RiStackLine } from "svelte-remixicon";
|
||||||
|
|
||||||
|
let { value = $bindable(0) } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ul class="grid">
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="privacy" value={0} bind:group={value} />
|
||||||
|
<RiPlaneLine />
|
||||||
|
Public
|
||||||
|
<small class="faint">your followers and public timeline</small>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="privacy" value=1 bind:group={value} />
|
||||||
|
<RiLink />
|
||||||
|
Unlisted
|
||||||
|
<small class="faint">your followers, hide from public timeline</small>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="privacy" value=2 bind:group={value} />
|
||||||
|
<RiStackLine />
|
||||||
|
Friends
|
||||||
|
<small class="faint">mutual followers only</small>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="privacy" value=3 bind:group={value} />
|
||||||
|
<RiLock2Line />
|
||||||
|
Only you
|
||||||
|
<small class="faint">nobody else</small>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
@media (max-width: 1099px) {
|
||||||
|
ul.grid {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 699px) {
|
||||||
|
ul.grid {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li:has(:checked) {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -2,18 +2,21 @@
|
||||||
import { RiInformationLine, RiMenu3Line, RiShieldLine } from "svelte-remixicon";
|
import { RiInformationLine, RiMenu3Line, RiShieldLine } from "svelte-remixicon";
|
||||||
|
|
||||||
let { children, title, left, right } = $props();
|
let { children, title, left, right } = $props();
|
||||||
|
|
||||||
|
let mobiLeftActive = $state(false);
|
||||||
|
let mobiRightActive = $state(false);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<div class="layout-title">{title}</div>
|
<div class="layout-title">{title}</div>
|
||||||
<div class="layout-left">{@render left()}</div>
|
<div class="layout-left" class:active={mobiLeftActive}>{@render left()}</div>
|
||||||
<div class="layout-content">{@render children()}</div>
|
<div class="layout-content">{@render children()}</div>
|
||||||
<div class="layout-right">{@render right()}</div>
|
<div class="layout-right" class:active={mobiRightActive}>{@render right()}</div>
|
||||||
|
|
||||||
<div class="layout-licon" onclick={() => {}}><RiMenu3Line /></div>
|
<div class="layout-licon" onclick={() => {mobiLeftActive = !mobiLeftActive;}}><RiMenu3Line /></div>
|
||||||
<div class="layout-ricon" onclick={() => {}}><RiInformationLine /></div>
|
<div class="layout-ricon" onclick={() => {mobiRightActive = !mobiRightActive;}}><RiInformationLine /></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -26,15 +29,31 @@ let { children, title, left, right } = $props();
|
||||||
". title ."
|
". title ."
|
||||||
"left center right";
|
"left center right";
|
||||||
margin: 1em 2em;
|
margin: 1em 2em;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-left { grid-area: left; }
|
.layout-left { grid-area: left; padding-right: 1em; }
|
||||||
.layout-right { grid-area: right; }
|
.layout-right { grid-area: right; padding-left: 1em; }
|
||||||
.layout-content { grid-area: center; }
|
.layout-content { grid-area: center; }
|
||||||
.layout-title { grid-area: title; text-align: center; font-size: 1.4em; }
|
.layout-title { grid-area: title; text-align: center; font-size: 1.4em; }
|
||||||
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
|
.layout {
|
||||||
|
grid-template-columns: 180px auto 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-licon, .layout-ricon { display: none; }
|
||||||
|
|
||||||
|
}
|
||||||
|
@media (min-width: 800px) and (max-width: 959px) {
|
||||||
|
.layout-left, .layout-right {
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (min-width: 960px) {
|
||||||
.layout {
|
.layout {
|
||||||
grid-template-columns: 240px auto 240px;
|
grid-template-columns: 240px auto 240px;
|
||||||
}
|
}
|
||||||
|
|
@ -42,13 +61,13 @@ let { children, title, left, right } = $props();
|
||||||
.layout-licon, .layout-ricon { display: none; }
|
.layout-licon, .layout-ricon { display: none; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 960px) {
|
@media (min-width: 1080px) {
|
||||||
.layout {
|
.layout {
|
||||||
grid-template-columns: 270px auto 270px;
|
grid-template-columns: 270px auto 270px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1080px) {
|
@media (min-width: 1200px) {
|
||||||
.layout {
|
.layout {
|
||||||
grid-template-columns: 300px auto 300px;
|
grid-template-columns: 300px auto 300px;
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +89,24 @@ let { children, title, left, right } = $props();
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-left, .layout-right { display: none; }
|
.layout-left, .layout-right { display: none; }
|
||||||
|
|
||||||
|
.layout-left.active, .layout-right.active {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 2.5em;
|
||||||
|
width: 80%;
|
||||||
|
min-height: calc(100vh - 6em);
|
||||||
|
background-color: var(--canvas);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-left.active {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.layout-right.active {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from "$app/environment";
|
import { getMe } from "$lib/globals.svelte";
|
||||||
import AsideCard from "./AsideCard.svelte";
|
import AsideCard from "./AsideCard.svelte";
|
||||||
import { RiCake2Line, RiCakeLine, RiInfoCardLine, RiShieldStarLine } from 'svelte-remixicon';
|
import { RiCake2Line, RiCakeLine, RiFlowerLine, RiInfoCardLine, RiShieldStarLine } from 'svelte-remixicon';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import type { UserEntry } from "./backend";
|
import type { UserEntry } from "./backend";
|
||||||
|
|
||||||
|
|
@ -24,6 +24,9 @@ let { badges = [] } = user;
|
||||||
{#if user.joined_at}
|
{#if user.joined_at}
|
||||||
<li><RiCakeLine /> Cake day: {DateTime.fromISO(user.joined_at).toFormat('d LLL yyyy')}</li>
|
<li><RiCakeLine /> Cake day: {DateTime.fromISO(user.joined_at).toFormat('d LLL yyyy')}</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if user.id === getMe()?.id && user.karma}
|
||||||
|
<li><RiFlowerLine /> {user.karma} karma</li>
|
||||||
|
{/if}
|
||||||
{#if badges.indexOf('administrator') >= 0}
|
{#if badges.indexOf('administrator') >= 0}
|
||||||
<li><RiShieldStarLine /> Administrator</li>
|
<li><RiShieldStarLine /> Administrator</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { RiUserLine } from "svelte-remixicon";
|
import { RiUserLine } from "svelte-remixicon";
|
||||||
import type { UserEntry } from "./backend";
|
import type { UserEntry } from "./backend";
|
||||||
|
import MenuLink from "./MenuLink.svelte";
|
||||||
|
|
||||||
let { user } : { user: UserEntry } = $props();
|
let { user } : { user: UserEntry } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<ul class="column">
|
<ul class="column">
|
||||||
<li><RiUserLine /> <a href="/@{user.username}">Profile</a></li>
|
<MenuLink icon={RiUserLine} href="/@{user.username}" label="Profile" />
|
||||||
</ul>
|
</ul>
|
||||||
41
src/lib/VoteButton.svelte
Normal file
41
src/lib/VoteButton.svelte
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { RiHeartFill, RiHeartLine, RiThumbDownFill, RiThumbDownLine } from "svelte-remixicon";
|
||||||
|
|
||||||
|
|
||||||
|
let vote = $state(0);
|
||||||
|
let { score } : { score?: number | null } = $props();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="upvote-button">
|
||||||
|
{#if vote > 0}
|
||||||
|
<button class="inline">
|
||||||
|
<RiHeartFill />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button class="inline">
|
||||||
|
<RiHeartLine />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<strong>{score ?? '-'}</strong>
|
||||||
|
|
||||||
|
{#if vote > 0}
|
||||||
|
<button class="inline">
|
||||||
|
<RiThumbDownFill />
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button class="inline">
|
||||||
|
<RiThumbDownLine />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.upvote-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
@ -18,7 +18,9 @@ export type GuildEntry = {
|
||||||
name: string,
|
name: string,
|
||||||
display_name?: string,
|
display_name?: string,
|
||||||
description?: string,
|
description?: string,
|
||||||
created_at?: string
|
created_at?: string,
|
||||||
|
subscriber_count?: number,
|
||||||
|
post_count?: number
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PostEntry = {
|
export type PostEntry = {
|
||||||
|
|
@ -37,10 +39,37 @@ export type ServerHealth = {
|
||||||
post_count: number,
|
post_count: number,
|
||||||
user_count: number,
|
user_count: number,
|
||||||
me: string | null,
|
me: string | null,
|
||||||
csrf_token?: string
|
csrf_token?: string,
|
||||||
|
color_theme?: number
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// caches an oath (CSRF token)
|
||||||
|
class OathKeeper {
|
||||||
|
lastOath : number;
|
||||||
|
csrfToken: string | null;
|
||||||
|
|
||||||
|
constructor () {
|
||||||
|
this.lastOath = 0;
|
||||||
|
this.csrfToken = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set (tok: string) {
|
||||||
|
this.lastOath = +new Date;
|
||||||
|
this.csrfToken = tok;
|
||||||
|
}
|
||||||
|
|
||||||
|
get () {
|
||||||
|
if (this.lastOath === 0 || +new Date() - this.lastOath > 1800000) {
|
||||||
|
// 30 minutes decay
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.csrfToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const oathKeeper = new OathKeeper();
|
||||||
|
|
||||||
export class Backend {
|
export class Backend {
|
||||||
static ENDPOINT_BASE = '/v1';
|
static ENDPOINT_BASE = '/v1';
|
||||||
|
|
||||||
|
|
@ -48,8 +77,7 @@ export class Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(url: string, options?: RequestInit) {
|
async fetch(url: string, options?: RequestInit) {
|
||||||
console.info(`fetch ${Backend.ENDPOINT_BASE}/${encodeURIComponent(url)}`)
|
return await fetch(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options);
|
||||||
return await fetch(`${Backend.ENDPOINT_BASE}/${encodeURIComponent(url)}`, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
withEvent(event: any) {
|
withEvent(event: any) {
|
||||||
|
|
@ -58,32 +86,42 @@ export class Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
class EventBackend extends Backend {
|
class EventBackend extends Backend {
|
||||||
event: any;
|
event: any | null;
|
||||||
|
|
||||||
constructor (event: any) {
|
constructor (event: any | null) {
|
||||||
super();
|
super();
|
||||||
this.event = event;
|
this.event = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
async oath (): Promise<CsrfEventBackend> {
|
async oath (): Promise<CsrfEventBackend> {
|
||||||
const resp = await this.fetch("oath");
|
let csrfToken = oathKeeper.get();
|
||||||
if (resp.status !== 200) {
|
if (!csrfToken) {
|
||||||
throw new Error();
|
const resp = await this.fetch("oath");
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
const respJ = await resp.json();
|
||||||
|
csrfToken = respJ.csrf_token;
|
||||||
|
}
|
||||||
|
if (!csrfToken) {
|
||||||
|
console.warn("csrf_token is null");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
oathKeeper.set(csrfToken);
|
||||||
}
|
}
|
||||||
const respJ = await resp.json();
|
|
||||||
const { csrfToken } = respJ;
|
|
||||||
return new CsrfEventBackend(this.event, csrfToken);
|
return new CsrfEventBackend(this.event, csrfToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetch(url: string, options?: RequestInit): Promise<Response> {
|
async fetch(url: string, options?: RequestInit): Promise<Response> {
|
||||||
return await this.event.fetch(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options);
|
console.debug(`fetch ${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`);
|
||||||
|
return await (this.event?.fetch||fetch)(`${Backend.ENDPOINT_BASE}/${url.replace(/^\/+/, '')}`, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CsrfEventBackend extends EventBackend {
|
class CsrfEventBackend extends EventBackend {
|
||||||
csrfToken: string;
|
csrfToken: string | null;
|
||||||
|
|
||||||
constructor(event:EventBackend, csrfToken: string) {
|
constructor(event: EventBackend | null, csrfToken: string | null) {
|
||||||
super(event);
|
super(event);
|
||||||
this.csrfToken = csrfToken;
|
this.csrfToken = csrfToken;
|
||||||
}
|
}
|
||||||
|
|
@ -94,7 +132,7 @@ class CsrfEventBackend extends EventBackend {
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/json',
|
'content-type': 'application/json',
|
||||||
'x-csrftoken': this.csrfToken
|
'x-csrftoken': this.csrfToken || ""
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,21 +3,23 @@ import type { ServerHealth, UserEntry } from "$lib/backend";
|
||||||
|
|
||||||
let health : {
|
let health : {
|
||||||
app_name: string, version: string, post_count: number,
|
app_name: string, version: string, post_count: number,
|
||||||
user_count: number, me: null | UserEntry
|
user_count: number, me: null | UserEntry, color_theme: number
|
||||||
} = $state({
|
} = $state({
|
||||||
app_name: 'app_name',
|
app_name: 'app_name',
|
||||||
version: "?.?",
|
version: "?.?",
|
||||||
post_count: NaN,
|
post_count: NaN,
|
||||||
user_count: NaN,
|
user_count: NaN,
|
||||||
me: null
|
me: null,
|
||||||
|
color_theme: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
export function setHealth ({ name, version, post_count, user_count }: ServerHealth) {
|
export function setHealth ({ name, version, post_count, user_count, color_theme = 0 }: ServerHealth) {
|
||||||
health.app_name = name;
|
health.app_name = name;
|
||||||
health.version = version;
|
health.version = version;
|
||||||
health.post_count = post_count;
|
health.post_count = post_count;
|
||||||
health.user_count = user_count;
|
health.user_count = user_count;
|
||||||
|
health.color_theme = color_theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -47,3 +49,10 @@ export function activeUserCount (): number{
|
||||||
return health.user_count || 0;
|
return health.user_count || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function sleep (ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColorThemeCode (): number {
|
||||||
|
return health.color_theme;
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { getColorThemeCode } from '$lib/globals.svelte';
|
||||||
import { version } from '$app/environment';
|
import { version } from '$app/environment';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
import type { ServerHealth, UserEntry } from '$lib/backend';
|
import type { ServerHealth, UserEntry } from '$lib/backend';
|
||||||
|
|
@ -17,6 +18,11 @@ let { data, children } : {
|
||||||
let {me} = $derived(data);
|
let {me} = $derived(data);
|
||||||
|
|
||||||
const flash = getFlash(page);
|
const flash = getFlash(page);
|
||||||
|
|
||||||
|
let colorThemeCode = $derived(getColorThemeCode());
|
||||||
|
let colorSchemeCode = $derived(Math.floor(colorThemeCode / 256));
|
||||||
|
let colorScheme = $derived(colorSchemeCode == 1? 'dark': colorSchemeCode == 2? 'light': 'system');
|
||||||
|
let colorThemeCls = $derived(`color-scheme-${colorScheme} color-theme-${colorThemeCode % 256}`);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|
@ -30,23 +36,26 @@ const flash = getFlash(page);
|
||||||
<!-- end SEO tags -->
|
<!-- end SEO tags -->
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<header>
|
<global-wrapper class={colorThemeCls}>
|
||||||
<h1>
|
<header>
|
||||||
<a href="/">{appName()}</a>
|
<h1>
|
||||||
</h1>
|
<a href="/">{appName()}</a>
|
||||||
<!-- .metanav -->
|
</h1>
|
||||||
<MetaNav user={me} />
|
<!-- .metanav -->
|
||||||
</header>
|
<MetaNav user={me} />
|
||||||
|
</header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
{#if $flash}
|
{#if $flash}
|
||||||
<FlashMessage message={$flash?.message}/>
|
<FlashMessage message={$flash?.message}/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<MobileFooter />
|
||||||
|
</global-wrapper>
|
||||||
|
|
||||||
<MobileFooter />
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
header {
|
header {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ async function loadMe(event: LoadEvent, me?: string): Promise<UserEntry | null>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function load(event): Promise<{site: ServerHealth | null, me: UserEntry | null} > {
|
export async function load(event) {
|
||||||
let site = await loadSite(event);
|
let site = await loadSite(event);
|
||||||
let me = await loadMe (event, site?.me || void 0);
|
let me = await loadMe (event, site?.me || void 0);
|
||||||
return { site, me };
|
return { site, me };
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,55 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PostEntry } from "$lib/backend";
|
import AsideCard from "$lib/AsideCard.svelte";
|
||||||
import Centered from "$lib/Centered.svelte";
|
import type { GuildEntry, PostEntry } from "$lib/backend";
|
||||||
import Feed from "$lib/Feed.svelte";
|
import Centered from "$lib/Centered.svelte";
|
||||||
import { activePostCount, activeUserCount, appName, getMe } from "$lib/globals.svelte";
|
import Feed from "$lib/Feed.svelte";
|
||||||
import SLayout from "$lib/SLayout.svelte";
|
import { activePostCount, activeUserCount, appName, getMe } from "$lib/globals.svelte";
|
||||||
|
import HomeMenu from "$lib/HomeMenu.svelte";
|
||||||
|
import SLayout from "$lib/SLayout.svelte";
|
||||||
|
|
||||||
let me = getMe();
|
let me = getMe();
|
||||||
let { data } : { data: { feed: PostEntry[] } } = $props();
|
let { data } : { data: { feed: PostEntry[], top_guilds: GuildEntry[] } } = $props();
|
||||||
let feed: PostEntry[] = $state([]);
|
let { feed } : { feed: PostEntry[] } = $derived(data);
|
||||||
let feedIndex = $state(0);
|
let { top_guilds }: { top_guilds: GuildEntry[] } = $derived(data);
|
||||||
|
|
||||||
$effect(() => {
|
// $effect(() => {
|
||||||
if (me && feed) {
|
// if (me && data?.feed) {
|
||||||
feed.push(...data.feed.slice(feedIndex));
|
// feed.push(...data.feed.slice(feedIndex));
|
||||||
feedIndex += feed.length;
|
// feedIndex += feed.length;
|
||||||
} else if (me) {
|
// } else if (me) {
|
||||||
console.log('feed is', feed)
|
// console.log('data.feed is', data?.feed)
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if me}
|
{#if me}
|
||||||
<SLayout title={appName()}>
|
<SLayout title={appName()}>
|
||||||
<Feed posts={feed} />
|
<Feed posts={feed} />
|
||||||
{#snippet left()}
|
|
||||||
...
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet right()}
|
{#snippet left()}
|
||||||
...
|
<HomeMenu />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet right()}
|
||||||
|
{#if top_guilds}
|
||||||
|
<AsideCard title="Top Communities">
|
||||||
|
<ul>
|
||||||
|
{#each top_guilds as gu}
|
||||||
|
<li>
|
||||||
|
<a href="/+{gu.name}">+{gu.name}</a> -
|
||||||
|
<strong>{gu.post_count ?? 0}</strong> posts -
|
||||||
|
<strong>{gu.subscriber_count ?? 0}</strong> subscribers
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</AsideCard>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
</SLayout>
|
</SLayout>
|
||||||
{:else}
|
{:else}
|
||||||
<Centered>
|
<Centered>
|
||||||
<p>{appName()} is a social media platform made by people like you.<br />
|
<p>{appName()} is a social media platform made by people like you.<br />
|
||||||
<a href="/login">Log in</a> or (sign up) to see {activePostCount()} posts and talk with {activeUserCount()} users right now!</p>
|
<a href="/login">Log in</a> or <a href="/register">sign up</a> to see {activePostCount()} posts and talk with {activeUserCount()} users right now!</p>
|
||||||
</Centered>
|
</Centered>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import { backend, type PostEntry } from '$lib/backend.js';
|
import { backend, type GuildEntry, type PostEntry } from '$lib/backend.js';
|
||||||
import { getMe } from '$lib/globals.svelte.js';
|
import { getMe, sleep } from '$lib/globals.svelte.js';
|
||||||
import type { LoadEvent } from '@sveltejs/kit';
|
import type { LoadEvent } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
let isFirstLoad = false;
|
||||||
|
|
||||||
async function loadFeed (event: LoadEvent): Promise<PostEntry[] | null> {
|
async function loadFeed (event: LoadEvent): Promise<PostEntry[] | null> {
|
||||||
const resp = await backend.withEvent(event).fetch('home/feed');
|
const resp = await backend.withEvent(event).fetch('home/feed');
|
||||||
|
|
||||||
if ([200].indexOf(resp.status) < 0) return null;
|
if ([200].indexOf(resp.status) < 0) {
|
||||||
|
console.error(`fetch feed returned status ${resp.status}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const respJ = await resp.json();
|
const respJ = await resp.json();
|
||||||
|
|
||||||
|
|
@ -17,16 +20,40 @@ async function loadFeed (event: LoadEvent): Promise<PostEntry[] | null> {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function load(event): Promise<{feed: PostEntry[] | null}> {
|
async function loadTopGuilds (event: LoadEvent): Promise<GuildEntry[] | null> {
|
||||||
|
const resp = await backend.withEvent(event).fetch('top/guilds');
|
||||||
|
|
||||||
|
if ([200].indexOf(resp.status) < 0) {
|
||||||
|
console.error(`fetch top_guilds returned status ${resp.status}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const respJ = await resp.json();
|
||||||
|
|
||||||
|
const { has: top_guilds } : { has: GuildEntry[] } = respJ;
|
||||||
|
return top_guilds;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load(event): Promise<{feed: PostEntry[] | null, top_guilds?: GuildEntry[] | null}> {
|
||||||
|
// delay loading after layout
|
||||||
|
if (!isFirstLoad) {
|
||||||
|
await sleep(2000);
|
||||||
|
isFirstLoad = true;
|
||||||
|
}
|
||||||
|
|
||||||
let feed = null;
|
let feed = null;
|
||||||
let me = getMe();
|
let me = getMe();
|
||||||
|
let top_guilds = null;
|
||||||
|
|
||||||
if (me) {
|
if (me) {
|
||||||
feed = await loadFeed(event);
|
feed = await loadFeed(event);
|
||||||
|
top_guilds = await loadTopGuilds(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { feed };
|
return { feed, top_guilds };
|
||||||
}
|
}
|
||||||
|
|
@ -1,31 +1,42 @@
|
||||||
|
|
||||||
|
|
||||||
import { backend } from '$lib/backend.js';
|
import { backend, type GuildEntry } from '$lib/backend.js';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function load(event) {
|
export async function load(event) {
|
||||||
const { params } = event;
|
const { params } = event;
|
||||||
const { id } = params;
|
const { id } = params;
|
||||||
|
|
||||||
|
|
||||||
const resp = await backend.withEvent(event).fetch('post/' + encodeURIComponent(id));
|
const resp = await backend.withEvent(event).fetch('post/' + encodeURIComponent(id));
|
||||||
|
|
||||||
if(resp.status === 404) {
|
if(resp.status === 404) {
|
||||||
error(404);
|
error(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let post;
|
||||||
try{
|
try{
|
||||||
const respJ = await resp.json();
|
const respJ = await resp.json();
|
||||||
|
|
||||||
let { posts } = respJ;
|
let { posts } = respJ;
|
||||||
return posts[id];
|
post = posts[id];
|
||||||
|
|
||||||
|
if (post?.to && post.to.type === 'guild') {
|
||||||
|
const guild: GuildEntry = post.to;
|
||||||
|
const guildResp = await backend.withEvent(event).fetch('guild/' + encodeURIComponent(guild.id));
|
||||||
|
const guildJson = await guildResp.json();
|
||||||
|
const guildInfo = guildJson?.guilds?.[guild.id];
|
||||||
|
guildInfo.type = 'guild';
|
||||||
|
post.to = guildInfo || guild;
|
||||||
|
console.log(post.to);
|
||||||
|
}
|
||||||
|
|
||||||
//return {};
|
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
error(502);
|
error(502);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return post;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { backend } from '$lib/backend.js';
|
import { backend } from '$lib/backend';
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
export async function load(event) {
|
export async function load(event) {
|
||||||
|
|
|
||||||
81
src/routes/create/+page.svelte
Normal file
81
src/routes/create/+page.svelte
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Centered from "$lib/Centered.svelte";
|
||||||
|
import { getMe } from "$lib/globals.svelte";
|
||||||
|
import GuildSelect from "$lib/GuildSelect.svelte";
|
||||||
|
import PrivacySelect from "$lib/PrivacySelect.svelte";
|
||||||
|
import SLayout from "$lib/SLayout.svelte";
|
||||||
|
import { RiErrorWarningLine } from "svelte-remixicon";
|
||||||
|
|
||||||
|
let me = getMe();
|
||||||
|
|
||||||
|
let content = $state("");
|
||||||
|
let privacy = $state(0);
|
||||||
|
|
||||||
|
let contentLength = $derived(content.length);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
{#if me}
|
||||||
|
<SLayout title="New post">
|
||||||
|
<form method="POST" class="card">
|
||||||
|
<p>Posting as <strong>@{me.username}</strong></p>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
Post to: <!-- TODO autocomplete! -->
|
||||||
|
<GuildSelect />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="text" name="title" maxlength=256 placeholder="An interesting title"/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<textarea bind:value={content}></textarea>
|
||||||
|
<output><small class="faint">{contentLength} chars</small></output>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<PrivacySelect bind:value={privacy} />
|
||||||
|
{#if privacy === 0}
|
||||||
|
<span class="warning"><RiErrorWarningLine /> Your post will be PUBLIC!</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<button class="card primary" disabled>Create</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{#snippet left()}
|
||||||
|
...
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet right()}
|
||||||
|
...
|
||||||
|
{/snippet}
|
||||||
|
</SLayout>
|
||||||
|
{:else}
|
||||||
|
<Centered>
|
||||||
|
You must be <a href="login">logged in</a> in order to create posts.
|
||||||
|
</Centered>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
background-color: inherit;
|
||||||
|
color: var(--text-primary);
|
||||||
|
min-height: 10em;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[name="title"] {
|
||||||
|
width: 100%;
|
||||||
|
margin: 6px 0;
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<card>
|
<card>
|
||||||
<form method="POST" use:enhance>
|
<form method="POST" use:enhance>
|
||||||
<label>
|
<label>
|
||||||
<span><RiUserLine /> Username / <RiMailLine /> E-mail:</span>
|
<span><RiUserLine /> Username / <RiMailLine /> E-mail</span>
|
||||||
<input type="text" name="username" />
|
<input type="text" name="username" />
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
<button class="primary">Log in</button>
|
<button class="primary">Log in</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- todo register link -->
|
||||||
</card>
|
</card>
|
||||||
</Centered>
|
</Centered>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export async function load (event) {
|
||||||
redirect(303, next);
|
redirect(303, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cookies managed by backend
|
||||||
const resp = await backend.withEvent(event).fetch("logout", {method: 'POST'});
|
const resp = await backend.withEvent(event).fetch("logout", {method: 'POST'});
|
||||||
|
|
||||||
if ([200, 204].indexOf(resp.status) >= 0) {
|
if ([200, 204].indexOf(resp.status) >= 0) {
|
||||||
|
|
|
||||||
50
src/routes/register/+page.svelte
Normal file
50
src/routes/register/+page.svelte
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from "$app/forms";
|
||||||
|
import Centered from "$lib/Centered.svelte";
|
||||||
|
import { RiInformationLine, RiUserLine } from "svelte-remixicon";
|
||||||
|
|
||||||
|
let username = $state("");
|
||||||
|
// TODO add effect
|
||||||
|
let usernameTaken = $state(true);
|
||||||
|
let email = $state("");
|
||||||
|
let disabled = $derived(username.length < 2 || username.length > 30 ||
|
||||||
|
usernameTaken || !/^.+@.+\.[a-z]{2,15}$/.test(email)
|
||||||
|
);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Centered narrow>
|
||||||
|
<card>
|
||||||
|
<form method="POST" use:enhance>
|
||||||
|
<label>
|
||||||
|
<span><RiUserLine /> Username</span>
|
||||||
|
<input type="text" name="username" bind:value={username} />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span><RiUserLine /> E-mail</span>
|
||||||
|
<small class="faint">Must be a working e-mail address <abbr title="Will be used for password recovery and important communications"><RiInformationLine /></abbr></small>
|
||||||
|
<input type="text" name="username" bind:value={email} />
|
||||||
|
</label>
|
||||||
|
...
|
||||||
|
|
||||||
|
<button class="primary" disabled={disabled}>Sign up</button>
|
||||||
|
</form>
|
||||||
|
</card>
|
||||||
|
</Centered>
|
||||||
|
|
||||||
|
|
||||||
|
<style>
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: 1em auto;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.primary {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { backend } from '$lib/backend';
|
||||||
|
import type { Action, Actions } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
default: async (event) => {
|
||||||
|
const { request } = event;
|
||||||
|
const data = await request.formData()
|
||||||
|
|
||||||
|
const query = data.get("query");
|
||||||
|
if ("string" !== typeof query) {
|
||||||
|
console.log("query is", query);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await backend.withEvent(event).oath();
|
||||||
|
const resp = await client.submitJson('search/top', { query });
|
||||||
|
|
||||||
|
const { status } = resp;
|
||||||
|
const respData = await resp.json();
|
||||||
|
|
||||||
|
if (status !== 200) {
|
||||||
|
event.locals.results = [];
|
||||||
|
event.locals.query = query;
|
||||||
|
console.log({ query, status })
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { has: results } = respData;
|
||||||
|
|
||||||
|
event.locals.results = results;
|
||||||
|
event.locals.query = query;
|
||||||
|
console.log(event.locals);
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} satisfies Actions;
|
||||||
|
|
||||||
|
export async function load (event) {
|
||||||
|
const { results, query } = event.locals;
|
||||||
|
|
||||||
|
console.log({ results, query });
|
||||||
|
return { results, query };
|
||||||
|
}
|
||||||
|
|
||||||
22
src/routes/search/+page.svelte
Normal file
22
src/routes/search/+page.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from "$app/forms";
|
||||||
|
import BigSearchInput from "$lib/BigSearchInput.svelte";
|
||||||
|
import Feed from "$lib/Feed.svelte";
|
||||||
|
import { RiSearchLine } from "svelte-remixicon";
|
||||||
|
import type { PageProps } from "./$types";
|
||||||
|
|
||||||
|
let { data }: PageProps = $props();
|
||||||
|
let { query, results } = $derived(data);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form method="POST" use:enhance>
|
||||||
|
<ul class="row">
|
||||||
|
<BigSearchInput bind:query />
|
||||||
|
<button class="inline"><RiSearchLine /></button>
|
||||||
|
</ul>
|
||||||
|
</form>
|
||||||
|
{#if query}
|
||||||
|
<Feed posts={results} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
|
||||||
21
src/routes/settings/+layout.svelte
Normal file
21
src/routes/settings/+layout.svelte
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import MenuLink from "$lib/MenuLink.svelte";
|
||||||
|
import SLayout from "$lib/SLayout.svelte";
|
||||||
|
import { RiBrush2Line } from "svelte-remixicon";
|
||||||
|
|
||||||
|
let { children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<SLayout title="Settings">
|
||||||
|
{@render children()}
|
||||||
|
{#snippet left()}
|
||||||
|
<ul class="column">
|
||||||
|
<MenuLink href="/settings/appearance" icon={RiBrush2Line} label="Appearance" />
|
||||||
|
</ul>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet right()}
|
||||||
|
<button class="primary card">Save</button>
|
||||||
|
{/snippet}
|
||||||
|
</SLayout>
|
||||||
2
src/routes/settings/+page.svelte
Normal file
2
src/routes/settings/+page.svelte
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
Select a setting from the left menu to change its settings
|
||||||
2
src/routes/settings/appearance/+page.svelte
Normal file
2
src/routes/settings/appearance/+page.svelte
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
@ -16,7 +16,6 @@ export async function load(event) {
|
||||||
const respJ = await resp.json();
|
const respJ = await resp.json();
|
||||||
|
|
||||||
let { users } = respJ;
|
let { users } = respJ;
|
||||||
console.log(users);
|
|
||||||
if (users[id]) {
|
if (users[id]) {
|
||||||
if (users[id].username) {
|
if (users[id].username) {
|
||||||
redirect(302, "/@" +users[id].username );
|
redirect(302, "/@" +users[id].username );
|
||||||
|
|
|
||||||
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 318 B |
Loading…
Add table
Add a link
Reference in a new issue