Compare commits
10 Commits
c4a37dcdc5
...
725c967b4f
Author | SHA1 | Date | |
---|---|---|---|
725c967b4f | |||
8435fd31b0 | |||
f5cec3dd22 | |||
02f0f318eb | |||
53fba96092 | |||
f6c332bc2e | |||
6c23d0631b | |||
64d3e51311 | |||
25f5675612 | |||
6b59fe77c4 |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
postgres
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
.output
|
.output
|
||||||
|
|
2
articles
2
articles
|
@ -1 +1 @@
|
||||||
Subproject commit bcab249efc4bef08205f16f5fcf6a45313868f93
|
Subproject commit b72a133cdd6acd08d3edf802af2ebfac0a630bf9
|
|
@ -1,3 +1,4 @@
|
||||||
FROM postgres:16
|
FROM postgres:16
|
||||||
|
|
||||||
COPY ./docker/initdb.d/ /docker-entrypoint-initdb.d/
|
COPY ./docker/initdb.d/ /docker-entrypoint-initdb.d/
|
||||||
|
# COPY ./docker/postgres.conf /var/lib/postgresql/data/postgresql.conf
|
||||||
|
|
1
docker/postgres.conf
Normal file
1
docker/postgres.conf
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
721
package-lock.json
generated
721
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -35,6 +35,7 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
"mdsvex": "^0.12.3",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"sass": "^1.77.8"
|
"sass": "^1.77.8"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@import 'variables.scss';
|
@import '../variables.scss';
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'ZenKakuGothicNew-Regular';
|
font-family: 'ZenKakuGothicNew-Regular';
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #000;
|
color: #ffffff;
|
||||||
font-family: 'ZenKakuGothicNew-Regular', '游ゴシック体', 'Yu Gothic', YuGothic, 'ヒラギノ角ゴシック Pro',
|
font-family: 'ZenKakuGothicNew-Regular', '游ゴシック体', 'Yu Gothic', YuGothic, 'ヒラギノ角ゴシック Pro',
|
||||||
'Hiragino Kaku Gothic Pro', 'メイリオ', Meiryo, Osaka, 'MS PGothic', sans-serif;
|
'Hiragino Kaku Gothic Pro', 'メイリオ', Meiryo, Osaka, 'MS PGothic', sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
|
@ -33,7 +33,7 @@
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="cursor" bind:this={cursor} />
|
<div class="cursor" bind:this={cursor}></div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.cursor {
|
.cursor {
|
||||||
|
|
15
src/lib/publish.ts
Normal file
15
src/lib/publish.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
export type Publish =
|
||||||
|
| 'public'
|
||||||
|
| 'limited' // noindex
|
||||||
|
| 'private'; // noindex nofollow
|
||||||
|
|
||||||
|
export function robots(publish: Publish) {
|
||||||
|
switch (publish) {
|
||||||
|
case 'public':
|
||||||
|
return '';
|
||||||
|
case 'limited':
|
||||||
|
return 'noindex';
|
||||||
|
case 'private':
|
||||||
|
return 'noindex, nofollow';
|
||||||
|
}
|
||||||
|
}
|
12
src/lib/server/database/article.ts
Normal file
12
src/lib/server/database/article.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
export type Article = {
|
||||||
|
seq: number;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
category: string;
|
||||||
|
released_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
tags: string[];
|
||||||
|
image: string;
|
||||||
|
publish: string;
|
||||||
|
content: string;
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
// initialize
|
// initialize
|
||||||
import PG from '$lib/server/database';
|
import PG from '$lib/server/database';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { compile } from 'mdsvex';
|
||||||
|
|
||||||
export default async function init() {
|
export default async function init() {
|
||||||
|
|
||||||
|
@ -10,21 +10,75 @@ export default async function init() {
|
||||||
{
|
{
|
||||||
name: 'article',
|
name: 'article',
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'id', type: 'serial', constraint: 'primary key' },
|
{ name: 'seq', type: 'serial', constraint: 'primary key' },
|
||||||
|
{ name: 'id', type: 'text', constraint: 'not null' },
|
||||||
{ name: 'title', type: 'text', constraint: 'not null' },
|
{ name: 'title', type: 'text', constraint: 'not null' },
|
||||||
|
{ name: 'category', type: 'text', constraint: 'not null' },
|
||||||
{ name: 'released_at', type: 'timestamp', constraint: 'not null' },
|
{ name: 'released_at', type: 'timestamp', constraint: 'not null' },
|
||||||
{ name: 'updated_at', type: 'timestamp', constraint: 'not null' },
|
{ name: 'updated_at', type: 'timestamp', constraint: 'not null' },
|
||||||
{ name: 'tags', type: 'text[]', constraint: 'not null' },
|
{ name: 'tags', type: 'text[]', constraint: 'not null' },
|
||||||
|
{ name: 'image', type: 'text', constraint: '' },
|
||||||
|
{ name: 'publish', type: 'text', constraint: 'not null' },
|
||||||
|
{ name: 'content', type: 'text', constraint: 'not null' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'article_comment',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'serial', constraint: 'primary key' },
|
||||||
|
{ name: 'article', type: 'integer', constraint: 'not null' },
|
||||||
|
{ name: 'posted_at', type: 'timestamp', constraint: 'not null' },
|
||||||
|
{ name: 'content', type: 'text', constraint: 'not null' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'thread',
|
||||||
|
columns: [
|
||||||
|
{ name: 'seq', type: 'serial', constraint: 'primary key' },
|
||||||
|
{ name: 'id', type: 'text', constraint: 'not null' },
|
||||||
|
{ name: 'title', type: 'text', constraint: 'not null' },
|
||||||
|
{ name: 'category', type: 'text', constraint: 'not null' },
|
||||||
|
{ name: 'created_at', type: 'timestamp', constraint: 'not null' },
|
||||||
|
{ name: 'updated_at', type: 'timestamp', constraint: 'not null' },
|
||||||
|
{ name: 'tags', type: 'text[]', constraint: 'not null' },
|
||||||
{ name: 'content', type: 'text', constraint: 'not null' },
|
{ name: 'content', type: 'text', constraint: 'not null' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'thread_post',
|
||||||
|
columns: [
|
||||||
|
{ name: 'seq', type: 'serial', constraint: 'primary key' },
|
||||||
|
{ name: 'thread_id', type: 'integer', constraint: 'not null' },
|
||||||
|
{ name: 'title', type: 'text', constraint: 'not null' },
|
||||||
|
{ name: 'posted_at', type: 'timestamp', constraint: 'not null' },
|
||||||
|
{ name: 'content', type: 'text', constraint: 'not null' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'thread_comment',
|
||||||
|
columns: [
|
||||||
|
{ name: 'id', type: 'serial', constraint: 'primary key' },
|
||||||
|
{ name: 'thread', type: 'integer', constraint: 'not null' },
|
||||||
|
{ name: 'posted_at', type: 'timestamp', constraint: 'not null' },
|
||||||
|
{ name: 'content', type: 'text', constraint: 'not null' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tag',
|
||||||
|
columns: [
|
||||||
|
{ name: 'seq', type: 'serial', constraint: 'primary key' },
|
||||||
|
{ name: 'name', type: 'text', constraint: 'not null' },
|
||||||
|
{ name: 'ref_count', type: 'integer', constraint: 'not null' },
|
||||||
|
],
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const db = await PG();
|
const db = await PG();
|
||||||
try {
|
try {
|
||||||
await db.begin();
|
await db.begin();
|
||||||
for (const schema of schemas) {
|
for (const schema of schemas) {
|
||||||
const res = await db.query(`select * from information_schema.tables where table_name = '${schema.name}'`)
|
const res = await db.query(`select * from information_schema.tables where table_name = '${schema.name}'`)
|
||||||
if (res.rowCount === null) {
|
if (res.rowCount == 0) {
|
||||||
console.log(`Creating table ${schema.name}`);
|
console.log(`Creating table ${schema.name}`);
|
||||||
const columnStr = schema.columns.map(c => `${c.name} ${c.type} ${c.constraint}`).join(', ');
|
const columnStr = schema.columns.map(c => `${c.name} ${c.type} ${c.constraint}`).join(', ');
|
||||||
await db.query(`create table ${schema.name} (${columnStr})`);
|
await db.query(`create table ${schema.name} (${columnStr})`);
|
||||||
|
@ -38,7 +92,6 @@ export default async function init() {
|
||||||
await db.rollback();
|
await db.rollback();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the article are already in the database
|
|
||||||
const articleFiles: ArticleFileItem[] = [];
|
const articleFiles: ArticleFileItem[] = [];
|
||||||
function scanDir(path: string) {
|
function scanDir(path: string) {
|
||||||
const files = fs.readdirSync(path);
|
const files = fs.readdirSync(path);
|
||||||
|
@ -48,13 +101,51 @@ export default async function init() {
|
||||||
if (stat.isDirectory()) {
|
if (stat.isDirectory()) {
|
||||||
scanDir(dir);
|
scanDir(dir);
|
||||||
} else {
|
} else {
|
||||||
// articleFiles.push({ path: `${path}/${file}`, id: file });
|
articleFiles.push({ path: `${path}/${file}`, id: file });
|
||||||
console.log(`${path}/${file}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
scanDir('./articles/dist');
|
scanDir('./articles/dist');
|
||||||
|
|
||||||
|
await db.query('update tag set ref_count = 0');
|
||||||
|
db.commit();
|
||||||
|
|
||||||
|
for (const { path, id } of articleFiles) {
|
||||||
|
const res = await db.query('select * from article where id = $1', [id]);
|
||||||
|
const md = await compile(fs.readFileSync(path, 'utf-8'));
|
||||||
|
const title = md.data.fm.title;
|
||||||
|
const category = path.split('/')[3];
|
||||||
|
const tags: string[] = md.data.fm.tags;
|
||||||
|
const released_at = new Date(md.data.fm.released_at);
|
||||||
|
const updated_at = new Date(md.data.fm.updated_at);
|
||||||
|
const image = md.data.fm.image;
|
||||||
|
const publish = md.data.fm.publish;
|
||||||
|
const content = md.code;
|
||||||
|
if (res.rowCount == 0) {
|
||||||
|
console.log(`New article: ${id}`);
|
||||||
|
await db.query(
|
||||||
|
'insert into article (id, title, category, released_at, updated_at, tags, image, publish, content) values ($1, $2, $3, $4, $5, $6, $7, $8, $9)',
|
||||||
|
[id, title, category, released_at, updated_at, tags, image, publish, content]
|
||||||
|
);
|
||||||
|
} else if (res.rows[0].updated_at < updated_at) {
|
||||||
|
console.log(`Update article: ${id}`);
|
||||||
|
await db.query(
|
||||||
|
'update article set title = $2, updated_at = $4, tags = $5, content = $6 where id = $1',
|
||||||
|
[id, title, updated_at, tags, content]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`Article ${id} is already up-to-date`);
|
||||||
|
}
|
||||||
|
for (const tag of tags) {
|
||||||
|
if ((await db.query('select * from tag where name = $1', [tag])).rowCount == 0) {
|
||||||
|
db.query('insert into tag (name, ref_count) values ($1, 1)', [tag]);
|
||||||
|
} else {
|
||||||
|
db.query('update tag set ref_count = ref_count + 1 where name = $1', [tag]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.commit();
|
||||||
await db.release();
|
await db.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -91,6 +91,13 @@
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
&:first-child {
|
&:first-child {
|
||||||
&::before {
|
&::before {
|
||||||
content: '🗀 ';
|
content: '🗀 ';
|
||||||
|
@ -100,13 +107,6 @@
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
margin-left: 5px;
|
|
||||||
padding: 0 5px;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import '../app.scss';
|
import '$lib/app.scss';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
|
@ -1,83 +1,47 @@
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import type { Content } from '$lib/article';
|
|
||||||
import PG from '$lib/server/database';
|
import PG from '$lib/server/database';
|
||||||
|
|
||||||
let data: {
|
let data: {
|
||||||
recent: Content[],
|
recent: {
|
||||||
|
title: string,
|
||||||
|
image: string,
|
||||||
|
date: string,
|
||||||
|
link: string,
|
||||||
|
tags: string[]
|
||||||
|
}[],
|
||||||
tags: string[],
|
tags: string[],
|
||||||
updated: string
|
updated: string,
|
||||||
} = {
|
} = {
|
||||||
recent: [
|
recent: [],
|
||||||
{
|
tags: [],
|
||||||
title: "title",
|
|
||||||
image: "image",
|
|
||||||
date: "date",
|
|
||||||
link: "link",
|
|
||||||
tags: ["tag1", "tag2"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
tags: ["hoge", "fuga", "piyo"],
|
|
||||||
updated: "",
|
updated: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params }) => {
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
const db = await PG();
|
const db = await PG();
|
||||||
const now = await db.query('SELECT NOW()');
|
await db.begin();
|
||||||
|
try {
|
||||||
|
const recent_articles = await db.query(
|
||||||
|
"SELECT * FROM article WHERE publish = 'public' ORDER BY updated_at DESC LIMIT 6"
|
||||||
|
);
|
||||||
|
data.recent = recent_articles.rows.map((row) => ({
|
||||||
|
title: row.title,
|
||||||
|
image: row.image,
|
||||||
|
date: row.updated_at.toISOString().slice(0, 10),
|
||||||
|
link: `/article/${row.category}/${row.id}`,
|
||||||
|
tags: row.tags,
|
||||||
|
}));
|
||||||
|
const tags = await db.query("SELECT * FROM tag ORDER BY ref_count DESC LIMIT 20");
|
||||||
|
data.tags = tags.rows.map((row) => row.name);
|
||||||
|
} catch (e) {
|
||||||
|
await db.rollback();
|
||||||
|
throw error(500, e as Error);
|
||||||
|
} finally {
|
||||||
await db.release();
|
await db.release();
|
||||||
|
|
||||||
data.updated = now.rows[0].now;
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// export const load: PageServerLoad = async ({ params }) => {
|
data.updated = data.recent[0].date;
|
||||||
// let data = {
|
|
||||||
// hero: [] as Content[],
|
|
||||||
// recent: [] as Content[],
|
|
||||||
// tags: Array.from(index.tags),
|
|
||||||
// updated: index.updated
|
|
||||||
// };
|
|
||||||
// for (const dat of index.articles.filter((article) => article.publish == "public").list.slice(0, 4).reverse()) {
|
|
||||||
// if (!dat) continue;
|
|
||||||
// data.hero.push({
|
|
||||||
// title: dat.title,
|
|
||||||
// image: dat.image,
|
|
||||||
// date: ((date) => {
|
|
||||||
// {
|
|
||||||
// // YYYY/MM/DD
|
|
||||||
// return `${date.getFullYear()}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date
|
|
||||||
// .getDate()
|
|
||||||
// .toString()
|
|
||||||
// .padStart(2, '0')}`;
|
|
||||||
// }
|
|
||||||
// })(dat.released),
|
|
||||||
// link: dat.series
|
|
||||||
// ? `/post/${dat.category}/${dat.series}/${dat.id}`
|
|
||||||
// : `/post/${dat.category}/${dat.id}`,
|
|
||||||
// tags: dat.tags
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
// //recent 10 articles
|
|
||||||
// for (const dat of index.articles.filter((article) => article.publish == "public").list.slice(0, 10)) {
|
|
||||||
// if (!dat) continue;
|
|
||||||
// data.recent.push({
|
|
||||||
// title: dat.title,
|
|
||||||
// image: dat.image,
|
|
||||||
// date: ((date) => {
|
|
||||||
// {
|
|
||||||
// // YYYY/MM/DD
|
|
||||||
// return `${date.getFullYear()}/${(date.getMonth() + 1).toString().padStart(2, '0')}/${date
|
|
||||||
// .getDate()
|
|
||||||
// .toString()
|
|
||||||
// .padStart(2, '0')}`;
|
|
||||||
// }
|
|
||||||
// })(dat.released),
|
|
||||||
// link: dat.series
|
|
||||||
// ? `/post/${dat.category}/${dat.series}/${dat.id}`
|
|
||||||
// : `/post/${dat.category}/${dat.id}`,
|
|
||||||
// tags: dat.tags
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return data;
|
return data;
|
||||||
// };
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
// import Cursor from '$lib/components/cursor.svelte';
|
import Cursor from '$lib/components/cursor.svelte';
|
||||||
import Footer from '$lib/components/footer.svelte';
|
import Footer from '$lib/components/footer.svelte';
|
||||||
import FormattedDate from '$lib/components/formatted_date.svelte';
|
import FormattedDate from '$lib/components/formatted_date.svelte';
|
||||||
|
|
||||||
|
@ -90,7 +90,14 @@
|
||||||
<ul>
|
<ul>
|
||||||
{#each data.recent as post}
|
{#each data.recent as post}
|
||||||
<li>
|
<li>
|
||||||
<a href={post.link}>{post.title} - {post.date}</a>
|
<a
|
||||||
|
href={post.link}
|
||||||
|
on:mouseenter={(e) =>
|
||||||
|
(e.target as HTMLAnchorElement).style.setProperty(
|
||||||
|
'--mouse-position',
|
||||||
|
`${e.offsetX}px`
|
||||||
|
)}>{post.title}</a
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -107,6 +114,7 @@
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
<Cursor />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -121,7 +129,7 @@
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.controls {
|
.controls {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -142,6 +150,8 @@
|
||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
> div {
|
> div {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
}
|
}
|
||||||
|
@ -250,6 +260,26 @@
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
text-decoration: none;
|
||||||
|
border-bottom: 1px solid var(--line-primary);
|
||||||
|
color: white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
--mouse-position: 0;
|
||||||
|
&::after {
|
||||||
|
content: ' ';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: var(--mouse-position);
|
||||||
|
right: calc(100% - var(--mouse-position));
|
||||||
|
height: 1px;
|
||||||
|
background-color: white;
|
||||||
|
transition: 0.2s ease-in-out;
|
||||||
|
transition-property: left right;
|
||||||
|
}
|
||||||
|
&:hover::after {
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
button {
|
button {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
@ -276,22 +306,20 @@
|
||||||
.tags {
|
.tags {
|
||||||
ul {
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: left;
|
||||||
|
flex-direction: row;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
li {
|
li {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
|
border: 1px solid var(--line-primary);
|
||||||
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px solid var(--line-primary);
|
|
||||||
color: white;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
main {
|
main {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
20
src/routes/article/+layout.server.ts
Normal file
20
src/routes/article/+layout.server.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import type { Article } from '$lib/server/database/article';
|
||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
export const load: LayoutServerLoad = async () => {
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
article: {
|
||||||
|
seq: 0,
|
||||||
|
id: 'test',
|
||||||
|
title: 'test',
|
||||||
|
category: 'test',
|
||||||
|
released_at: new Date(),
|
||||||
|
updated_at: new Date(),
|
||||||
|
tags: ['test'],
|
||||||
|
image: 'test',
|
||||||
|
publish: 'test',
|
||||||
|
content: 'test',
|
||||||
|
} as Article,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
161
src/routes/article/+layout.svelte
Normal file
161
src/routes/article/+layout.svelte
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import '$lib/app.scss';
|
||||||
|
import '$lib/blog.scss';
|
||||||
|
|
||||||
|
import Footer from '$lib/components/footer.svelte';
|
||||||
|
import * as publish from '$lib/publish';
|
||||||
|
import FormattedDate from '$lib/components/formatted_date.svelte';
|
||||||
|
|
||||||
|
import type { LayoutData } from './$types';
|
||||||
|
export let data: LayoutData;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{data.props.article.title} | Blog | HareWorks</title>
|
||||||
|
<meta property="og:title" content={data.props.article.title + '- HareWorks'} />
|
||||||
|
<!-- <meta property="og:description" content={data.props.article.description} /> -->
|
||||||
|
<meta property="og:image" content={data.props.article.image} />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="article:published_time" content={data.props.article.released_at.toISOString()} />
|
||||||
|
{#if data.props.article.updated_at}
|
||||||
|
<meta property="article:modified_time" content={data.props.article.updated_at.toISOString()} />
|
||||||
|
{/if}
|
||||||
|
<meta property="article:author" content="HareWorks" />
|
||||||
|
<meta property="article:section" content="Blog" />
|
||||||
|
<meta property="article:tag" content={data.props.article.tags.join(',')} />
|
||||||
|
<meta name="robots" content={data.props.article.publish as publish.Publish} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<!-- <div class="back">
|
||||||
|
<img src={data.props.article.image} alt="" />
|
||||||
|
</div> -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="title">
|
||||||
|
<h1>{data.props.article.title}</h1>
|
||||||
|
<div class="meta">
|
||||||
|
<span>
|
||||||
|
released <FormattedDate date={data.props.article.released_at} />
|
||||||
|
{#if data.props.article.updated_at}<br />
|
||||||
|
updated <FormattedDate date={data.props.article.updated_at} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<a href="/category/{data.props.article.category}"
|
||||||
|
>{data.props.article.category[0].toUpperCase() + data.props.article.category.slice(1)}</a
|
||||||
|
>
|
||||||
|
{#each data.props.article.tags as tag}
|
||||||
|
<a href={`/search?tag=${tag}`}>{tag}</a>
|
||||||
|
{/each}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<main>
|
||||||
|
<div class="document">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
:global(body) {
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
}
|
||||||
|
.back {
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #000;
|
||||||
|
color: white;
|
||||||
|
filter: brightness(0.8) grayscale(0.5);
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
color: white;
|
||||||
|
min-height: 100%;
|
||||||
|
padding-top: 100px;
|
||||||
|
width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px 100px 12px 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
span {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
&:first-child {
|
||||||
|
&::before {
|
||||||
|
content: '🗀 ';
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: '|';
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
flex: 1;
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 1000px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
h1 {
|
||||||
|
padding: 8px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
46
src/routes/article/[id]/+page.server.ts
Normal file
46
src/routes/article/[id]/+page.server.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import type { Article } from '$lib/server/database/article';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import PG from '$lib/server/database';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const { id } = params;
|
||||||
|
|
||||||
|
const db = await PG();
|
||||||
|
await db.begin();
|
||||||
|
try {
|
||||||
|
const article = await db.query(
|
||||||
|
"SELECT * FROM article WHERE id = $1",
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
if (article.rowCount === 0) {
|
||||||
|
return {
|
||||||
|
status: 404,
|
||||||
|
error: new Error('Not found'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const row = article.rows[0];
|
||||||
|
const data: Article = {
|
||||||
|
seq: row.seq,
|
||||||
|
id: row.id,
|
||||||
|
title: row.title,
|
||||||
|
category: row.category,
|
||||||
|
released_at: row.released_at,
|
||||||
|
updated_at: row.updated_at,
|
||||||
|
tags: row.tags,
|
||||||
|
image: row.image,
|
||||||
|
publish: row.publish,
|
||||||
|
content: row.content,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
props: { data },
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
await db.rollback();
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
error: e as Error,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
await db.release();
|
||||||
|
}
|
||||||
|
};
|
3
src/routes/article/[id]/+page.svelte
Normal file
3
src/routes/article/[id]/+page.svelte
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
</script>
|
|
@ -1,5 +0,0 @@
|
||||||
<script>
|
|
||||||
import '../app.scss';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -1,5 +1,6 @@
|
||||||
:root {
|
:root {
|
||||||
--background-primary: #343434;
|
--background-primary: #1b1b1b;
|
||||||
--line-primary: rgba(255, 255, 255, 0.1);
|
--line-primary: rgba(255, 255, 255, 0.2);
|
||||||
|
--line-secondary: rgba(255, 255, 255, 0.1);
|
||||||
--highlight-primary: rgba(255, 115, 0, 0.2);
|
--highlight-primary: rgba(255, 115, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user