Added voting. Improved comments.
This commit is contained in:
parent
754a89b913
commit
f42daa2f83
|
@ -0,0 +1,235 @@
|
|||
<template>
|
||||
<div class="post">
|
||||
<div class="votes">
|
||||
<div
|
||||
class="vote bump"
|
||||
:class="{ active: post.hasBump }"
|
||||
@click="vote(1)"
|
||||
>+</div>
|
||||
|
||||
<div class="tally">{{ tally }}</div>
|
||||
|
||||
<div
|
||||
class="vote sink"
|
||||
:class="{ active: post.hasSink }"
|
||||
@click="vote(-1)"
|
||||
>-</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="post.link || `/s/${post.shelf.slug}/post/${post.id}`"
|
||||
target="_blank"
|
||||
class="title-link"
|
||||
>
|
||||
<img
|
||||
class="thumbnail"
|
||||
:src="blockedIcon"
|
||||
>
|
||||
</a>
|
||||
|
||||
<div class="body">
|
||||
<div class="header">
|
||||
<h2 class="title">
|
||||
<a
|
||||
:href="`/s/${post.shelf.slug}/post/${post.id}`"
|
||||
class="title-link"
|
||||
>{{ post.title }}</a>
|
||||
</h2>
|
||||
|
||||
<a
|
||||
v-if="post.link"
|
||||
:href="post.link"
|
||||
target="_blank"
|
||||
class="link"
|
||||
>{{ post.link }}</a>
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
<a
|
||||
:href="`/s/${post.shelf.slug}`"
|
||||
class="shelf link"
|
||||
>s/{{ post.shelf.slug }}</a>
|
||||
|
||||
<a
|
||||
:href="`/user/${post.user.username}`"
|
||||
class="username link"
|
||||
>u/{{ post.user.username }}</a>
|
||||
|
||||
<span
|
||||
:title="format(post.createdAt, 'MMMM d, yyyy hh:mm:ss')"
|
||||
class="timestamp"
|
||||
>{{ formatDistance(post.createdAt, now, { includeSeconds: true }) }} ago</span>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<a
|
||||
:href="`/s/${post.shelf.slug}/post/${post.id}`"
|
||||
class="link comments"
|
||||
>{{ post.commentCount }} comments</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="`/s/${post.shelf.slug}/post/${post.id}`"
|
||||
class="fill"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
|
||||
import blockedIcon from '../../assets/icons/blocked.svg?url'; // eslint-disable-line import/no-unresolved
|
||||
import { usePageContext } from '../../renderer/usePageContext';
|
||||
import * as api from '../../assets/js/api';
|
||||
|
||||
const { now } = usePageContext();
|
||||
|
||||
const props = defineProps({
|
||||
post: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
shelf: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const tally = ref(props.post.tally);
|
||||
|
||||
async function vote(value) {
|
||||
const effect = props.post.hasBump || props.post.hasSink ? 0 : value;
|
||||
|
||||
await api.post(`/posts/${props.post.id}/votes`, { value: effect });
|
||||
|
||||
tally.value += effect;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.post {
|
||||
display: flex;
|
||||
color: var(--text);
|
||||
border-radius: .25rem;
|
||||
margin-bottom: .25rem;
|
||||
background: var(--background);
|
||||
text-decoration: none;
|
||||
|
||||
& :hover {
|
||||
cursor: pointer;
|
||||
|
||||
.title {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
width: 7rem;
|
||||
height: 4rem;
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
border-radius: .25rem;
|
||||
margin: .5rem;
|
||||
background: var(--grey-light-10);
|
||||
opacity: .25;
|
||||
}
|
||||
|
||||
.votes {
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vote,
|
||||
.tally {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vote {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1rem;
|
||||
box-sizing: border-box;
|
||||
padding: .6rem;
|
||||
margin: .25rem 0;
|
||||
border-radius: .5rem;
|
||||
background: var(--shadow-weak-40);
|
||||
}
|
||||
|
||||
.tally {
|
||||
color: var(--shadow-strong-20);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.bump.active {
|
||||
color: var(--text-light);
|
||||
background: var(--bump);
|
||||
}
|
||||
|
||||
.sink.active {
|
||||
color: var(--text-light);
|
||||
background: var(--sink);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: .25rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.shelf {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--grey-dark-20);
|
||||
}
|
||||
|
||||
.comments {
|
||||
color: inherit;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.fill {
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
|
@ -41,6 +41,21 @@
|
|||
}
|
||||
}
|
||||
|
||||
.button-cancel {
|
||||
background: none;
|
||||
color: var(--shadow-strong-10);
|
||||
font-weight: normal;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--error);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--shadow-weak-10);
|
||||
}
|
||||
}
|
||||
|
||||
.radio {
|
||||
margin: 0 .5rem 0 0;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,23 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nolink-active {
|
||||
display: inline-block;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.nobutton {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nobar {
|
||||
scrollbar-width: none;
|
||||
-mis-overflow-style: none;
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
--background-dark-10: #f8f8f8;
|
||||
--background: #fff;
|
||||
|
||||
--shadow-weak-40: rgba(0, 0, 0, .05);
|
||||
--shadow-weak-30: rgba(0, 0, 0, .1);
|
||||
--shadow-weak-20: rgba(0, 0, 0, .2);
|
||||
--shadow-weak-10: rgba(0, 0, 0, .35);
|
||||
|
@ -35,4 +36,9 @@
|
|||
|
||||
--link: #48f;
|
||||
--error: #f66;
|
||||
|
||||
--bump: var(--primary);
|
||||
--sink: var(--error);
|
||||
|
||||
--op: #5be;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ function getQuery(data) {
|
|||
return `?${new URLSearchParams(data).toString()}`;
|
||||
}
|
||||
|
||||
export async function get(path, query = {}) {
|
||||
async function get(path, query = {}) {
|
||||
const res = await fetch(`/api${path}${getQuery(query)}`);
|
||||
const body = await res.json();
|
||||
|
||||
|
@ -25,7 +25,7 @@ export async function get(path, query = {}) {
|
|||
throw new Error(body.message);
|
||||
}
|
||||
|
||||
export async function post(path, data, { query } = {}) {
|
||||
async function post(path, data, { query } = {}) {
|
||||
const res = await fetch(`/api${path}${getQuery(query)}`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
|
@ -45,7 +45,7 @@ export async function post(path, data, { query } = {}) {
|
|||
throw new Error(body.statusMessage);
|
||||
}
|
||||
|
||||
export async function patch(path, data, { query } = {}) {
|
||||
async function patch(path, data, { query } = {}) {
|
||||
const res = await fetch(`/api${path}${getQuery(query)}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
|
@ -65,7 +65,7 @@ export async function patch(path, data, { query } = {}) {
|
|||
throw new Error(body.message);
|
||||
}
|
||||
|
||||
export async function del(path, { data, query } = {}) {
|
||||
async function del(path, { data, query } = {}) {
|
||||
const res = await fetch(`/api${path}${getQuery(query)}`, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(data),
|
||||
|
@ -84,3 +84,10 @@ export async function del(path, { data, query } = {}) {
|
|||
|
||||
throw new Error(body.message);
|
||||
}
|
||||
|
||||
export {
|
||||
get,
|
||||
post,
|
||||
patch,
|
||||
del,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// centralize navigation to simplify switching between client and server routing
|
||||
export default function navigate(path) {
|
||||
export function navigate(path) {
|
||||
window.location.href = path;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,23 +1,84 @@
|
|||
<template>
|
||||
<div class="comment">
|
||||
<div class="header">
|
||||
<a
|
||||
:href="`/user/${comment.user.username}`"
|
||||
class="username link"
|
||||
>u/{{ comment.user.username }}</a>
|
||||
<div class="container">
|
||||
<div class="frame">
|
||||
<div class="crumbs">
|
||||
<div
|
||||
v-for="index in depth"
|
||||
:key="`${comment.id}-${index}`"
|
||||
:style="{ color: `hsl(${index * 60}, 50%, 75%)`, 'border-color': `hsl(${index * 60}, 50%, 90%)` }"
|
||||
class="crumb"
|
||||
>{{ String.fromCharCode(96 + index) }}</div>
|
||||
</div>
|
||||
|
||||
<span
|
||||
:title="format(comment.createdAt, 'MMM d, yyyy hh:mm:ss')"
|
||||
class="timestamp"
|
||||
>{{ formatDistance(comment.createdAt, now, { includeSeconds: true }) }} ago</span>
|
||||
<div
|
||||
class="comment"
|
||||
:class="{ nested: comment.parentId }"
|
||||
:style="{ 'border-left': `solid 2px hsl(${(depth + 1) * 60}, 50%, 90%)` }"
|
||||
>
|
||||
<div class="header">
|
||||
<img
|
||||
src="/assets/icons/blocked.svg"
|
||||
class="avatar"
|
||||
>
|
||||
|
||||
<a
|
||||
:href="`/user/${comment.user.username}`"
|
||||
class="username link"
|
||||
>u/{{ comment.user.username }}</a>
|
||||
|
||||
<ul class="labels nolist">
|
||||
<li
|
||||
v-if="comment.user.id === post.user.id"
|
||||
class="label op"
|
||||
>op</li>
|
||||
</ul>
|
||||
|
||||
<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 class="actions">
|
||||
<button
|
||||
type="button"
|
||||
class="action link nobutton"
|
||||
@click="isReplying = !isReplying"
|
||||
>reply</button>
|
||||
</div>
|
||||
|
||||
<Writer
|
||||
v-if="isReplying"
|
||||
:comment="comment"
|
||||
:post="post"
|
||||
@cancel="isReplying = false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="body">{{ comment.body }}</p>
|
||||
<ul class="replies nolist">
|
||||
<li
|
||||
v-for="reply in comment.comments"
|
||||
:key="reply.id"
|
||||
>
|
||||
<Comment
|
||||
:comment="reply"
|
||||
:post="post"
|
||||
:depth="depth + 1"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
|
||||
import Writer from './writer.vue';
|
||||
|
||||
import { usePageContext } from '../../renderer/usePageContext';
|
||||
|
||||
const { now } = usePageContext();
|
||||
|
@ -27,20 +88,37 @@ defineProps({
|
|||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
post: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
|
||||
const isReplying = ref(false);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.comment {
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
background: var(--background);
|
||||
padding: .5rem;
|
||||
border-radius: .25rem;
|
||||
margin-bottom: .25rem;
|
||||
box-sizing: border-box;
|
||||
border-radius: .25rem .25rem .25rem 0;
|
||||
margin: .25rem 0;
|
||||
|
||||
&.nested {
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
font-size: .9rem;
|
||||
margin-bottom: .5rem;
|
||||
padding: .5rem .5rem .25rem .5rem;
|
||||
}
|
||||
|
||||
.username {
|
||||
|
@ -54,6 +132,67 @@ defineProps({
|
|||
}
|
||||
|
||||
.body {
|
||||
padding: .25rem .5rem .5rem .5rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
border-top: solid 1px var(--shadow-weak-40);
|
||||
}
|
||||
|
||||
.action {
|
||||
padding: .5rem;
|
||||
color: var(--grey-dark-20);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.labels {
|
||||
display: inline-block;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
background: var(--grey);
|
||||
border-radius: .25rem;
|
||||
color: var(--text-light);
|
||||
font-weight: bold;
|
||||
|
||||
&.op {
|
||||
color: var(--op);
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin-right: .5rem;
|
||||
opacity: .25;
|
||||
}
|
||||
|
||||
.replies {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.frame {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.crumbs {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
margin-top: -.5rem;
|
||||
}
|
||||
|
||||
.crumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
border-left: solid 2px var(--shadow-weak-40);
|
||||
color: var(--shadow-weak-20);
|
||||
font-size: .8rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<form
|
||||
class="writer"
|
||||
@submit.prevent="addComment"
|
||||
>
|
||||
<textarea
|
||||
ref="input"
|
||||
v-model="body"
|
||||
placeholder="Write a new comment"
|
||||
class="input"
|
||||
/>
|
||||
|
||||
<div class="actions">
|
||||
<button
|
||||
:disabled="body.length === 0"
|
||||
class="button button-submit action submit"
|
||||
>Comment</button>
|
||||
|
||||
<button
|
||||
v-if="comment"
|
||||
type="button"
|
||||
class="button button-cancel action cancel"
|
||||
@click="$emit('cancel')"
|
||||
>Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
|
||||
import * as api from '../../assets/js/api';
|
||||
import { reload } from '../../assets/js/navigate';
|
||||
|
||||
const props = defineProps({
|
||||
comment: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
post: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const body = ref('');
|
||||
const input = ref(null);
|
||||
|
||||
async function addComment() {
|
||||
await api.post(`/posts/${props.post.id}/comments`, {
|
||||
body: body.value,
|
||||
parentId: props.comment?.id,
|
||||
});
|
||||
|
||||
reload();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.comment) {
|
||||
input.value.focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.writer {
|
||||
width: 100%;
|
||||
background: var(--background);
|
||||
box-sizing: border-box;
|
||||
padding: .5rem;
|
||||
border-radius: .5rem;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-right: .25rem;
|
||||
}
|
||||
</style>
|
|
@ -1,5 +1,26 @@
|
|||
<template>
|
||||
<div class="post">
|
||||
<div class="votes noselect">
|
||||
<div
|
||||
class="vote bump"
|
||||
:class="{ active: hasBump }"
|
||||
title="Bump"
|
||||
@click="vote(1)"
|
||||
>+</div>
|
||||
|
||||
<div
|
||||
class="tally"
|
||||
:title="`${post.votes.total} ${post.votes.total === 1 ? 'vote' : 'votes'}`"
|
||||
>{{ tally }}</div>
|
||||
|
||||
<div
|
||||
class="vote sink"
|
||||
:class="{ active: hasSink }"
|
||||
title="Sink"
|
||||
@click="vote(-1)"
|
||||
>-</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="post.link || `/s/${post.shelf.slug}/post/${post.id}`"
|
||||
target="_blank"
|
||||
|
@ -47,32 +68,61 @@
|
|||
|
||||
<div class="actions">
|
||||
<a
|
||||
:href="`/s/shack/post/${post.id}`"
|
||||
:href="`/s/${post.shelf.slug}/post/${post.id}`"
|
||||
class="link comments"
|
||||
>{{ post.commentCount }} comments</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="`/s/shack/post/${post.id}`"
|
||||
:href="`/s/${post.shelf.slug}/post/${post.id}`"
|
||||
class="fill"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
|
||||
import blockedIcon from '../../assets/icons/blocked.svg?url'; // eslint-disable-line import/no-unresolved
|
||||
import { usePageContext } from '../../renderer/usePageContext';
|
||||
import * as api from '../../assets/js/api';
|
||||
|
||||
const { now } = usePageContext();
|
||||
const { me, now } = usePageContext();
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
post: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
shelf: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const tally = ref(props.post.votes.tally);
|
||||
const hasBump = ref(props.post.votes.bump);
|
||||
const hasSink = ref(props.post.votes.sink);
|
||||
const voting = ref(false);
|
||||
|
||||
async function vote(value) {
|
||||
if (!me || voting.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
voting.value = true;
|
||||
|
||||
const undo = (value > 0 && hasBump.value) || (value < 0 && hasSink.value);
|
||||
const votes = await api.post(`/posts/${props.post.id}/votes`, { value: undo ? 0 : value });
|
||||
|
||||
tally.value = votes.tally;
|
||||
hasBump.value = votes.bump;
|
||||
hasSink.value = votes.sink;
|
||||
|
||||
voting.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -127,6 +177,50 @@ defineProps({
|
|||
opacity: .25;
|
||||
}
|
||||
|
||||
.votes {
|
||||
width: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.vote,
|
||||
.tally {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.vote {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1rem;
|
||||
box-sizing: border-box;
|
||||
padding: .6rem;
|
||||
margin: .25rem 0;
|
||||
border-radius: .5rem;
|
||||
background: var(--shadow-weak-40);
|
||||
}
|
||||
|
||||
.tally {
|
||||
color: var(--shadow-strong-20);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.bump.active {
|
||||
color: var(--text-light);
|
||||
background: var(--bump);
|
||||
}
|
||||
|
||||
.sink.active {
|
||||
color: var(--text-light);
|
||||
background: var(--sink);
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
|
|
@ -39,6 +39,8 @@ export async function up(knex) {
|
|||
table.increments('id');
|
||||
|
||||
table.text('slug')
|
||||
.unique()
|
||||
.index()
|
||||
.notNullable();
|
||||
|
||||
table.integer('founder_id')
|
||||
|
@ -102,6 +104,30 @@ export async function up(knex) {
|
|||
|
||||
await knex.raw('ALTER TABLE posts ADD CONSTRAINT post_content CHECK (body IS NOT NULL OR link IS NOT NULL)');
|
||||
|
||||
await knex.schema.createTable('posts_votes', (table) => {
|
||||
table.increments('id');
|
||||
|
||||
table.tinyint('value')
|
||||
.notNullable()
|
||||
.defaultTo(1);
|
||||
|
||||
table.integer('user_id')
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('users');
|
||||
|
||||
table.string('post_id')
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('posts');
|
||||
|
||||
table.datetime('created_at')
|
||||
.notNullable()
|
||||
.defaultTo(knex.fn.now());
|
||||
|
||||
table.unique(['user_id', 'post_id']);
|
||||
});
|
||||
|
||||
await knex.schema.createTable('comments', (table) => {
|
||||
table.text('id', 8)
|
||||
.primary()
|
||||
|
@ -112,7 +138,7 @@ export async function up(knex) {
|
|||
.references('id')
|
||||
.inTable('posts');
|
||||
|
||||
table.integer('parent_comment_id')
|
||||
table.text('parent_id')
|
||||
.references('id')
|
||||
.inTable('comments');
|
||||
|
||||
|
@ -123,6 +149,32 @@ export async function up(knex) {
|
|||
|
||||
table.text('body');
|
||||
|
||||
table.boolean('deleted')
|
||||
.notNullable()
|
||||
.defaultTo(false);
|
||||
|
||||
table.datetime('created_at')
|
||||
.notNullable()
|
||||
.defaultTo(knex.fn.now());
|
||||
});
|
||||
|
||||
await knex.schema.createTable('comments_votes', (table) => {
|
||||
table.increments('id');
|
||||
|
||||
table.tinyint('value')
|
||||
.notNullable()
|
||||
.defaultTo(1);
|
||||
|
||||
table.integer('user_id')
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('users');
|
||||
|
||||
table.string('comment_id')
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('comments');
|
||||
|
||||
table.datetime('created_at')
|
||||
.notNullable()
|
||||
.defaultTo(knex.fn.now());
|
||||
|
@ -130,11 +182,13 @@ export async function up(knex) {
|
|||
}
|
||||
|
||||
export async function down(knex) {
|
||||
await knex.schema.dropTable('comments');
|
||||
await knex.schema.dropTable('posts');
|
||||
await knex.schema.dropTable('shelves_settings');
|
||||
await knex.schema.dropTable('shelves');
|
||||
await knex.schema.dropTable('users');
|
||||
await knex.schema.dropTableIfExists('comments_votes');
|
||||
await knex.schema.dropTableIfExists('comments');
|
||||
await knex.schema.dropTableIfExists('posts_votes');
|
||||
await knex.schema.dropTableIfExists('posts');
|
||||
await knex.schema.dropTableIfExists('shelves_settings');
|
||||
await knex.schema.dropTableIfExists('shelves');
|
||||
await knex.schema.dropTableIfExists('users');
|
||||
|
||||
await knex.raw(`
|
||||
DROP FUNCTION IF EXISTS shack_id;
|
||||
|
|
|
@ -88,7 +88,7 @@ import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
|||
|
||||
import Checkbox from '../../components/form/checkbox.vue';
|
||||
|
||||
import navigate from '../../assets/js/navigate';
|
||||
import { navigate } from '../../assets/js/navigate';
|
||||
import { post } from '../../assets/js/api';
|
||||
|
||||
const config = CONFIG;
|
||||
|
|
|
@ -65,7 +65,7 @@ import { ref } from 'vue';
|
|||
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
|
||||
import { post } from '../../assets/js/api';
|
||||
import navigate from '../../assets/js/navigate';
|
||||
import { navigate } from '../../assets/js/navigate';
|
||||
|
||||
const config = CONFIG;
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
class="link"
|
||||
>Create new shelf</a></li>
|
||||
|
||||
<li v-if="!user"><a
|
||||
<li v-if="!me"><a
|
||||
href="/account/login"
|
||||
class="link"
|
||||
>Log in</a></li>
|
||||
|
@ -32,6 +32,6 @@
|
|||
<script setup>
|
||||
import { usePageContext } from '../../renderer/usePageContext';
|
||||
|
||||
const { user, pageData } = usePageContext();
|
||||
const { me, pageData } = usePageContext();
|
||||
const { shelves } = pageData;
|
||||
</script>
|
||||
|
|
|
@ -1 +1 @@
|
|||
export default '/s/@id/post/@postId';
|
||||
export default '/s/@shelfId/post/@postId';
|
||||
|
|
|
@ -1,33 +1,20 @@
|
|||
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),
|
||||
fetchShelf(pageContext.routeParams.shelfId),
|
||||
fetchPost(pageContext.routeParams.postId, pageContext.session.user),
|
||||
fetchPostComments(pageContext.routeParams.postId),
|
||||
]);
|
||||
|
||||
if (!shelf) {
|
||||
throw RenderErrorPage({
|
||||
pageContext: {
|
||||
pageProps: {
|
||||
errorInfo: 'This shelf does not exist',
|
||||
},
|
||||
},
|
||||
});
|
||||
throw new Error('This shelf does not exist');
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
throw RenderErrorPage({
|
||||
pageContext: {
|
||||
pageProps: {
|
||||
errorInfo: 'This post does not exist',
|
||||
},
|
||||
},
|
||||
});
|
||||
throw new Error('This post does not exist');
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,28 +1,46 @@
|
|||
<template>
|
||||
<div class="page">
|
||||
<div class="body">
|
||||
<Post :post="post" />
|
||||
<div class="content">
|
||||
<Post
|
||||
:post="post"
|
||||
:shelf="shelf"
|
||||
/>
|
||||
|
||||
<form
|
||||
class="writer"
|
||||
@submit.prevent="addComment"
|
||||
<p
|
||||
v-if="post.body"
|
||||
class="body"
|
||||
>{{ post.body }}</p>
|
||||
|
||||
<Writer
|
||||
v-if="me"
|
||||
:post="post"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="body"
|
||||
>
|
||||
<textarea
|
||||
v-model="newComment"
|
||||
placeholder="Write a new comment"
|
||||
class="input"
|
||||
/>
|
||||
|
||||
<div class="actions">
|
||||
<button class="button">Comment</button>
|
||||
</div>
|
||||
</form>
|
||||
<a
|
||||
href="/account/login"
|
||||
class="link"
|
||||
>Log in</a> or
|
||||
<a
|
||||
href="/account/create"
|
||||
class="link"
|
||||
>sign up</a> to comment
|
||||
</div>
|
||||
|
||||
<ul class="comments nolist">
|
||||
<li
|
||||
v-for="comment in comments"
|
||||
:key="comment.id"
|
||||
><Comment :comment="comment" /></li>
|
||||
>
|
||||
<Comment
|
||||
:comment="comment"
|
||||
:post="post"
|
||||
:shelf="shelf"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -33,70 +51,46 @@
|
|||
</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 Writer from '../../components/comments/writer.vue';
|
||||
|
||||
import { usePageContext } from '../../renderer/usePageContext';
|
||||
|
||||
const { pageData } = usePageContext();
|
||||
const { me, 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;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.body {
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
border-radius: .5rem;
|
||||
margin: 0 0 .5rem 0;
|
||||
background: var(--background);
|
||||
}
|
||||
|
||||
.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;
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export default '/shelf/create';
|
|
@ -126,6 +126,7 @@
|
|||
import { ref } from 'vue';
|
||||
import Checkbox from '../../components/form/checkbox.vue';
|
||||
import { post } from '../../assets/js/api';
|
||||
import { navigate } from '../../assets/js/navigate';
|
||||
|
||||
const slug = ref();
|
||||
const title = ref();
|
||||
|
@ -137,7 +138,7 @@ const postAccess = ref('registered');
|
|||
const isNsfw = ref(false);
|
||||
|
||||
async function create() {
|
||||
await post('/shelves', {
|
||||
const shelf = await post('/shelves', {
|
||||
slug: slug.value,
|
||||
title: title.value,
|
||||
description: description.value,
|
||||
|
@ -147,6 +148,9 @@ async function create() {
|
|||
isNsfw: isNsfw.value,
|
||||
},
|
||||
});
|
||||
|
||||
console.log(shelf);
|
||||
navigate(`/s/${shelf.slug}`);
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1 @@
|
|||
export default '/s/@id';
|
|
@ -1,10 +1,10 @@
|
|||
import { RenderErrorPage } from 'vite-plugin-ssr/RenderErrorPage';
|
||||
import { fetchShelf } from '../../../src/shelves';
|
||||
import { fetchShelfPosts } from '../../../src/posts';
|
||||
import { fetchShelf } from '../../src/shelves';
|
||||
import { fetchShelfPosts } from '../../src/posts';
|
||||
|
||||
async function getPageData(pageContext) {
|
||||
const shelf = await fetchShelf(pageContext.routeParams.id);
|
||||
const posts = await fetchShelfPosts(pageContext.routeParams.id, { limit: 50 });
|
||||
const posts = await fetchShelfPosts(pageContext.routeParams.id, { user: pageContext.session.user, limit: 50 });
|
||||
|
||||
if (!shelf) {
|
||||
throw RenderErrorPage({
|
|
@ -6,7 +6,12 @@
|
|||
<li
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
><Post :post="post" /></li>
|
||||
>
|
||||
<Post
|
||||
:post="post"
|
||||
:shelf="shelf"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<form
|
||||
|
@ -46,10 +51,11 @@
|
|||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Post from '../../../components/posts/post.vue';
|
||||
import Post from '../../components/posts/post.vue';
|
||||
|
||||
import * as api from '../../../assets/js/api';
|
||||
import { usePageContext } from '../../../renderer/usePageContext';
|
||||
import * as api from '../../assets/js/api';
|
||||
import { navigate } from '../../assets/js/navigate';
|
||||
import { usePageContext } from '../../renderer/usePageContext';
|
||||
|
||||
const { pageData, routeParams } = usePageContext();
|
||||
|
||||
|
@ -63,11 +69,13 @@ const link = ref();
|
|||
const body = ref();
|
||||
|
||||
async function submitPost() {
|
||||
await api.post(`/api/shelves/${routeParams.id}/posts`, {
|
||||
const post = await api.post(`/shelves/${routeParams.id}/posts`, {
|
||||
title: title.value,
|
||||
link: link.value,
|
||||
body: body.value,
|
||||
});
|
||||
|
||||
navigate(`/s/${shelf.slug}/post/${post.id}`);
|
||||
}
|
||||
</script>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// import { renderToString as renderToString_ } from '@vue/server-renderer';
|
||||
import { renderToNodeStream } from '@vue/server-renderer';
|
||||
import { escapeInject } from 'vite-plugin-ssr/server';
|
||||
import { RenderErrorPage } from 'vite-plugin-ssr/RenderErrorPage';
|
||||
|
||||
import { createApp } from './app';
|
||||
import { useUser } from '../stores/user';
|
||||
|
@ -45,16 +46,30 @@ async function render(pageContext) {
|
|||
}
|
||||
|
||||
async function onBeforeRender(pageContext) {
|
||||
const pageData = await pageContext.exports.getPageData?.(pageContext, pageContext.session);
|
||||
try {
|
||||
const pageData = await pageContext.exports.getPageData?.(pageContext, pageContext.session);
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
// initialState: store.state.value,
|
||||
pageData,
|
||||
user: pageContext.session.user,
|
||||
now: new Date(),
|
||||
},
|
||||
};
|
||||
return {
|
||||
pageContext: {
|
||||
// initialState: store.state.value,
|
||||
pageData,
|
||||
me: pageContext.session.user,
|
||||
now: new Date(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
throw RenderErrorPage({
|
||||
pageContext: {
|
||||
pageProps: {
|
||||
errorInfo: error.statusMessage,
|
||||
},
|
||||
me: pageContext.session.user,
|
||||
now: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -62,4 +77,4 @@ export {
|
|||
onBeforeRender,
|
||||
};
|
||||
|
||||
export const passToClient = ['urlPathname', 'initialState', 'pageData', 'pageProps', 'routeParams', 'user', 'now'];
|
||||
export const passToClient = ['urlPathname', 'initialState', 'pageData', 'pageProps', 'routeParams', 'me', 'now'];
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
</div>
|
||||
|
||||
<span
|
||||
v-if="user"
|
||||
v-if="me"
|
||||
class="userpanel"
|
||||
@click="logout"
|
||||
>{{ user.username }}</span>
|
||||
>{{ me.username }}</span>
|
||||
</header>
|
||||
|
||||
<div class="content-container">
|
||||
|
@ -35,14 +35,15 @@
|
|||
<script setup>
|
||||
import logo from '../assets/img/logo.svg?raw'; // eslint-disable-line import/no-unresolved
|
||||
import { del } from '../assets/js/api';
|
||||
import { navigate } from '../assets/js/navigate';
|
||||
import { usePageContext } from './usePageContext';
|
||||
|
||||
const { user } = usePageContext();
|
||||
const { me } = usePageContext();
|
||||
const version = CLIENT_VERSION;
|
||||
|
||||
async function logout() {
|
||||
await del('/api/session');
|
||||
window.location.href = '/account/login';
|
||||
await del('/session');
|
||||
navigate('/account/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,41 +1,80 @@
|
|||
import knex from './knex';
|
||||
import { fetchUsers } from './users';
|
||||
import { verifyPrivilege } from './privileges';
|
||||
import { HttpError } from './errors';
|
||||
|
||||
function curateDatabaseComment(comment, { users }) {
|
||||
return {
|
||||
id: comment.id,
|
||||
body: comment.body,
|
||||
parentId: comment.parent_id,
|
||||
userId: comment.user_id,
|
||||
user: users.find((user) => user.id === comment.user_id),
|
||||
createdAt: comment.created_at,
|
||||
};
|
||||
}
|
||||
|
||||
function threadComments(thread, commentsByParentId) {
|
||||
if (!thread) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return thread.map((comment) => ({
|
||||
...comment,
|
||||
comments: threadComments(commentsByParentId[comment.id], commentsByParentId),
|
||||
}));
|
||||
}
|
||||
|
||||
function nestComments(comments) {
|
||||
const commentsByParentId = comments.reduce((acc, comment) => {
|
||||
if (!acc[comment.parentId]) {
|
||||
acc[comment.parentId] = [];
|
||||
}
|
||||
|
||||
acc[comment.parentId].push(comment);
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return threadComments(commentsByParentId.null, commentsByParentId);
|
||||
}
|
||||
|
||||
async function fetchPostComments(postId, { limit = 100 } = {}) {
|
||||
const comments = await knex('comments')
|
||||
.where('post_id', postId)
|
||||
.orderBy('created_at', 'asc')
|
||||
.orderBy('created_at', 'desc')
|
||||
.limit(limit);
|
||||
|
||||
const users = await fetchUsers(comments.map((comment) => comment.user_id));
|
||||
const curatedComments = comments.map((comment) => curateDatabaseComment(comment, { users }));
|
||||
|
||||
return comments.map((comment) => curateDatabaseComment(comment, { users }));
|
||||
const nestedComments = nestComments(curatedComments);
|
||||
|
||||
return nestedComments;
|
||||
}
|
||||
|
||||
async function addComment(comment, postId, user) {
|
||||
await verifyPrivilege('addComment', user);
|
||||
|
||||
const commentEntry = await knex('comments')
|
||||
if (!comment.body) {
|
||||
throw new HttpError({
|
||||
statusMessage: 'Comment cannot be empty',
|
||||
statusCode: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const [commentEntry] = await knex('comments')
|
||||
.insert({
|
||||
body: comment.body,
|
||||
post_id: postId,
|
||||
parent_id: comment.parentId,
|
||||
user_id: user.id,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
console.log(comment, user);
|
||||
return curateDatabaseComment(commentEntry);
|
||||
const users = await fetchUsers([commentEntry.user_id]);
|
||||
|
||||
return curateDatabaseComment(commentEntry, { users });
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
75
src/posts.js
75
src/posts.js
|
@ -6,7 +6,9 @@ import { HttpError } from './errors';
|
|||
import { fetchShelf } from './shelves';
|
||||
import { fetchUsers } from './users';
|
||||
|
||||
function curatePost(post, { shelf, users }) {
|
||||
function curateDatabasePost(post, {
|
||||
shelf, users, votes,
|
||||
}) {
|
||||
const curatedPost = {
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
|
@ -16,13 +18,40 @@ function curatePost(post, { shelf, users }) {
|
|||
createdAt: post.created_at,
|
||||
shelf,
|
||||
user: users.find((user) => user.id === post.user_id),
|
||||
votes: votes[post.id],
|
||||
commentCount: Number(post.comment_count),
|
||||
};
|
||||
|
||||
return curatedPost;
|
||||
}
|
||||
|
||||
async function fetchShelfPosts(shelfId, { limit = 100 } = {}) {
|
||||
function curatePostVote(vote) {
|
||||
return {
|
||||
tally: Number(vote.tally),
|
||||
total: Number(vote.total),
|
||||
bump: !!vote.bump,
|
||||
sink: !!vote.sink,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchPostVotes(postIds, user) {
|
||||
const votes = await knex('posts_votes')
|
||||
.select(
|
||||
'post_id',
|
||||
knex.raw('sum(value) as tally'),
|
||||
knex.raw('count(id) as total'),
|
||||
...(user ? [
|
||||
knex.raw('bool_or(user_id = :userId and value = 1) as bump', { userId: knex.raw(user.id) }),
|
||||
knex.raw('bool_or(user_id = :userId and value = -1) as sink', { userId: knex.raw(user.id) })]
|
||||
: []),
|
||||
)
|
||||
.whereIn('post_id', postIds)
|
||||
.groupBy('post_id');
|
||||
|
||||
return Object.fromEntries(votes.map((vote) => [vote.post_id, curatePostVote(vote)]));
|
||||
}
|
||||
|
||||
async function fetchShelfPosts(shelfId, { user, limit = 100 } = {}) {
|
||||
const shelf = await fetchShelf(shelfId);
|
||||
|
||||
const posts = await knex('posts')
|
||||
|
@ -33,15 +62,19 @@ async function fetchShelfPosts(shelfId, { limit = 100 } = {}) {
|
|||
.groupBy('posts.id')
|
||||
.limit(limit);
|
||||
|
||||
const users = await fetchUsers(posts.map((post) => post.user_id));
|
||||
const [users, votes] = await Promise.all([
|
||||
fetchUsers(posts.map((post) => post.user_id)),
|
||||
fetchPostVotes(posts.map((post) => post.id), user),
|
||||
]);
|
||||
|
||||
return posts.map((post) => curatePost(post, { shelf, users }));
|
||||
return posts.map((post) => curateDatabasePost(post, { shelf, users, votes }));
|
||||
}
|
||||
|
||||
async function fetchPost(postId) {
|
||||
async function fetchPost(postId, user) {
|
||||
const post = await knex('posts')
|
||||
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
|
||||
.leftJoin('comments', 'comments.post_id', 'posts.id')
|
||||
.leftJoin('posts_votes', 'posts_votes.post_id', 'posts.id')
|
||||
.where('posts.id', postId)
|
||||
.groupBy('posts.id')
|
||||
.first();
|
||||
|
@ -53,12 +86,13 @@ async function fetchPost(postId) {
|
|||
});
|
||||
}
|
||||
|
||||
const [shelf, users] = await Promise.all([
|
||||
const [shelf, users, votes] = await Promise.all([
|
||||
fetchShelf(post.shelf_id),
|
||||
fetchUsers([post.user_id]),
|
||||
fetchPostVotes([post.id], user),
|
||||
]);
|
||||
|
||||
return curatePost(post, { shelf, users });
|
||||
return curateDatabasePost(post, { shelf, users, votes });
|
||||
}
|
||||
|
||||
async function createPost(post, shelfId, user) {
|
||||
|
@ -73,9 +107,7 @@ async function createPost(post, shelfId, user) {
|
|||
});
|
||||
}
|
||||
|
||||
console.log(post);
|
||||
|
||||
const postId = await knex('posts')
|
||||
const [postEntry] = await knex('posts')
|
||||
.insert({
|
||||
title: post.title,
|
||||
body: post.body,
|
||||
|
@ -83,13 +115,32 @@ async function createPost(post, shelfId, user) {
|
|||
shelf_id: shelf.id,
|
||||
user_id: user.id,
|
||||
})
|
||||
.returning('id');
|
||||
.returning('*');
|
||||
|
||||
return postId;
|
||||
const users = await fetchUsers([postEntry.user_id]);
|
||||
|
||||
return curateDatabasePost(postEntry, { shelf, users });
|
||||
}
|
||||
|
||||
async function votePost(postId, value, user) {
|
||||
await knex('posts_votes')
|
||||
.insert({
|
||||
value,
|
||||
post_id: postId,
|
||||
user_id: user.id,
|
||||
})
|
||||
.onConflict(['post_id', 'user_id'])
|
||||
.merge()
|
||||
.returning('value');
|
||||
|
||||
const votes = await fetchPostVotes([postId], user);
|
||||
|
||||
return votes[postId];
|
||||
}
|
||||
|
||||
export {
|
||||
createPost,
|
||||
fetchPost,
|
||||
fetchShelfPosts,
|
||||
votePost,
|
||||
};
|
||||
|
|
|
@ -36,15 +36,13 @@ async function fetchShelves({ limit = 10 } = {}) {
|
|||
}
|
||||
|
||||
async function createShelf(shelf, user) {
|
||||
const shelfEntry = await knex('shelves')
|
||||
const [shelfEntry] = await knex('shelves')
|
||||
.insert({
|
||||
slug: shelf.slug,
|
||||
founder_id: user.id,
|
||||
})
|
||||
.returning('*');
|
||||
|
||||
console.log('entry', shelfEntry);
|
||||
|
||||
return curateDatabaseShelf(shelfEntry);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createPost } from '../posts';
|
||||
import { createPost, votePost } from '../posts';
|
||||
|
||||
async function createPostApi(req, res) {
|
||||
const post = await createPost(req.body, req.params.shelfId, req.user);
|
||||
|
@ -6,6 +6,13 @@ async function createPostApi(req, res) {
|
|||
res.send(post);
|
||||
}
|
||||
|
||||
async function votePostApi(req, res) {
|
||||
const votes = await votePost(req.params.postId, req.body.value, req.user);
|
||||
|
||||
res.send(votes);
|
||||
}
|
||||
|
||||
export {
|
||||
createPostApi as createPost,
|
||||
votePostApi as votePost,
|
||||
};
|
||||
|
|
|
@ -25,7 +25,7 @@ import {
|
|||
|
||||
import { createShelf } from './shelves';
|
||||
|
||||
import { createPost } from './posts';
|
||||
import { createPost, votePost } from './posts';
|
||||
import { addComment } from './comments';
|
||||
|
||||
const logger = initLogger();
|
||||
|
@ -70,6 +70,7 @@ async function startServer() {
|
|||
|
||||
// POSTS
|
||||
router.post('/api/shelves/:shelfId/posts', createPost);
|
||||
router.post('/api/posts/:postId/votes', votePost);
|
||||
|
||||
// COMMENTS
|
||||
router.post('/api/posts/:postId/comments', addComment);
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { createShelf } from '../shelves';
|
||||
|
||||
async function createShelfApi(req) {
|
||||
async function createShelfApi(req, res) {
|
||||
const shelf = await createShelf(req.body, req.user);
|
||||
|
||||
return shelf;
|
||||
res.send(shelf);
|
||||
}
|
||||
|
||||
export {
|
||||
|
|
Loading…
Reference in New Issue