initial commit

This commit is contained in:
Yusur 2025-09-12 19:20:30 +02:00
commit 155aa524f3
48 changed files with 3943 additions and 0 deletions

23
src/routes/+error.svelte Normal file
View 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
View 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
View 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
View 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
View 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 };
}

View 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>

View 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);
}
}

View 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} />

View 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);
}
}

View 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} />

View 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}

View 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);
}
}

View 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;

View 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>

View 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);
}

View file

View 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);
}