Refactor: Better PostMetric API and Rendering

This commit is contained in:
Donatas Kirda 2024-05-15 18:07:40 +03:00
parent 63ab10318f
commit 499fd6fa5b
Signed by: bloodwiing
GPG Key ID: 63020D8D3F4A164F
13 changed files with 185 additions and 154 deletions

View File

@ -1,5 +1,6 @@
<script>
import Rating from "./rating.svelte";
import Iconvalue from "./iconvalue.svelte";
import Rating from "./rating.svelte";
/**
* @type {import("$types/base").Post | import("$types/base").Comment}
@ -14,9 +15,11 @@
width: 100%;
flex: 1;
position: relative;
gap: 24px;
}
</style>
<div class="actionBar">
<Rating rating={message.rating}></Rating>
<Rating rating={message.rating} style="gap: 24px;"></Rating>
<slot />
</div>

32
src/comp/iconvalue.svelte Normal file
View File

@ -0,0 +1,32 @@
<script>
import Tablericon from "./tablericon.svelte";
/**
* @type {string | undefined}
*/
export let name = undefined;
/**
* @type {string}
*/
export let iconName;
/**
* @type {number}
*/
export let iconSize = 16;
</script>
<style type="scss">
.pair {
display: flex;
flex-flow: row nowrap;
gap: 8px;
align-items: center;
}
</style>
<div class="pair" title={name}>
<Tablericon name={iconName} size={iconSize} color="white"></Tablericon>
<span><slot /></span>
</div>

View File

@ -3,7 +3,11 @@
import Ago from "$comp/ago.svelte";
import Avatar from "$comp/avatar.svelte";
import CommentList from "$comp/commentlist.svelte";
import Iconvalue from "$comp/iconvalue.svelte";
import Mention from "$comp/mention.svelte";
import Tablericon from "$comp/tablericon.svelte";
import { round10 } from "expected-round";
import moment from "moment";
/**
* @type {import("$types/base").Post}
@ -15,7 +19,17 @@
*/
export let commentTree = null;
$: isPreview = commentTree == null;
/**
* @type {boolean}
*/
export let showMetrics = false;
let showStats = false;
function togglePostStats(/** @type {MouseEvent | TouchEvent} */ e) {
e.stopPropagation();
showStats = !showStats;
}
</script>
<style lang="scss">
@ -69,6 +83,40 @@
justify-content: space-between;
width: 100%;
}
.title {
text-align: left;
padding: 0.3em 0;
line-height: 1.4;
}
.metricsSpacer {
flex: 1;
}
.stats {
color: var(--white-dim);
& > table {
text-align: left;
font-size: 0.8em;
& td {
padding: 0;
&:first-child {
padding-right: 1em;
}
}
}
}
.statsTitle {
color: var(--white-dim);
font-size: 0.8em;
text-align: left;
padding-left: 0.1em;
}
</style>
<div class="body">
@ -80,7 +128,7 @@
<Mention user={post.author}></Mention>
<Ago date={post.postDate}></Ago>
</div>
<h1 class="typeTitle">{post.name}</h1>
<h1 class="typeTitle title">{post.name}</h1>
</div>
</div>
@ -88,7 +136,37 @@
{post.content}
</p>
<Actionbar message={post}></Actionbar>
<Actionbar message={post}>
{#if showMetrics && post.metrics}
<Iconvalue name="Comment Count" iconName="message">{post.metrics.commentCount}</Iconvalue>
<Iconvalue name="User Count" iconName="user">{post.metrics.userCount}</Iconvalue>
<Iconvalue name="Relevancy" iconName="chart-bar">{round10(post.metrics.relevancy, -2)}</Iconvalue>
{/if}
<div class="metricsSpacer"></div>
<button class="button transparent" on:click={togglePostStats}>
<Iconvalue name="More detailed metrics" iconName="database">stats</Iconvalue>
</button>
</Actionbar>
{#if showStats}
<div class="stats">
<h6 class="statsTitle">Debug stats</h6>
<table>
<tr><td>ID</td><td>{post.id}</td></tr>
<tr><td>Author</td><td>{post.author?.id}</td></tr>
<tr><td>Created Date</td><td>{post.postDate}</td></tr>
<tr><td>Score</td><td>{post.rating.likes - post.rating.dislikes}</td></tr>
{#if post.metrics}
<tr><td>Comment Count</td><td>{post.metrics.commentCount}</td></tr>
<tr><td>User Count</td><td>{post.metrics.userCount}</td></tr>
<tr><td>Latest Activity</td><td>{post.metrics.latestActivity}</td></tr>
<tr><td>Engagement</td><td>{post.metrics.engagement}</td></tr>
<tr><td>Age</td><td>{post.metrics.age}</td></tr>
<tr><td>Relevancy</td><td>{round10(post.metrics.relevancy, -5)}</td></tr>
{/if}
</table>
</div>
{/if}
</article>
{#if commentTree}

View File

@ -9,8 +9,10 @@
*/
export let posts = [];
/** @type {import("$types/base").Post | null} */
$: currentPreview = null;
/**
* @type {import('$types/base').Post | null}
*/
export let glancePost = null;
/** @type {boolean[]} */
$: hidden = Array(posts.length).fill(false);
@ -19,33 +21,33 @@
/** @type {import("$types/base").Post} */
const post = e.detail.post;
currentPreview = post;
posts.forEach((p, index) => {
hidden[index] = p == post;
});
goto(`/posts?glance=${currentPreview.id}`, {
goto(`/posts?glance=${post.id}`, {
replaceState: true,
noScroll: true
});
}
function dismiss() {
currentPreview = null;
function dismiss(/** @type {MouseEvent | TouchEvent} */ e) {
e.stopPropagation();
hidden = Array(posts.length).fill(false);
// goto(`/posts`, {
// replaceState: true,
// noScroll: true
// });
goto(`/posts`, {
replaceState: true,
noScroll: true
});
}
function expand() {
if (currentPreview == null) return;
function expand(/** @type {MouseEvent | TouchEvent} */ e) {
if (glancePost == null) return;
e.stopPropagation();
goto(`/posts/${getNamedId(currentPreview.id, currentPreview.name)}`, {
goto(`/posts/${getNamedId(glancePost.id, glancePost.name)}`, {
replaceState: false
});
}
@ -109,10 +111,10 @@
{/each}
</div>
<button class="postPreview" data-visible={currentPreview != null || null} on:click={dismiss}>
<button class="postPreview" data-visible={glancePost != null || null} on:click={dismiss}>
<button class="preview" on:click={expand}>
{#if currentPreview}
<Post post={currentPreview} commentTree={null}></Post>
{#if glancePost}
<Post post={glancePost} commentTree={null} showMetrics={true}></Post>
{/if}
</button>
</button>

View File

@ -1,4 +1,6 @@
<script>
import Iconvalue from "./iconvalue.svelte";
/**
* @type {import("$types/base").Rating}
*/
@ -10,28 +12,13 @@
display: flex;
flex-flow: row nowrap;
gap: 10px;
gap: 24px;
align-items: center;
}
svg.icon {
height: 16px;
width: 16px;
& > use {
color: white;
}
}
</style>
<div class="rating">
<svg class="icon" viewBox="0 0 24 24">
<use href="/icon/heart.svg#icon"></use>
</svg>
<span>{rating.likes}</span>
<svg class="icon" viewBox="0 0 24 24">
<use href="/icon/thumb-down.svg#icon"></use>
</svg>
<span>{rating.dislikes}</span>
<div class="rating" {...$$restProps}>
<Iconvalue name="Likes" iconName="heart">{rating.likes}</Iconvalue>
<Iconvalue name="Dislikes" iconName="thumb-down">{rating.dislikes}</Iconvalue>
</div>

View File

@ -46,15 +46,24 @@ function parsePostFromRow(author, category, row, withMetrics = false) {
/**
* @param {import('postgres').Sql} sql
* @param {import('$types/base').Category | undefined} category
* @param {number} limit
* @param {number} offset
* @param {boolean} withMetrics
* @param {{
* category?: import('$types/base').Category | undefined,
* limit?: number,
* offset?: number,
* withMetrics?: boolean
* }} opts
* @returns {Promise<Post[]>}
*/
export async function getPosts(sql, category = undefined, limit = 10, offset = 0, withMetrics = false) {
const filter = category === undefined ? sql`` : sql`WHERE category_id = ${ category.id }`;
const metrics = !withMetrics ? sql`` : sql`, comment_count, user_count, latest_activity, engagement, age, relevancy`;
export async function getPosts(sql, opts = {}) {
const {
category = undefined,
limit = 10,
offset = 0,
withMetrics = false,
} = opts;
const filter = category ? sql`WHERE category_id = ${ category.id }` : sql``;
const metrics = withMetrics ? sql`, comment_count, user_count, latest_activity, engagement, age, relevancy` : sql``;
const query = sql`
SELECT id, author_id, name, category_id, latest_content, created_date, likes, dislikes ${ metrics }
@ -83,11 +92,20 @@ export async function getPosts(sql, category = undefined, limit = 10, offset = 0
*
* @param {import('postgres').Sql} sql
* @param {number} post_id
* @param {{
* withMetrics?: boolean
* }} opts
* @returns {Promise<Post | import('$types/error').Error>}
*/
export async function getPost(sql, post_id) {
export async function getPost(sql, post_id, opts = {}) {
const {
withMetrics = false
} = opts;
const metrics = withMetrics ? sql`, comment_count, user_count, latest_activity, engagement, age, relevancy` : sql``;
const query = sql`
SELECT id, author_id, name, category_id, latest_content, created_date, likes, dislikes
SELECT id, author_id, name, category_id, latest_content, created_date, likes, dislikes ${ metrics }
FROM doki8902.message_post
WHERE id = ${ post_id };`;
@ -130,5 +148,5 @@ export async function getPost(sql, post_id) {
/**
* @type {Post}
*/
return parsePostFromRow(author, category, post);
return parsePostFromRow(author, category, post, withMetrics);
}

View File

@ -1,89 +0,0 @@
// /**
// * @typedef {import('$types/base').PostMetrics} PostMetrics
// */
// import { sql } from '$lib/db.server';
// /**
// * @param {import('$types/base').Post} post
// * @param {import('postgres').Row} row
// * @returns {PostMetrics}
// */
// function parsePostMetricsFromRowAndPost(post, row) {
// return {
// postId: post.id,
// commentCount: BigInt(row['comment_count']),
// userCount: BigInt(row['user_count']),
// latestActivity: row['latest_activity'],
// rating: post.rating,
// score: BigInt(row['score']),
// engagement: BigInt(row['engagement']),
// age: row['age'],
// relevancy: row['relevancy']
// };
// }
// /**
// * @param {import('postgres').Row} row
// * @returns {PostMetrics}
// */
// function parsePostMetricsFromRow(row) {
// return {
// postId: row['post_id'],
// commentCount: BigInt(row['comment_count']),
// userCount: BigInt(row['user_count']),
// latestActivity: row['latest_activity'],
// rating: {
// likes: row['likes'],
// dislikes: row['dislikes']
// },
// score: BigInt(row['score']),
// engagement: BigInt(row['engagement']),
// age: row['age'],
// relevancy: row['relevancy']
// };
// }
// /**
// * @param {import('$types/base').Post} post
// * @returns {Promise<PostMetrics | import('$types/error').Error>}
// */
// export async function getPostMetricsForPost(post) {
// const query = sql`
// SELECT comment_count, user_count, latest_activity, score, engagement, age, relevancy
// FROM doki8902.post_metrics
// WHERE post_id = ${ post.id }`;
// const result = await query;
// if (result.length == 0) {
// return {
// error: true,
// msg: `Could not find PostMetrics for Post ID ${post.id}`
// };
// }
// return parsePostMetricsFromRowAndPost(post, result[0]);
// }
// /**
// * @param {number} post_id
// * @returns {Promise<PostMetrics | import('$types/error').Error>}
// */
// export async function getPostMetricsForPostId(post_id) {
// const query = sql`
// SELECT post_id, comment_count, user_count, latest_activity, likes, dislikes, score, engagement, age, relevancy
// FROM doki8902.post_metrics
// WHERE post_id = ${ post_id }`;
// const result = await query;
// if (result.length == 0) {
// return {
// error: true,
// msg: `Could not find PostMetrics for Post ID ${post_id}`
// };
// }
// return parsePostMetricsFromRow(result[0]);
// }

View File

@ -1,22 +1,17 @@
import { getPosts } from '$lib/server/db/post';
import { getPostMetricsForPostId } from '$lib/server/db/postmertics';
import { getPost, getPosts } from '$lib/server/db/post';
/** @type {import('./$types').PageServerLoad} */
export async function load({ locals, url }) {
const result = await getPosts(locals.sql, undefined, 10, 0, true);
const result = await getPosts(locals.sql);
// const glance = url.searchParams.get('glance');
// const glanceID = glance ? parseInt(glance) : null
const glance = url.searchParams.get('glance');
const glanceID = glance ? parseInt(glance) : null
// let postMetrics = null;
// if (glanceID) {
// postMetrics = await getPostMetricsForPostId(glanceID);
// }
console.log(result);
const glancePost = glanceID ? await getPost(locals.sql, glanceID, { withMetrics: true }) : null;
return {
posts: result
posts: result,
glancePost: glancePost
};
}

View File

@ -3,11 +3,12 @@
/**
* @type {{
* posts: import("$types/base").Post[]
* posts: import("$types/base").Post[],
* glancePost: import("$types/base").Post | null
* }}
*/
export let data;
</script>
<h1>Posts</h1>
<Postlist posts={data.posts}></Postlist>
<Postlist posts={data.posts} glancePost={data.glancePost}></Postlist>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chart-bar"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M9 8m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M15 4m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v14a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" /><path d="M4 20l14 0" /></svg>

After

Width:  |  Height:  |  Size: 621 B

1
static/icon/database.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-database"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 6m-8 0a8 3 0 1 0 16 0a8 3 0 1 0 -16 0" /><path d="M4 6v6a8 3 0 0 0 16 0v-6" /><path d="M4 12v6a8 3 0 0 0 16 0v-6" /></svg>

After

Width:  |  Height:  |  Size: 455 B

1
static/icon/message.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-message"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 9h8" /><path d="M8 13h6" /><path d="M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3h12z" /></svg>

After

Width:  |  Height:  |  Size: 465 B

1
static/icon/user.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" id="icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-user"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /></svg>

After

Width:  |  Height:  |  Size: 422 B