Added basic comments.
This commit is contained in:
parent
9a9b92a6b1
commit
0d5744e3ff
|
@ -1,14 +1,16 @@
|
||||||
.input {
|
.input {
|
||||||
|
box-sizing: border-box;
|
||||||
padding: .5rem .75rem;
|
padding: .5rem .75rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
flex-basis: 0;
|
flex-basis: 0;
|
||||||
border: solid 1px var(--grey-light-30);
|
border: solid 1px var(--grey-light-30);
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
|
background: var(--grey-light-60);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--primary-light-30);
|
border-color: var(--primary-light-50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
--primary-light-10: hsl(300, 50%, 40%);
|
--primary-light-10: hsl(300, 50%, 40%);
|
||||||
--primary-light-20: hsl(300, 50%, 50%);
|
--primary-light-20: hsl(300, 50%, 50%);
|
||||||
--primary-light-30: hsl(300, 50%, 60%);
|
--primary-light-30: hsl(300, 50%, 60%);
|
||||||
|
--primary-light-40: hsl(300, 50%, 75%);
|
||||||
|
--primary-light-50: hsl(300, 50%, 80%);
|
||||||
|
|
||||||
--grey-dark-40: #222;
|
--grey-dark-40: #222;
|
||||||
--grey-dark-30: #444;
|
--grey-dark-30: #444;
|
||||||
|
@ -13,6 +15,8 @@
|
||||||
--grey-light-20: #ccc;
|
--grey-light-20: #ccc;
|
||||||
--grey-light-30: #ddd;
|
--grey-light-30: #ddd;
|
||||||
--grey-light-40: #eee;
|
--grey-light-40: #eee;
|
||||||
|
--grey-light-50: #fafafa;
|
||||||
|
--grey-light-60: #fcfcfc;
|
||||||
|
|
||||||
--background-dark-20: #eee;
|
--background-dark-20: #eee;
|
||||||
--background-dark-10: #f8f8f8;
|
--background-dark-10: #f8f8f8;
|
||||||
|
|
|
@ -15,7 +15,7 @@ function getQuery(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get(path, query = {}) {
|
export async function get(path, query = {}) {
|
||||||
const res = await fetch(`${path}${getQuery(query)}`);
|
const res = await fetch(`/api${path}${getQuery(query)}`);
|
||||||
const body = await res.json();
|
const body = await res.json();
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
@ -26,7 +26,7 @@ export async function get(path, query = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function post(path, data, { query } = {}) {
|
export async function post(path, data, { query } = {}) {
|
||||||
const res = await fetch(`${path}${getQuery(query)}`, {
|
const res = await fetch(`/api${path}${getQuery(query)}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
...postHeaders,
|
...postHeaders,
|
||||||
|
@ -46,7 +46,7 @@ export async function post(path, data, { query } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function patch(path, data, { query } = {}) {
|
export async function patch(path, data, { query } = {}) {
|
||||||
const res = await fetch(`${path}${getQuery(query)}`, {
|
const res = await fetch(`/api${path}${getQuery(query)}`, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
...postHeaders,
|
...postHeaders,
|
||||||
|
@ -66,7 +66,7 @@ export async function patch(path, data, { query } = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function del(path, { data, query } = {}) {
|
export async function del(path, { data, query } = {}) {
|
||||||
const res = await fetch(`${path}${getQuery(query)}`, {
|
const res = await fetch(`/api${path}${getQuery(query)}`, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
...postHeaders,
|
...postHeaders,
|
||||||
|
|
|
@ -2,3 +2,7 @@
|
||||||
export default function navigate(path) {
|
export default function navigate(path) {
|
||||||
window.location.href = path;
|
window.location.href = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function reload() {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<div class="comment">
|
||||||
|
<div class="header">
|
||||||
|
<a
|
||||||
|
:href="`/user/${comment.user.username}`"
|
||||||
|
class="username link"
|
||||||
|
>u/{{ comment.user.username }}</a>
|
||||||
|
|
||||||
|
<span
|
||||||
|
:title="format(comment.createdAt, 'MMM d, yyyy hh:mm:ss')"
|
||||||
|
class="timestamp"
|
||||||
|
>{{ formatDistance(comment.createdAt, now, { includeSeconds: true }) }} ago</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="body">{{ comment.body }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { format, formatDistance } from 'date-fns';
|
||||||
|
import { usePageContext } from '../../renderer/usePageContext';
|
||||||
|
|
||||||
|
const { now } = usePageContext();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
comment: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.comment {
|
||||||
|
background: var(--background);
|
||||||
|
padding: .5rem;
|
||||||
|
border-radius: .25rem;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-size: .9rem;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
color: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timestamp {
|
||||||
|
color: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,24 +1,36 @@
|
||||||
<template>
|
<template>
|
||||||
|
<div class="post">
|
||||||
<a
|
<a
|
||||||
:href="`/s/shack/posts/${post.id}`"
|
:href="post.link || `/s/${post.shelf.slug}/post/${post.id}`"
|
||||||
class="post"
|
target="_blank"
|
||||||
|
class="title-link"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="thumbnail"
|
class="thumbnail"
|
||||||
:src="blockedIcon"
|
:src="blockedIcon"
|
||||||
>
|
>
|
||||||
|
</a>
|
||||||
|
|
||||||
<div class="body">
|
<div class="body">
|
||||||
|
<div class="header">
|
||||||
<h2 class="title">
|
<h2 class="title">
|
||||||
<a
|
<a
|
||||||
:href="`/s/shack/posts/${post.id}`"
|
:href="`/s/${post.shelf.slug}/post/${post.id}`"
|
||||||
class="link"
|
class="title-link"
|
||||||
>{{ post.title }}</a>
|
>{{ post.title }}</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
<a
|
||||||
|
v-if="post.link"
|
||||||
|
:href="post.link"
|
||||||
|
target="_blank"
|
||||||
|
class="link"
|
||||||
|
>{{ post.link }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<a
|
<a
|
||||||
:href="`/user/${post.shelf.slug}`"
|
:href="`/s/${post.shelf.slug}`"
|
||||||
class="shelf link"
|
class="shelf link"
|
||||||
>s/{{ post.shelf.slug }}</a>
|
>s/{{ post.shelf.slug }}</a>
|
||||||
|
|
||||||
|
@ -30,15 +42,30 @@
|
||||||
<span
|
<span
|
||||||
:title="format(post.createdAt, 'MMMM d, yyyy hh:mm:ss')"
|
:title="format(post.createdAt, 'MMMM d, yyyy hh:mm:ss')"
|
||||||
class="timestamp"
|
class="timestamp"
|
||||||
>{{ formatDistance(post.createdAt, new Date(), { includeSeconds: true }) }} ago</span>
|
>{{ formatDistance(post.createdAt, now, { includeSeconds: true }) }} ago</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<a
|
||||||
|
:href="`/s/shack/post/${post.id}`"
|
||||||
|
class="link comments"
|
||||||
|
>{{ post.commentCount }} comments</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
|
<a
|
||||||
|
:href="`/s/shack/post/${post.id}`"
|
||||||
|
class="fill"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { format, formatDistance } from 'date-fns';
|
import { format, formatDistance } from 'date-fns';
|
||||||
import blockedIcon from '../../assets/icons/blocked.svg?url'; // eslint-ignore import/no-unresolved
|
import blockedIcon from '../../assets/icons/blocked.svg?url'; // eslint-disable-line import/no-unresolved
|
||||||
|
import { usePageContext } from '../../renderer/usePageContext';
|
||||||
|
|
||||||
|
const { now } = usePageContext();
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
post: {
|
post: {
|
||||||
|
@ -53,6 +80,7 @@ defineProps({
|
||||||
display: flex;
|
display: flex;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
|
margin-bottom: .25rem;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
||||||
|
@ -69,17 +97,23 @@ defineProps({
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.header {
|
||||||
padding: .5rem 0;
|
display: flex;
|
||||||
margin: 0;
|
align-items: center;
|
||||||
font-size: 1.25rem;
|
}
|
||||||
font-weight: normal;
|
|
||||||
color: var(--grey-dark-30);
|
|
||||||
|
|
||||||
.link {
|
.title {
|
||||||
|
display: inline-block;
|
||||||
|
padding: .25rem 0;
|
||||||
|
margin: .25rem 1rem 0 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--grey-dark-30);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-link {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail {
|
.thumbnail {
|
||||||
|
@ -96,9 +130,14 @@ defineProps({
|
||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
margin-bottom: .25rem;
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.shelf {
|
.shelf {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -107,4 +146,13 @@ defineProps({
|
||||||
.timestamp {
|
.timestamp {
|
||||||
color: var(--grey-dark-20);
|
color: var(--grey-dark-20);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.comments {
|
||||||
|
color: inherit;
|
||||||
|
font-size: .9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fill {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -83,7 +83,7 @@ export async function up(knex) {
|
||||||
.notNullable();
|
.notNullable();
|
||||||
|
|
||||||
table.text('body');
|
table.text('body');
|
||||||
table.text('url');
|
table.text('link');
|
||||||
|
|
||||||
table.integer('shelf_id')
|
table.integer('shelf_id')
|
||||||
.notNullable()
|
.notNullable()
|
||||||
|
@ -100,7 +100,7 @@ export async function up(knex) {
|
||||||
.defaultTo(knex.fn.now());
|
.defaultTo(knex.fn.now());
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex.raw('ALTER TABLE posts ADD CONSTRAINT post_content CHECK (body IS NOT NULL OR url IS NOT NULL)');
|
await knex.raw('ALTER TABLE posts ADD CONSTRAINT post_content CHECK (body IS NOT NULL OR link IS NOT NULL)');
|
||||||
|
|
||||||
await knex.schema.createTable('comments', (table) => {
|
await knex.schema.createTable('comments', (table) => {
|
||||||
table.text('id', 8)
|
table.text('id', 8)
|
||||||
|
@ -112,6 +112,10 @@ export async function up(knex) {
|
||||||
.references('id')
|
.references('id')
|
||||||
.inTable('posts');
|
.inTable('posts');
|
||||||
|
|
||||||
|
table.integer('parent_comment_id')
|
||||||
|
.references('id')
|
||||||
|
.inTable('comments');
|
||||||
|
|
||||||
table.integer('user_id')
|
table.integer('user_id')
|
||||||
.notNullable()
|
.notNullable()
|
||||||
.references('id')
|
.references('id')
|
||||||
|
|
|
@ -104,7 +104,7 @@ const errorMsg = ref(null);
|
||||||
|
|
||||||
async function signup() {
|
async function signup() {
|
||||||
try {
|
try {
|
||||||
await post('/api/users', {
|
await post('/users', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
|
|
|
@ -76,7 +76,7 @@ const errorMsg = ref(null);
|
||||||
|
|
||||||
async function signup() {
|
async function signup() {
|
||||||
try {
|
try {
|
||||||
await post('/api/session', {
|
await post('/session', {
|
||||||
username: username.value,
|
username: username.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,15 +1,11 @@
|
||||||
import { fetchShelves } from '../../src/shelves';
|
import { fetchShelves } from '../../src/shelves';
|
||||||
|
|
||||||
async function onBeforeRender(_pageContext) {
|
async function getPageData() {
|
||||||
const shelves = await fetchShelves();
|
const shelves = await fetchShelves();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
|
||||||
pageData: {
|
|
||||||
shelves,
|
shelves,
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { onBeforeRender };
|
export { getPageData };
|
||||||
|
|
|
@ -1,17 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a
|
|
||||||
href="/shelf/1"
|
|
||||||
class="link"
|
|
||||||
>Go to shelf</a></li>
|
|
||||||
|
|
||||||
<li><a
|
<li><a
|
||||||
href="/shelf/create"
|
href="/shelf/create"
|
||||||
class="link"
|
class="link"
|
||||||
>Create new shelf</a></li>
|
>Create new shelf</a></li>
|
||||||
|
|
||||||
<li><a
|
<li v-if="!user"><a
|
||||||
href="/account/login"
|
href="/account/login"
|
||||||
class="link"
|
class="link"
|
||||||
>Log in</a></li>
|
>Log in</a></li>
|
||||||
|
@ -37,6 +32,6 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { usePageContext } from '../../renderer/usePageContext';
|
import { usePageContext } from '../../renderer/usePageContext';
|
||||||
|
|
||||||
const { pageData } = usePageContext();
|
const { user, pageData } = usePageContext();
|
||||||
const { shelves } = pageData;
|
const { shelves } = pageData;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export default '/s/@id/post/@postId';
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { RenderErrorPage } from 'vite-plugin-ssr/RenderErrorPage';
|
||||||
|
import { fetchShelf } from '../../src/shelves';
|
||||||
|
import { fetchPost } from '../../src/posts';
|
||||||
|
import { fetchPostComments } from '../../src/comments';
|
||||||
|
|
||||||
|
async function getPageData(pageContext) {
|
||||||
|
const [shelf, post, comments] = await Promise.all([
|
||||||
|
fetchShelf(pageContext.routeParams.id),
|
||||||
|
fetchPost(pageContext.routeParams.postId),
|
||||||
|
fetchPostComments(pageContext.routeParams.postId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!shelf) {
|
||||||
|
throw RenderErrorPage({
|
||||||
|
pageContext: {
|
||||||
|
pageProps: {
|
||||||
|
errorInfo: 'This shelf does not exist',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
throw RenderErrorPage({
|
||||||
|
pageContext: {
|
||||||
|
pageProps: {
|
||||||
|
errorInfo: 'This post does not exist',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
shelf,
|
||||||
|
post,
|
||||||
|
comments,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getPageData };
|
|
@ -0,0 +1,107 @@
|
||||||
|
<template>
|
||||||
|
<div class="page">
|
||||||
|
<div class="body">
|
||||||
|
<Post :post="post" />
|
||||||
|
|
||||||
|
<form
|
||||||
|
class="writer"
|
||||||
|
@submit.prevent="addComment"
|
||||||
|
>
|
||||||
|
<textarea
|
||||||
|
v-model="newComment"
|
||||||
|
placeholder="Write a new comment"
|
||||||
|
class="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="button">Comment</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<ul class="comments nolist">
|
||||||
|
<li
|
||||||
|
v-for="comment in comments"
|
||||||
|
:key="comment.id"
|
||||||
|
><Comment :comment="comment" /></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
{{ shelf.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import Post from '../../components/posts/post.vue';
|
||||||
|
import Comment from '../../components/comments/comment.vue';
|
||||||
|
import * as api from '../../assets/js/api';
|
||||||
|
import { reload } from '../../assets/js/navigate';
|
||||||
|
|
||||||
|
import { usePageContext } from '../../renderer/usePageContext';
|
||||||
|
|
||||||
|
const { pageData } = usePageContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
shelf,
|
||||||
|
post,
|
||||||
|
comments,
|
||||||
|
} = pageData;
|
||||||
|
|
||||||
|
const newComment = ref('');
|
||||||
|
|
||||||
|
async function addComment() {
|
||||||
|
await api.post(`/posts/${post.id}/comments`, {
|
||||||
|
body: newComment.value,
|
||||||
|
parentId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.page {
|
||||||
|
display: flex;
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0 0 .5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post {
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.writer {
|
||||||
|
width: 40rem;
|
||||||
|
background: var(--background);
|
||||||
|
padding: .5rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
margin-bottom: .5rem;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background: var(--background);
|
||||||
|
width: 20rem;
|
||||||
|
padding: .5rem;
|
||||||
|
border-radius: .5rem;
|
||||||
|
margin-left: .5rem;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -2,7 +2,7 @@ import { RenderErrorPage } from 'vite-plugin-ssr/RenderErrorPage';
|
||||||
import { fetchShelf } from '../../../src/shelves';
|
import { fetchShelf } from '../../../src/shelves';
|
||||||
import { fetchShelfPosts } from '../../../src/posts';
|
import { fetchShelfPosts } from '../../../src/posts';
|
||||||
|
|
||||||
async function onBeforeRender(pageContext) {
|
async function getPageData(pageContext) {
|
||||||
const shelf = await fetchShelf(pageContext.routeParams.id);
|
const shelf = await fetchShelf(pageContext.routeParams.id);
|
||||||
const posts = await fetchShelfPosts(pageContext.routeParams.id, { limit: 50 });
|
const posts = await fetchShelfPosts(pageContext.routeParams.id, { limit: 50 });
|
||||||
|
|
||||||
|
@ -17,13 +17,9 @@ async function onBeforeRender(pageContext) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
|
||||||
pageData: {
|
|
||||||
shelf,
|
shelf,
|
||||||
posts,
|
posts,
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export { onBeforeRender };
|
export { getPageData };
|
||||||
|
|
|
@ -1,10 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
class="link"
|
|
||||||
>Go back home</a>
|
|
||||||
|
|
||||||
<h3>{{ shelf.slug }}</h3>
|
<h3>{{ shelf.slug }}</h3>
|
||||||
|
|
||||||
<ul class="posts nolist">
|
<ul class="posts nolist">
|
||||||
|
|
|
@ -137,7 +137,7 @@ const postAccess = ref('registered');
|
||||||
const isNsfw = ref(false);
|
const isNsfw = ref(false);
|
||||||
|
|
||||||
async function create() {
|
async function create() {
|
||||||
await post('/api/shelves', {
|
await post('/shelves', {
|
||||||
slug: slug.value,
|
slug: slug.value,
|
||||||
title: title.value,
|
title: title.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
|
|
|
@ -12,7 +12,7 @@ async function render(pageContext) {
|
||||||
const title = (documentProps && documentProps.title) || 'shack';
|
const title = (documentProps && documentProps.title) || 'shack';
|
||||||
const desc = (documentProps && documentProps.description) || 'Shack';
|
const desc = (documentProps && documentProps.description) || 'Shack';
|
||||||
|
|
||||||
const { app, store } = createApp(pageContext);
|
const { app } = createApp(pageContext);
|
||||||
const stream = renderToNodeStream(app);
|
const stream = renderToNodeStream(app);
|
||||||
|
|
||||||
const documentHtml = escapeInject`
|
const documentHtml = escapeInject`
|
||||||
|
@ -38,14 +38,28 @@ async function render(pageContext) {
|
||||||
return {
|
return {
|
||||||
documentHtml,
|
documentHtml,
|
||||||
pageContext: {
|
pageContext: {
|
||||||
initialState: store.state.value,
|
// initialState: store.state.value,
|
||||||
enableEagerStreaming: true,
|
enableEagerStreaming: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onBeforeRender(pageContext) {
|
||||||
|
const pageData = await pageContext.exports.getPageData?.(pageContext, pageContext.session);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageContext: {
|
||||||
|
// initialState: store.state.value,
|
||||||
|
pageData,
|
||||||
|
user: pageContext.session.user,
|
||||||
|
now: new Date(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
render,
|
render,
|
||||||
|
onBeforeRender,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const passToClient = ['urlPathname', 'initialState', 'pageData', 'pageProps', 'routeParams'];
|
export const passToClient = ['urlPathname', 'initialState', 'pageData', 'pageProps', 'routeParams', 'user', 'now'];
|
||||||
|
|
|
@ -33,20 +33,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
// import { onMounted } from 'vue';
|
import logo from '../assets/img/logo.svg?raw'; // eslint-disable-line import/no-unresolved
|
||||||
// import { usePageContext } from './usePageContext';
|
|
||||||
import { storeToRefs } from 'pinia';
|
|
||||||
import logo from '../assets/img/logo.svg?raw';
|
|
||||||
|
|
||||||
import { del } from '../assets/js/api';
|
import { del } from '../assets/js/api';
|
||||||
import { useUser } from '../stores/user';
|
import { usePageContext } from './usePageContext';
|
||||||
|
|
||||||
// const pageContext = usePageContext();
|
const { user } = usePageContext();
|
||||||
const version = CLIENT_VERSION;
|
const version = CLIENT_VERSION;
|
||||||
|
|
||||||
const userStore = useUser();
|
|
||||||
const { user } = storeToRefs(userStore);
|
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
await del('/api/session');
|
await del('/api/session');
|
||||||
window.location.href = '/account/login';
|
window.location.href = '/account/login';
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import knex from './knex';
|
||||||
|
import { fetchUsers } from './users';
|
||||||
|
import { verifyPrivilege } from './privileges';
|
||||||
|
|
||||||
|
function curateDatabaseComment(comment, { users }) {
|
||||||
|
return {
|
||||||
|
id: comment.id,
|
||||||
|
body: comment.body,
|
||||||
|
userId: comment.user_id,
|
||||||
|
user: users.find((user) => user.id === comment.user_id),
|
||||||
|
createdAt: comment.created_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPostComments(postId, { limit = 100 } = {}) {
|
||||||
|
const comments = await knex('comments')
|
||||||
|
.where('post_id', postId)
|
||||||
|
.orderBy('created_at', 'asc')
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
const users = await fetchUsers(comments.map((comment) => comment.user_id));
|
||||||
|
|
||||||
|
return comments.map((comment) => curateDatabaseComment(comment, { users }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addComment(comment, postId, user) {
|
||||||
|
await verifyPrivilege('addComment', user);
|
||||||
|
|
||||||
|
const commentEntry = await knex('comments')
|
||||||
|
.insert({
|
||||||
|
body: comment.body,
|
||||||
|
post_id: postId,
|
||||||
|
user_id: user.id,
|
||||||
|
})
|
||||||
|
.returning('*');
|
||||||
|
|
||||||
|
console.log(comment, user);
|
||||||
|
return curateDatabaseComment(commentEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
fetchPostComments,
|
||||||
|
addComment,
|
||||||
|
};
|
53
src/posts.js
53
src/posts.js
|
@ -3,36 +3,62 @@ import { verifyPrivilege } from './privileges';
|
||||||
import knex from './knex';
|
import knex from './knex';
|
||||||
import { HttpError } from './errors';
|
import { HttpError } from './errors';
|
||||||
|
|
||||||
import { fetchShelf, curateDatabaseShelf } from './shelves';
|
import { fetchShelf } from './shelves';
|
||||||
import { curateDatabaseUser } from './users';
|
import { fetchUsers } from './users';
|
||||||
|
|
||||||
function curatePost(post) {
|
function curatePost(post, { shelf, users }) {
|
||||||
const curatedPost = {
|
const curatedPost = {
|
||||||
id: post.id,
|
id: post.id,
|
||||||
title: post.title,
|
title: post.title,
|
||||||
body: post.body,
|
body: post.body,
|
||||||
url: post.url,
|
link: post.link,
|
||||||
shelfId: post.shelf_id,
|
shelfId: post.shelf_id,
|
||||||
createdAt: post.created_at,
|
createdAt: post.created_at,
|
||||||
shelf: curateDatabaseShelf(post.shelf),
|
shelf,
|
||||||
user: curateDatabaseUser(post.user),
|
user: users.find((user) => user.id === post.user_id),
|
||||||
|
commentCount: Number(post.comment_count),
|
||||||
};
|
};
|
||||||
|
|
||||||
return curatedPost;
|
return curatedPost;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchShelfPosts(shelfId, limit = 100) {
|
async function fetchShelfPosts(shelfId, { limit = 100 } = {}) {
|
||||||
const shelf = await fetchShelf(shelfId);
|
const shelf = await fetchShelf(shelfId);
|
||||||
|
|
||||||
const posts = await knex('posts')
|
const posts = await knex('posts')
|
||||||
.select('posts.*', knex.raw('row_to_json(users) as user'), knex.raw('row_to_json(shelves) as shelf'))
|
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
|
||||||
.leftJoin('users', 'users.id', 'posts.user_id')
|
.leftJoin('comments', 'comments.post_id', 'posts.id')
|
||||||
.leftJoin('shelves', 'shelves.id', 'posts.shelf_id')
|
|
||||||
.where('shelf_id', shelf.id)
|
.where('shelf_id', shelf.id)
|
||||||
.orderBy('created_at', 'desc')
|
.orderBy('created_at', 'desc')
|
||||||
|
.groupBy('posts.id')
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
return posts.map((post) => curatePost(post));
|
const users = await fetchUsers(posts.map((post) => post.user_id));
|
||||||
|
|
||||||
|
return posts.map((post) => curatePost(post, { shelf, users }));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchPost(postId) {
|
||||||
|
const post = await knex('posts')
|
||||||
|
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
|
||||||
|
.leftJoin('comments', 'comments.post_id', 'posts.id')
|
||||||
|
.where('posts.id', postId)
|
||||||
|
.groupBy('posts.id')
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusMessage: 'This post does not exist',
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [shelf, users] = await Promise.all([
|
||||||
|
fetchShelf(post.shelf_id),
|
||||||
|
fetchUsers([post.user_id]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return curatePost(post, { shelf, users });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPost(post, shelfId, user) {
|
async function createPost(post, shelfId, user) {
|
||||||
|
@ -47,11 +73,13 @@ async function createPost(post, shelfId, user) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(post);
|
||||||
|
|
||||||
const postId = await knex('posts')
|
const postId = await knex('posts')
|
||||||
.insert({
|
.insert({
|
||||||
title: post.title,
|
title: post.title,
|
||||||
body: post.body,
|
body: post.body,
|
||||||
url: post.url,
|
link: post.link,
|
||||||
shelf_id: shelf.id,
|
shelf_id: shelf.id,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
})
|
})
|
||||||
|
@ -62,5 +90,6 @@ async function createPost(post, shelfId, user) {
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createPost,
|
createPost,
|
||||||
|
fetchPost,
|
||||||
fetchShelfPosts,
|
fetchShelfPosts,
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,7 @@ function curateDatabaseShelf(shelf) {
|
||||||
return {
|
return {
|
||||||
id: shelf.id,
|
id: shelf.id,
|
||||||
slug: shelf.slug,
|
slug: shelf.slug,
|
||||||
|
name: shelf.slug,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,12 @@ async function login(credentials) {
|
||||||
return curateDatabaseUser(user);
|
return curateDatabaseUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchUsers(userIds) {
|
||||||
|
const users = await knex('users').whereIn('id', userIds);
|
||||||
|
|
||||||
|
return users.map((user) => curateDatabaseUser(user));
|
||||||
|
}
|
||||||
|
|
||||||
async function createUser(credentials, context) {
|
async function createUser(credentials, context) {
|
||||||
if (!credentials.username) {
|
if (!credentials.username) {
|
||||||
throw new HttpError({
|
throw new HttpError({
|
||||||
|
@ -145,5 +151,6 @@ async function createUser(credentials, context) {
|
||||||
export {
|
export {
|
||||||
curateDatabaseUser,
|
curateDatabaseUser,
|
||||||
createUser,
|
createUser,
|
||||||
|
fetchUsers,
|
||||||
login,
|
login,
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { addComment } from '../comments';
|
||||||
|
|
||||||
|
async function addCommentApi(req, res) {
|
||||||
|
const comment = await addComment(req.body, req.params.postId, req.user);
|
||||||
|
|
||||||
|
res.send(comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
addCommentApi as addComment,
|
||||||
|
};
|
|
@ -21,8 +21,6 @@ export default async function initDefaultHandler() {
|
||||||
|
|
||||||
const body = await httpResponse.getBody();
|
const body = await httpResponse.getBody();
|
||||||
|
|
||||||
console.log(pageContext.pageData);
|
|
||||||
|
|
||||||
if (res.writeEarlyHints) {
|
if (res.writeEarlyHints) {
|
||||||
res.writeEarlyHints({ link: earlyHints.map((e) => e.earlyHintLink) });
|
res.writeEarlyHints({ link: earlyHints.map((e) => e.earlyHintLink) });
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {
|
||||||
import { createShelf } from './shelves';
|
import { createShelf } from './shelves';
|
||||||
|
|
||||||
import { createPost } from './posts';
|
import { createPost } from './posts';
|
||||||
|
import { addComment } from './comments';
|
||||||
|
|
||||||
const logger = initLogger();
|
const logger = initLogger();
|
||||||
|
|
||||||
|
@ -70,6 +71,9 @@ async function startServer() {
|
||||||
// POSTS
|
// POSTS
|
||||||
router.post('/api/shelves/:shelfId/posts', createPost);
|
router.post('/api/shelves/:shelfId/posts', createPost);
|
||||||
|
|
||||||
|
// COMMENTS
|
||||||
|
router.post('/api/posts/:postId/comments', addComment);
|
||||||
|
|
||||||
router.get('*', defaultHandler);
|
router.get('*', defaultHandler);
|
||||||
router.use(errorHandler);
|
router.use(errorHandler);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue