Compare commits

..

No commits in common. "80ecfd67471527843e29329cbd51d85c923427a3" and "54cafaa1da0a93fe12f6c5863725abe3ce90934f" have entirely different histories.

23 changed files with 85 additions and 376 deletions

View file

@ -1,7 +1,7 @@
{
"name": "@yusurko/vigil",
"private": true,
"version": "0.1.0-dev43",
"version": "0.1.0-dev42",
"type": "module",
"scripts": {
"dev": "vite dev",

2
src/app.d.ts vendored
View file

@ -14,7 +14,7 @@ declare global {
site: ServerHealth | null,
me: UserEntry | null,
results?: object[],
query?: string,
query?: string
}
// interface PageState {}
// interface Platform {}

View file

@ -1,24 +0,0 @@
<script lang="ts">
import { RiChat3Line } from "svelte-remixicon";
let { count } = $props();
</script>
<div class="comment-count">
<RiChat3Line />
<strong>{count??'-'}</strong>
</div>
<style>
.comment-count {
display: flex;
flex-direction: column;
align-items: center;
}
.comment-count :global(svg) {
color: var(--border);
}
</style>

View file

@ -1,16 +0,0 @@
<script lang="ts">
import type { CommentEntry } from "./backend";
import Centered from "./Centered.svelte";
let comments: null | CommentEntry = $state(null);
let { post } = $props();
</script>
{#if comments === null}
<Centered>
<button class="inline">Show comments</button>
</Centered>
{/if}

View file

@ -1,35 +0,0 @@
<script lang="ts">
import PrivacySelect from "./PrivacySelect.svelte";
let { content = $bindable(""), privacy = $bindable(0) } = $props();
let contentLength = $derived(content.length);
</script>
<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} />
<style>
textarea {
width: 100%;
background-color: inherit;
color: var(--text-primary);
min-height: 10em;
margin-top: 4px;
}
input[name="title"] {
width: 100%;
margin: 6px 0;
font-size: 1.25em;
}
</style>

View file

@ -1,68 +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";
import VoteButton from "./VoteButton.svelte";
import CommentCount from "./CommentCount.svelte";
let { post }: { post: PostEntry } = $props();
let { id, title, content = "", votes, my_vote, comment_count } = post;
let { id, title, created_at, content } = post;
</script>
<article class="card">
<div class="post-frame">
<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 } />
<SvelteShowdown content={ content || "" } />
</div>
<aside class="message-stats">
<VoteButton score={votes} vote={my_vote} {id} />
<CommentCount count={comment_count} />
</aside>
</div>
</article>
</card>
<style>
.post-frame {
padding-inline-start: 2em;
position: relative;
}
.message-stats {
position: absolute;
inset-inline-start: 0;
top: 0;
width: 2em;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: center;
}
.post-title {
line-height: 1.2;
}
.shorten {
max-height: 18em;
overflow-y: hidden;
position: relative;
}
.shorten::after {
content: '';
position: absolute;
z-index: 10;
top: 16em;
left: 0;
width: 100%;
height: 2em;
display: block;
background: linear-gradient(to bottom, rgba(0,0,0,0) 0%, var(--background) 100%);
}
</style>

View file

@ -1,5 +1,7 @@
<script lang="ts">
import { RiEditLine, RiFlagLine } from "svelte-remixicon";
import { DateTime } from "luxon";
import { RiEditLine, RiFlagLine, RiHistoryLine, RiHome2Line, RiUserLine } from "svelte-remixicon";
import type { PostEntry } from "./backend";
import SLayout from "./SLayout.svelte";
import GuildAbout from "./GuildAbout.svelte";
@ -10,15 +12,13 @@
import { SvelteShowdown } from "svelte-showdown";
import VoteButton from "./VoteButton.svelte";
import { getMe } from "./globals.svelte";
import CommentCount from "./CommentCount.svelte";
import CommentSection from "./CommentSection.svelte";
let { post }: { post: PostEntry } = $props();
let me = getMe();
let { title, id, content = '', to, votes, my_vote, comment_count } = post;
let { title, created_at, id, content = '', to } = post;
</script>
<SLayout title={to.display_name + (to.type === 'guild' ? ` (+${to.name})` : to.type === 'user' ? ` (@${to.username})` : '')}>
@ -34,24 +34,21 @@ let { title, id, content = '', to, votes, my_vote, comment_count } = post;
<!-- content, formatted as markdown -->
</div>
</div><!-- .post-body -->
<aside class="message-stats">
<div class="message-stats">
<!-- upvotes / downvotes -->
<VoteButton score={votes} vote={my_vote} {id} />
<CommentCount count={comment_count} />
</aside>
<VoteButton />
</div>
<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/={id}"><RiEditLine/> Edit</a></li>
<li><a href="/edit/post/{id}"><RiEditLine/> Edit</a></li>
{/if}
</ul>
</div>
</article>
<CommentSection {post} />
{#snippet left()}
{#if to.type === 'guild'}
<GuildMenu guild={to} />
@ -77,13 +74,13 @@ let { title, id, content = '', to, votes, my_vote, comment_count } = post;
overflow-x: auto;
}
.post-body {
margin-inline-start: 2em;
margin-inline-start: 3em;
}
.message-stats {
position: absolute;
inset-inline-start: 0;
top: 0;
width: 2em;
width: 3em;
display: flex;
flex-direction: column;
justify-content: flex-start;

View file

@ -9,6 +9,7 @@ let { guild }: { guild: GuildEntry } = $props();
</script>
{#if guild}
<AsideCard title={'About ' + (guild.display_name? `${guild.display_name} (+${guild.name})`: `+${guild.name}`)}>
<ul>

View file

@ -2,7 +2,7 @@
import { backend, type GuildEntry } from "./backend";
let { value = $bindable("") } = $props();
let value = $state("");
let suggestions: Promise<GuildEntry[]> = $derived(getSuggestions(value));

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { RiAddLine, RiLoginBoxLine, RiLogoutBoxLine, RiSearch2Line, RiSearchLine, RiSettings3Line, RiShieldStarLine, RiUserLine } from "svelte-remixicon";
import { RiLoginBoxLine, RiLogoutBoxLine, RiSearch2Line, RiSearchLine, RiSettings3Line } from "svelte-remixicon";
import { activePostCount } from "./globals.svelte";
import type { UserEntry } from "./backend";
@ -35,23 +35,7 @@ let enable_search = $derived(user !== null);
<span><a href="/@{user.username}">@{user.username}</a></span>
<span>{user.karma || 0} karma</span>
</div>
<a class="mobileonly" href="/@{user.username}">
<RiUserLine />
</a>
</li>
<li class="nomobile">
<a href="/create">
<button>Create post</button>
</a>
</li>
{#if user.badges && user.badges.indexOf("administrator") >= 0}
<li>
<a href="/admin/">
<RiShieldStarLine />
</a>
</li>
{/if}
<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>
@ -114,9 +98,7 @@ let enable_search = $derived(user !== null);
overflow: hidden;
}
button {
font-size: 1rem;
}

View file

@ -1,28 +1,23 @@
<script lang="ts">
import { browser } from "$app/environment";
import { RiAddCircleLine, RiChat2Line, RiCompassDiscoverLine, RiHome2Line, RiHomeLine, RiNotificationLine, RiUserLine } from "svelte-remixicon";
import { getMe } from "./globals.svelte";
import { RiAddCircleLine, RiChat2Line, RiCompassDiscoverLine, RiHome2Line, RiHomeLine, RiNotificationLine } from "svelte-remixicon";
function rickroll (){
if (browser) {
window.open( "https://youtu.be/dQw4w9WgXcQ" );
}
}
let me = $derived(getMe());
</script>
{#if me}
<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="/create" title="Create"><RiAddCircleLine size="2em" /></a></li>
<li><a href="/@{me.username}" title="Profile"><RiUserLine 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>
{/if}
<style>

View file

@ -1,46 +1,31 @@
<script lang="ts">
import { RiHeartFill, RiHeartLine, RiThumbDownFill, RiThumbDownLine } from "svelte-remixicon";
import { backend } from "./backend";
let { score = $bindable(null), vote = $bindable(0), id } : { score?: number | null, vote?: 0 | 1 | -1, id: string } = $props();
let vote = $state(0);
let { score } : { score?: number | null } = $props();
async function castVote(v: 0 | 1 | -1) {
let readyBackend = await backend.withEvent(null).oath();
let result = await readyBackend.submitJson(`post/${id}/upvote`, {
vote: v
});
if (score === null) { return; }
if (result.status >= 400) {
// TODO toast error?
console.error("error:", (await result.json()));
return;
}
let {votes} = await result.json();
vote = v;
score = votes;
}
</script>
<div class="upvote-button">
{#if vote > 0}
<button class="inline up" onclick={() => { castVote(0).then(() => {}); }}>
<button class="inline">
<RiHeartFill />
</button>
{:else}
<button class="inline" onclick={() => { castVote(1).then(() => {}); }}>
<button class="inline">
<RiHeartLine />
</button>
{/if}
<strong>{score ?? '-'}</strong>
{#if vote < 0}
<button class="inline down" onclick={() => { castVote(0).then(() => {}); }}>
{#if vote > 0}
<button class="inline">
<RiThumbDownFill />
</button>
{:else}
<button class="inline" onclick={() => { castVote(-1).then(() => {}); }}>
<button class="inline">
<RiThumbDownLine />
</button>
{/if}
@ -52,18 +37,5 @@ async function castVote(v: 0 | 1 | -1) {
flex-direction: column;
align-items: center;
}
button.inline {
color: var(--border);
}
button.inline.up {
color: var(--accent);
}
button.inline.down {
color: var(--c11-accent);
}
:global(.color-theme-11) button.inline.down, :global(.color-theme-9) button.inline.down {
color: var(--c14-accent);
}
</style>

View file

@ -1,8 +0,0 @@
<script lang="ts">
import { RiBarricadeLine } from "svelte-remixicon";
import Centered from "./Centered.svelte";
</script>
<Centered>
<faint><RiBarricadeLine /></faint>
</Centered>

View file

@ -29,21 +29,8 @@ export type PostEntry = {
title: string,
created_at: string,
author?: UserEntry | null,
content?: string,
to: UserEntry | GuildEntry ,
privacy?: number,
votes?: number | null,
my_vote?: 1 | -1 | 0,
comment_count?: number | null
};
export type CommentEntry = {
id: string,
parent?: {id: string},
locked?: boolean,
removed?: number | true,
content?: string,
created_at: string
content?: string | null,
to: UserEntry | GuildEntry
};
export type ServerHealth = {

View file

@ -22,6 +22,8 @@ export function setHealth ({ name, version, post_count, user_count, color_theme
health.color_theme = color_theme;
}
export function setMe (me: UserEntry | null) {
health.me = me;
}

View file

@ -36,7 +36,7 @@ let colorThemeCls = $derived(`color-scheme-${colorScheme} color-theme-${colorThe
<!-- end SEO tags -->
</svelte:head>
<div class="{colorThemeCls} contents">
<global-wrapper class={colorThemeCls}>
<header>
<h1>
<a href="/">{appName()}</a>
@ -54,7 +54,7 @@ let colorThemeCls = $derived(`color-scheme-${colorScheme} color-theme-${colorThe
</main>
<MobileFooter />
</div>
</global-wrapper>
<style>
@ -88,13 +88,8 @@ let colorThemeCls = $derived(`color-scheme-${colorScheme} color-theme-${colorThe
@media screen and (max-width: 799px) {
main {
min-height: 100vh;
width: 100vw;
height: 100vh;
}
}
.contents {
position: relative;
}
</style>

View file

@ -1,19 +1,18 @@
<script lang="ts">
import Centered from "$lib/Centered.svelte";
import EditPost from "$lib/EditPost.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 Wip from "$lib/Wip.svelte";
import { RiErrorWarningLine } from "svelte-remixicon";
let me = getMe();
let content = $state("");
let privacy = $state(0);
let guildName = $state("");
let enablePost = $derived(!!content && !!guildName && false);
let contentLength = $derived(content.length);
</script>
@ -23,26 +22,34 @@
<p>Posting as <strong>@{me.username}</strong></p>
<label>
Post to:
<GuildSelect bind:value={guildName} />
Post to: <!-- TODO autocomplete! -->
<GuildSelect />
</label>
<EditPost bind:content bind:privacy />
<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={!enablePost}>Create</button>
<button class="card primary" disabled>Create</button>
</form>
{#snippet left()}
<Wip />
...
{/snippet}
{#snippet right()}
<Wip />
...
{/snippet}
</SLayout>
{:else}
@ -53,10 +60,22 @@
<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>

View file

@ -1,65 +0,0 @@
<script lang="ts">
import type { PostEntry } from "$lib/backend";
import Centered from "$lib/Centered.svelte";
import EditPost from "$lib/EditPost.svelte";
import { getMe } from "$lib/globals.svelte";
import GuildSelect from "$lib/GuildSelect.svelte";
import SLayout from "$lib/SLayout.svelte";
import Wip from "$lib/Wip.svelte";
import { RiErrorWarningLine } from "svelte-remixicon";
let me = getMe();
let { data }: { data: PostEntry } = $props();
let post = $state(data) ;
let { content = "", privacy } = $derived(post);
</script>
{#if me?.id === post.author?.id}
<SLayout title="New post">
<form method="POST" class="card">
<p>Posting as <strong>@{me?.username}</strong></p>
<label>
Post to:
<GuildSelect />
</label>
<EditPost bind:content bind: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()}
<Wip />
{/snippet}
{#snippet right()}
<Wip />
{/snippet}
</SLayout>
{:else if me}
You can't edit posts that are not your own.
{:else}
<Centered>
You must be <a href="login">logged in</a> in order to edit your own posts.
</Centered>
{/if}
<style>
form {
display: flex;
flex-direction: column;
}
</style>

View file

@ -1,51 +0,0 @@
import { backend, type GuildEntry } from '$lib/backend.js';
import { getMe } from '$lib/globals.svelte';
import { error, isHttpError, redirect } 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);
}
let post;
try{
const respJ = await resp.json();
let { posts } = respJ;
post = posts[id];
let me = getMe();
if (!me) {
redirect(303, "/login?next=" + encodeURIComponent(event.url.pathname));
}
if ( post.author.id !== me?.id) {
error(403);
}
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);
}
} catch (e) {
if (isHttpError(e)) throw e;
console.error(e);
error(502);
}
return post;
}

View file

@ -5,6 +5,7 @@ import { redirect } from 'sveltekit-flash-message/server';
export const actions = {
default: async (event) => {
// TODO login
const { request } = event;

View file

@ -1,7 +1,6 @@
<script lang="ts">
import { enhance } from "$app/forms";
import Centered from "$lib/Centered.svelte";
import Wip from "$lib/Wip.svelte";
import { RiInformationLine, RiUserLine } from "svelte-remixicon";
let username = $state("");
@ -26,7 +25,7 @@ let disabled = $derived(username.length < 2 || username.length > 30 ||
<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>
<Wip />
...
<button class="primary" disabled={disabled}>Sign up</button>
</form>

View file

@ -32,12 +32,14 @@ export const actions = {
event.locals.results = results;
event.locals.query = query;
console.log(event.locals);
return
}
} satisfies Actions;
export function load (event) {
export async function load (event) {
const { results, query } = event.locals;
console.log({ results, query });
return { results, query };
}

View file

@ -6,7 +6,6 @@
import type { PageProps } from "./$types";
let { data }: PageProps = $props();
console.log(data);
let { query, results } = $derived(data);
</script>