Refactor: Better PostMetric API and Rendering
This commit is contained in:
parent
63ab10318f
commit
499fd6fa5b
@ -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
32
src/comp/iconvalue.svelte
Normal 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>
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
goto(`/posts/${getNamedId(currentPreview.id, currentPreview.name)}`, {
|
||||
e.stopPropagation();
|
||||
|
||||
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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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]);
|
||||
// }
|
||||
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
1
static/icon/chart-bar.svg
Normal file
1
static/icon/chart-bar.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="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
1
static/icon/database.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="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
1
static/icon/message.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="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
1
static/icon/user.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="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 |
Loading…
x
Reference in New Issue
Block a user