Compare commits
No commits in common. "e5b102ce07b1e8b5bce587f2ce7c021f815d2d9b" and "77085c5755907b5df85ed0181902eca749e314f0" have entirely different histories.
e5b102ce07
...
77085c5755
|
@ -22,12 +22,6 @@
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
cursor: pointer;
|
|
||||||
background: var(--primary);
|
|
||||||
color: var(--text-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
@import 'inputs';
|
@import 'inputs';
|
||||||
@import 'forms';
|
@import 'forms';
|
||||||
@import 'markdown';
|
@import 'markdown';
|
||||||
@import 'tooltip';
|
|
||||||
|
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
.tooltip {}
|
|
||||||
|
|
||||||
.menu-item {
|
|
||||||
display: block;
|
|
||||||
padding: .5rem;
|
|
||||||
}
|
|
|
@ -90,5 +90,4 @@ export {
|
||||||
post,
|
post,
|
||||||
patch,
|
patch,
|
||||||
del,
|
del,
|
||||||
del as delete,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
@submit.prevent="addComment"
|
@submit.prevent="addComment"
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
ref="inputRef"
|
ref="input"
|
||||||
v-model="body"
|
v-model="body"
|
||||||
placeholder="Write a new comment"
|
placeholder="Write a new comment"
|
||||||
class="input"
|
class="input"
|
||||||
|
@ -44,7 +44,7 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = ref('');
|
const body = ref('');
|
||||||
const inputRef = ref(null);
|
const input = ref(null);
|
||||||
|
|
||||||
async function addComment() {
|
async function addComment() {
|
||||||
await api.post(`/posts/${props.post.id}/comments`, {
|
await api.post(`/posts/${props.post.id}/comments`, {
|
||||||
|
@ -57,7 +57,7 @@ async function addComment() {
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.comment) {
|
if (props.comment) {
|
||||||
inputRef.value.focus();
|
input.value.focus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
:href="post.link || `/s/${post.shelf.slug}/post/${post.id}/${post.slug}`"
|
:href="post.link || `/s/${post.shelf.slug}/post/${post.id}`"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
class="title-link"
|
class="title-link"
|
||||||
>
|
>
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h2 class="title">
|
<h2 class="title">
|
||||||
<a
|
<a
|
||||||
:href="`/s/${post.shelf.slug}/post/${post.id}/${post.slug}`"
|
:href="`/s/${post.shelf.slug}/post/${post.id}`"
|
||||||
class="title-link"
|
class="title-link"
|
||||||
>{{ post.title }}</a>
|
>{{ post.title }}</a>
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
:href="`/s/${post.shelf.slug}/post/${post.id}/${post.slug}`"
|
:href="`/s/${post.shelf.slug}/post/${post.id}`"
|
||||||
class="fill"
|
class="fill"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -133,7 +133,6 @@ async function submitVote(value) {
|
||||||
margin-bottom: .25rem;
|
margin-bottom: .25rem;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
& :hover {
|
& :hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -151,7 +150,6 @@ async function submitVote(value) {
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
@ -225,8 +223,7 @@ async function submitVote(value) {
|
||||||
|
|
||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
gap: 1rem;
|
||||||
gap: 0 1rem;
|
|
||||||
margin-bottom: .25rem;
|
margin-bottom: .25rem;
|
||||||
font-size: .9rem;
|
font-size: .9rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,24 +76,6 @@ export async function up(knex) {
|
||||||
table.boolean('is_nsfw');
|
table.boolean('is_nsfw');
|
||||||
});
|
});
|
||||||
|
|
||||||
await knex.schema.createTable('shelves_subscriptions', (table) => {
|
|
||||||
table.increments('id');
|
|
||||||
|
|
||||||
table.integer('shelf_id')
|
|
||||||
.references('id')
|
|
||||||
.inTable('shelves');
|
|
||||||
|
|
||||||
table.integer('user_id')
|
|
||||||
.references('id')
|
|
||||||
.inTable('users');
|
|
||||||
|
|
||||||
table.unique(['shelf_id', 'user_id']);
|
|
||||||
|
|
||||||
table.datetime('created_at')
|
|
||||||
.notNullable()
|
|
||||||
.defaultTo(knex.fn.now());
|
|
||||||
});
|
|
||||||
|
|
||||||
await knex.schema.createTable('posts', (table) => {
|
await knex.schema.createTable('posts', (table) => {
|
||||||
table.text('id', 8)
|
table.text('id', 8)
|
||||||
.primary()
|
.primary()
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "shack",
|
"name": "shack",
|
||||||
"version": "0.4.0",
|
"version": "0.3.1",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "shack",
|
"name": "shack",
|
||||||
"version": "0.4.0",
|
"version": "0.3.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/cli": "^7.21.5",
|
"@babel/cli": "^7.21.5",
|
||||||
"@babel/core": "^7.21.8",
|
"@babel/core": "^7.21.8",
|
||||||
|
@ -30,7 +30,6 @@
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"express-promise-router": "^4.1.1",
|
"express-promise-router": "^4.1.1",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"floating-vue": "^2.0.0-beta.22",
|
|
||||||
"ip-cidr": "^3.1.0",
|
"ip-cidr": "^3.1.0",
|
||||||
"knex": "^2.4.2",
|
"knex": "^2.4.2",
|
||||||
"knex-migrate": "^1.7.4",
|
"knex-migrate": "^1.7.4",
|
||||||
|
@ -2127,19 +2126,6 @@
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
|
||||||
"version": "1.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz",
|
|
||||||
"integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g=="
|
|
||||||
},
|
|
||||||
"node_modules/@floating-ui/dom": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/core": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@hcaptcha/vue3-hcaptcha": {
|
"node_modules/@hcaptcha/vue3-hcaptcha": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.2.1.tgz",
|
||||||
|
@ -4567,18 +4553,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
|
||||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
|
||||||
},
|
},
|
||||||
"node_modules/floating-vue": {
|
|
||||||
"version": "2.0.0-beta.22",
|
|
||||||
"resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-2.0.0-beta.22.tgz",
|
|
||||||
"integrity": "sha512-1iqpX3Rc3KpghLgBZ7zXfn6EceezujBUDKXYcTKPxv9P8CZxhS+3oouayd1fv3o+x/xeVSryWhF0Q7+4HEM7rA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@floating-ui/dom": "~1.1.1",
|
|
||||||
"vue-resize": "^2.0.0-alpha.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fn.name": {
|
"node_modules/fn.name": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||||
|
@ -7689,14 +7663,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||||
},
|
},
|
||||||
"node_modules/vue-resize": {
|
|
||||||
"version": "2.0.0-alpha.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz",
|
|
||||||
"integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"vue": "^3.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
@ -9237,19 +9203,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.41.0.tgz",
|
||||||
"integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA=="
|
"integrity": "sha512-LxcyMGxwmTh2lY9FwHPGWOHmYFCZvbrFCBZL4FzSSsxsRPuhrYUg/49/0KDfW8tnIEaEHtfmn6+NPN+1DqaNmA=="
|
||||||
},
|
},
|
||||||
"@floating-ui/core": {
|
|
||||||
"version": "1.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.3.1.tgz",
|
|
||||||
"integrity": "sha512-Bu+AMaXNjrpjh41znzHqaz3r2Nr8hHuHZT6V2LBKMhyMl0FgKA62PNYbqnfgmzOhoWZj70Zecisbo4H1rotP5g=="
|
|
||||||
},
|
|
||||||
"@floating-ui/dom": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==",
|
|
||||||
"requires": {
|
|
||||||
"@floating-ui/core": "^1.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@hcaptcha/vue3-hcaptcha": {
|
"@hcaptcha/vue3-hcaptcha": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.2.1.tgz",
|
||||||
|
@ -11094,15 +11047,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
|
||||||
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
|
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
|
||||||
},
|
},
|
||||||
"floating-vue": {
|
|
||||||
"version": "2.0.0-beta.22",
|
|
||||||
"resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-2.0.0-beta.22.tgz",
|
|
||||||
"integrity": "sha512-1iqpX3Rc3KpghLgBZ7zXfn6EceezujBUDKXYcTKPxv9P8CZxhS+3oouayd1fv3o+x/xeVSryWhF0Q7+4HEM7rA==",
|
|
||||||
"requires": {
|
|
||||||
"@floating-ui/dom": "~1.1.1",
|
|
||||||
"vue-resize": "^2.0.0-alpha.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fn.name": {
|
"fn.name": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
|
||||||
|
@ -13267,12 +13211,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-resize": {
|
|
||||||
"version": "2.0.0-alpha.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz",
|
|
||||||
"integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==",
|
|
||||||
"requires": {}
|
|
||||||
},
|
|
||||||
"which": {
|
"which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "shack",
|
"name": "shack",
|
||||||
"version": "0.4.0",
|
"version": "0.3.1",
|
||||||
"description": "Shack is a self-hosted social news aggregate",
|
"description": "Shack is a self-hosted social news aggregate",
|
||||||
"main": "src/web/server.js",
|
"main": "src/web/server.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
@ -47,7 +47,6 @@
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"express-promise-router": "^4.1.1",
|
"express-promise-router": "^4.1.1",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"floating-vue": "^2.0.0-beta.22",
|
|
||||||
"ip-cidr": "^3.1.0",
|
"ip-cidr": "^3.1.0",
|
||||||
"knex": "^2.4.2",
|
"knex": "^2.4.2",
|
||||||
"knex-migrate": "^1.7.4",
|
"knex-migrate": "^1.7.4",
|
||||||
|
|
|
@ -121,9 +121,7 @@ async function signup() {
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-grow: 1;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,8 +91,6 @@ async function signup() {
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { fetchUserPosts, fetchAllPosts } from '../../src/posts';
|
import { fetchShelves } from '../../src/shelves';
|
||||||
|
|
||||||
async function getPageData(pageContext) {
|
async function getPageData() {
|
||||||
const posts = pageContext.session.user
|
const shelves = await fetchShelves();
|
||||||
? await fetchUserPosts(pageContext.session.user)
|
|
||||||
: await fetchAllPosts();
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
posts,
|
shelves,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,37 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<ul class="posts nolist">
|
<ul>
|
||||||
|
<li><a
|
||||||
|
href="/shelf/create"
|
||||||
|
class="link"
|
||||||
|
>Create new shelf</a></li>
|
||||||
|
|
||||||
|
<li v-if="!me"><a
|
||||||
|
href="/account/login"
|
||||||
|
class="link"
|
||||||
|
>Log in</a></li>
|
||||||
|
|
||||||
|
<li><a
|
||||||
|
href="/account/create"
|
||||||
|
class="link"
|
||||||
|
>Sign up</a></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul>
|
||||||
<li
|
<li
|
||||||
v-for="post in posts"
|
v-for="shelf in shelves"
|
||||||
:key="post.id"
|
:key="shelf.id"
|
||||||
>
|
><a
|
||||||
<Post :post="post" />
|
:href="`/s/${shelf.slug}`"
|
||||||
</li>
|
class="link"
|
||||||
|
>{{ shelf.slug }}</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { usePageContext } from '../../renderer/usePageContext';
|
import { usePageContext } from '../../renderer/usePageContext';
|
||||||
import Post from '../../components/posts/post.vue';
|
|
||||||
|
|
||||||
const { pageData } = usePageContext();
|
const { me, pageData } = usePageContext();
|
||||||
const { posts } = pageData;
|
const { shelves } = pageData;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
export default '/s/@shelfId/post/@postId/@postSlug?';
|
export default '/s/@shelfId/post/@postId';
|
||||||
|
|
|
@ -65,7 +65,7 @@ const {
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.content {
|
.content {
|
||||||
padding: 0;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { fetchShelf } from '../../src/shelves';
|
||||||
import { fetchShelfPosts } from '../../src/posts';
|
import { fetchShelfPosts } from '../../src/posts';
|
||||||
|
|
||||||
async function getPageData(pageContext) {
|
async function getPageData(pageContext) {
|
||||||
const shelf = await fetchShelf(pageContext.routeParams.id, { user: pageContext.session.user });
|
const shelf = await fetchShelf(pageContext.routeParams.id);
|
||||||
const posts = await fetchShelfPosts(pageContext.routeParams.id, { user: pageContext.session.user, limit: 50 });
|
const posts = await fetchShelfPosts(pageContext.routeParams.id, { user: pageContext.session.user, limit: 50 });
|
||||||
|
|
||||||
if (!shelf) {
|
if (!shelf) {
|
||||||
|
|
|
@ -33,7 +33,6 @@ const {
|
||||||
.posts {
|
.posts {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,6 @@ import { createApp } from './app';
|
||||||
import { useUser } from '../stores/user';
|
import { useUser } from '../stores/user';
|
||||||
import logoUrl from './logo.svg';
|
import logoUrl from './logo.svg';
|
||||||
|
|
||||||
import { fetchAllShelves } from '../src/shelves';
|
|
||||||
|
|
||||||
async function render(pageContext) {
|
async function render(pageContext) {
|
||||||
// See https://vite-plugin-ssr.com/head
|
// See https://vite-plugin-ssr.com/head
|
||||||
const { documentProps } = pageContext.exports;
|
const { documentProps } = pageContext.exports;
|
||||||
|
@ -50,15 +48,11 @@ async function render(pageContext) {
|
||||||
async function onBeforeRender(pageContext) {
|
async function onBeforeRender(pageContext) {
|
||||||
try {
|
try {
|
||||||
const pageData = await pageContext.exports.getPageData?.(pageContext, pageContext.session);
|
const pageData = await pageContext.exports.getPageData?.(pageContext, pageContext.session);
|
||||||
const shelves = await fetchAllShelves({ user: pageContext.session.user });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pageContext: {
|
pageContext: {
|
||||||
// initialState: store.state.value,
|
// initialState: store.state.value,
|
||||||
pageData: {
|
pageData,
|
||||||
...pageData,
|
|
||||||
shelves,
|
|
||||||
},
|
|
||||||
me: pageContext.session.user,
|
me: pageContext.session.user,
|
||||||
now: new Date(),
|
now: new Date(),
|
||||||
},
|
},
|
||||||
|
@ -68,7 +62,9 @@ async function onBeforeRender(pageContext) {
|
||||||
|
|
||||||
throw RenderErrorPage({
|
throw RenderErrorPage({
|
||||||
pageContext: {
|
pageContext: {
|
||||||
pageProps: error,
|
pageProps: {
|
||||||
|
errorInfo: error.statusMessage,
|
||||||
|
},
|
||||||
me: pageContext.session.user,
|
me: pageContext.session.user,
|
||||||
now: new Date(),
|
now: new Date(),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div v-if="is404">
|
<div v-if="is404">
|
||||||
<h1>{{ statusCode }}</h1>
|
<h1>404 Page Not Found</h1>
|
||||||
|
|
||||||
<p v-if="statusMessage">{{ statusMessage }}</p>
|
<p v-if="errorInfo">{{ errorInfo }}</p>
|
||||||
<p v-else>This page could not be found.</p>
|
<p v-else>This page could not be found.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h1>500 Internal Error</h1>
|
<h1>500 Internal Error</h1>
|
||||||
|
|
||||||
<p v-if="statusMessage">{{ errorInfo }}</p>
|
<p v-if="errorInfo">{{ errorInfo }}</p>
|
||||||
<p v-else>Something went wrong.</p>
|
<p v-else>Something went wrong.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,5 +20,5 @@
|
||||||
import { usePageContext } from './usePageContext';
|
import { usePageContext } from './usePageContext';
|
||||||
|
|
||||||
const { pageProps } = usePageContext();
|
const { pageProps } = usePageContext();
|
||||||
const { is404, statusCode, statusMessage } = pageProps;
|
const { is404, errorInfo } = pageProps;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import { createSSRApp, h } from 'vue';
|
import { createSSRApp, h } from 'vue';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import FloatingVue from 'floating-vue';
|
|
||||||
|
|
||||||
import Container from './container.vue';
|
import Container from './container.vue';
|
||||||
import { setPageContext } from './usePageContext';
|
import { setPageContext } from './usePageContext';
|
||||||
|
|
||||||
import '../assets/css/style.css';
|
import '../assets/css/style.css';
|
||||||
import 'floating-vue/dist/style.css';
|
|
||||||
|
|
||||||
function createApp(pageContext) {
|
function createApp(pageContext) {
|
||||||
const PageWithLayout = {
|
const PageWithLayout = {
|
||||||
|
@ -23,7 +20,6 @@ function createApp(pageContext) {
|
||||||
const store = createPinia();
|
const store = createPinia();
|
||||||
|
|
||||||
app.use(store);
|
app.use(store);
|
||||||
app.use(FloatingVue);
|
|
||||||
|
|
||||||
// We make pageContext available from any Vue component
|
// We make pageContext available from any Vue component
|
||||||
setPageContext(app, pageContext);
|
setPageContext(app, pageContext);
|
||||||
|
|
|
@ -18,63 +18,11 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<span
|
||||||
<VDropdown>
|
v-if="me"
|
||||||
<button class="button">Explore</button>
|
class="userpanel"
|
||||||
|
@click="logout"
|
||||||
<template #popper>
|
>{{ me.username }}</span>
|
||||||
<ul class="tooltip nolist">
|
|
||||||
<li class="tooltip-item">
|
|
||||||
<a
|
|
||||||
href="/shelf/create"
|
|
||||||
class="menu-item"
|
|
||||||
>
|
|
||||||
<button class="button button-submit">New shelf</button>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li
|
|
||||||
v-for="shelf in shelves"
|
|
||||||
:key="shelf.id"
|
|
||||||
class="tooltip-item"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
:href="`/s/${shelf.slug}`"
|
|
||||||
class="link shelf"
|
|
||||||
>s/{{ shelf.slug }}</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<VDropdown v-if="me">
|
|
||||||
<span class="userpanel">{{ me.username }}</span>
|
|
||||||
|
|
||||||
<template #popper>
|
|
||||||
<ul class="tooltip nolist">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
:href="`/user/${me.username}`"
|
|
||||||
class="link menu-item"
|
|
||||||
>Profile</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="menu-item">
|
|
||||||
<button
|
|
||||||
class="button"
|
|
||||||
@click="logout"
|
|
||||||
>Log out</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</template>
|
|
||||||
</VDropdown>
|
|
||||||
|
|
||||||
<a
|
|
||||||
v-else
|
|
||||||
href="/account/login"
|
|
||||||
class="link userpanel"
|
|
||||||
>Log in</a>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content-container">
|
<div class="content-container">
|
||||||
|
@ -90,9 +38,7 @@ import { del } from '../assets/js/api';
|
||||||
import { navigate } from '../assets/js/navigate';
|
import { navigate } from '../assets/js/navigate';
|
||||||
import { usePageContext } from './usePageContext';
|
import { usePageContext } from './usePageContext';
|
||||||
|
|
||||||
const { pageData, me } = usePageContext();
|
const { me } = usePageContext();
|
||||||
const { shelves } = pageData;
|
|
||||||
|
|
||||||
const version = CLIENT_VERSION;
|
const version = CLIENT_VERSION;
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
|
@ -102,13 +48,6 @@ async function logout() {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.content {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo svg {
|
.logo svg {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
@ -132,7 +71,6 @@ async function logout() {
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
box-shadow: 0 0 3px var(--shadow-weak-10);
|
box-shadow: 0 0 3px var(--shadow-weak-10);
|
||||||
|
@ -161,32 +99,11 @@ async function logout() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.userpanel {
|
.userpanel {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
width: 10rem;
|
|
||||||
padding-bottom: .25rem;
|
|
||||||
|
|
||||||
.button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shelf {
|
|
||||||
display: block;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: .25rem .5rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
|
120
src/posts.js
120
src/posts.js
|
@ -3,9 +3,8 @@ import { verifyPrivilege } from './privileges';
|
||||||
import knex from './knex';
|
import knex from './knex';
|
||||||
import { HttpError } from './errors';
|
import { HttpError } from './errors';
|
||||||
|
|
||||||
import { fetchShelf, fetchShelves } from './shelves';
|
import { fetchShelf } from './shelves';
|
||||||
import { fetchUsers } from './users';
|
import { fetchUsers } from './users';
|
||||||
import slugify from './utils/slugify';
|
|
||||||
|
|
||||||
const emptyVote = {
|
const emptyVote = {
|
||||||
tally: 0,
|
tally: 0,
|
||||||
|
@ -15,19 +14,18 @@ const emptyVote = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function curateDatabasePost(post, {
|
function curateDatabasePost(post, {
|
||||||
shelf, shelves, users, vote, votes,
|
shelf, users, votes,
|
||||||
}) {
|
}) {
|
||||||
const curatedPost = {
|
const curatedPost = {
|
||||||
id: post.id,
|
id: post.id,
|
||||||
title: post.title,
|
title: post.title,
|
||||||
slug: slugify(post.title, { limit: 50 }),
|
|
||||||
body: post.body,
|
body: post.body,
|
||||||
link: post.link,
|
link: post.link,
|
||||||
shelfId: post.shelf_id,
|
shelfId: post.shelf_id,
|
||||||
createdAt: post.created_at,
|
createdAt: post.created_at,
|
||||||
shelf: shelf || shelves?.[post.shelf_id],
|
shelf,
|
||||||
user: users.find((user) => user.id === post.user_id),
|
user: users.find((user) => user.id === post.user_id),
|
||||||
vote: vote || votes?.[post.id] || emptyVote,
|
vote: votes[post.id] || emptyVote,
|
||||||
commentCount: Number(post.comment_count),
|
commentCount: Number(post.comment_count),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -60,13 +58,13 @@ async function fetchPostVotes(postIds, user) {
|
||||||
return Object.fromEntries(votes.map((vote) => [vote.post_id, curatePostVote(vote)]));
|
return Object.fromEntries(votes.map((vote) => [vote.post_id, curatePostVote(vote)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchShelfPosts(shelfIds, { user, limit = 100 } = {}) {
|
async function fetchShelfPosts(shelfId, { user, limit = 100 } = {}) {
|
||||||
const shelves = await fetchShelves([].concat(shelfIds));
|
const shelf = await fetchShelf(shelfId);
|
||||||
|
|
||||||
const posts = await knex('posts')
|
const posts = await knex('posts')
|
||||||
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
|
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
|
||||||
.leftJoin('comments', 'comments.post_id', 'posts.id')
|
.leftJoin('comments', 'comments.post_id', 'posts.id')
|
||||||
.whereIn('shelf_id', Object.keys(shelves))
|
.where('shelf_id', shelf.id)
|
||||||
.orderBy('created_at', 'desc')
|
.orderBy('created_at', 'desc')
|
||||||
.groupBy('posts.id')
|
.groupBy('posts.id')
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
@ -76,50 +74,7 @@ async function fetchShelfPosts(shelfIds, { user, limit = 100 } = {}) {
|
||||||
fetchPostVotes(posts.map((post) => post.id), user),
|
fetchPostVotes(posts.map((post) => post.id), user),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return posts.map((post) => curateDatabasePost(post, { shelves, users, votes }));
|
return posts.map((post) => curateDatabasePost(post, { shelf, users, votes }));
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchUserPosts(user, { limit = 20 } = {}) {
|
|
||||||
if (!user) {
|
|
||||||
throw new HttpError({
|
|
||||||
statusMessage: 'You are not logged in',
|
|
||||||
statusCode: 401,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const posts = await knex('shelves_subscriptions')
|
|
||||||
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
|
|
||||||
.leftJoin('posts', 'posts.shelf_id', 'shelves_subscriptions.shelf_id')
|
|
||||||
.leftJoin('comments', 'comments.post_id', 'posts.id')
|
|
||||||
.where('shelves_subscriptions.user_id', user.id)
|
|
||||||
.groupBy('posts.id')
|
|
||||||
.orderBy('created_at', 'desc')
|
|
||||||
.limit(limit);
|
|
||||||
|
|
||||||
const [shelves, users, votes] = await Promise.all([
|
|
||||||
fetchShelves(posts.map((post) => post.shelf_id)),
|
|
||||||
fetchUsers(posts.map((post) => post.user_id)),
|
|
||||||
fetchPostVotes(posts.map((post) => post.id), user),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return posts.map((post) => curateDatabasePost(post, { shelves, users, votes }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchAllPosts({ user, limit = 20 } = {}) {
|
|
||||||
const posts = await knex('posts')
|
|
||||||
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
|
|
||||||
.leftJoin('comments', 'comments.post_id', 'posts.id')
|
|
||||||
.orderBy('created_at', 'desc')
|
|
||||||
.groupBy('posts.id')
|
|
||||||
.limit(limit);
|
|
||||||
|
|
||||||
const [shelves, users, votes] = await Promise.all([
|
|
||||||
fetchShelves(posts.map((post) => post.shelf_id)),
|
|
||||||
fetchUsers(posts.map((post) => post.user_id)),
|
|
||||||
fetchPostVotes(posts.map((post) => post.id), user),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return posts.map((post) => curateDatabasePost(post, { shelves, users, votes }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPost(postId, user) {
|
async function fetchPost(postId, user) {
|
||||||
|
@ -147,6 +102,33 @@ async function fetchPost(postId, user) {
|
||||||
return curateDatabasePost(post, { shelf, users, votes });
|
return curateDatabasePost(post, { shelf, users, votes });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createPost(post, shelfId, user) {
|
||||||
|
await verifyPrivilege('createPost', user);
|
||||||
|
|
||||||
|
const shelf = await fetchShelf(shelfId);
|
||||||
|
|
||||||
|
if (!shelf) {
|
||||||
|
throw new HttpError({
|
||||||
|
statusMessage: 'The target shelf does not exist',
|
||||||
|
statusCode: 404,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [postEntry] = await knex('posts')
|
||||||
|
.insert({
|
||||||
|
title: post.title,
|
||||||
|
body: post.body,
|
||||||
|
link: post.link,
|
||||||
|
shelf_id: shelf.id,
|
||||||
|
user_id: user.id,
|
||||||
|
})
|
||||||
|
.returning('*');
|
||||||
|
|
||||||
|
const users = await fetchUsers([postEntry.user_id]);
|
||||||
|
|
||||||
|
return curateDatabasePost(postEntry, { shelf, users });
|
||||||
|
}
|
||||||
|
|
||||||
async function votePost(postId, value, user) {
|
async function votePost(postId, value, user) {
|
||||||
if (value === 0) {
|
if (value === 0) {
|
||||||
await knex('posts_votes')
|
await knex('posts_votes')
|
||||||
|
@ -171,41 +153,9 @@ async function votePost(postId, value, user) {
|
||||||
return votes[postId] || emptyVote;
|
return votes[postId] || emptyVote;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPost(post, shelfId, user) {
|
|
||||||
await verifyPrivilege('createPost', user);
|
|
||||||
|
|
||||||
const shelf = await fetchShelf(shelfId);
|
|
||||||
|
|
||||||
if (!shelf) {
|
|
||||||
throw new HttpError({
|
|
||||||
statusMessage: 'The target shelf does not exist',
|
|
||||||
statusCode: 404,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const [postEntry] = await knex('posts')
|
|
||||||
.insert({
|
|
||||||
title: post.title,
|
|
||||||
body: post.body,
|
|
||||||
link: post.link,
|
|
||||||
shelf_id: shelf.id,
|
|
||||||
user_id: user.id,
|
|
||||||
})
|
|
||||||
.returning('*');
|
|
||||||
|
|
||||||
const [users, vote] = await Promise.all([
|
|
||||||
fetchUsers([postEntry.user_id]),
|
|
||||||
votePost(postEntry.id, 1, user),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return curateDatabasePost(postEntry, { shelf, users, vote });
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createPost,
|
createPost,
|
||||||
fetchPost,
|
fetchPost,
|
||||||
fetchShelfPosts,
|
fetchShelfPosts,
|
||||||
fetchUserPosts,
|
|
||||||
fetchAllPosts,
|
|
||||||
votePost,
|
votePost,
|
||||||
};
|
};
|
||||||
|
|
113
src/shelves.js
113
src/shelves.js
|
@ -1,5 +1,4 @@
|
||||||
import knex from './knex';
|
import knex from './knex';
|
||||||
import { HttpError } from './errors';
|
|
||||||
|
|
||||||
function curateDatabaseShelf(shelf) {
|
function curateDatabaseShelf(shelf) {
|
||||||
if (!shelf) {
|
if (!shelf) {
|
||||||
|
@ -10,70 +9,32 @@ function curateDatabaseShelf(shelf) {
|
||||||
id: shelf.id,
|
id: shelf.id,
|
||||||
slug: shelf.slug,
|
slug: shelf.slug,
|
||||||
name: shelf.slug,
|
name: shelf.slug,
|
||||||
subscribed: !!shelf.subscribed,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function identityQuery(builder, shelfId) {
|
async function fetchShelf(shelfId) {
|
||||||
const id = Number(shelfId);
|
|
||||||
|
|
||||||
if (Number.isNaN(id)) {
|
|
||||||
builder.where('slug', shelfId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.where('id', shelfId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function identitiesQuery(builder, shelfIds) {
|
|
||||||
const ids = Array.from(new Set(shelfIds.filter((shelfId) => !Number.isNaN(Number(shelfId)))));
|
|
||||||
const slugs = Array.from(new Set(shelfIds.filter((shelfId) => Number.isNaN(Number(shelfId)))));
|
|
||||||
|
|
||||||
builder
|
|
||||||
.whereIn('id', ids)
|
|
||||||
.orWhereIn('slug', slugs);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isMemberQuery(builder, user) {
|
|
||||||
if (user) {
|
|
||||||
builder.select(knex.raw('shelves_subscriptions.id IS NOT NULL as subscribed'));
|
|
||||||
|
|
||||||
builder.leftJoin('shelves_subscriptions', (joinBuilder) => {
|
|
||||||
joinBuilder.on('shelf_id', 'shelves.id');
|
|
||||||
joinBuilder.andOnVal('user_id', user.id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchShelf(shelfId, { user } = {}) {
|
|
||||||
const shelfEntry = await knex('shelves')
|
const shelfEntry = await knex('shelves')
|
||||||
.select('shelves.*')
|
.where((builder) => {
|
||||||
.modify((builder) => isMemberQuery(builder, user))
|
const id = Number(shelfId);
|
||||||
.where((builder) => identityQuery(builder, shelfId))
|
|
||||||
|
if (Number.isNaN(id)) {
|
||||||
|
builder.where('slug', shelfId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.where('id', shelfId);
|
||||||
|
})
|
||||||
.first();
|
.first();
|
||||||
|
|
||||||
return curateDatabaseShelf(shelfEntry);
|
return curateDatabaseShelf(shelfEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAllShelves({ user, limit = 100 } = {}) {
|
async function fetchShelves({ limit = 10 } = {}) {
|
||||||
const shelfEntries = await knex('shelves')
|
const shelfEntries = await knex('shelves').limit(limit);
|
||||||
.select('shelves.*')
|
|
||||||
.modify((builder) => isMemberQuery(builder, user))
|
|
||||||
.orderBy('slug', 'asc')
|
|
||||||
.limit(limit);
|
|
||||||
|
|
||||||
return shelfEntries.map((shelfEntry) => curateDatabaseShelf(shelfEntry));
|
return shelfEntries.map((shelfEntry) => curateDatabaseShelf(shelfEntry));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchShelves(shelfIds, { user } = {}) {
|
|
||||||
const shelfEntries = await knex('shelves')
|
|
||||||
.select('shelves.*')
|
|
||||||
.where((builder) => identitiesQuery(builder, shelfIds))
|
|
||||||
.modify((builder) => isMemberQuery(builder, user));
|
|
||||||
|
|
||||||
return Object.fromEntries(shelfEntries.map((shelfEntry) => [shelfEntry.id, curateDatabaseShelf(shelfEntry)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createShelf(shelf, user) {
|
async function createShelf(shelf, user) {
|
||||||
const [shelfEntry] = await knex('shelves')
|
const [shelfEntry] = await knex('shelves')
|
||||||
.insert({
|
.insert({
|
||||||
|
@ -85,57 +46,9 @@ async function createShelf(shelf, user) {
|
||||||
return curateDatabaseShelf(shelfEntry);
|
return curateDatabaseShelf(shelfEntry);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function subscribe(shelfId, user) {
|
|
||||||
const shelf = await fetchShelf(shelfId);
|
|
||||||
|
|
||||||
if (!shelf) {
|
|
||||||
throw new HttpError({
|
|
||||||
statusMessage: `Shelf ${shelfId} does not exist`,
|
|
||||||
statusCode: 404,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await knex('shelves_subscriptions').insert({
|
|
||||||
shelf_id: shelf.id,
|
|
||||||
user_id: user.id,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === '23505') {
|
|
||||||
throw new HttpError({
|
|
||||||
statusMessage: `You are already subscribed to s/${shelf.slug}`,
|
|
||||||
statusCode: 409,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function unsubscribe(shelfId, user) {
|
|
||||||
const shelf = await fetchShelf(shelfId);
|
|
||||||
|
|
||||||
if (!shelf) {
|
|
||||||
throw new HttpError({
|
|
||||||
statusMessage: `Shelf ${shelfId} does not exist`,
|
|
||||||
statusCode: 404,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await knex('shelves_subscriptions')
|
|
||||||
.where({
|
|
||||||
shelf_id: shelf.id,
|
|
||||||
user_id: user.id,
|
|
||||||
})
|
|
||||||
.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
fetchShelf,
|
fetchShelf,
|
||||||
fetchShelves,
|
fetchShelves,
|
||||||
fetchAllShelves,
|
|
||||||
createShelf,
|
createShelf,
|
||||||
curateDatabaseShelf,
|
curateDatabaseShelf,
|
||||||
subscribe,
|
|
||||||
unsubscribe,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
const substitutes = {
|
|
||||||
à: 'a',
|
|
||||||
á: 'a',
|
|
||||||
ä: 'a',
|
|
||||||
å: 'a',
|
|
||||||
ã: 'a',
|
|
||||||
æ: 'ae',
|
|
||||||
ç: 'c',
|
|
||||||
è: 'e',
|
|
||||||
é: 'e',
|
|
||||||
ë: 'e',
|
|
||||||
ẽ: 'e',
|
|
||||||
ì: 'i',
|
|
||||||
í: 'i',
|
|
||||||
ï: 'i',
|
|
||||||
ĩ: 'i',
|
|
||||||
ǹ: 'n',
|
|
||||||
ń: 'n',
|
|
||||||
ñ: 'n',
|
|
||||||
ò: 'o',
|
|
||||||
ó: 'o',
|
|
||||||
ö: 'o',
|
|
||||||
õ: 'o',
|
|
||||||
ø: 'o',
|
|
||||||
œ: 'oe',
|
|
||||||
ß: 'ss',
|
|
||||||
ù: 'u',
|
|
||||||
ú: 'u',
|
|
||||||
ü: 'u',
|
|
||||||
ũ: 'u',
|
|
||||||
ỳ: 'y',
|
|
||||||
ý: 'y',
|
|
||||||
ÿ: 'y',
|
|
||||||
ỹ: 'y',
|
|
||||||
};
|
|
||||||
|
|
||||||
function slugify(strings, {
|
|
||||||
delimiter = '-',
|
|
||||||
encode = false,
|
|
||||||
removeAccents = true,
|
|
||||||
removePunctuation = false,
|
|
||||||
limit = 1000,
|
|
||||||
} = {}) {
|
|
||||||
if (!strings || (typeof strings !== 'string' && !Array.isArray(strings))) {
|
|
||||||
return strings;
|
|
||||||
}
|
|
||||||
|
|
||||||
const slugComponents = []
|
|
||||||
.concat(strings)
|
|
||||||
.filter(Boolean)
|
|
||||||
.flatMap((string) => string
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(removePunctuation && /[.,:;'"_-]/g, '')
|
|
||||||
.match(/[A-Za-zÀ-ÖØ-öø-ÿ0-9]+/g));
|
|
||||||
|
|
||||||
if (!slugComponents) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const slug = slugComponents.reduce((acc, component, index) => {
|
|
||||||
const accSlug = `${acc}${index > 0 ? delimiter : ''}${component}`;
|
|
||||||
|
|
||||||
if (accSlug.length < limit) {
|
|
||||||
if (removeAccents) {
|
|
||||||
return accSlug.replace(/[à-ÿ]/g, (match) => substitutes[match] || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
return accSlug;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
return encode ? encodeURI(slug) : slug;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default slugify;
|
|
|
@ -23,11 +23,7 @@ import {
|
||||||
createUser,
|
createUser,
|
||||||
} from './users';
|
} from './users';
|
||||||
|
|
||||||
import {
|
import { createShelf } from './shelves';
|
||||||
createShelf,
|
|
||||||
subscribe,
|
|
||||||
unsubscribe,
|
|
||||||
} from './shelves';
|
|
||||||
|
|
||||||
import { createPost, votePost } from './posts';
|
import { createPost, votePost } from './posts';
|
||||||
import { addComment } from './comments';
|
import { addComment } from './comments';
|
||||||
|
@ -72,10 +68,6 @@ async function startServer() {
|
||||||
// SHELVES
|
// SHELVES
|
||||||
router.post('/api/shelves', createShelf);
|
router.post('/api/shelves', createShelf);
|
||||||
|
|
||||||
// MEMBERS
|
|
||||||
router.post('/api/shelves/:shelfId/members', subscribe);
|
|
||||||
router.delete('/api/shelves/:shelfId/members', unsubscribe);
|
|
||||||
|
|
||||||
// POSTS
|
// POSTS
|
||||||
router.post('/api/shelves/:shelfId/posts', createPost);
|
router.post('/api/shelves/:shelfId/posts', createPost);
|
||||||
router.post('/api/posts/:postId/votes', votePost);
|
router.post('/api/posts/:postId/votes', votePost);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createShelf, subscribe, unsubscribe } from '../shelves';
|
import { createShelf } from '../shelves';
|
||||||
|
|
||||||
async function createShelfApi(req, res) {
|
async function createShelfApi(req, res) {
|
||||||
const shelf = await createShelf(req.body, req.user);
|
const shelf = await createShelf(req.body, req.user);
|
||||||
|
@ -6,18 +6,6 @@ async function createShelfApi(req, res) {
|
||||||
res.send(shelf);
|
res.send(shelf);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function subscribeApi(req, res) {
|
|
||||||
await subscribe(req.params.shelfId, req.user);
|
|
||||||
res.status(204).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function unsubscribeApi(req, res) {
|
|
||||||
await unsubscribe(req.params.shelfId, req.user);
|
|
||||||
res.status(204).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createShelfApi as createShelf,
|
createShelfApi as createShelf,
|
||||||
subscribeApi as subscribe,
|
|
||||||
unsubscribeApi as unsubscribe,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,18 +15,6 @@
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<h4 class="sidebar-title">{{ shelf.slug }}</h4>
|
<h4 class="sidebar-title">{{ shelf.slug }}</h4>
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="subscribed"
|
|
||||||
class="button button-submit subscribe"
|
|
||||||
@click="unsubscribe"
|
|
||||||
>Unsubscribe</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-else
|
|
||||||
class="button button-submit subscribe"
|
|
||||||
@click="subscribe"
|
|
||||||
>Subscribe</button>
|
|
||||||
|
|
||||||
<form
|
<form
|
||||||
class="form compose"
|
class="form compose"
|
||||||
@submit.prevent="submitPost"
|
@submit.prevent="submitPost"
|
||||||
|
@ -56,10 +44,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button
|
<button class="button button-submit">Post</button>
|
||||||
class="button button-submit"
|
|
||||||
:disabled="!body && !link"
|
|
||||||
>Post</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -68,7 +53,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, defineProps } from 'vue';
|
||||||
import * as api from '../../assets/js/api';
|
import * as api from '../../assets/js/api';
|
||||||
import { navigate } from '../../assets/js/navigate';
|
import { navigate } from '../../assets/js/navigate';
|
||||||
import { usePageContext } from '../../renderer/usePageContext';
|
import { usePageContext } from '../../renderer/usePageContext';
|
||||||
|
@ -83,9 +68,8 @@ const props = defineProps({
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = ref();
|
const title = ref();
|
||||||
const link = ref(null);
|
const link = ref();
|
||||||
const body = ref(null);
|
const body = ref();
|
||||||
const subscribed = ref(props.shelf.subscribed);
|
|
||||||
|
|
||||||
async function submitPost() {
|
async function submitPost() {
|
||||||
const post = await api.post(`/shelves/${routeParams.id}/posts`, {
|
const post = await api.post(`/shelves/${routeParams.id}/posts`, {
|
||||||
|
@ -94,17 +78,7 @@ async function submitPost() {
|
||||||
body: body.value,
|
body: body.value,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(`/s/${props.shelf.slug}/post/${post.id}/${post.slug}`);
|
navigate(`/s/${props.shelf.slug}/post/${post.id}`);
|
||||||
}
|
|
||||||
|
|
||||||
async function subscribe() {
|
|
||||||
subscribed.value = true;
|
|
||||||
await api.post(`/shelves/${routeParams.id}/members`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function unsubscribe() {
|
|
||||||
subscribed.value = false;
|
|
||||||
await api.delete(`/shelves/${routeParams.id}/members`);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -149,9 +123,4 @@ async function unsubscribe() {
|
||||||
margin: 0 0 1rem 0;
|
margin: 0 0 1rem 0;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribe {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue