Compare commits
No commits in common. "e389a3d2f92a4374747b2e4b927ae47732f43ea0" and "4e2422fa0927a1353abfe0ee19ce24fb667773b2" have entirely different histories.
e389a3d2f9
...
4e2422fa09
35
package-lock.json
generated
35
package-lock.json
generated
|
@ -11,8 +11,7 @@
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"mdsvex": "^0.12.3",
|
"mdsvex": "^0.12.3",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8"
|
||||||
"simple-git": "^3.26.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.28.1",
|
"@playwright/test": "^1.28.1",
|
||||||
|
@ -300,21 +299,6 @@
|
||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@kwsites/file-exists": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"debug": "^4.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@kwsites/promise-deferred": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
@ -1269,6 +1253,7 @@
|
||||||
"version": "4.3.6",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
|
@ -2423,6 +2408,7 @@
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
|
@ -3327,21 +3313,6 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-git": {
|
|
||||||
"version": "3.26.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.26.0.tgz",
|
|
||||||
"integrity": "sha512-5tbkCSzuskR6uA7uA23yjasmA0RzugVo8QM2bpsnxkrgP13eisFT7TMS4a+xKEJvbmr4qf+l0WT3eKa9IxxUyw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@kwsites/file-exists": "^1.1.1",
|
|
||||||
"@kwsites/promise-deferred": "^1.1.1",
|
|
||||||
"debug": "^4.3.5"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/steveukx/git-js?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sirv": {
|
"node_modules/sirv": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
|
||||||
|
|
|
@ -38,7 +38,6 @@
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"mdsvex": "^0.12.3",
|
"mdsvex": "^0.12.3",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.77.8"
|
||||||
"simple-git": "^3.26.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-size: 18px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
@ -16,5 +16,5 @@ body {
|
||||||
color: #ffffff;
|
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: 1rem;
|
font-size: 16px;
|
||||||
}
|
}
|
|
@ -7,8 +7,6 @@ export type Article = {
|
||||||
category: string;
|
category: string;
|
||||||
released_at: Date;
|
released_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
author: string;
|
|
||||||
email: string;
|
|
||||||
tags: string[];
|
tags: string[];
|
||||||
image: string;
|
image: string;
|
||||||
publish: Publish;
|
publish: Publish;
|
||||||
|
|
|
@ -1,243 +1,239 @@
|
||||||
.document {
|
.document {
|
||||||
--color-text: #ffffff;
|
--color-text: #ffffff;
|
||||||
--color-concept: hsla(40, 100%, 90%, 0.5);
|
--color-concept: hsla(40, 100%, 90%, 0.5);
|
||||||
--color-concept-hsl: 39, 100%, 25%;
|
--color-concept-hsl: 39, 100%, 25%;
|
||||||
--color-outline: #ffffff40;
|
--color-outline: #ffffff40;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
|
|
||||||
padding-left: 2rem;
|
h1 {
|
||||||
box-sizing: border-box;
|
position: relative;
|
||||||
|
width: auto;
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
h1 {
|
hr {
|
||||||
position: relative;
|
padding: 0;
|
||||||
width: auto;
|
margin: 0;
|
||||||
font-size: 1.8rem;
|
border: none;
|
||||||
margin-bottom: 0.5rem;
|
border-top: 1px solid var(--color-outline);
|
||||||
margin-left: -1rem;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
>*:not(h1, hr) {
|
||||||
padding: 0;
|
margin-left: 2rem;
|
||||||
margin: 0;
|
}
|
||||||
border: none;
|
|
||||||
border-top: 1px solid var(--color-outline);
|
|
||||||
margin-left: -1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
|
||||||
// &::before {
|
&::before {
|
||||||
// content: '#';
|
content: '#';
|
||||||
// position: absolute;
|
position: absolute;
|
||||||
// top: 0;
|
top: 0;
|
||||||
// left: -1.5rem;
|
left: -1.5rem;
|
||||||
// font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
// color: rgb(131, 131, 131);
|
color: rgb(131, 131, 131);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--color-outline);
|
border-top: 1px solid var(--color-outline);
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transform: translate(0, 0);
|
transform: translate(0, 0);
|
||||||
text-shadow: 0 0 1rem hsl(var(--color-concept-hsl));
|
text-shadow: 0 0 1rem hsl(var(--color-concept-hsl));
|
||||||
transition: 0.2s ease-out;
|
transition: 0.2s ease-out;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translate(0, -0.1rem);
|
transform: translate(0, -0.1rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
p {
|
||||||
position: relative;
|
margin-top: 0.25rem;
|
||||||
font-size: 1.1rem;
|
margin-bottom: 0.25rem;
|
||||||
font-weight: bold;
|
}
|
||||||
padding: 0;
|
|
||||||
padding-left: 1rem;
|
|
||||||
color: rgb(162, 162, 162);
|
|
||||||
|
|
||||||
&::before {
|
blockquote {
|
||||||
content: '“';
|
position: relative;
|
||||||
position: absolute;
|
font-size: 1.1rem;
|
||||||
top: 0;
|
font-weight: bold;
|
||||||
left: -0.5rem;
|
padding: 0;
|
||||||
font-size: 2rem;
|
padding-left: 1rem;
|
||||||
color: rgb(131, 131, 131);
|
color: rgb(162, 162, 162);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
warn {
|
&::before {
|
||||||
position: relative;
|
content: '“';
|
||||||
display: block;
|
position: absolute;
|
||||||
padding: 1rem 0.7rem 0.7rem;
|
top: 0;
|
||||||
background-image: linear-gradient(to right, var(--color-outline), transparent 75%);
|
left: -0.5rem;
|
||||||
background-origin: border-box;
|
font-size: 2rem;
|
||||||
box-shadow: inset 0 0 0 100vh var(--background-color);
|
color: rgb(131, 131, 131);
|
||||||
text-shadow: 0 0 1rem hsl(var(--color-concept-hsl), 1);
|
}
|
||||||
border: 1px solid transparent;
|
}
|
||||||
border-radius: 0.5rem;
|
|
||||||
|
|
||||||
&::before {
|
warn {
|
||||||
content: 'note:warn';
|
position: relative;
|
||||||
position: relative;
|
display: block;
|
||||||
display: block;
|
padding: 1rem 0.7rem 0.7rem;
|
||||||
top: 0;
|
background-image: linear-gradient(to right, var(--color-outline), transparent 75%);
|
||||||
font-size: 0.8rem;
|
background-origin: border-box;
|
||||||
margin-bottom: -0.3rem;
|
box-shadow: inset 0 0 0 100vh var(--background-color);
|
||||||
transform: translate(0, -0.5rem);
|
text-shadow: 0 0 1rem hsl(var(--color-concept-hsl), 1);
|
||||||
text-shadow: 0 0 1rem red;
|
border: 1px solid transparent;
|
||||||
}
|
border-radius: 0.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
code:not(pre > code) {
|
&::before {
|
||||||
display: inline-block;
|
content: 'note:warn';
|
||||||
font-family: monospace;
|
position: relative;
|
||||||
font-size: 0.9rem;
|
display: block;
|
||||||
background: #66666666;
|
top: 0;
|
||||||
padding: 0 0.3rem;
|
font-size: 0.8rem;
|
||||||
border-radius: 0.5rem;
|
margin-bottom: -0.3rem;
|
||||||
}
|
transform: translate(0, -0.5rem);
|
||||||
|
text-shadow: 0 0 1rem red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
// border: 2px solid #ffffff40;
|
border: 2px solid #ffffff40;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
overflow-x: auto;
|
overflow-x: scroll;
|
||||||
font-size: 0.9rem;
|
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
background: #3d3d3d;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
&::-webkit-scrollbar-thumb {
|
||||||
background: #6f6f6f;
|
background: #999;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
&::-webkit-scrollbar-track {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> code {
|
>code {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border-radius: 0.6rem;
|
border-radius: 0.6rem;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
details {
|
details {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
transition: 0.2s ease-out;
|
transition: 0.2s ease-out;
|
||||||
transition-property: height;
|
transition-property: height;
|
||||||
padding-inline-start: 0.1rem;
|
padding-inline-start: 0.1rem;
|
||||||
|
|
||||||
> summary {
|
>summary {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
list-style: '+ ' outside;
|
list-style: '+ ' outside;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
background: linear-gradient(to right, rgba(255, 255, 255, 0.25), transparent 200px);
|
background: linear-gradient(to right, rgba(255, 255, 255, 0.25), transparent 200px);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-shadow: 0 0 0.25rem rgb(255, 255, 255);
|
text-shadow: 0 0 0.25rem rgb(255, 255, 255);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> p {
|
>p {
|
||||||
margin-block-start: 0.5rem;
|
margin-block-start: 0.5rem;
|
||||||
margin-block-end: 0.5rem;
|
margin-block-end: 0.5rem;
|
||||||
margin-left: 1rem;
|
margin-left: 1rem;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[open] {
|
&[open] {
|
||||||
> summary {
|
>summary {
|
||||||
list-style: '- ' outside;
|
list-style: '- ' outside;
|
||||||
}
|
}
|
||||||
|
|
||||||
> p {
|
>p {
|
||||||
animation: detailsIn 0.5s ease;
|
animation: detailsIn 0.5s ease;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes detailsIn {
|
@keyframes detailsIn {
|
||||||
0% {
|
0% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-10px);
|
transform: translateY(-10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
table {
|
table {
|
||||||
border-spacing: 0;
|
border-spacing: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border: 1px solid var(--color-outline);
|
border: 1px solid var(--color-outline);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 0 0.5rem hsl(var(--color-concept-hsl), 0.5);
|
box-shadow: 0 0 0.5rem hsl(var(--color-concept-hsl), 0.5);
|
||||||
|
|
||||||
thead {
|
thead {
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr {
|
tr {
|
||||||
&:nth-child(2n) {
|
&:nth-child(2n) {
|
||||||
background-color: #ffffff0d;
|
background-color: #ffffff0d;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border-left: 1px solid var(--color-outline);
|
border-left: 1px solid var(--color-outline);
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,9 +3,22 @@
|
||||||
|
|
||||||
if (typeof date === 'string') date = new Date(date);
|
if (typeof date === 'string') date = new Date(date);
|
||||||
|
|
||||||
import { format } from "$lib/scripts/formatted_date";
|
const pad = function (str: string): string {
|
||||||
|
return ('0' + str).slice(-2);
|
||||||
|
};
|
||||||
|
|
||||||
const formattedDate = format(date);
|
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>
|
</script>
|
||||||
|
|
||||||
{formattedDate}
|
{formattedDate}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import type { Article } from '$lib/article';
|
import type { Article } from '$lib/article';
|
||||||
export let data: Article;
|
export let data: Article;
|
||||||
const isUpdated = data.updated_at.getTime() != data.released_at.getTime();
|
const isUpdated = data.updated_at.getTime() != data.released_at.getTime();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
{#if isUpdated}
|
{#if isUpdated}
|
||||||
<meta property="article:modified_time" content={data.updated_at.toISOString()} />
|
<meta property="article:modified_time" content={data.updated_at.toISOString()} />
|
||||||
{/if}
|
{/if}
|
||||||
<meta property="article:author" content={data.author} />
|
<meta property="article:author" content="HareWorks" />
|
||||||
<meta property="article:section" content="Blog" />
|
<meta property="article:section" content="Blog" />
|
||||||
<meta property="article:tag" content={data.tags.join(',')} />
|
<meta property="article:tag" content={data.tags.join(',')} />
|
||||||
<meta name="robots" content={data.publish as publish.Publish} />
|
<meta name="robots" content={data.publish as publish.Publish} />
|
||||||
|
@ -35,29 +35,20 @@
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>{data.title}</h1>
|
<h1>{data.title}</h1>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<div class="left">
|
<span>
|
||||||
<span>
|
released <FormattedDate date={data.released_at} />
|
||||||
released <FormattedDate date={data.released_at} />
|
{#if isUpdated}<br />
|
||||||
{#if isUpdated}<br />
|
updated <FormattedDate date={data.updated_at} />
|
||||||
updated <FormattedDate date={data.updated_at} />
|
{/if}
|
||||||
{/if}
|
</span>
|
||||||
</span>
|
<span>
|
||||||
<span>
|
<a href="/category/{data.category}"
|
||||||
<a href="/category/{data.category}"
|
>{data.category[0].toUpperCase() + data.category.slice(1)}</a
|
||||||
>{data.category[0].toUpperCase() + data.category.slice(1)}</a
|
>
|
||||||
>
|
{#each data.tags as tag}
|
||||||
{#each data.tags as tag}
|
<a href={`/search?tag=${tag}`}>{tag}</a>
|
||||||
<a href={`/search?tag=${tag}`}>{tag}</a>
|
{/each}
|
||||||
{/each}
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="right">
|
|
||||||
<img
|
|
||||||
src="https://gitea.hareworks.net/avatars/e595ec10f8052671deab92caca78220fc838f63259bcd542a0d93f53e2371d52?size=512"
|
|
||||||
alt="Author"
|
|
||||||
/>
|
|
||||||
<div>@{data.author}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
|
@ -100,11 +91,6 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.card {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
main {
|
main {
|
||||||
border: 1px solid var(--line-primary);
|
border: 1px solid var(--line-primary);
|
||||||
border-top: none;
|
border-top: none;
|
||||||
|
@ -114,7 +100,7 @@
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
padding-top: 100px;
|
padding-top: 100px;
|
||||||
max-width: 800px;
|
max-width: 1000px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -124,7 +110,7 @@
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 12px;
|
padding: 12px 100px 12px 0;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -134,54 +120,27 @@
|
||||||
.meta {
|
.meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-direction: row;
|
|
||||||
padding: 0 20px;
|
padding: 0 20px;
|
||||||
font-size: 1rem;
|
font-size: 0.8rem;
|
||||||
> .left {
|
span {
|
||||||
display: flex;
|
opacity: 0.6;
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: left;
|
|
||||||
span {
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
margin-left: 5px;
|
|
||||||
padding: 0 5px;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
&:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
&::before {
|
|
||||||
content: '🗀 ';
|
|
||||||
}
|
|
||||||
&::after {
|
|
||||||
content: '|';
|
|
||||||
margin-left: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
> .right {
|
a {
|
||||||
display: flex;
|
margin-left: 5px;
|
||||||
flex-direction: row;
|
padding: 0 5px;
|
||||||
justify-content: right;
|
color: inherit;
|
||||||
align-items: center;
|
text-decoration: none;
|
||||||
gap: 10px;
|
&:hover {
|
||||||
height: 50px;
|
text-decoration: underline;
|
||||||
margin: 10px;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border-radius: 15%;
|
|
||||||
}
|
}
|
||||||
div {
|
&:first-child {
|
||||||
font-size: 1rem;
|
&::before {
|
||||||
|
content: '🗀 ';
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: '|';
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
export const format = (
|
|
||||||
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());
|
|
||||||
|
|
||||||
return `${year}-${month}-${day}T${hour}:${min}:${sec}${sign}${tzHour}:${tzMin}`;
|
|
||||||
};
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { simpleGit, } from 'simple-git';
|
|
||||||
import type { SimpleGit, SimpleGitOptions } from 'simple-git';
|
|
||||||
|
|
||||||
console.log(process.cwd()+'/articles');
|
|
||||||
export const gitOptions: Partial<SimpleGitOptions> = {
|
|
||||||
baseDir: process.cwd()+'/articles',
|
|
||||||
binary: 'git',
|
|
||||||
maxConcurrentProcesses: 6,
|
|
||||||
config: [
|
|
||||||
'core.sshCommand=ssh -i ../key -F /dev/null'
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const git: SimpleGit = simpleGit(gitOptions);
|
|
||||||
export default git;
|
|
|
@ -1,4 +1,3 @@
|
||||||
//@ts-ignore
|
|
||||||
import { compile, escapeSvelte } from "mdsvex";
|
import { compile, escapeSvelte } from "mdsvex";
|
||||||
import { createHighlighter } from "shiki";
|
import { createHighlighter } from "shiki";
|
||||||
import { kanagawa_dark as theme } from "$lib/kanagawa"
|
import { kanagawa_dark as theme } from "$lib/kanagawa"
|
||||||
|
|
|
@ -10,14 +10,10 @@ import {
|
||||||
} from '$env/static/private'
|
} from '$env/static/private'
|
||||||
const connectionString = `postgres://${PG_USER}:${PG_PASS}@${PG_HOST}:${PG_PORT}/${PG_DB}`;
|
const connectionString = `postgres://${PG_USER}:${PG_PASS}@${PG_HOST}:${PG_PORT}/${PG_DB}`;
|
||||||
|
|
||||||
let pool = new Pool({ connectionString });
|
const pool = new Pool({ connectionString });
|
||||||
|
|
||||||
export const getConnection = () => pool;
|
export const getConnection = () => pool;
|
||||||
|
|
||||||
export const reConnect = async () => {
|
|
||||||
pool = new Pool({ connectionString });
|
|
||||||
}
|
|
||||||
|
|
||||||
import init_db from '$lib/server/database/init';
|
import init_db from '$lib/server/database/init';
|
||||||
import PG from '$lib/server/database';
|
import PG from '$lib/server/database';
|
||||||
await init_db(await PG(pool));
|
await init_db(await PG(pool));
|
|
@ -1,25 +1,30 @@
|
||||||
// initialize
|
// initialize
|
||||||
import type { Postgres } from '$lib/server/database';
|
import type { Postgres } from '$lib/server/database';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
import compile from '$lib/server/article';
|
import compile from '$lib/server/article';
|
||||||
import article_git from '$lib/server/aritcle-git';
|
|
||||||
|
|
||||||
import { format } from '$lib/scripts/formatted_date';
|
|
||||||
|
|
||||||
export default async function init(db: Postgres) {
|
export default async function init(db: Postgres) {
|
||||||
await cloneRepo();
|
if (fs.existsSync('./articles/')) {
|
||||||
await createTable(db, [
|
console.log('Pulling articles from git..');
|
||||||
|
const stdout = execSync('git -c core.sshCommand="ssh -i ../key -F /dev/null" pull', { cwd: './articles/' });
|
||||||
|
console.log(stdout.toString());
|
||||||
|
} else {
|
||||||
|
console.log('Cloning articles from git..');
|
||||||
|
const stdout = execSync('git -c core.sshCommand="ssh -i ./key -F /dev/null" clone git@gitea.hareworks.net:Hare/blog-articles.git articles', { cwd: './' });
|
||||||
|
console.log(stdout.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemas = [
|
||||||
{
|
{
|
||||||
name: 'article',
|
name: 'article',
|
||||||
columns: [
|
columns: [
|
||||||
{ name: 'seq', type: 'serial', constraint: 'primary key' },
|
{ name: 'seq', type: 'serial', constraint: 'primary key' },
|
||||||
{ name: 'id', type: 'text', constraint: 'not null' },
|
{ name: 'id', type: 'text', constraint: 'not null' },
|
||||||
{ name: 'released_at', type: 'timestamp', constraint: 'not null' },
|
|
||||||
{ name: 'updated_at', type: 'timestamp', constraint: 'not null' },
|
|
||||||
{ name: 'author', type: 'text', constraint: 'not null' },
|
|
||||||
{ name: 'email', 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: '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: 'tags', type: 'text[]', constraint: 'not null' },
|
||||||
{ name: 'image', type: 'text', constraint: '' },
|
{ name: 'image', type: 'text', constraint: '' },
|
||||||
{ name: 'publish', type: 'text', constraint: 'not null' },
|
{ name: 'publish', type: 'text', constraint: 'not null' },
|
||||||
|
@ -75,53 +80,73 @@ export default async function init(db: Postgres) {
|
||||||
{ name: 'ref_count', type: 'integer', constraint: 'not null' },
|
{ name: 'ref_count', type: 'integer', constraint: 'not null' },
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
]);
|
];
|
||||||
|
|
||||||
|
await db.begin();
|
||||||
|
try {
|
||||||
|
// Create tables
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
await db.rollback();
|
||||||
|
} finally {
|
||||||
|
await db.commit();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.replace('.md', '') });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scanDir('./articles/article');
|
||||||
|
|
||||||
const articleFiles = await crawlArticles(db);
|
|
||||||
await db.query('update tag set ref_count = 0');
|
await db.query('update tag set ref_count = 0');
|
||||||
|
|
||||||
for (const { path, id } of articleFiles) {
|
for (const { path, id } of articleFiles) {
|
||||||
console.log(`Processing ${id}...`);
|
|
||||||
await db.begin();
|
await db.begin();
|
||||||
try {
|
try {
|
||||||
const gitlog = await article_git.log({
|
|
||||||
file: path.slice(11),
|
|
||||||
strictDate: true,
|
|
||||||
});
|
|
||||||
const res = await db.query('select * from article where id = $1', [id]);
|
const res = await db.query('select * from article where id = $1', [id]);
|
||||||
|
|
||||||
// console.log(gitlog);
|
|
||||||
const latest = gitlog.latest!;
|
|
||||||
const oldest = gitlog.all[gitlog.all.length - 1];
|
|
||||||
|
|
||||||
const author = oldest.author_name;
|
|
||||||
const email = oldest.author_email;
|
|
||||||
const released_at = new Date(oldest.date);
|
|
||||||
const updated_at = new Date(latest.date);
|
|
||||||
console.log(`Author: ${author} <${email}>\nReleased at: ${format(released_at)}\nUpdated at: ${format(updated_at)}`);
|
|
||||||
|
|
||||||
const category = path.split('/')[3];
|
|
||||||
const compiled = await compile(fs.readFileSync(path, 'utf-8'));
|
const compiled = await compile(fs.readFileSync(path, 'utf-8'));
|
||||||
|
|
||||||
const title = compiled.data.fm.title;
|
const title = compiled.data.fm.title;
|
||||||
|
const category = path.split('/')[3];
|
||||||
const tags: string[] = compiled.data.fm.tags;
|
const tags: string[] = compiled.data.fm.tags;
|
||||||
|
const released_at = new Date(compiled.data.fm.released_at);
|
||||||
|
const updated_at = (compiled.data.fm.updated_at !== null) ? new Date(compiled.data.fm.updated_at) : released_at;
|
||||||
const image = compiled.data.fm.image;
|
const image = compiled.data.fm.image;
|
||||||
const publish = compiled.data.fm.publish;
|
const publish = compiled.data.fm.publish;
|
||||||
const content = compiled.code
|
const content = compiled.code
|
||||||
.replace(/>{@html `<code class="language-/g, '><code class="language-')
|
.replace(/>{@html `<code class="language-/g, '><code class="language-')
|
||||||
.replace(/<\/code>`}<\/pre>/g, '</code></pre>')
|
.replace(/<\/code>`}<\/pre>/g, '</code></pre>')
|
||||||
|
|
||||||
if (res.rowCount == 0) {
|
if (res.rowCount == 0) {
|
||||||
console.log(`New article: ${id}`);
|
console.log(`New article: ${id}`);
|
||||||
await db.query(
|
await db.query(
|
||||||
'insert into article (id, released_at, updated_at, author, email, title, category, tags, image, publish, content) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)',
|
'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, released_at, updated_at, author, email, title, category, tags, image, publish, content]
|
[id, title, category, released_at, updated_at, tags, image, publish, content]
|
||||||
);
|
);
|
||||||
// } else if (res.rows[0].updated_at < updated_at) {
|
} else if (res.rows[0].updated_at < updated_at) {
|
||||||
} else if (true) {
|
// } else if (true) {
|
||||||
console.log(`Update article: ${id}`);
|
console.log(`Update article: ${id}`);
|
||||||
await db.query(
|
await db.query(
|
||||||
'update article set released_at = $2, updated_at = $3, author = $4, email = $5, title = $6, category = $7, tags = $8, image = $9, publish = $10, content = $11 where id = $1',
|
'update article set title = $2, category = $3, released_at = $4, updated_at = $5, tags = $6, image = $7, publish = $8, content = $9 where id = $1',
|
||||||
[id, released_at, updated_at, author, email, title, category, tags, image, publish, content]
|
[id, title, category, released_at, updated_at, tags, image, publish, content]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Article ${id} is already up-to-date`);
|
console.log(`Article ${id} is already up-to-date`);
|
||||||
|
@ -137,7 +162,6 @@ export default async function init(db: Postgres) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
await db.rollback();
|
await db.rollback();
|
||||||
} finally {
|
} finally {
|
||||||
console.log('');
|
|
||||||
await db.commit();
|
await db.commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -156,53 +180,4 @@ export type TableSchema = {
|
||||||
type: string,
|
type: string,
|
||||||
constraint: string,
|
constraint: string,
|
||||||
}[],
|
}[],
|
||||||
}
|
|
||||||
|
|
||||||
const cloneRepo = async () => {
|
|
||||||
if (fs.existsSync('./articles/')) {
|
|
||||||
console.log('Pulling articles from git..');
|
|
||||||
await article_git.pull();
|
|
||||||
} else {
|
|
||||||
console.log('Cloning articles from git..');
|
|
||||||
await article_git.clone('git@gitea.hareworks.net:Hare/blog-articles.git', 'articles');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const createTable = async (db: Postgres, schemas: TableSchema[]) => {
|
|
||||||
await db.begin();
|
|
||||||
try {
|
|
||||||
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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
await db.rollback();
|
|
||||||
} finally {
|
|
||||||
await db.commit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const crawlArticles = async (db: Postgres): Promise<ArticleFileItem[]> => {
|
|
||||||
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.replace('.md', '') });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
scanDir('./articles/article');
|
|
||||||
return articleFiles;
|
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
import PG from '$lib/server/database';
|
import PG from '$lib/server/database';
|
||||||
import { format } from '$lib/scripts/formatted_date';
|
|
||||||
|
|
||||||
let data: {
|
let data: {
|
||||||
recent: {
|
recent: {
|
||||||
|
@ -29,7 +28,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
||||||
data.recent = recent_articles.rows.map((row) => ({
|
data.recent = recent_articles.rows.map((row) => ({
|
||||||
title: row.title,
|
title: row.title,
|
||||||
image: row.image,
|
image: row.image,
|
||||||
date: format(row.updated_at),
|
date: row.updated_at.toISOString().slice(0, 10),
|
||||||
link: `/article/${row.category}/${row.id}`,
|
link: `/article/${row.category}/${row.id}`,
|
||||||
tags: row.tags,
|
tags: row.tags,
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -130,7 +130,7 @@
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
backdrop-filter: blur(2px);
|
backdrop-filter: blur(2px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
.controls {
|
.controls {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -305,15 +305,15 @@
|
||||||
.tags {
|
.tags {
|
||||||
ul {
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: left;
|
justify-content: left;
|
||||||
flex-direction: row;
|
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: 1px solid var(--line-primary);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user