Added basic post creation.
This commit is contained in:
parent
de757efc6e
commit
9a9b92a6b1
|
@ -0,0 +1,5 @@
|
|||
<!-- Generated by IcoMoon.io -->
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<title>blocked</title>
|
||||
<path d="M27.314 4.686c-3.022-3.022-7.040-4.686-11.314-4.686s-8.292 1.664-11.314 4.686c-3.022 3.022-4.686 7.040-4.686 11.314s1.664 8.292 4.686 11.314c3.022 3.022 7.040 4.686 11.314 4.686s8.292-1.664 11.314-4.686c3.022-3.022 4.686-7.040 4.686-11.314s-1.664-8.292-4.686-11.314zM28 16c0 2.588-0.824 4.987-2.222 6.949l-16.727-16.727c1.962-1.399 4.361-2.222 6.949-2.222 6.617 0 12 5.383 12 12zM4 16c0-2.588 0.824-4.987 2.222-6.949l16.727 16.727c-1.962 1.399-4.361 2.222-6.949 2.222-6.617 0-12-5.383-12-12z"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 671 B |
|
@ -42,7 +42,7 @@ export async function post(path, data, { query } = {}) {
|
|||
return body;
|
||||
}
|
||||
|
||||
throw new Error(body.message);
|
||||
throw new Error(body.statusMessage);
|
||||
}
|
||||
|
||||
export async function patch(path, data, { query } = {}) {
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
<template>
|
||||
<a
|
||||
:href="`/s/shack/posts/${post.id}`"
|
||||
class="post"
|
||||
>
|
||||
<img
|
||||
class="thumbnail"
|
||||
:src="blockedIcon"
|
||||
>
|
||||
|
||||
<div class="body">
|
||||
<h2 class="title">
|
||||
<a
|
||||
:href="`/s/shack/posts/${post.id}`"
|
||||
class="link"
|
||||
>{{ post.title }}</a>
|
||||
</h2>
|
||||
|
||||
<div class="meta">
|
||||
<a
|
||||
:href="`/user/${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, new Date(), { includeSeconds: true }) }} ago</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { format, formatDistance } from 'date-fns';
|
||||
import blockedIcon from '../../assets/icons/blocked.svg?url'; // eslint-ignore import/no-unresolved
|
||||
|
||||
defineProps({
|
||||
post: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.post {
|
||||
display: flex;
|
||||
color: var(--text);
|
||||
border-radius: .25rem;
|
||||
background: var(--background);
|
||||
text-decoration: none;
|
||||
|
||||
& :hover {
|
||||
cursor: pointer;
|
||||
|
||||
.title {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: .5rem 0;
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: normal;
|
||||
color: var(--grey-dark-30);
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.shelf {
|
||||
color: inherit;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--grey-dark-20);
|
||||
}
|
||||
</style>
|
|
@ -1,6 +1,6 @@
|
|||
const config = require('config');
|
||||
import config from 'config';
|
||||
|
||||
module.exports = {
|
||||
export default {
|
||||
client: 'pg',
|
||||
connection: config.database,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
exports.up = async function(knex) {
|
||||
import fs from 'fs';
|
||||
|
||||
export async function up(knex) {
|
||||
const nanoidFn = await fs.promises.readFile('./migrations/nanoid.sql', 'utf8'); // from https://github.com/viascom/nanoid-postgres
|
||||
|
||||
await knex.raw(nanoidFn);
|
||||
|
||||
await knex.raw(`
|
||||
CREATE FUNCTION shack_id(length smallint DEFAULT 8) RETURNS TEXT AS $$
|
||||
SELECT nanoid(length, '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz');
|
||||
$$ LANGUAGE SQL STABLE;
|
||||
`);
|
||||
|
||||
await knex.schema.createTable('users', (table) => {
|
||||
table.increments('id');
|
||||
|
||||
|
@ -63,7 +75,9 @@ exports.up = async function(knex) {
|
|||
});
|
||||
|
||||
await knex.schema.createTable('posts', (table) => {
|
||||
table.increments('id');
|
||||
table.text('id', 8)
|
||||
.primary()
|
||||
.defaultTo(knex.raw('shack_id()'));
|
||||
|
||||
table.text('title')
|
||||
.notNullable();
|
||||
|
@ -86,12 +100,39 @@ exports.up = async function(knex) {
|
|||
.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 url IS NOT NULL)');
|
||||
|
||||
exports.down = async function(knex) {
|
||||
await knex.schema.createTable('comments', (table) => {
|
||||
table.text('id', 8)
|
||||
.primary()
|
||||
.defaultTo(knex.raw('shack_id()'));
|
||||
|
||||
table.text('post_id', 8)
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('posts');
|
||||
|
||||
table.integer('user_id')
|
||||
.notNullable()
|
||||
.references('id')
|
||||
.inTable('users');
|
||||
|
||||
table.text('body');
|
||||
|
||||
table.datetime('created_at')
|
||||
.notNullable()
|
||||
.defaultTo(knex.fn.now());
|
||||
});
|
||||
}
|
||||
|
||||
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.raw(`
|
||||
DROP FUNCTION IF EXISTS shack_id;
|
||||
`);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright 2023 Viascom Ltd liab. Co
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
CREATE OR REPLACE FUNCTION nanoid(
|
||||
size int DEFAULT 21,
|
||||
alphabet text DEFAULT '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
)
|
||||
RETURNS text
|
||||
LANGUAGE plpgsql
|
||||
volatile
|
||||
AS
|
||||
$$
|
||||
DECLARE
|
||||
idBuilder text := '';
|
||||
counter int := 0;
|
||||
bytes bytea;
|
||||
alphabetIndex int;
|
||||
alphabetArray text[];
|
||||
alphabetLength int;
|
||||
mask int;
|
||||
step int;
|
||||
BEGIN
|
||||
alphabetArray := regexp_split_to_array(alphabet, '');
|
||||
alphabetLength := array_length(alphabetArray, 1);
|
||||
mask := (2 << cast(floor(log(alphabetLength - 1) / log(2)) as int)) - 1;
|
||||
step := cast(ceil(1.6 * mask * size / alphabetLength) AS int);
|
||||
|
||||
while true
|
||||
loop
|
||||
bytes := gen_random_bytes(step);
|
||||
while counter < step
|
||||
loop
|
||||
alphabetIndex := (get_byte(bytes, counter) & mask) + 1;
|
||||
if alphabetIndex <= alphabetLength then
|
||||
idBuilder := idBuilder || alphabetArray[alphabetIndex];
|
||||
if length(idBuilder) = size then
|
||||
return idBuilder;
|
||||
end if;
|
||||
end if;
|
||||
counter := counter + 1;
|
||||
end loop;
|
||||
|
||||
counter := 0;
|
||||
end loop;
|
||||
END
|
||||
$$;
|
|
@ -22,6 +22,7 @@
|
|||
"config": "^3.3.9",
|
||||
"connect-redis": "^7.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
|
@ -36,6 +37,7 @@
|
|||
"pg": "^8.11.0",
|
||||
"pinia": "^2.1.3",
|
||||
"redis": "^4.6.6",
|
||||
"short-uuid": "^4.2.2",
|
||||
"sirv": "^2.0.2",
|
||||
"vite": "^4.0.3",
|
||||
"vite-plugin-ssr": "^0.4.126",
|
||||
|
@ -2623,6 +2625,11 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/any-base": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
|
||||
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
|
@ -3381,6 +3388,21 @@
|
|||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -6747,6 +6769,26 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/short-uuid": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/short-uuid/-/short-uuid-4.2.2.tgz",
|
||||
"integrity": "sha512-IE7hDSGV2U/VZoCsjctKX6l5t5ak2jE0+aeGJi3KtvjIUNuZVmHVYUjNBhmo369FIWGDtaieRaO8A83Lvwfpqw==",
|
||||
"dependencies": {
|
||||
"any-base": "^1.1.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/short-uuid/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
|
@ -9570,6 +9612,11 @@
|
|||
"color-convert": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"any-base": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/any-base/-/any-base-1.1.0.tgz",
|
||||
"integrity": "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
|
@ -10135,6 +10182,14 @@
|
|||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz",
|
||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
@ -12534,6 +12589,22 @@
|
|||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="
|
||||
},
|
||||
"short-uuid": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/short-uuid/-/short-uuid-4.2.2.tgz",
|
||||
"integrity": "sha512-IE7hDSGV2U/VZoCsjctKX6l5t5ak2jE0+aeGJi3KtvjIUNuZVmHVYUjNBhmo369FIWGDtaieRaO8A83Lvwfpqw==",
|
||||
"requires": {
|
||||
"any-base": "^1.1.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"side-channel": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
"build": "vite build",
|
||||
"server": "node --experimental-specifier-resolution=node ./src/web/server",
|
||||
"server:prod": "cross-env NODE_ENV=production node ./src/web/server",
|
||||
"migrate-make": "knex-migrate generate",
|
||||
"migrate": "knex-migrate up",
|
||||
"rollback": "knex-migrate down"
|
||||
"migrate-make": "knex migrate:make",
|
||||
"migrate": "knex migrate:latest",
|
||||
"rollback": "knex migrate:rollback"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/cli": "^7.21.5",
|
||||
|
@ -39,6 +39,7 @@
|
|||
"config": "^3.3.9",
|
||||
"connect-redis": "^7.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"error-stack-parser": "^2.1.4",
|
||||
"eslint": "^8.41.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
|
@ -53,6 +54,7 @@
|
|||
"pg": "^8.11.0",
|
||||
"pinia": "^2.1.3",
|
||||
"redis": "^4.6.6",
|
||||
"short-uuid": "^4.2.2",
|
||||
"sirv": "^2.0.2",
|
||||
"vite": "^4.0.3",
|
||||
"vite-plugin-ssr": "^0.4.126",
|
||||
|
|
|
@ -33,21 +33,24 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div
|
||||
v-if="$config.public.captchaEnabled"
|
||||
v-if="config.captchaEnabled"
|
||||
class="form-row captcha"
|
||||
>
|
||||
<VueHcaptcha
|
||||
:sitekey="$config.public.captchaKey"
|
||||
:sitekey="config.captchaKey"
|
||||
@verify="(token) => captchaToken = token"
|
||||
@expired="() => captchaToken = null"
|
||||
@error="() => captchaToken = null"
|
||||
/>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="form-row form-actions">
|
||||
<a
|
||||
href="/account/create"
|
||||
class="link"
|
||||
>Sign up</a>
|
||||
|
||||
<button
|
||||
:disabled="!username || !password"
|
||||
class="button button-submit"
|
||||
|
@ -59,11 +62,13 @@
|
|||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
// import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
import VueHcaptcha from '@hcaptcha/vue3-hcaptcha';
|
||||
|
||||
import { post } from '../../assets/js/api';
|
||||
import navigate from '../../assets/js/navigate';
|
||||
|
||||
const config = CONFIG;
|
||||
|
||||
const username = ref('');
|
||||
const password = ref('');
|
||||
|
||||
|
@ -78,7 +83,7 @@ async function signup() {
|
|||
|
||||
navigate('/');
|
||||
} catch (error) {
|
||||
errorMsg.value = error.statusMessage;
|
||||
errorMsg.value = error.message;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -137,6 +142,7 @@ async function signup() {
|
|||
}
|
||||
|
||||
.form-actions {
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import { fetchShelves } from '../../src/shelves';
|
||||
|
||||
async function onBeforeRender(_pageContext) {
|
||||
const shelves = await fetchShelves();
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
pageData: {
|
||||
shelves,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { onBeforeRender };
|
|
@ -0,0 +1,29 @@
|
|||
import { RenderErrorPage } from 'vite-plugin-ssr/RenderErrorPage';
|
||||
import { fetchShelf } from '../../../src/shelves';
|
||||
import { fetchShelfPosts } from '../../../src/posts';
|
||||
|
||||
async function onBeforeRender(pageContext) {
|
||||
const shelf = await fetchShelf(pageContext.routeParams.id);
|
||||
const posts = await fetchShelfPosts(pageContext.routeParams.id, { limit: 50 });
|
||||
|
||||
if (!shelf) {
|
||||
throw RenderErrorPage({
|
||||
pageContext: {
|
||||
pageProps: {
|
||||
errorInfo: 'No shelf with this name exists',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
pageContext: {
|
||||
pageData: {
|
||||
shelf,
|
||||
posts,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export { onBeforeRender };
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="content">
|
||||
<a
|
||||
href="/"
|
||||
class="link"
|
||||
>Go back home</a>
|
||||
|
||||
<h3>{{ shelf.slug }}</h3>
|
||||
|
||||
<ul class="posts nolist">
|
||||
<li
|
||||
v-for="post in posts"
|
||||
:key="post.id"
|
||||
><Post :post="post" /></li>
|
||||
</ul>
|
||||
|
||||
<form
|
||||
class="form compose"
|
||||
@submit.prevent="submitPost"
|
||||
>
|
||||
<div class="form-row">
|
||||
<input
|
||||
v-model="title"
|
||||
placeholder="Title"
|
||||
class="input"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<input
|
||||
v-model="link"
|
||||
class="input"
|
||||
placeholder="Link"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<textarea
|
||||
v-model="body"
|
||||
placeholder="Body"
|
||||
class="input body"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button class="button button-submit">Post</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import Post from '../../../components/posts/post.vue';
|
||||
|
||||
import * as api from '../../../assets/js/api';
|
||||
import { usePageContext } from '../../../renderer/usePageContext';
|
||||
|
||||
const { pageData, routeParams } = usePageContext();
|
||||
|
||||
const {
|
||||
shelf,
|
||||
posts,
|
||||
} = pageData;
|
||||
|
||||
const title = ref();
|
||||
const link = ref();
|
||||
const body = ref();
|
||||
|
||||
async function submitPost() {
|
||||
await api.post(`/api/shelves/${routeParams.id}/posts`, {
|
||||
title: title.value,
|
||||
link: link.value,
|
||||
body: body.value,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.posts {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -48,4 +48,4 @@ export {
|
|||
render,
|
||||
};
|
||||
|
||||
export const passToClient = ['urlPathname', 'initialState', 'pageData', 'pageProps'];
|
||||
export const passToClient = ['urlPathname', 'initialState', 'pageData', 'pageProps', 'routeParams'];
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
|
||||
<div class="content-container">
|
||||
<slot />
|
||||
<footer class="footer">shuck {{ version }}</footer>
|
||||
<footer class="footer">shack {{ version }}</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
// import knex from './knex';
|
||||
import { verifyPrivilege } from './privileges';
|
||||
import knex from './knex';
|
||||
import { HttpError } from './errors';
|
||||
|
||||
import { fetchShelf, curateDatabaseShelf } from './shelves';
|
||||
import { curateDatabaseUser } from './users';
|
||||
|
||||
function curatePost(post) {
|
||||
const curatedPost = {
|
||||
id: post.id,
|
||||
title: post.title,
|
||||
body: post.body,
|
||||
url: post.url,
|
||||
shelfId: post.shelf_id,
|
||||
createdAt: post.created_at,
|
||||
shelf: curateDatabaseShelf(post.shelf),
|
||||
user: curateDatabaseUser(post.user),
|
||||
};
|
||||
|
||||
return curatedPost;
|
||||
}
|
||||
|
||||
async function fetchShelfPosts(shelfId, limit = 100) {
|
||||
const shelf = await fetchShelf(shelfId);
|
||||
|
||||
const posts = await knex('posts')
|
||||
.select('posts.*', knex.raw('row_to_json(users) as user'), knex.raw('row_to_json(shelves) as shelf'))
|
||||
.leftJoin('users', 'users.id', 'posts.user_id')
|
||||
.leftJoin('shelves', 'shelves.id', 'posts.shelf_id')
|
||||
.where('shelf_id', shelf.id)
|
||||
.orderBy('created_at', 'desc')
|
||||
.limit(limit);
|
||||
|
||||
return posts.map((post) => curatePost(post));
|
||||
}
|
||||
|
||||
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 postId = await knex('posts')
|
||||
.insert({
|
||||
title: post.title,
|
||||
body: post.body,
|
||||
url: post.url,
|
||||
shelf_id: shelf.id,
|
||||
user_id: user.id,
|
||||
})
|
||||
.returning('id');
|
||||
|
||||
return postId;
|
||||
}
|
||||
|
||||
export {
|
||||
createPost,
|
||||
fetchShelfPosts,
|
||||
};
|
|
@ -0,0 +1,18 @@
|
|||
import { HttpError } from './errors';
|
||||
|
||||
function verifyPrivilege(privilege, user, context) {
|
||||
if (!user) {
|
||||
throw new HttpError({
|
||||
statusMessage: 'You are not authenticated',
|
||||
statusCode: 401,
|
||||
});
|
||||
}
|
||||
|
||||
console.log('verify privilege', privilege, user, context);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export {
|
||||
verifyPrivilege,
|
||||
};
|
|
@ -51,4 +51,5 @@ export {
|
|||
fetchShelf,
|
||||
fetchShelves,
|
||||
createShelf,
|
||||
curateDatabaseShelf,
|
||||
};
|
||||
|
|
|
@ -143,6 +143,7 @@ async function createUser(credentials, context) {
|
|||
}
|
||||
|
||||
export {
|
||||
curateDatabaseUser,
|
||||
createUser,
|
||||
login,
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ export default function errorHandler(error, req, res, _next) {
|
|||
if (error.statusCode) {
|
||||
res.status(error.statusCode).send({
|
||||
statusCode: error.statusCode,
|
||||
message: error.statusMessage,
|
||||
statusMessage: error.statusMessage,
|
||||
});
|
||||
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
import { createPost } from '../posts';
|
||||
|
||||
async function createPostApi(req, res) {
|
||||
const post = await createPost(req.body, req.params.shelfId, req.user);
|
||||
|
||||
res.send(post);
|
||||
}
|
||||
|
||||
export {
|
||||
createPostApi as createPost,
|
||||
};
|
|
@ -25,6 +25,8 @@ import {
|
|||
|
||||
import { createShelf } from './shelves';
|
||||
|
||||
import { createPost } from './posts';
|
||||
|
||||
const logger = initLogger();
|
||||
|
||||
async function startServer() {
|
||||
|
@ -54,13 +56,19 @@ async function startServer() {
|
|||
app.use(viteDevMiddleware);
|
||||
}
|
||||
|
||||
// SESSIONS
|
||||
router.get('/api/session', fetchUser);
|
||||
router.post('/api/session', login);
|
||||
router.delete('/api/session', logout);
|
||||
|
||||
// USERS
|
||||
router.post('/api/users', createUser);
|
||||
|
||||
// SHELVES
|
||||
router.post('/api/shelves', createShelf);
|
||||
|
||||
router.post('/api/users', createUser);
|
||||
// POSTS
|
||||
router.post('/api/shelves/:shelfId/posts', createPost);
|
||||
|
||||
router.get('*', defaultHandler);
|
||||
router.use(errorHandler);
|
||||
|
|
Loading…
Reference in New Issue