Rush: Voting + Account management
This commit is contained in:
parent
73a2e20cf7
commit
494f2209c0
@ -20,6 +20,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="actionBar">
|
<div class="actionBar">
|
||||||
<Rating rating={message.rating} style="gap: 24px;"></Rating>
|
<Rating messageId={message.id} rating={message.rating} ownVote={message.ownVote} style="gap: 24px;"></Rating>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
/**
|
/**
|
||||||
* @type {string | undefined}
|
* @type {string | undefined}
|
||||||
*/
|
*/
|
||||||
export let iconSrc;
|
export let iconSrc = undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {number}
|
* @type {number}
|
||||||
|
|||||||
@ -15,6 +15,21 @@
|
|||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
export let iconSize = 16;
|
export let iconSize = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export let color = "white";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
export let iconColor = "white";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string | undefined}
|
||||||
|
*/
|
||||||
|
export let textStyle = undefined;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style type="scss">
|
<style type="scss">
|
||||||
@ -26,7 +41,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="pair" title={name}>
|
<div class="pair" title={name} style="color: {color};">
|
||||||
<Tablericon name={iconName} size={iconSize} color="white"></Tablericon>
|
<Tablericon name={iconName} size={iconSize} color={iconColor}></Tablericon>
|
||||||
<span><slot /></span>
|
<span style="color: {color}; {textStyle}"><slot /></span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,6 +17,8 @@
|
|||||||
export let opened = false;
|
export let opened = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
console.log(post);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style type="scss">
|
<style type="scss">
|
||||||
|
|||||||
@ -1,10 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
import Iconvalue from "./iconvalue.svelte";
|
import { enhance } from "$app/forms";
|
||||||
|
import Iconvalue from "./iconvalue.svelte";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
export let messageId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {import("$types/base").Rating}
|
* @type {import("$types/base").Rating}
|
||||||
*/
|
*/
|
||||||
export let rating;
|
export let rating;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {number | null}
|
||||||
|
*/
|
||||||
|
export let ownVote = null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style type="scss">
|
<style type="scss">
|
||||||
@ -16,9 +27,42 @@
|
|||||||
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="rating" {...$$restProps}>
|
<div class="rating" {...$$restProps}>
|
||||||
<Iconvalue name="Likes" iconName="heart">{rating.likes}</Iconvalue>
|
{#if ownVote == 1}
|
||||||
<Iconvalue name="Dislikes" iconName="thumb-down">{rating.dislikes}</Iconvalue>
|
<form action="/vote?/remove&messageid={messageId}" method="post" use:enhance>
|
||||||
|
<button>
|
||||||
|
<Iconvalue name="Likes" iconName="heart-fill" color="var(--accent)" textStyle="font-weight: bold;" iconColor="var(--accent)">{rating.likes}</Iconvalue>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<form action="/vote?/create&messageid={messageId}&value={1}" method="post" use:enhance>
|
||||||
|
<button>
|
||||||
|
<Iconvalue name="Likes" iconName="heart">{rating.likes}</Iconvalue>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if ownVote == -1}
|
||||||
|
<form action="/vote?/remove&messageid={messageId}" method="post" use:enhance>
|
||||||
|
<button>
|
||||||
|
<Iconvalue name="Dislikes" iconName="thumb-down-fill" color="var(--accent)" textStyle="font-weight: bold;" iconColor="var(--accent)">{rating.dislikes}</Iconvalue>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<form action="/vote?/create&messageid={messageId}&value={-1}" method="post" use:enhance>
|
||||||
|
<button>
|
||||||
|
<Iconvalue name="Dislikes" iconName="thumb-down">{rating.dislikes}</Iconvalue>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
91
src/comp/topbar.svelte
Normal file
91
src/comp/topbar.svelte
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<script>
|
||||||
|
import activeSession from "$lib/memory/session";
|
||||||
|
import { getNamedId } from "$lib/util";
|
||||||
|
import Avatar from "./avatar.svelte";
|
||||||
|
import Useritem from "./useritem.svelte";
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.topBar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--background-black);
|
||||||
|
z-index: 99999;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topContent {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
max-width: 1028px;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loggedInAs {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
align-items: end;
|
||||||
|
|
||||||
|
> .label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .name {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.webTitle {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="topBar">
|
||||||
|
|
||||||
|
<div class="topContent">
|
||||||
|
<a class="webTitle typeDisplay" href="/posts">ECHO</a>
|
||||||
|
|
||||||
|
<a href="/compose">Compose</a>
|
||||||
|
|
||||||
|
<div class="separator"></div>
|
||||||
|
|
||||||
|
{#if $activeSession}
|
||||||
|
<a class="user" href="/users/{getNamedId($activeSession.id, $activeSession.name)}">
|
||||||
|
<div class="loggedInAs">
|
||||||
|
<span class="label">Logged in as</span>
|
||||||
|
<span class="name">{$activeSession.name}</span>
|
||||||
|
</div>
|
||||||
|
<Avatar user={$activeSession} size={40}></Avatar>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<a class="user" href="/login">
|
||||||
|
<div class="loggedInAs">
|
||||||
|
<span class="label">Private</span>
|
||||||
|
<span class="name">Log in now</span>
|
||||||
|
</div>
|
||||||
|
<Avatar user={null} size={40}></Avatar>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
17
src/lib/memory/session.js
Normal file
17
src/lib/memory/session.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
/** @type {import("svelte/store").Writable<import("$types/base").User | null>} */
|
||||||
|
const activeSession = writable(null);
|
||||||
|
|
||||||
|
export default activeSession;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("$types/base").User} user
|
||||||
|
*/
|
||||||
|
export function logInAs(user) {
|
||||||
|
activeSession.set(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logOut() {
|
||||||
|
activeSession.set(null);
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { sql } from '$lib/db.server';
|
import { sql } from '$lib/db.server';
|
||||||
import { getUsersCachedByRef } from './user';
|
import { getUsersCachedByRef, sqlUserFromToken } from './user';
|
||||||
|
import { sqlOwnVote } from './vote';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('$types/base').Comment} Comment
|
* @typedef {import('$types/base').Comment} Comment
|
||||||
@ -22,17 +23,21 @@ function parseCommentFromRow(author, row) {
|
|||||||
rating: {
|
rating: {
|
||||||
likes: BigInt(row['likes']),
|
likes: BigInt(row['likes']),
|
||||||
dislikes: BigInt(row['dislikes']),
|
dislikes: BigInt(row['dislikes']),
|
||||||
}
|
},
|
||||||
|
ownVote: row['own_vote'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param {string | null} token
|
||||||
* @param {number} post_id
|
* @param {number} post_id
|
||||||
* @returns {Promise<Comment[]>}
|
* @returns {Promise<Comment[]>}
|
||||||
*/
|
*/
|
||||||
export async function getCommentsForPost(post_id) {
|
export async function getCommentsForPost(token, post_id) {
|
||||||
|
const ownVote = token ? sql`, (${ sqlOwnVote( sql`( ${sqlUserFromToken(token)} )` ) } AND message_id = id) as "own_vote"` : sql``;
|
||||||
|
|
||||||
const query = sql`
|
const query = sql`
|
||||||
SELECT id, author_id, latest_content, edit_count, parent_comment_id, created_date, likes, dislikes
|
SELECT id, author_id, latest_content, edit_count, parent_comment_id, created_date, likes, dislikes ${ ownVote }
|
||||||
FROM doki8902.message_comment
|
FROM doki8902.message_comment
|
||||||
WHERE post_id = ${ post_id };`;
|
WHERE post_id = ${ post_id };`;
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { sql } from '$lib/db.server';
|
import { sql } from '$lib/db.server';
|
||||||
import { getCategoriesCachedByRef, getCategoryCached } from './category';
|
import { getCategoriesCachedByRef, getCategoryCached } from './category';
|
||||||
import { isPostgresError } from './root';
|
import { isPostgresError } from './root';
|
||||||
import { getUser, getUsersCachedByRef, sqlUserFromToken } from './user';
|
import { getUser, getUsersCachedByRef, sqlElevatedUserFromToken, sqlUserFromToken } from './user';
|
||||||
|
import { sqlOwnVote } from './vote';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('$types/base').Post} Post
|
* @typedef {import('$types/base').Post} Post
|
||||||
@ -46,6 +47,7 @@ function parsePostFromRow(author, category, row, withMetrics = false) {
|
|||||||
dislikes: BigInt(row['dislikes']),
|
dislikes: BigInt(row['dislikes']),
|
||||||
},
|
},
|
||||||
metrics: withMetrics ? parsePostMetricsFromRow(row) : null,
|
metrics: withMetrics ? parsePostMetricsFromRow(row) : null,
|
||||||
|
ownVote: row['own_vote'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,6 +75,7 @@ export async function getPostCount(opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param {string | null} token
|
||||||
* @param {{
|
* @param {{
|
||||||
* category?: import('$types/base').Category | undefined,
|
* category?: import('$types/base').Category | undefined,
|
||||||
* limit?: number,
|
* limit?: number,
|
||||||
@ -81,7 +84,7 @@ export async function getPostCount(opts = {}) {
|
|||||||
* }} opts
|
* }} opts
|
||||||
* @returns {Promise<Post[]>}
|
* @returns {Promise<Post[]>}
|
||||||
*/
|
*/
|
||||||
export async function getPosts(opts = {}) {
|
export async function getPosts(token = null, opts = {}) {
|
||||||
const {
|
const {
|
||||||
category = undefined,
|
category = undefined,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
@ -92,8 +95,10 @@ export async function getPosts(opts = {}) {
|
|||||||
const filter = category ? sql`AND category_id = ${ category.id }` : sql``;
|
const filter = category ? sql`AND category_id = ${ category.id }` : sql``;
|
||||||
const metrics = withMetrics ? sql`, comment_count, user_count, latest_activity, engagement, age, relevancy` : sql``;
|
const metrics = withMetrics ? sql`, comment_count, user_count, latest_activity, engagement, age, relevancy` : sql``;
|
||||||
|
|
||||||
|
const ownVote = token ? sql`, (${ sqlOwnVote( sql`( ${sqlUserFromToken(token)} )` ) } AND message_id = id) as "own_vote"` : sql``;
|
||||||
|
|
||||||
const query = sql`
|
const query = sql`
|
||||||
SELECT id, author_id, name, category_id, latest_content, reviewed, edit_count, created_date, likes, dislikes ${ metrics }
|
SELECT id, author_id, name, category_id, latest_content, reviewed, edit_count, created_date, likes, dislikes ${ metrics } ${ ownVote }
|
||||||
FROM doki8902.message_post
|
FROM doki8902.message_post
|
||||||
WHERE reviewed ${ filter }
|
WHERE reviewed ${ filter }
|
||||||
FETCH FIRST ${ limit } ROWS ONLY
|
FETCH FIRST ${ limit } ROWS ONLY
|
||||||
@ -131,10 +136,12 @@ export async function getPost(post_id, token = null, opts = {}) {
|
|||||||
|
|
||||||
const metrics = withMetrics ? sql`, comment_count, user_count, latest_activity, engagement, age, relevancy` : sql``;
|
const metrics = withMetrics ? sql`, comment_count, user_count, latest_activity, engagement, age, relevancy` : sql``;
|
||||||
|
|
||||||
|
const ownVote = token ? sql`, (${ sqlOwnVote( sql`( ${sqlUserFromToken(token)} )` ) } AND message_id = id) as "own_vote"` : sql``;
|
||||||
|
|
||||||
const allowOwn = token ? sql`OR author_id = (${ sqlUserFromToken(token) })` : sql``;
|
const allowOwn = token ? sql`OR author_id = (${ sqlUserFromToken(token) })` : sql``;
|
||||||
|
|
||||||
const query = sql`
|
const query = sql`
|
||||||
SELECT id, author_id, name, category_id, latest_content, reviewed, edit_count, created_date, likes, dislikes ${ metrics }
|
SELECT id, author_id, name, category_id, latest_content, reviewed, edit_count, created_date, likes, dislikes ${ metrics } ${ ownVote }
|
||||||
FROM doki8902.message_post
|
FROM doki8902.message_post
|
||||||
WHERE id = ${ post_id } AND (reviewed ${ allowOwn });`;
|
WHERE id = ${ post_id } AND (reviewed ${ allowOwn });`;
|
||||||
|
|
||||||
@ -189,21 +196,45 @@ export async function getPost(post_id, token = null, opts = {}) {
|
|||||||
* @param {import('$types/base').Category} category
|
* @param {import('$types/base').Category} category
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {string} content
|
* @param {string} content
|
||||||
|
* @param {{
|
||||||
|
* auto_approve?: boolean
|
||||||
|
* }} opts
|
||||||
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
|
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
|
||||||
*/
|
*/
|
||||||
export async function createPost(token, category, name, content) {
|
export async function createPost(token, category, name, content, opts = {}) {
|
||||||
const insert = sql`
|
const {
|
||||||
INSERT INTO doki8902.message_post (author_id, category_id, name, latest_content)
|
auto_approve = false
|
||||||
VALUES (
|
} = opts;
|
||||||
(${ sqlUserFromToken(token) }),
|
|
||||||
${ category.id }, ${ name }, ${ content }
|
const transaction = sql.begin(async sql => {
|
||||||
)
|
if (auto_approve) {
|
||||||
RETURNING id;`;
|
const result = await sql`
|
||||||
|
INSERT INTO doki8902.message_post (author_id, category_id, name, latest_content, reviewed)
|
||||||
|
VALUES (
|
||||||
|
(${ sqlElevatedUserFromToken(token) }),
|
||||||
|
${ category.id }, ${ name }, ${ content }, TRUE
|
||||||
|
)
|
||||||
|
RETURNING id;`;
|
||||||
|
|
||||||
|
await sql`
|
||||||
|
REFRESH MATERIALIZED VIEW doki8902.message_content_latest;`;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sql`
|
||||||
|
INSERT INTO doki8902.message_post (author_id, category_id, name, latest_content)
|
||||||
|
VALUES (
|
||||||
|
(${ sqlUserFromToken(token) }),
|
||||||
|
${ category.id }, ${ name }, ${ content }
|
||||||
|
)
|
||||||
|
RETURNING id;`
|
||||||
|
})
|
||||||
|
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = await insert;
|
result = await transaction;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const pgerr = isPostgresError(e);
|
const pgerr = isPostgresError(e);
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,9 @@ function parseUserFromRow(row) {
|
|||||||
return {
|
return {
|
||||||
id: row['id'],
|
id: row['id'],
|
||||||
name: row['username'],
|
name: row['username'],
|
||||||
joinDate: row['join_time']
|
about: row['about'],
|
||||||
|
joinDate: row['join_time'],
|
||||||
|
access: row['access'],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,7 +40,23 @@ const updateUserCache = cacheUpdater(cache);
|
|||||||
* @returns {import('postgres').PendingQuery<import('postgres').Row[]>}
|
* @returns {import('postgres').PendingQuery<import('postgres').Row[]>}
|
||||||
*/
|
*/
|
||||||
export function sqlUserFromToken(token) {
|
export function sqlUserFromToken(token) {
|
||||||
return sql`SELECT user_id FROM doki8902.user_session WHERE token = ${ token }`;
|
return sql`
|
||||||
|
SELECT user_id
|
||||||
|
FROM doki8902.user_session
|
||||||
|
WHERE token = ${ token }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {import('postgres').PendingQuery<import('postgres').Row[]>}
|
||||||
|
*/
|
||||||
|
export function sqlElevatedUserFromToken(token) {
|
||||||
|
return sql`
|
||||||
|
SELECT user_id
|
||||||
|
FROM doki8902.user_session
|
||||||
|
JOIN doki8902.user ON user_id = id
|
||||||
|
WHERE token = ${ token }
|
||||||
|
AND access = 'elevated'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,7 +75,7 @@ export async function getUsers(user_ids) {
|
|||||||
if (user_ids.length == 0) return new Map();
|
if (user_ids.length == 0) return new Map();
|
||||||
|
|
||||||
const query = sql`
|
const query = sql`
|
||||||
SELECT id, username, join_time
|
SELECT id, username, about, join_time, access
|
||||||
FROM doki8902.user
|
FROM doki8902.user
|
||||||
WHERE id IN ${ sql(user_ids) };`;
|
WHERE id IN ${ sql(user_ids) };`;
|
||||||
|
|
||||||
@ -84,7 +102,9 @@ export async function getUser(user_id) {
|
|||||||
|
|
||||||
return users.get(user_id) || {
|
return users.get(user_id) || {
|
||||||
error: true,
|
error: true,
|
||||||
msg: `Could not find user of ID ${user_id}`
|
title: 'User not found',
|
||||||
|
msg: `Could not find user of ID ${user_id}`,
|
||||||
|
expected: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,6 +172,32 @@ export async function createUser(username, password) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
|
||||||
|
*/
|
||||||
|
export async function deleteUser(token) {
|
||||||
|
const del = sql`
|
||||||
|
DELETE FROM doki8902.user
|
||||||
|
WHERE id = (${ sqlUserFromToken(token) });`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
title: 'Fail to process',
|
||||||
|
msg: 'Unknown error (notify dev)',
|
||||||
|
expected: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} username
|
* @param {string} username
|
||||||
* @param {string} password
|
* @param {string} password
|
||||||
@ -200,6 +246,32 @@ export async function createUserSession(username, password) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
|
||||||
|
*/
|
||||||
|
export async function removeUserSession(token) {
|
||||||
|
const del = sql`
|
||||||
|
DELETE FROM doki8902.user_session
|
||||||
|
WHERE token = ${ token };`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
title: 'Fail to process',
|
||||||
|
msg: 'Unknown error (notify dev)',
|
||||||
|
expected: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} token
|
* @param {string} token
|
||||||
* @returns {Promise<number | import('$types/status').Error>}
|
* @returns {Promise<number | import('$types/status').Error>}
|
||||||
@ -230,9 +302,10 @@ export async function getUserIDOfSession(token) {
|
|||||||
*/
|
*/
|
||||||
export async function getUserOfSession(token) {
|
export async function getUserOfSession(token) {
|
||||||
const query = sql`
|
const query = sql`
|
||||||
SELECT user_id
|
SELECT id, username, about, join_time, access
|
||||||
FROM doki8902.user_session
|
FROM doki8902.user_session
|
||||||
WHERE token = ${ token }`;
|
JOIN doki8902.user ON user_id = id
|
||||||
|
WHERE token = ${ token };`;
|
||||||
|
|
||||||
const result = await query;
|
const result = await query;
|
||||||
|
|
||||||
@ -245,5 +318,100 @@ export async function getUserOfSession(token) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result[0]['user_id'];
|
return parseUserFromRow(result[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @param {string} about
|
||||||
|
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
|
||||||
|
*/
|
||||||
|
export async function updateUserAbout(token, about) {
|
||||||
|
const update = sql`
|
||||||
|
UPDATE doki8902.user
|
||||||
|
SET about = ${ about }
|
||||||
|
WHERE id = (${ sqlUserFromToken(token) });`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await update;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
title: 'Fail to process',
|
||||||
|
msg: 'Unknown error (notify dev)',
|
||||||
|
expected: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @param {string} currentPassword
|
||||||
|
* @param {string} newPassword
|
||||||
|
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
|
||||||
|
*/
|
||||||
|
export async function changeUserPassword(token, currentPassword, newPassword) {
|
||||||
|
const select = sql`
|
||||||
|
SELECT password, id
|
||||||
|
FROM doki8902.user_session
|
||||||
|
JOIN doki8902.user ON id = user_id
|
||||||
|
WHERE token = ${ token };`;
|
||||||
|
|
||||||
|
const result = await select;
|
||||||
|
|
||||||
|
if (result.length == 0) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
title: 'Invalid data',
|
||||||
|
msg: 'Username or password is incorrect',
|
||||||
|
expected: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = result[0]['password'];
|
||||||
|
|
||||||
|
const isMatch = await verify(hashedPassword, currentPassword);
|
||||||
|
|
||||||
|
if (!isMatch) {
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
title: 'Invalid data',
|
||||||
|
msg: 'Username or password is incorrect',
|
||||||
|
expected: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newHashedPassword = await hash(newPassword, {
|
||||||
|
type: argon2id,
|
||||||
|
memoryCost: 2 ** 16,
|
||||||
|
timeCost: 4,
|
||||||
|
parallelism: 1,
|
||||||
|
hashLength: 64,
|
||||||
|
});
|
||||||
|
|
||||||
|
const update = sql`
|
||||||
|
UPDATE doki8902.user
|
||||||
|
SET password = ${ newHashedPassword }
|
||||||
|
WHERE id = (${ result[0]['id'] });`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await update;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
title: 'Fail to process',
|
||||||
|
msg: 'Unknown error (notify dev)',
|
||||||
|
expected: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
107
src/lib/server/db/vote.js
Normal file
107
src/lib/server/db/vote.js
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import { sql } from '$lib/db.server';
|
||||||
|
import { sqlUserFromToken } from './user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number | import('postgres').PendingQuery<import('postgres').Row[]>} user_id
|
||||||
|
* @returns {import('postgres').PendingQuery<import('postgres').Row[]>}
|
||||||
|
*/
|
||||||
|
export function sqlOwnVote(user_id) {
|
||||||
|
return sql`
|
||||||
|
SELECT vote
|
||||||
|
FROM doki8902.message_vote
|
||||||
|
WHERE user_id = ${ user_id }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @param {number} message_id
|
||||||
|
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
|
||||||
|
*/
|
||||||
|
export async function getVote(token, message_id) {
|
||||||
|
const insert = await sql`
|
||||||
|
SELECT vote
|
||||||
|
FROM doki8902.message_vote
|
||||||
|
WHERE user_id = ${ sqlUserFromToken(token) }
|
||||||
|
AND message_id = ${ message_id };`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [result] = await insert;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
result: result,
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
title: 'Fail to process',
|
||||||
|
msg: 'Unknown error (notify dev)',
|
||||||
|
expected: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @param {number} message_id
|
||||||
|
* @param {number} value
|
||||||
|
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
|
||||||
|
*/
|
||||||
|
export async function createVote(token, message_id, value) {
|
||||||
|
const insert = await sql`
|
||||||
|
INSERT INTO doki8902.message_vote (message_id, user_id, vote)
|
||||||
|
VALUES (
|
||||||
|
${ message_id },
|
||||||
|
(${ sqlUserFromToken(token) }),
|
||||||
|
${ value }
|
||||||
|
)
|
||||||
|
ON CONFLICT (message_id, user_id) DO UPDATE
|
||||||
|
SET vote = ${ value };`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await insert;
|
||||||
|
|
||||||
|
console.log(result);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
title: 'Fail to process',
|
||||||
|
msg: 'Unknown error (notify dev)',
|
||||||
|
expected: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} token
|
||||||
|
* @param {number} message_id
|
||||||
|
* @returns {Promise<import('$types/status').Success | import('$types/status').Error>}
|
||||||
|
*/
|
||||||
|
export async function removeVote(token, message_id) {
|
||||||
|
const del = await sql`
|
||||||
|
DELETE FROM doki8902.message_vote
|
||||||
|
WHERE user_id = (${ sqlUserFromToken(token) })
|
||||||
|
AND message_id = ${ message_id };`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await del;
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e);
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
title: 'Fail to process',
|
||||||
|
msg: 'Unknown error (notify dev)',
|
||||||
|
expected: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -1,10 +1,16 @@
|
|||||||
import { getCategories } from "$lib/server/db/category";
|
import { getCategories } from "$lib/server/db/category";
|
||||||
|
import { getUserOfSession } from "$lib/server/db/user";
|
||||||
|
|
||||||
/** @type {import("@sveltejs/kit").ServerLoad} */
|
/** @type {import("@sveltejs/kit").ServerLoad} */
|
||||||
export async function load() {
|
export async function load({ cookies }) {
|
||||||
|
const token = cookies.get('token')?.toString() ?? null;
|
||||||
|
|
||||||
const categories = await getCategories();
|
const categories = await getCategories();
|
||||||
|
|
||||||
|
const loggedInUser = token ? await getUserOfSession(token) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories: categories
|
categories: categories,
|
||||||
|
loggedInUser: loggedInUser
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,36 @@
|
|||||||
<script>
|
<script>
|
||||||
import Sidebar from "$comp/sidebar.svelte";
|
import Sidebar from "$comp/sidebar.svelte";
|
||||||
|
import Topbar from "$comp/topbar.svelte";
|
||||||
|
import { logInAs, logOut } from "$lib/memory/session";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {{
|
* @type {{
|
||||||
* categories: import("$types/base").Category[]
|
* categories: import("$types/base").Category[],
|
||||||
|
* loggedInUser: import("$types/base").User | null
|
||||||
* }}
|
* }}
|
||||||
*/
|
*/
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
$: categories = data.categories;
|
$: categories = data.categories;
|
||||||
|
$: loggedInUser = data.loggedInUser;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (loggedInUser) {
|
||||||
|
logInAs(loggedInUser);
|
||||||
|
} else {
|
||||||
|
logOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.content {
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sideContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
/* width: calc(100% - 16px); */
|
/* width: calc(100% - 16px); */
|
||||||
@ -32,8 +50,12 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<Sidebar categories={categories}></Sidebar>
|
<Topbar></Topbar>
|
||||||
<main>
|
|
||||||
<slot />
|
<div class="sideContent">
|
||||||
</main>
|
<Sidebar categories={categories}></Sidebar>
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,6 +33,7 @@ async function POST({ request, cookies }) {
|
|||||||
const categoryId = parseIntNull(data.get('category')?.toString());
|
const categoryId = parseIntNull(data.get('category')?.toString());
|
||||||
const name = data.get('name')?.toString();
|
const name = data.get('name')?.toString();
|
||||||
const content = data.get('content')?.toString();
|
const content = data.get('content')?.toString();
|
||||||
|
const autoapprove = data.get('autoapprove')?.toString() === 'on';
|
||||||
|
|
||||||
if (!categoryId) {
|
if (!categoryId) {
|
||||||
return fail(400, {
|
return fail(400, {
|
||||||
@ -60,7 +61,9 @@ async function POST({ request, cookies }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await createPost(userToken, category, name, content);
|
const result = await createPost(userToken, category, name, content, {
|
||||||
|
auto_approve: autoapprove
|
||||||
|
});
|
||||||
|
|
||||||
runIfSuccess(result, (success) => {
|
runIfSuccess(result, (success) => {
|
||||||
redirect(303, `/posts/${success.result}`);
|
redirect(303, `/posts/${success.result}`);
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { applyAction, enhance } from "$app/forms";
|
import { applyAction, enhance } from "$app/forms";
|
||||||
import { page } from "$app/stores";
|
import { page } from "$app/stores";
|
||||||
|
import activeSession from "$lib/memory/session";
|
||||||
import toasts, { addToast } from "$lib/memory/toast";
|
import toasts, { addToast } from "$lib/memory/toast";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,6 +63,13 @@
|
|||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkboxInput {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@ -95,6 +103,13 @@
|
|||||||
<label for="content">Content</label>
|
<label for="content">Content</label>
|
||||||
<textarea name="content" id="content" rows="4" placeholder="write something nice..."></textarea>
|
<textarea name="content" id="content" rows="4" placeholder="write something nice..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $activeSession?.access == 'elevated'}
|
||||||
|
<div class="checkboxInput">
|
||||||
|
<input type="checkbox" name="autoapprove" id="autoapprove">
|
||||||
|
<label for="autoapprove">Automatically approve? (This is an administrator feature)</label>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="typeTitle">Post!</button>
|
<button type="submit" class="typeTitle">Post!</button>
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import { parseIntNull } from '$lib/util';
|
|||||||
|
|
||||||
|
|
||||||
/** @type {import('./$types').PageServerLoad} */
|
/** @type {import('./$types').PageServerLoad} */
|
||||||
export async function load({ url }) {
|
export async function load({ cookies, url }) {
|
||||||
|
const userToken = cookies.get('token');
|
||||||
|
|
||||||
const count = await getPostCount();
|
const count = await getPostCount();
|
||||||
|
|
||||||
const page = parseIntNull(url.searchParams.get('page')) ?? 0;
|
const page = parseIntNull(url.searchParams.get('page')) ?? 0;
|
||||||
@ -11,7 +13,7 @@ export async function load({ url }) {
|
|||||||
|
|
||||||
const pageSize = Math.min(Math.max(items, 1), 30);
|
const pageSize = Math.min(Math.max(items, 1), 30);
|
||||||
|
|
||||||
const result = await getPosts({
|
const result = await getPosts(userToken, {
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
offset: pageSize * page,
|
offset: pageSize * page,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export async function load({ params, cookies }) {
|
|||||||
error(404, postError.msg);
|
error(404, postError.msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
const comments = await getCommentsForPost(post_id);
|
const comments = await getCommentsForPost(token, post_id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
post: post,
|
post: post,
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { getUser } from "$lib/server/db/user";
|
import { changeUserPassword, createUserSession, deleteUser, getUser, updateUserAbout } from "$lib/server/db/user";
|
||||||
|
import { runIfError } from "$lib/status";
|
||||||
|
import { errorToFail } from "$lib/status.server";
|
||||||
import { getIdFromName } from "$lib/util";
|
import { getIdFromName } from "$lib/util";
|
||||||
import { error } from "@sveltejs/kit";
|
import { error, fail, redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
/** @type {import("@sveltejs/kit").ServerLoad} */
|
/** @type {import("@sveltejs/kit").ServerLoad} */
|
||||||
export async function load({ params }) {
|
export async function load({ params }) {
|
||||||
@ -19,4 +21,91 @@ export async function load({ params }) {
|
|||||||
return {
|
return {
|
||||||
user: user
|
user: user
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/kit").Action} */
|
||||||
|
async function changeAboutAction({ request, cookies }) {
|
||||||
|
const userToken = cookies.get('token');
|
||||||
|
|
||||||
|
if (!userToken) {
|
||||||
|
return fail(401, {
|
||||||
|
error: true,
|
||||||
|
title: 'Invalid session',
|
||||||
|
msg: 'Need to be logged in to perform this operation',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await request.formData();
|
||||||
|
const about = data.get('about')?.toString();
|
||||||
|
|
||||||
|
if (!about) {
|
||||||
|
return fail(400, {
|
||||||
|
error: true,
|
||||||
|
title: 'Bad data',
|
||||||
|
msg: 'About cannot be empty',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await updateUserAbout(userToken, about);
|
||||||
|
|
||||||
|
return runIfError(result, (error) => {
|
||||||
|
return errorToFail(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/kit").Action} */
|
||||||
|
async function deleteAccountAction({ cookies }) {
|
||||||
|
const userToken = cookies.get('token');
|
||||||
|
|
||||||
|
if (userToken) {
|
||||||
|
await deleteUser(userToken);
|
||||||
|
cookies.delete('token', { path: '/' })
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(302, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/kit").Action} */
|
||||||
|
async function changePasswordAction({ request, cookies }) {
|
||||||
|
const userToken = cookies.get('token');
|
||||||
|
|
||||||
|
if (!userToken) {
|
||||||
|
return fail(401, {
|
||||||
|
error: true,
|
||||||
|
title: 'Invalid session',
|
||||||
|
msg: 'Need to be logged in to perform this operation',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await request.formData();
|
||||||
|
const oldPassword = data.get('oldpassword')?.toString();
|
||||||
|
const newPassword = data.get('newpassword')?.toString();
|
||||||
|
|
||||||
|
if (!oldPassword || !newPassword) {
|
||||||
|
return fail(400, {
|
||||||
|
error: true,
|
||||||
|
title: 'Bad data',
|
||||||
|
msg: 'Fields cannot be empty',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userToken) {
|
||||||
|
const result = await changeUserPassword(userToken, oldPassword, newPassword);
|
||||||
|
|
||||||
|
if ('error' in result) {
|
||||||
|
return runIfError(result, (error) => {
|
||||||
|
return errorToFail(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cookies.delete('token', { path: '/' });
|
||||||
|
redirect(302, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/kit").Actions} */
|
||||||
|
export let actions = {
|
||||||
|
changeAbout: changeAboutAction,
|
||||||
|
deleteAccount: deleteAccountAction,
|
||||||
|
changePassword: changePasswordAction,
|
||||||
|
};
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { enhance } from "$app/forms";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
import Avatar from "$comp/avatar.svelte";
|
||||||
|
import Glowfx from "$comp/fx/glowfx.svelte";
|
||||||
|
import activeSession from "$lib/memory/session";
|
||||||
import { gotoNamedId } from "$lib/util";
|
import { gotoNamedId } from "$lib/util";
|
||||||
|
import moment from "moment";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,7 +20,184 @@
|
|||||||
onMount(() => {
|
onMount(() => {
|
||||||
gotoNamedId(user.id, user.name);
|
gotoNamedId(user.id, user.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** @type {HTMLInputElement} */
|
||||||
|
let aboutInput;
|
||||||
|
|
||||||
|
let showAboutModal = false;
|
||||||
|
|
||||||
|
function updateAboutMe() {
|
||||||
|
if ($activeSession) {
|
||||||
|
$activeSession.about = aboutInput.value;
|
||||||
|
}
|
||||||
|
showAboutModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let showPasswordModal = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1>{data.user.name}</h1>
|
<style lang="scss">
|
||||||
<p>{data.user.joinDate}</p>
|
.user {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
gap: 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.joinTime {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.me {
|
||||||
|
position: relative;
|
||||||
|
padding: 32px;
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
background: var(--gray-dim);
|
||||||
|
border-radius: 16px;
|
||||||
|
gap: 32px;
|
||||||
|
align-items: normal;
|
||||||
|
|
||||||
|
> .text {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
|
||||||
|
> h4 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> .list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionItem {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.button {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalBg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
background: #0005;
|
||||||
|
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.aboutModal {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(-50%);
|
||||||
|
|
||||||
|
background: var(--gray-dim);
|
||||||
|
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
border-radius: 16px;
|
||||||
|
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0px 10px 32px #0005;
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<link rel="stylesheet" href="/css/form.scss">
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
{#if showAboutModal}
|
||||||
|
<div class="modalBg"></div>
|
||||||
|
<div class="aboutModal">
|
||||||
|
<form action="?/changeAbout" method="post" use:enhance on:submit={updateAboutMe}>
|
||||||
|
<label for="about">New About me</label>
|
||||||
|
<input type="text" name="about" id="about" bind:this={aboutInput}>
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button formButton" type="button" on:click={() => {showAboutModal = false;}}>Cancel</button>
|
||||||
|
<button class="button formButton" type="submit">Save</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showPasswordModal}
|
||||||
|
<div class="modalBg"></div>
|
||||||
|
<div class="aboutModal">
|
||||||
|
<form action="?/changePassword" method="post" use:enhance on:submit={() => {showPasswordModal = false;}}>
|
||||||
|
<label for="oldpassword">Current password</label>
|
||||||
|
<input type="password" name="oldpassword" id="oldpassword" autocomplete="current-password">
|
||||||
|
<label for="newpassword">New password</label>
|
||||||
|
<input type="password" name="newpassword" id="newpassword" autocomplete="new-password">
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="button formButton" type="button" on:click={() => {showPasswordModal = false;}}>Cancel</button>
|
||||||
|
<button class="button formButton" type="submit">Change</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="user">
|
||||||
|
<Avatar user={user} size={128}></Avatar>
|
||||||
|
<h1 class="typeTitle">@ {user.name}</h1>
|
||||||
|
<span>{user.about}</span>
|
||||||
|
<p class="joinTime">Joined {moment(user.joinDate).fromNow()}</p>
|
||||||
|
|
||||||
|
{#if user.id == $activeSession?.id}
|
||||||
|
<Glowfx borderRadius={16}>
|
||||||
|
<div class="me">
|
||||||
|
<div class="text">
|
||||||
|
<h4 class="typeTitle">Self-actions</h4>
|
||||||
|
<span>These actions are only available to you as the account owner</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list">
|
||||||
|
<div class="actionItem">
|
||||||
|
<span>Change my about me</span>
|
||||||
|
<form action="">
|
||||||
|
<button class="typeTitle button formButton" type="button" on:click={() => {showAboutModal = true;}}>Edit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actionItem">
|
||||||
|
<span>Change my current password</span>
|
||||||
|
<form action="">
|
||||||
|
<button class="typeTitle button formButton" type="button" on:click={() => {showPasswordModal = true;}}>Change</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actionItem">
|
||||||
|
<span>Log out of the current session</span>
|
||||||
|
<form action="">
|
||||||
|
<button class="typeTitle button formButton" type="button" on:click={() => {goto('/logout')}}>Log out</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actionItem">
|
||||||
|
<span>Permanently delete the account</span>
|
||||||
|
<form action="?/deleteAccount" method="post">
|
||||||
|
<button class="typeTitle button formButton" type="submit">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Glowfx>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|||||||
79
src/routes/(app)/vote/+page.server.js
Normal file
79
src/routes/(app)/vote/+page.server.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { createUserSession } from "$lib/server/db/user";
|
||||||
|
import { createVote, removeVote } from "$lib/server/db/vote";
|
||||||
|
import { runIfError } from "$lib/status";
|
||||||
|
import { errorToFail } from "$lib/status.server";
|
||||||
|
import { parseIntNull } from "$lib/util";
|
||||||
|
import { fail, redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/kit").Action} */
|
||||||
|
async function createVoteAction({ url, cookies }) {
|
||||||
|
const userToken = cookies.get('token');
|
||||||
|
|
||||||
|
if (!userToken) {
|
||||||
|
return fail(401, {
|
||||||
|
error: true,
|
||||||
|
title: 'Invalid session',
|
||||||
|
msg: 'Need to be logged in to perform this operation',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageID = parseIntNull(url.searchParams.get('messageid'));
|
||||||
|
const value = parseIntNull(url.searchParams.get('value'));
|
||||||
|
|
||||||
|
if (value != 1 && value != -1) {
|
||||||
|
return fail(400, {
|
||||||
|
error: true,
|
||||||
|
title: 'Bad data',
|
||||||
|
msg: 'Value can only be 1 or -1',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!messageID) {
|
||||||
|
return fail(400, {
|
||||||
|
error: true,
|
||||||
|
title: 'Bad data',
|
||||||
|
msg: 'MessageID cannot be empty',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await createVote(userToken, messageID, value);
|
||||||
|
|
||||||
|
return runIfError(result, (error) => {
|
||||||
|
return errorToFail(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/kit").Action} */
|
||||||
|
async function removeVoteAction({ url, cookies }) {
|
||||||
|
const userToken = cookies.get('token');
|
||||||
|
|
||||||
|
if (!userToken) {
|
||||||
|
return fail(401, {
|
||||||
|
error: true,
|
||||||
|
title: 'Invalid session',
|
||||||
|
msg: 'Need to be logged in to perform this operation',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageID = parseIntNull(url.searchParams.get('messageid'));
|
||||||
|
|
||||||
|
if (!messageID) {
|
||||||
|
return fail(400, {
|
||||||
|
error: true,
|
||||||
|
title: 'Bad data',
|
||||||
|
msg: 'MessageID cannot be empty',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await removeVote(userToken, messageID);
|
||||||
|
|
||||||
|
return runIfError(result, (error) => {
|
||||||
|
return errorToFail(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/kit").Actions} */
|
||||||
|
export let actions = {
|
||||||
|
create: createVoteAction,
|
||||||
|
remove: removeVoteAction,
|
||||||
|
};
|
||||||
16
src/routes/(entry)/logout/+page.server.js
Normal file
16
src/routes/(entry)/logout/+page.server.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { createUserSession, removeUserSession } from "$lib/server/db/user";
|
||||||
|
import { runIfError } from "$lib/status";
|
||||||
|
import { errorToFail } from "$lib/status.server";
|
||||||
|
import { fail, redirect } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/kit").ServerLoad} */
|
||||||
|
export async function load({ cookies }) {
|
||||||
|
const userToken = cookies.get('token');
|
||||||
|
|
||||||
|
if (userToken) {
|
||||||
|
await removeUserSession(userToken);
|
||||||
|
cookies.delete('token', { path: '/' })
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(302, '/');
|
||||||
|
}
|
||||||
19
src/routes/api/user/+server.js
Normal file
19
src/routes/api/user/+server.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { jsonSerialize } from "$lib/serialize/base";
|
||||||
|
import { getUser } from "$lib/server/db/user";
|
||||||
|
import { parseIntNull } from "$lib/util";
|
||||||
|
import { error } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
/** @type {import("@sveltejs/kit").Action} */
|
||||||
|
export async function GET({ url }) {
|
||||||
|
const user_id = parseIntNull(url.searchParams.get('id'));
|
||||||
|
|
||||||
|
if (user_id === null) {
|
||||||
|
error(404, `No User of ID ${url.searchParams.get('id')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await getUser(user_id);
|
||||||
|
|
||||||
|
return new Response(jsonSerialize({
|
||||||
|
user: user,
|
||||||
|
}));
|
||||||
|
}
|
||||||
@ -3,7 +3,9 @@ export type Result<T> = Map<number, T>;
|
|||||||
export type User = {
|
export type User = {
|
||||||
id: number,
|
id: number,
|
||||||
name: string,
|
name: string,
|
||||||
|
about: string,
|
||||||
joinDate: Date,
|
joinDate: Date,
|
||||||
|
access: 'normal' | 'elevated'
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Rating = {
|
export type Rating = {
|
||||||
@ -37,6 +39,7 @@ export type Post = {
|
|||||||
postDate: Date,
|
postDate: Date,
|
||||||
rating: Rating,
|
rating: Rating,
|
||||||
metrics: PostMetrics | null,
|
metrics: PostMetrics | null,
|
||||||
|
ownVote: number | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Comment = {
|
export type Comment = {
|
||||||
@ -48,6 +51,7 @@ export type Comment = {
|
|||||||
commentDate: Date,
|
commentDate: Date,
|
||||||
rating: Rating,
|
rating: Rating,
|
||||||
parentCommentId: number,
|
parentCommentId: number,
|
||||||
|
ownVote: number | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CommentTreeNode = {
|
export type CommentTreeNode = {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
||||||
--background: #111118;
|
--background: #111118;
|
||||||
|
--background-black: #060608c0;
|
||||||
|
|
||||||
--accent-gray: #b182a3;
|
--accent-gray: #b182a3;
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,8 @@ form {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 32px;
|
gap: 32px;
|
||||||
|
|
||||||
button[type=submit] {
|
button[type=submit],
|
||||||
|
.formButton {
|
||||||
background: var(--accent-dim);
|
background: var(--accent-dim);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.3em 0.8em;
|
padding: 0.3em 0.8em;
|
||||||
|
|||||||
1
static/icon/heart-fill.svg
Normal file
1
static/icon/heart-fill.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-heart"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19.5 12.572l-7.5 7.428l-7.5 -7.428a5 5 0 1 1 7.5 -6.566a5 5 0 1 1 7.5 6.572" /></svg>
|
||||||
|
After Width: | Height: | Size: 420 B |
1
static/icon/thumb-down-fill.svg
Normal file
1
static/icon/thumb-down-fill.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="icon" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-thumb-down"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 13v-8a1 1 0 0 0 -1 -1h-2a1 1 0 0 0 -1 1v7a1 1 0 0 0 1 1h3a4 4 0 0 1 4 4v1a2 2 0 0 0 4 0v-5h3a2 2 0 0 0 2 -2l-1 -5a2 3 0 0 0 -2 -2h-7a3 3 0 0 0 -3 3" /></svg>
|
||||||
|
After Width: | Height: | Size: 499 B |
Loading…
x
Reference in New Issue
Block a user