initial commit
This commit is contained in:
commit
155aa524f3
48 changed files with 3943 additions and 0 deletions
13
src/app.d.ts
vendored
Normal file
13
src/app.d.ts
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// See https://svelte.dev/docs/kit/types#app.d.ts
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface PageState {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
245
src/app.html
Normal file
245
src/app.html
Normal file
|
|
@ -0,0 +1,245 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!--
|
||||
Copyright (c) 2025 Sakuragasaki46.
|
||||
This Service is available "AS IS", with NO WARRANTY, explicit or implied.
|
||||
Sakuragasaki46 is NOT legally liable for Your use of the Service.
|
||||
This service is age-restricted; do not access if underage.
|
||||
More info: https://{{ domain_name }}/terms
|
||||
-->
|
||||
%sveltekit.head%
|
||||
<style>
|
||||
/* CSS reset */
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; line-height: 1.5; font-size: 18px; background-color: var(--canvas); color: var(--text-primary); position: relative; }
|
||||
body { font-family: var(--ui-font, sans-serif); }
|
||||
button, input, select, textarea { font: inherit; }
|
||||
a:link, a:visited { color: var(--accent); }
|
||||
img { max-width: 100%; max-height: 100vh; }
|
||||
faint, .faint { opacity: .75; }
|
||||
strong faint, strong .faint { font-weight: 400; }
|
||||
.callout { color: var(--text-alt); }
|
||||
.success { color: var(--success); }
|
||||
.error { color: var(--error); }
|
||||
.warning { color: var(--warning); }
|
||||
card, .card { display: block; background-color: var(--background); border: var(--canvas) 1px solid; border-radius: 12px; margin: 12px auto; padding: 12px; max-width: 960px; }
|
||||
|
||||
/* color themes */
|
||||
:root {
|
||||
--c0-accent: #ff7300;
|
||||
--c1-accent: #ff7300;
|
||||
--c2-accent: #f837ce;
|
||||
--c3-accent: #38b8ff;
|
||||
--c4-accent: #ffe338;
|
||||
--c5-accent: #78f038;
|
||||
--c6-accent: #ff9aae;
|
||||
--c7-accent: #606080;
|
||||
--c8-accent: #aeaac0;
|
||||
--c9-accent: #3ae0b8;
|
||||
--c10-accent: #8828ea;
|
||||
--c11-accent: #1871d8;
|
||||
--c12-accent: #885a18;
|
||||
--c13-accent: #38a856;
|
||||
--c14-accent: #ff3018;
|
||||
--c15-accent: #ff1668;
|
||||
|
||||
--light-text-primary: #181818;
|
||||
--light-text-alt: #444;
|
||||
--light-border: #999;
|
||||
--light-success: #73af00;
|
||||
--light-error: #e04830;
|
||||
--light-warning: #dea800;
|
||||
--light-canvas: #eaecee;
|
||||
--light-background: #f9f9f9;
|
||||
--light-bg-sharp: #fdfdff;
|
||||
|
||||
--dark-text-primary: #e8e8e8;
|
||||
--dark-text-alt: #c0cad3;
|
||||
--dark-border: #777;
|
||||
--dark-success: #93cf00;
|
||||
--dark-error: #e04830;
|
||||
--dark-warning: #dea800;
|
||||
--dark-canvas: #0a0a0e;
|
||||
--dark-background: #181a21;
|
||||
--dark-bg-sharp: #080808;
|
||||
|
||||
--accent: var(--c0-accent);
|
||||
--text-primary: var(--light-text-primary);
|
||||
--text-alt: var(--light-text-alt);
|
||||
--border: var(--light-border);
|
||||
--success: var(--light-success);
|
||||
--error: var(--light-error);
|
||||
--warning: var(--light-warning);
|
||||
--canvas: var(--light-canvas);
|
||||
--background: var(--light-background);
|
||||
--bg-sharp: var(--light-bg-sharp);
|
||||
|
||||
/* NOT color themes */
|
||||
--ui-font: system-ui, -apple-system, BlinkMacSystemFont, "Noto Sans", sans-serif;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) { :root {
|
||||
--text-primary: var(--dark-text-primary);
|
||||
--text-alt: var(--dark-text-alt);
|
||||
--border: var(--dark-border);
|
||||
--success: var(--dark-success);
|
||||
--error: var(--dark-error);
|
||||
--warning: var(--dark-warning);
|
||||
--canvas: var(--dark-canvas);
|
||||
--background: var(--dark-background);
|
||||
--bg-sharp: var(--dark-bg-sharp);
|
||||
} }
|
||||
|
||||
.color-scheme-light {
|
||||
--text-primary: var(--light-text-primary);
|
||||
--text-alt: var(--light-text-alt);
|
||||
--border: var(--light-border);
|
||||
--success: var(--light-success);
|
||||
--error: var(--light-error);
|
||||
--warning: var(--light-warning);
|
||||
--canvas: var(--light-canvas);
|
||||
--background: var(--light-background);
|
||||
--bg-sharp: var(--light-bg-sharp);
|
||||
}
|
||||
|
||||
.color-scheme-dark {
|
||||
--text-primary: var(--dark-text-primary);
|
||||
--text-alt: var(--dark-text-alt);
|
||||
--border: var(--dark-border);
|
||||
--success: var(--dark-success);
|
||||
--error: var(--dark-error);
|
||||
--warning: var(--dark-warning);
|
||||
--canvas: var(--dark-canvas);
|
||||
--background: var(--dark-background);
|
||||
--bg-sharp: var(--dark-bg-sharp);
|
||||
}
|
||||
|
||||
.color-theme-1 { --accent: var(--c1-accent); }
|
||||
|
||||
.color-theme-2 { --accent: var(--c2-accent); }
|
||||
|
||||
.color-theme-3 { --accent: var(--c3-accent); }
|
||||
|
||||
.color-theme-4 { --accent: var(--c4-accent); }
|
||||
|
||||
.color-theme-5 { --accent: var(--c5-accent); }
|
||||
|
||||
.color-theme-6 { --accent: var(--c6-accent); }
|
||||
|
||||
.color-theme-7 { --accent: var(--c7-accent); }
|
||||
|
||||
.color-theme-8 { --accent: var(--c8-accent); }
|
||||
|
||||
.color-theme-9 { --accent: var(--c9-accent); }
|
||||
|
||||
.color-theme-10 { --accent: var(--c10-accent); }
|
||||
|
||||
.color-theme-11 { --accent: var(--c11-accent); }
|
||||
|
||||
.color-theme-12 { --accent: var(--c12-accent); }
|
||||
|
||||
.color-theme-13 { --accent: var(--c13-accent); }
|
||||
|
||||
.color-theme-14 { --accent: var(--c14-accent); }
|
||||
|
||||
.color-theme-15 { --accent: var(--c15-accent); }
|
||||
/* ... */
|
||||
|
||||
/* mobile styles */
|
||||
@media screen and (min-width: 800px) { .mobileonly { display: none !important; } }
|
||||
@media screen and (max-width: 799px) { .nomobile { display: none !important; } }
|
||||
|
||||
/* other global styles */
|
||||
aside.card > ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0; }
|
||||
|
||||
aside.card > ul > li {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px; }
|
||||
|
||||
aside.card > ul > li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
aside.card > p {
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ul.row {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.column {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a svg {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
input:not([type="button"], [type="submit"], [type="reset"]) {
|
||||
border-radius: 0;
|
||||
border: 0;
|
||||
border-bottom: 2px solid var(--border);
|
||||
background-color: inherit;
|
||||
color: var(--text-alt);
|
||||
}
|
||||
input:not([type="button"], [type="submit"], [type="reset"]):focus {
|
||||
background-color: var(--bg-sharp);
|
||||
border-color: var(--accent);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
button, select, button.card {
|
||||
color: var(--accent);
|
||||
background-color: transparent;
|
||||
border: var(--accent) 1px solid;
|
||||
}
|
||||
|
||||
button.primary, button.primary.card {
|
||||
color: var(--bg-main);
|
||||
background-color: var(--accent);
|
||||
}
|
||||
|
||||
button.inline {
|
||||
display: inline;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button, input:is([type="button"], [type="submit"], [type="reset"]), select, textarea {
|
||||
border-radius: 9px;
|
||||
padding: .5em;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button.card {
|
||||
width: 100%;
|
||||
padding: .5em 1em;
|
||||
border-radius: 1em;
|
||||
}
|
||||
|
||||
article h1, article h2 {
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<global-wrapper style="display: contents">%sveltekit.body%</global-wrapper>
|
||||
</body>
|
||||
</html>
|
||||
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 @@
|
|||
|
||||
23
src/routes/+error.svelte
Normal file
23
src/routes/+error.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/state";
|
||||
import Centered from "$lib/Centered.svelte";
|
||||
|
||||
let errorMessage = page.error?.message || null;
|
||||
if (errorMessage?.startsWith('Error: ')) { errorMessage = errorMessage.slice(7); }
|
||||
if (errorMessage && /^[0-9]+$/.test(errorMessage) && +errorMessage === page.status) {
|
||||
errorMessage = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Centered>
|
||||
<h1>{page.status}</h1>
|
||||
|
||||
{#if errorMessage}
|
||||
<p>{errorMessage}</p>
|
||||
{/if}
|
||||
{#if page.status >= 500}
|
||||
<p><button onclick={() => {history.go(0);}} class="inline">Refresh</button></p>
|
||||
{:else}
|
||||
<p><a href="/">Back to homepage</a></p>
|
||||
{/if}
|
||||
</Centered>
|
||||
86
src/routes/+layout.svelte
Normal file
86
src/routes/+layout.svelte
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<script lang="ts">
|
||||
import { version } from '$app/environment';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ServerHealth, UserEntry } from '$lib/backend';
|
||||
import { appName, version as bkVersion } from '$lib/globals.svelte';
|
||||
import MobileFooter from '$lib/MobileFooter.svelte';
|
||||
import MetaNav from '$lib/MetaNav.svelte';
|
||||
import { getFlash } from 'sveltekit-flash-message';
|
||||
import { page } from "$app/state";
|
||||
import FlashMessage from '$lib/FlashMessage.svelte';
|
||||
|
||||
let { data, children } : {
|
||||
data: {me: UserEntry},
|
||||
children: Snippet
|
||||
} = $props();
|
||||
|
||||
let {me} = $derived(data);
|
||||
|
||||
const flash = getFlash(page);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="og:site_name" content="app_name" />
|
||||
{#if data}
|
||||
<meta name="generator" content={`app_name (frontend ${version} + Svelte 5, backend ${bkVersion()})`} />
|
||||
{/if}
|
||||
<!-- TODO csrf token? -->
|
||||
<!-- icon styles? -->
|
||||
<!-- SEO tags start -->
|
||||
<!-- end SEO tags -->
|
||||
</svelte:head>
|
||||
|
||||
<header>
|
||||
<h1>
|
||||
<a href="/">{appName()}</a>
|
||||
</h1>
|
||||
<!-- .metanav -->
|
||||
<MetaNav user={me} />
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{#if $flash}
|
||||
<FlashMessage message={$flash?.message}/>
|
||||
{/if}
|
||||
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<MobileFooter />
|
||||
|
||||
<style>
|
||||
header {
|
||||
background-color: var(--background);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
height: 3em;
|
||||
padding: .75em 1.5em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
header a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
main {
|
||||
min-height: 70vh;
|
||||
margin: 12px auto;
|
||||
max-width: 1600px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 799px) {
|
||||
main {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
45
src/routes/+layout.ts
Normal file
45
src/routes/+layout.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { backend, type ServerHealth, type UserEntry } from '$lib/backend.js';
|
||||
import { setHealth, setMe } from '$lib/globals.svelte.js';
|
||||
import type { LoadEvent } from '@sveltejs/kit';
|
||||
|
||||
|
||||
async function loadSite (event: LoadEvent): Promise<ServerHealth | null> {
|
||||
const resp = await backend.withEvent(event).fetch('health');
|
||||
|
||||
if ([200].indexOf(resp.status) < 0) return null;
|
||||
try {
|
||||
|
||||
const respJ = await resp.json();
|
||||
|
||||
setHealth(respJ);
|
||||
|
||||
return respJ;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMe(event: LoadEvent, me?: string): Promise<UserEntry | null> {
|
||||
if (!me) return null;
|
||||
|
||||
const resp = await backend.withEvent(event).fetch('user/@me');
|
||||
|
||||
if ([200].indexOf(resp.status) < 0) return null;
|
||||
try {
|
||||
const respJ = await resp.json();
|
||||
const { users } = respJ;
|
||||
const meJ = users[me];
|
||||
setMe(meJ);
|
||||
return meJ;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function load(event): Promise<{site: ServerHealth | null, me: UserEntry | null} > {
|
||||
let site = await loadSite(event);
|
||||
let me = await loadMe (event, site?.me || void 0);
|
||||
return { site, me };
|
||||
}
|
||||
|
||||
40
src/routes/+page.svelte
Normal file
40
src/routes/+page.svelte
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
import type { PostEntry } from "$lib/backend";
|
||||
import Centered from "$lib/Centered.svelte";
|
||||
import Feed from "$lib/Feed.svelte";
|
||||
import { activePostCount, activeUserCount, appName, getMe } from "$lib/globals.svelte";
|
||||
import SLayout from "$lib/SLayout.svelte";
|
||||
|
||||
let me = getMe();
|
||||
let { data } : { data: { feed: PostEntry[] } } = $props();
|
||||
let feed: PostEntry[] = $state([]);
|
||||
let feedIndex = $state(0);
|
||||
|
||||
$effect(() => {
|
||||
if (me && feed) {
|
||||
feed.push(...data.feed.slice(feedIndex));
|
||||
feedIndex += feed.length;
|
||||
} else if (me) {
|
||||
console.log('feed is', feed)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if me}
|
||||
<SLayout title={appName()}>
|
||||
<Feed posts={feed} />
|
||||
{#snippet left()}
|
||||
...
|
||||
{/snippet}
|
||||
|
||||
{#snippet right()}
|
||||
...
|
||||
{/snippet}
|
||||
</SLayout>
|
||||
{:else}
|
||||
<Centered>
|
||||
<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>
|
||||
</Centered>
|
||||
{/if}
|
||||
|
||||
32
src/routes/+page.ts
Normal file
32
src/routes/+page.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { backend, type PostEntry } from '$lib/backend.js';
|
||||
import { getMe } from '$lib/globals.svelte.js';
|
||||
import type { LoadEvent } from '@sveltejs/kit';
|
||||
|
||||
|
||||
async function loadFeed (event: LoadEvent): Promise<PostEntry[] | null> {
|
||||
const resp = await backend.withEvent(event).fetch('home/feed');
|
||||
|
||||
if ([200].indexOf(resp.status) < 0) return null;
|
||||
|
||||
try {
|
||||
const respJ = await resp.json();
|
||||
|
||||
const { feed } : { feed: PostEntry[] } = respJ;
|
||||
|
||||
return feed;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export async function load(event): Promise<{feed: PostEntry[] | null}> {
|
||||
let feed = null;
|
||||
let me = getMe();
|
||||
|
||||
if (me) {
|
||||
feed = await loadFeed(event);
|
||||
}
|
||||
|
||||
return { feed };
|
||||
}
|
||||
23
src/routes/[x+2b][name]/+page.svelte
Normal file
23
src/routes/[x+2b][name]/+page.svelte
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { GuildEntry, PostEntry } from "$lib/backend";
|
||||
import Feed from "$lib/Feed.svelte";
|
||||
import GuildAbout from "$lib/GuildAbout.svelte";
|
||||
import GuildMenu from "$lib/GuildMenu.svelte";
|
||||
import SLayout from "$lib/SLayout.svelte";
|
||||
|
||||
let { data } : { data: { guild: GuildEntry , feed: PostEntry[] } } = $props();
|
||||
let { guild, feed = [] } = $derived(data);
|
||||
|
||||
</script>
|
||||
|
||||
<SLayout title={guild.display_name? `${guild.display_name} (+${guild.name})` :`+${guild.name}`}>
|
||||
<Feed posts={feed} />
|
||||
|
||||
{#snippet left()}
|
||||
<GuildMenu {guild} />
|
||||
{/snippet}
|
||||
|
||||
{#snippet right()}
|
||||
<GuildAbout {guild} />
|
||||
{/snippet}
|
||||
</SLayout>
|
||||
36
src/routes/[x+2b][name]/+page.ts
Normal file
36
src/routes/[x+2b][name]/+page.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { backend } from '$lib/backend.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load(event) {
|
||||
const { params } = event;
|
||||
const { name } = params;
|
||||
|
||||
|
||||
const resp = await backend.withEvent(event).fetch('guild/@' + encodeURIComponent(name) + "/feed");
|
||||
|
||||
if(resp.status === 404) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
try{
|
||||
const respJ = await resp.json();
|
||||
|
||||
let { guilds, feed } = respJ;
|
||||
let guild = null;
|
||||
for (let g in guilds) {
|
||||
if (guilds[g].name === name) {
|
||||
guild = guilds[g];
|
||||
}
|
||||
}
|
||||
|
||||
if (!guild) error(404);
|
||||
|
||||
return { guild, feed };
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
error(502);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
11
src/routes/[x+3d][id]/+page.svelte
Normal file
11
src/routes/[x+3d][id]/+page.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import type { PostEntry } from "$lib/backend.ts";
|
||||
import FullPost from "$lib/FullPost.svelte";
|
||||
|
||||
|
||||
let { data } : { data: PostEntry } = $props();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<FullPost post={data} />
|
||||
31
src/routes/[x+3d][id]/+page.ts
Normal file
31
src/routes/[x+3d][id]/+page.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
|
||||
|
||||
import { backend } from '$lib/backend.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load(event) {
|
||||
const { params } = event;
|
||||
const { id } = params;
|
||||
|
||||
|
||||
const resp = await backend.withEvent(event).fetch('post/' + encodeURIComponent(id));
|
||||
|
||||
if(resp.status === 404) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
try{
|
||||
const respJ = await resp.json();
|
||||
|
||||
let { posts } = respJ;
|
||||
return posts[id];
|
||||
|
||||
//return {};
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
error(502);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
11
src/routes/[x+3d][id]/[slug]/+page.svelte
Normal file
11
src/routes/[x+3d][id]/[slug]/+page.svelte
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import type { PostEntry } from "$lib/backend.ts";
|
||||
import FullPost from "$lib/FullPost.svelte";
|
||||
|
||||
|
||||
let { data } : { data: PostEntry } = $props();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<FullPost post={data} />
|
||||
28
src/routes/[x+40][name]/+page.svelte
Normal file
28
src/routes/[x+40][name]/+page.svelte
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<script lang="ts">
|
||||
import type { PostEntry, UserEntry } from "$lib/backend.js";
|
||||
import Feed from "$lib/Feed.svelte";
|
||||
import SLayout from "$lib/SLayout.svelte";
|
||||
import UserAbout from "$lib/UserAbout.svelte";
|
||||
import UserMenu from "$lib/UserMenu.svelte";
|
||||
import { RiUserLine } from "svelte-remixicon";
|
||||
|
||||
let { data } : { data: { user: UserEntry, feed: PostEntry[] } } = $props();
|
||||
|
||||
// TEMP make it work!
|
||||
let { user, feed } = $derived(data);
|
||||
let username = $derived(user.username);
|
||||
</script>
|
||||
|
||||
{#if username}
|
||||
<SLayout title={'@' + username}>
|
||||
<Feed posts={feed} />
|
||||
|
||||
{#snippet left()}
|
||||
<UserMenu {user} />
|
||||
{/snippet}
|
||||
|
||||
{#snippet right()}
|
||||
<UserAbout {user} />
|
||||
{/snippet}
|
||||
</SLayout>
|
||||
{/if}
|
||||
34
src/routes/[x+40][name]/+page.ts
Normal file
34
src/routes/[x+40][name]/+page.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { backend } from '$lib/backend.js';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
export async function load(event) {
|
||||
const { params } = event;
|
||||
const { name } = params;
|
||||
|
||||
|
||||
const resp = await backend.withEvent(event).fetch('user/@' + encodeURIComponent(name) + '/feed');
|
||||
|
||||
if(resp.status === 404) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
try{
|
||||
const respJ = await resp.json();
|
||||
|
||||
let { users, feed } = respJ;
|
||||
let user;
|
||||
for (let u in users) {
|
||||
if (users[u].username === name) {
|
||||
user = users[u];
|
||||
}
|
||||
}
|
||||
|
||||
return { user, feed };
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
error(502);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
53
src/routes/login/+page.server.ts
Normal file
53
src/routes/login/+page.server.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { backend } from "$lib/backend";
|
||||
import { error, type Actions } from "@sveltejs/kit";
|
||||
import { redirect } from 'sveltekit-flash-message/server';
|
||||
|
||||
|
||||
export const actions = {
|
||||
default: async (event) => {
|
||||
// TODO login
|
||||
|
||||
const { request } = event;
|
||||
|
||||
const data = await request.formData();
|
||||
const username = data.get('username');
|
||||
const password = data.get('password');
|
||||
const remember = !!data.get('remember');
|
||||
|
||||
const backend2 = await backend.withEvent(event).oath();
|
||||
const resp = await backend2.submitJson('login', {
|
||||
username,
|
||||
password,
|
||||
remember
|
||||
});
|
||||
|
||||
const { status } = resp;
|
||||
const respData = await resp.json();
|
||||
|
||||
if ([200, 204].indexOf(status) < 0) {
|
||||
// login error
|
||||
console.log(`/login: status ${status}, data below`);
|
||||
console.debug(respData);
|
||||
switch(status) {
|
||||
case 400:
|
||||
redirect({message: 'Invalid login'}, event);
|
||||
break;
|
||||
case 404:
|
||||
redirect({message: 'Invalid username or password'}, event);
|
||||
break;
|
||||
case 403:
|
||||
redirect({message: 'Login not allowed'}, event);
|
||||
break;
|
||||
default:
|
||||
redirect({message: `Unknown error (HTTP ${status})`}, event);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// login success
|
||||
const { id: myId } = respData;
|
||||
|
||||
redirect(303, "/user/" + myId);
|
||||
}
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
||||
48
src/routes/login/+page.svelte
Normal file
48
src/routes/login/+page.svelte
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { enhance } from "$app/forms";
|
||||
import Centered from "$lib/Centered.svelte";
|
||||
import CheckboxLabel from "$lib/CheckboxLabel.svelte";
|
||||
import PasswordInput from "$lib/PasswordInput.svelte";
|
||||
import { RiEyeLine, RiEyeOffLine, RiKeyLine, RiMailLine, RiUserLine } from "svelte-remixicon";
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<Centered narrow>
|
||||
<card>
|
||||
<form method="POST" use:enhance>
|
||||
<label>
|
||||
<span><RiUserLine /> Username / <RiMailLine /> E-mail:</span>
|
||||
<input type="text" name="username" />
|
||||
</label>
|
||||
<label>
|
||||
<span><RiKeyLine /> Password</span>
|
||||
<PasswordInput name="password" />
|
||||
</label>
|
||||
<CheckboxLabel name="remember">
|
||||
Remember me
|
||||
</CheckboxLabel>
|
||||
|
||||
<button class="primary">Log in</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>
|
||||
26
src/routes/logout/+page.server.ts
Normal file
26
src/routes/logout/+page.server.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { backend } from '$lib/backend.js';
|
||||
import { getMe, setMe } from '$lib/globals.svelte.js';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
|
||||
|
||||
|
||||
export async function load (event) {
|
||||
const me = getMe();
|
||||
|
||||
const next = "/";
|
||||
|
||||
// already logged out
|
||||
if (me == null) {
|
||||
redirect(303, next);
|
||||
}
|
||||
|
||||
const resp = await backend.withEvent(event).fetch("logout", {method: 'POST'});
|
||||
|
||||
if ([200, 204].indexOf(resp.status) >= 0) {
|
||||
setMe(null);
|
||||
redirect(303, next);
|
||||
}
|
||||
|
||||
console.error(`status ${resp.status} received, not logging out`)
|
||||
error(500);
|
||||
}
|
||||
0
src/routes/search/+page.server.ts
Normal file
0
src/routes/search/+page.server.ts
Normal file
28
src/routes/user/[id]/+page.ts
Normal file
28
src/routes/user/[id]/+page.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { backend } from '$lib/backend';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
|
||||
export async function load(event) {
|
||||
const { params } = event;
|
||||
const { id } = params;
|
||||
|
||||
const resp = await backend.withEvent(event).fetch('user/' + encodeURIComponent(id));
|
||||
|
||||
if(resp.status === 404) {
|
||||
error(404);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const respJ = await resp.json();
|
||||
|
||||
let { users } = respJ;
|
||||
console.log(users);
|
||||
if (users[id]) {
|
||||
if (users[id].username) {
|
||||
redirect(302, "/@" +users[id].username );
|
||||
}
|
||||
}
|
||||
|
||||
error(404);
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue