It is now possible to display #1
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
node_modules
|
node_modules
|
||||||
|
postgres
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
.output
|
.output
|
||||||
|
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "articles"]
|
||||||
|
path = articles
|
||||||
|
url = git@gitea.hareworks.net:Hare/blog-articles.git
|
1
articles
Submodule
1
articles
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 172da3590a2e7cec7b41dcf9a60306db277b7159
|
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
services:
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./
|
||||||
|
dockerfile: ./docker/app.Dockerfile
|
||||||
|
environment:
|
||||||
|
- PORT=${WEB_PORT}
|
||||||
|
ports:
|
||||||
|
- ${WEB_PORT}:${WEB_PORT}
|
||||||
|
volumes:
|
||||||
|
- ./src:/app/src
|
||||||
|
- ./static:/app/static
|
||||||
|
- ./vite.config.js:/app/vite.config.js
|
||||||
|
- ./tsconfig.json:/app/tsconfig.json
|
||||||
|
- ./svelte.config.js:/app/svelte.config.js
|
||||||
|
db:
|
||||||
|
build:
|
||||||
|
context: ./
|
||||||
|
dockerfile: ./docker/postgres.Dockerfile
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=${PG_PASS}
|
||||||
|
- POSTGRES_USER=${PG_USER}
|
||||||
|
- POSTGRES_DB=${PG_DB}
|
||||||
|
ports:
|
||||||
|
- ${PG_PORT}:5432
|
||||||
|
volumes:
|
||||||
|
- ./postgres:/var/lib/postgresql/data
|
26
docker/app.Dockerfile
Normal file
26
docker/app.Dockerfile
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# For Build
|
||||||
|
FROM node:22-slim as builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
COPY package-lock.json ./
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# For Run
|
||||||
|
FROM node:22-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/build ./build
|
||||||
|
COPY --from=builder /app/package.json .
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
CMD ["node", "./build"]
|
0
docker/initdb.d/user-db.sql
Normal file
0
docker/initdb.d/user-db.sql
Normal file
4
docker/postgres.Dockerfile
Normal file
4
docker/postgres.Dockerfile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
FROM postgres:16
|
||||||
|
|
||||||
|
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 @@
|
||||||
|
|
1403
package-lock.json
generated
1403
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
@ -15,9 +15,11 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.28.1",
|
"@playwright/test": "^1.28.1",
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
"@sveltejs/adapter-node": "^5.2.2",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@types/eslint": "^9.6.0",
|
"@types/eslint": "^9.6.0",
|
||||||
|
"@types/pg": "^8.11.6",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-svelte": "^2.36.0",
|
"eslint-plugin-svelte": "^2.36.0",
|
||||||
|
@ -30,5 +32,11 @@
|
||||||
"typescript-eslint": "^8.0.0",
|
"typescript-eslint": "^8.0.0",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.0.3"
|
||||||
},
|
},
|
||||||
"type": "module"
|
"type": "module",
|
||||||
|
"dependencies": {
|
||||||
|
"dotenv": "^16.4.5",
|
||||||
|
"mdsvex": "^0.12.3",
|
||||||
|
"pg": "^8.12.0",
|
||||||
|
"sass": "^1.77.8"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
19
src/lib/app.scss
Normal file
19
src/lib/app.scss
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
@import '../variables.scss';
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'ZenKakuGothicNew-Regular';
|
||||||
|
src: url(/fonts/ZenKakuGothicNew-Regular.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: 'ZenKakuGothicNew-Regular', '游ゴシック体', 'Yu Gothic', YuGothic, 'ヒラギノ角ゴシック Pro',
|
||||||
|
'Hiragino Kaku Gothic Pro', 'メイリオ', Meiryo, Osaka, 'MS PGothic', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
14
src/lib/article.ts
Normal file
14
src/lib/article.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Publish } from "./publish";
|
||||||
|
|
||||||
|
export type Article = {
|
||||||
|
seq: number;
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
category: string;
|
||||||
|
released_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
tags: string[];
|
||||||
|
image: string;
|
||||||
|
publish: Publish;
|
||||||
|
content: string;
|
||||||
|
};
|
174
src/lib/blog.scss
Normal file
174
src/lib/blog.scss
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
.document {
|
||||||
|
--color-text: #ffffff;
|
||||||
|
--color-concept: hsl(39, 100%, 25%, 0.5);
|
||||||
|
--color-concept-hsl: 39, 100%, 25%;
|
||||||
|
--color-outline: #ffffff40;
|
||||||
|
* {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
font-size: 2rem;
|
||||||
|
border-bottom: 1px solid var(--color-outline);
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
> *:not(h1) {
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
&::before {
|
||||||
|
content: '#';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -1.5rem;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
color: rgb(131, 131, 131);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
}
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--color-outline);
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
transform: translate(0, 0);
|
||||||
|
text-shadow: 0 0 1rem hsl(var(--color-concept-hsl));
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
font-weight: normal;
|
||||||
|
&:hover {
|
||||||
|
transform: translate(0, -0.1rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
blockquote {
|
||||||
|
position: relative;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: 1rem;
|
||||||
|
color: rgb(162, 162, 162);
|
||||||
|
&::before {
|
||||||
|
content: '“';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -0.5rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: rgb(131, 131, 131);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warn {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
padding: 1rem 0.7rem 0.7rem;
|
||||||
|
background-image: linear-gradient(to right, var(--color-outline), transparent 75%);
|
||||||
|
background-origin: border-box;
|
||||||
|
box-shadow: inset 0 0 0 100vh var(--background-color);
|
||||||
|
text-shadow: 0 0 1rem hsl(var(--color-concept-hsl),1);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
&::before {
|
||||||
|
content: 'note:warn';
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
top: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: -0.3rem;
|
||||||
|
transform: translate(0, -0.5rem);
|
||||||
|
text-shadow: 0 0 1rem red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
font-family: monospace;
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.2rem;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
border-radius: 0.2rem;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
transition: 0.2s ease-out;
|
||||||
|
transition-property: height;
|
||||||
|
padding-inline-start: 0.1rem;
|
||||||
|
> summary {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
list-style: '+ ' outside;
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
background: linear-gradient(to right, rgba(255, 255, 255, 0.25), transparent 200px);
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
text-shadow: 0 0 0.25rem rgb(255, 255, 255);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
> p {
|
||||||
|
margin-block-start: 0.5rem;
|
||||||
|
margin-block-end: 0.5rem;
|
||||||
|
margin-left: 1rem;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&[open] {
|
||||||
|
> summary {
|
||||||
|
list-style: '- ' outside;
|
||||||
|
}
|
||||||
|
> p {
|
||||||
|
animation: detailsIn 0.5s ease;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes detailsIn {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
border: none;
|
||||||
|
border: 1px solid var(--color-outline);
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 0.5rem hsl(var(--color-concept-hsl), 0.5);
|
||||||
|
thead {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
&:nth-child(2n) {
|
||||||
|
background-color: #ffffff0d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
padding: 0.5em;
|
||||||
|
border-left: 1px solid var(--color-outline);
|
||||||
|
&:first-child {
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
52
src/lib/components/cursor.svelte
Normal file
52
src/lib/components/cursor.svelte
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
let cursor: HTMLDivElement;
|
||||||
|
|
||||||
|
type position = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let disabled = false;
|
||||||
|
let rawPosition: position = { x: 0, y: -100 };
|
||||||
|
let position: position = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
window.addEventListener('mousemove', (e) => {
|
||||||
|
rawPosition = { x: e.clientX, y: e.clientY };
|
||||||
|
});
|
||||||
|
window.addEventListener('touchstart', (e) => {
|
||||||
|
cursor.style.display = 'none';
|
||||||
|
disabled = true;
|
||||||
|
});
|
||||||
|
const loop = () => {
|
||||||
|
if (disabled) return;
|
||||||
|
position.x += (rawPosition.x - position.x) / 12;
|
||||||
|
position.y += (rawPosition.y - position.y) / 12;
|
||||||
|
cursor.style.top = `${position.y}px`;
|
||||||
|
cursor.style.left = `${position.x}px`;
|
||||||
|
cursor.style.rotate = `rotate(${position.x}deg)`;
|
||||||
|
requestAnimationFrame(loop);
|
||||||
|
};
|
||||||
|
loop();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cursor" bind:this={cursor}></div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.cursor {
|
||||||
|
position: fixed;
|
||||||
|
top: -100px;
|
||||||
|
left: 0px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ffffff;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 999;
|
||||||
|
mix-blend-mode: difference;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
</style>
|
90
src/lib/components/footer.svelte
Normal file
90
src/lib/components/footer.svelte
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<script lang="ts">
|
||||||
|
// let location = window.location.pathname;
|
||||||
|
let links = [
|
||||||
|
{ name: 'Home', url: '/' },
|
||||||
|
{ name: 'Search', url: '/search' },
|
||||||
|
{ name: 'About Me', url: 'https://me.hareworks.net/' },
|
||||||
|
];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<div class="banner">
|
||||||
|
<a href="https://me.hareworks.net/" target="_blank">
|
||||||
|
<img src="/img/logo.png" alt="HareWorks" width="100px" height="100px" />
|
||||||
|
</a>
|
||||||
|
<div class="copyright">
|
||||||
|
<p>© 2024 Hare</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="separator"></div>
|
||||||
|
<div class="panel">
|
||||||
|
<ul>
|
||||||
|
{#each links as link}
|
||||||
|
<li><a href={link.url}>{link.name}</a></li>
|
||||||
|
{/each}
|
||||||
|
<li><p>Contact: hello at hareworks dot net</p></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
footer {
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 10px 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.banner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
a {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.separator {
|
||||||
|
width: 1px;
|
||||||
|
margin: 0 20px;
|
||||||
|
background-color: var(--line-primary);
|
||||||
|
}
|
||||||
|
.panel {
|
||||||
|
width: 300px;
|
||||||
|
margin: 20px 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: top;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
> a,
|
||||||
|
> p {
|
||||||
|
color: lightgrey;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 2px;
|
||||||
|
margin: 0;
|
||||||
|
display: block;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
}
|
||||||
|
> p {
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
24
src/lib/components/formatted_date.svelte
Normal file
24
src/lib/components/formatted_date.svelte
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let date: string | Date;
|
||||||
|
|
||||||
|
if (typeof date === 'string') date = new Date(date);
|
||||||
|
|
||||||
|
const pad = function (str: string): string {
|
||||||
|
return ('0' + str).slice(-2);
|
||||||
|
};
|
||||||
|
|
||||||
|
const year = date.getFullYear().toString();
|
||||||
|
const month = pad((date.getMonth() + 1).toString());
|
||||||
|
const day = pad(date.getDate().toString());
|
||||||
|
const hour = pad(date.getHours().toString());
|
||||||
|
const min = pad(date.getMinutes().toString());
|
||||||
|
const sec = pad(date.getSeconds().toString());
|
||||||
|
const tz = -date.getTimezoneOffset();
|
||||||
|
const sign = tz >= 0 ? '+' : '-';
|
||||||
|
const tzHour = pad((tz / 60).toString());
|
||||||
|
const tzMin = pad((tz % 60).toString());
|
||||||
|
|
||||||
|
let formattedDate = `${year}-${month}-${day}T${hour}:${min}:${sec}${sign}${tzHour}:${tzMin}`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{formattedDate}
|
19
src/lib/components/head.svelte
Normal file
19
src/lib/components/head.svelte
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Article } from '$lib/article';
|
||||||
|
import * as publish from '$lib/publish';
|
||||||
|
export let article: Article;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<title>{article.title} | Blog | HareWorks</title>
|
||||||
|
<meta property="og:title" content={article.title + '- HareWorks'} />
|
||||||
|
<!-- <meta property="og:description" content={article.description} /> -->
|
||||||
|
<meta property="og:image" content={article.image} />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="article:published_time" content={article.released_at.toISOString()} />
|
||||||
|
{#if article.updated_at}
|
||||||
|
<meta property="article:modified_time" content={article.updated_at.toISOString()} />
|
||||||
|
{/if}
|
||||||
|
<meta property="article:author" content="HareWorks" />
|
||||||
|
<meta property="article:section" content="Blog" />
|
||||||
|
<meta property="article:tag" content={article.tags.join(',')} />
|
||||||
|
<meta name="robots" content={article.publish as publish.Publish} />
|
|
@ -1 +0,0 @@
|
||||||
// place files you want to import through the `$lib` alias in this folder.
|
|
162
src/lib/page/article_page.svelte
Normal file
162
src/lib/page/article_page.svelte
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
<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 Head from '$lib/components/head.svelte';
|
||||||
|
|
||||||
|
import type { Article } from '$lib/article';
|
||||||
|
export let data: Article;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{data.title} | Blog | HareWorks</title>
|
||||||
|
<meta property="og:title" content={data.title + '- HareWorks'} />
|
||||||
|
<!-- <meta property="og:description" content={data.description} /> -->
|
||||||
|
<meta property="og:image" content={data.image} />
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="article:published_time" content={data.released_at.toISOString()} />
|
||||||
|
{#if data.updated_at}
|
||||||
|
<meta property="article:modified_time" content={data.updated_at.toISOString()} />
|
||||||
|
{/if}
|
||||||
|
<meta property="article:author" content="HareWorks" />
|
||||||
|
<meta property="article:section" content="Blog" />
|
||||||
|
<meta property="article:tag" content={data.tags.join(',')} />
|
||||||
|
<meta name="robots" content={data.publish as publish.Publish} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<!-- <div class="back">
|
||||||
|
<img src={data.image} alt="" />
|
||||||
|
</div> -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="title">
|
||||||
|
<h1>{data.title}</h1>
|
||||||
|
<div class="meta">
|
||||||
|
<span>
|
||||||
|
released <FormattedDate date={data.released_at} />
|
||||||
|
{#if data.updated_at}<br />
|
||||||
|
updated <FormattedDate date={data.updated_at} />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<a href="/category/{data.category}"
|
||||||
|
>{data.category[0].toUpperCase() + data.category.slice(1)}</a
|
||||||
|
>
|
||||||
|
{#each data.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 {
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
&:first-child {
|
||||||
|
&::before {
|
||||||
|
content: '🗀 ';
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: '|';
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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>
|
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';
|
||||||
|
}
|
||||||
|
}
|
47
src/lib/server/database.ts
Normal file
47
src/lib/server/database.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import pg from 'pg';
|
||||||
|
const { Pool } = pg;
|
||||||
|
|
||||||
|
import {
|
||||||
|
PG_USER,
|
||||||
|
PG_PASS,
|
||||||
|
PG_HOST,
|
||||||
|
PG_PORT,
|
||||||
|
PG_DB,
|
||||||
|
} from '$env/static/private'
|
||||||
|
const connectionString = `postgres://${PG_USER}:${PG_PASS}@${PG_HOST}:${PG_PORT}/${PG_DB}`;
|
||||||
|
|
||||||
|
const pool = new Pool({ connectionString });
|
||||||
|
class Postgres {
|
||||||
|
client: pg.PoolClient | null = null;
|
||||||
|
public static async new() {
|
||||||
|
const pg = new Postgres();
|
||||||
|
pg.client = await pool.connect();
|
||||||
|
return pg;
|
||||||
|
}
|
||||||
|
|
||||||
|
async query(query: string, params: any[] = []) {
|
||||||
|
return (await this.client!.query(query, params));
|
||||||
|
}
|
||||||
|
|
||||||
|
async release() {
|
||||||
|
await this.client!.release(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async begin() {
|
||||||
|
await this.client!.query('begin');
|
||||||
|
}
|
||||||
|
|
||||||
|
async commit() {
|
||||||
|
await this.client!.query('commit');
|
||||||
|
}
|
||||||
|
|
||||||
|
async rollback() {
|
||||||
|
await this.client!.query('rollback');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async () => { return await Postgres.new(); }
|
||||||
|
|
||||||
|
|
||||||
|
import init from '$lib/server/database/init_db';
|
||||||
|
await init();
|
165
src/lib/server/database/init_db.ts
Normal file
165
src/lib/server/database/init_db.ts
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
// initialize
|
||||||
|
import PG from '$lib/server/database';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { compile } from 'mdsvex';
|
||||||
|
|
||||||
|
export default async function init() {
|
||||||
|
|
||||||
|
// Create tables(when not exists)
|
||||||
|
const schemas = [
|
||||||
|
{
|
||||||
|
name: 'article',
|
||||||
|
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: 'released_at', type: 'timestamp', constraint: 'not null' },
|
||||||
|
{ name: 'updated_at', type: 'timestamp', 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: '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();
|
||||||
|
try {
|
||||||
|
await db.begin();
|
||||||
|
for (const schema of schemas) {
|
||||||
|
const res = await db.query(`select * from information_schema.tables where table_name = '${schema.name}'`)
|
||||||
|
if (res.rowCount == 0) {
|
||||||
|
console.log(`Creating table ${schema.name}`);
|
||||||
|
const columnStr = schema.columns.map(c => `${c.name} ${c.type} ${c.constraint}`).join(', ');
|
||||||
|
await db.query(`create table ${schema.name} (${columnStr})`);
|
||||||
|
} else {
|
||||||
|
console.log(`Table ${schema.name} already exists`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await db.commit();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
await db.rollback();
|
||||||
|
}
|
||||||
|
|
||||||
|
const articleFiles: ArticleFileItem[] = [];
|
||||||
|
function scanDir(path: string) {
|
||||||
|
const files = fs.readdirSync(path);
|
||||||
|
for (const file of files) {
|
||||||
|
const dir = `${path}/${file}`;
|
||||||
|
const stat = fs.statSync(dir);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
scanDir(dir);
|
||||||
|
} else {
|
||||||
|
articleFiles.push({ path: `${path}/${file}`, id: file });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scanDir('./articles/article');
|
||||||
|
|
||||||
|
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 compiled = await compile(fs.readFileSync(path, 'utf-8'));
|
||||||
|
const title = compiled.data.fm.title;
|
||||||
|
const category = path.split('/')[3];
|
||||||
|
const tags: string[] = compiled.data.fm.tags;
|
||||||
|
const released_at = new Date(compiled.data.fm.released_at);
|
||||||
|
const updated_at = new Date(compiled.data.fm.updated_at);
|
||||||
|
const image = compiled.data.fm.image;
|
||||||
|
const publish = compiled.data.fm.publish;
|
||||||
|
const content = compiled.code
|
||||||
|
.replace(/>{@html `<code class="language-/g, '><code class="language-')
|
||||||
|
.replace(/<\/code>`}<\/pre>/g, '</code></pre>');
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArticleFileItem = {
|
||||||
|
path: string,
|
||||||
|
id: string,
|
||||||
|
}
|
||||||
|
export type TableSchema = {
|
||||||
|
name: string,
|
||||||
|
columns: {
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
constraint: string,
|
||||||
|
}[],
|
||||||
|
}
|
142
src/routes/+error.svelte
Normal file
142
src/routes/+error.svelte
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import '$lib/blog.scss';
|
||||||
|
|
||||||
|
export let data: any;
|
||||||
|
|
||||||
|
data.date = new Date();
|
||||||
|
data.date = [
|
||||||
|
data.date.getFullYear(),
|
||||||
|
(data.date.getMonth() + 1).toString().padStart(2, '0'),
|
||||||
|
data.date.getDate().toString().padStart(2, '0')
|
||||||
|
].join('-');
|
||||||
|
data.image = '/img/error.jpg';
|
||||||
|
data.title = 'Something went wrong :(';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$page.status} | Blog | HareWorks</title>
|
||||||
|
<meta name="robots" content="noindex" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<!-- <div class="hero">
|
||||||
|
<img src={data.image} alt="" />
|
||||||
|
</div> -->
|
||||||
|
<div class="container">
|
||||||
|
<div class="title">
|
||||||
|
<h1>{data.title}</h1>
|
||||||
|
<div class="meta">
|
||||||
|
<span>{data.date}</span>
|
||||||
|
<span />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel">
|
||||||
|
<main>
|
||||||
|
<div class="document">
|
||||||
|
<h1>{$page.status} {$page.error?.message}</h1>
|
||||||
|
<a href="/">Home</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</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 {
|
||||||
|
margin-left: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
&:first-child {
|
||||||
|
&::before {
|
||||||
|
content: '🗀 ';
|
||||||
|
}
|
||||||
|
& ::after {
|
||||||
|
content: '|';
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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>
|
5
src/routes/+layout.svelte
Normal file
5
src/routes/+layout.svelte
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import '$lib/app.scss';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
47
src/routes/+page.server.ts
Normal file
47
src/routes/+page.server.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import PG from '$lib/server/database';
|
||||||
|
|
||||||
|
let data: {
|
||||||
|
recent: {
|
||||||
|
title: string,
|
||||||
|
image: string,
|
||||||
|
date: string,
|
||||||
|
link: string,
|
||||||
|
tags: string[]
|
||||||
|
}[],
|
||||||
|
tags: string[],
|
||||||
|
updated: string,
|
||||||
|
} = {
|
||||||
|
recent: [],
|
||||||
|
tags: [],
|
||||||
|
updated: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const db = await PG();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
data.updated = data.recent[0].date;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
|
@ -1,2 +1,335 @@
|
||||||
<h1>Welcome to SvelteKit</h1>
|
<script lang="ts">
|
||||||
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>
|
import type { PageData } from './$types';
|
||||||
|
import Cursor from '$lib/components/cursor.svelte';
|
||||||
|
import Footer from '$lib/components/footer.svelte';
|
||||||
|
import FormattedDate from '$lib/components/formatted_date.svelte';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
|
||||||
|
let refresh = true;
|
||||||
|
let searchInput: HTMLInputElement;
|
||||||
|
let suggest: HTMLDivElement;
|
||||||
|
const emojis = ['( ᐢ˙꒳˙ᐢ )', '(=`•ω•´)', '₍ᐢ。•༝•。ᐢ₎', 'ヽ(・ω・*)', '( |||| ᐢ˙꒳˙ᐢ )'];
|
||||||
|
const searchChange = (e: Event) => {
|
||||||
|
if (refresh === false) return;
|
||||||
|
const value = (e.target as HTMLInputElement).value;
|
||||||
|
if (value.length === 0) return;
|
||||||
|
refresh = false;
|
||||||
|
let xhr = new XMLHttpRequest();
|
||||||
|
// xhr.open('GET', `/api/search?q=${value}`);
|
||||||
|
// xhr.send();
|
||||||
|
// xhr.onload = () => {
|
||||||
|
// setTimeout(() => {q
|
||||||
|
// refresh = true;
|
||||||
|
// }, 1000);
|
||||||
|
// };
|
||||||
|
setTimeout(() => {
|
||||||
|
suggest.textContent = emojis[Math.floor(Math.random() * emojis.length)] + 'ミジッソウダヨ...';
|
||||||
|
setTimeout(() => {
|
||||||
|
refresh = true;
|
||||||
|
}, 1000);
|
||||||
|
}, Math.random() * 100);
|
||||||
|
};
|
||||||
|
const searchKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key !== 'Enter') return;
|
||||||
|
// const input = e.target as HTMLInputElement;
|
||||||
|
// const value = input.value;
|
||||||
|
// if (value.length > 0) {
|
||||||
|
// window.location.href = `/search?s=${value}`;
|
||||||
|
// }
|
||||||
|
suggest.textContent = emojis[Math.floor(Math.random() * emojis.length)] + 'アン?';
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Home | Blog | HareWorks</title>
|
||||||
|
<meta name="description" content="Hareの個人ブログです。" />
|
||||||
|
<meta property="og:title" content="Home - HareWorks" />
|
||||||
|
<meta property="og:description" content="Hareの個人ブログです。" />
|
||||||
|
<meta property="og:image" content="/img/logo.png" />
|
||||||
|
<meta property="og:url" content={`https://blog.hare.works/`} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="controls"></div>
|
||||||
|
<main>
|
||||||
|
<h1>blog.hareworks.net</h1>
|
||||||
|
<p>
|
||||||
|
このブログはHareの個人ブログです。プログラミング、情報技術全般、デザイン、その他興味のあることについて書いています。
|
||||||
|
</p>
|
||||||
|
<div class="links">
|
||||||
|
<ul>
|
||||||
|
<li><a href="/about">ABOUT</a></li>
|
||||||
|
<li><a href="/contact">CONTACT</a></li>
|
||||||
|
<li><a href="/discord" target="_blank">DISCORD</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="notice">
|
||||||
|
<p>( ᐢ˙꒳˙ᐢ ) ← cute</p>
|
||||||
|
</div>
|
||||||
|
<div class="serch">
|
||||||
|
<div class="box">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search"
|
||||||
|
on:focus={() => {
|
||||||
|
suggest.classList.remove('hidden');
|
||||||
|
}}
|
||||||
|
on:blur={() => {
|
||||||
|
suggest.classList.add('hidden');
|
||||||
|
}}
|
||||||
|
on:input={searchChange}
|
||||||
|
on:keydown={searchKeydown}
|
||||||
|
bind:this={searchInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="suggest hidden" bind:this={suggest}></div>
|
||||||
|
</div>
|
||||||
|
<div class="recent">
|
||||||
|
<h2>RECENT<span>updated: <FormattedDate date={data.updated} /></span></h2>
|
||||||
|
<ul>
|
||||||
|
{#each data.recent as post}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href={post.link}
|
||||||
|
on:mouseenter={(e) =>
|
||||||
|
(e.target as HTMLAnchorElement).style.setProperty(
|
||||||
|
'--mouse-position',
|
||||||
|
`${e.offsetX}px`
|
||||||
|
)}>{post.title}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="tags">
|
||||||
|
<h2>TAGS</h2>
|
||||||
|
<ul>
|
||||||
|
{#each data.tags as tag}
|
||||||
|
<li>
|
||||||
|
<button on:click={() => (searchInput.value = tag)}>{tag}</button>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<Footer />
|
||||||
|
</main>
|
||||||
|
<Cursor />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
:global(body) {
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--background-primary);
|
||||||
|
background-size: cover;
|
||||||
|
background-blend-mode: overlay;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100dvh;
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
border: 1px solid var(--line-primary);
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 8px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
height: 100dvh;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
> div {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
.links {
|
||||||
|
margin-left: auto;
|
||||||
|
width: 80%;
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: end;
|
||||||
|
gap: 4px;
|
||||||
|
border-top: 1px solid var(--line-primary);
|
||||||
|
li {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 0 3px 0 3px;
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: white;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.notice {
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid var(--line-primary);
|
||||||
|
p {
|
||||||
|
padding: 15px 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
background-color: rgba(105, 105, 105, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.serch {
|
||||||
|
width: 100%;
|
||||||
|
.box {
|
||||||
|
display: flex;
|
||||||
|
&::before {
|
||||||
|
content: ' ';
|
||||||
|
position: relative;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 30%;
|
||||||
|
border-bottom: 1px solid var(--line-primary);
|
||||||
|
}
|
||||||
|
input[type='text'] {
|
||||||
|
display: inline;
|
||||||
|
width: 70%;
|
||||||
|
margin-left: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: transparent;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid var(--line-primary);
|
||||||
|
border-bottom: none;
|
||||||
|
border-right: none;
|
||||||
|
color: white;
|
||||||
|
&:active,
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.suggest {
|
||||||
|
display: block;
|
||||||
|
margin-left: 30%;
|
||||||
|
width: 70%;
|
||||||
|
height: 30px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-bottom: 1px solid var(--line-primary);
|
||||||
|
transition: 0.5s ease-in-out;
|
||||||
|
transition-property: height border-bottom;
|
||||||
|
&.hidden {
|
||||||
|
height: 0;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 400;
|
||||||
|
margin: 5px;
|
||||||
|
padding: 0 20px 0 10px;
|
||||||
|
border-left: 1px solid var(--line-primary);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
> span {
|
||||||
|
padding-left: 30px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
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 {
|
||||||
|
font-size: 1rem;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
background-color: transparent;
|
||||||
|
border-bottom: 1px solid transparent;
|
||||||
|
&:hover {
|
||||||
|
border-bottom: 1px solid var(--line-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 0 0 30px;
|
||||||
|
margin: 0;
|
||||||
|
li {
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.recent {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.tags {
|
||||||
|
ul {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: left;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 3px;
|
||||||
|
li {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 3px 5px;
|
||||||
|
border: 1px solid var(--line-primary);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
main {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
.notice {
|
||||||
|
p {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
6
src/routes/article/+layout.svelte
Normal file
6
src/routes/article/+layout.svelte
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import '$lib/app.scss';
|
||||||
|
import '$lib/blog.scss';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot></slot>
|
29
src/routes/article/[category]/[id]/+page.server.ts
Normal file
29
src/routes/article/[category]/[id]/+page.server.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import type { Article } from '$lib/article';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import PG from '$lib/server/database';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const { category, id } = params;
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
|
const db = await PG();
|
||||||
|
await db.begin();
|
||||||
|
try {
|
||||||
|
const article = await db.query(
|
||||||
|
"SELECT * FROM article WHERE id = $1 AND category = $2",
|
||||||
|
[id, category]
|
||||||
|
);
|
||||||
|
if (article.rowCount === 0) {
|
||||||
|
error(404, 'Not found');
|
||||||
|
}
|
||||||
|
const row = article.rows[0];
|
||||||
|
const data: Article = { ...row };
|
||||||
|
return data
|
||||||
|
} catch (e) {
|
||||||
|
await db.rollback();
|
||||||
|
error(500, (e as Error).message);
|
||||||
|
} finally {
|
||||||
|
await db.release();
|
||||||
|
}
|
||||||
|
};
|
10
src/routes/article/[category]/[id]/+page.svelte
Normal file
10
src/routes/article/[category]/[id]/+page.svelte
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
import ArticlePage from '$lib/page/article_page.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ArticlePage {data}>
|
||||||
|
{@html data.content}
|
||||||
|
</ArticlePage>
|
29
src/routes/article/[category]/[series]/[id]/+page.server.ts
Normal file
29
src/routes/article/[category]/[series]/[id]/+page.server.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import type { Article } from '$lib/article';
|
||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import PG from '$lib/server/database';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const { category, id, series } = params;
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
|
const db = await PG();
|
||||||
|
await db.begin();
|
||||||
|
try {
|
||||||
|
const article = await db.query(
|
||||||
|
"SELECT * FROM article WHERE id = $1 AND category = $2 AND series = $3",
|
||||||
|
[id, category, series]
|
||||||
|
);
|
||||||
|
if (article.rowCount === 0) {
|
||||||
|
error(404, 'Not found');
|
||||||
|
}
|
||||||
|
const row = article.rows[0];
|
||||||
|
const data: Article = { ...row };
|
||||||
|
return data
|
||||||
|
} catch (e) {
|
||||||
|
await db.rollback();
|
||||||
|
error(500, (e as Error).message);
|
||||||
|
} finally {
|
||||||
|
await db.release();
|
||||||
|
}
|
||||||
|
};
|
10
src/routes/article/[category]/[series]/[id]/+page.svelte
Normal file
10
src/routes/article/[category]/[series]/[id]/+page.svelte
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
export let data: PageData;
|
||||||
|
import ArticlePage from '$lib/page/article_page.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ArticlePage {data} >
|
||||||
|
{@html data.content}
|
||||||
|
</ArticlePage>
|
5
src/routes/discord/+page.ts
Normal file
5
src/routes/discord/+page.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export function load() {
|
||||||
|
throw redirect(308, 'https://discord.gg/9RfkJeQQfB');
|
||||||
|
}
|
5
src/routes/threads/+layout.svelte
Normal file
5
src/routes/threads/+layout.svelte
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import '../app.scss';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
0
src/routes/threads/+page.svelte
Normal file
0
src/routes/threads/+page.svelte
Normal file
0
src/routes/threads/[category]/[id]/+page.server.ts
Normal file
0
src/routes/threads/[category]/[id]/+page.server.ts
Normal file
0
src/routes/threads/[category]/[id]/+page.svelte
Normal file
0
src/routes/threads/[category]/[id]/+page.svelte
Normal file
0
src/routes/threads/[id]/+page.server.ts
Normal file
0
src/routes/threads/[id]/+page.server.ts
Normal file
0
src/routes/threads/[id]/+page.svelte
Normal file
0
src/routes/threads/[id]/+page.svelte
Normal file
6
src/variables.scss
Normal file
6
src/variables.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
:root {
|
||||||
|
--background-primary: #1b1b1b;
|
||||||
|
--line-primary: rgba(255, 255, 255, 0.2);
|
||||||
|
--line-secondary: rgba(255, 255, 255, 0.1);
|
||||||
|
--highlight-primary: rgba(255, 115, 0, 0.2);
|
||||||
|
}
|
BIN
static/fonts/ZenKakuGothicNew-Black.ttf
Normal file
BIN
static/fonts/ZenKakuGothicNew-Black.ttf
Normal file
Binary file not shown.
BIN
static/fonts/ZenKakuGothicNew-Bold.ttf
Normal file
BIN
static/fonts/ZenKakuGothicNew-Bold.ttf
Normal file
Binary file not shown.
BIN
static/fonts/ZenKakuGothicNew-Light.ttf
Normal file
BIN
static/fonts/ZenKakuGothicNew-Light.ttf
Normal file
Binary file not shown.
BIN
static/fonts/ZenKakuGothicNew-Medium.ttf
Normal file
BIN
static/fonts/ZenKakuGothicNew-Medium.ttf
Normal file
Binary file not shown.
BIN
static/fonts/ZenKakuGothicNew-Regular.ttf
Normal file
BIN
static/fonts/ZenKakuGothicNew-Regular.ttf
Normal file
Binary file not shown.
BIN
static/img/logo.png
Normal file
BIN
static/img/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 41 KiB |
|
@ -1,4 +1,4 @@
|
||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapter from '@sveltejs/adapter-node';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
|
Loading…
Reference in New Issue
Block a user