mirror of
https://github.com/Sevichecc/Urara-Blog.git
synced 2025-04-30 12:59:30 +08:00
init
This commit is contained in:
commit
330de1dee2
255 changed files with 18282 additions and 0 deletions
20
.eslintrc.json
Normal file
20
.eslintrc.json
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||||
|
"plugins": ["svelte3", "@typescript-eslint"],
|
||||||
|
"ignorePatterns": ["*.cjs"],
|
||||||
|
"overrides": [{ "files": ["*.svelte"], "processor": "svelte3/svelte3" }],
|
||||||
|
"settings": {
|
||||||
|
"svelte3/typescript": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"sourceType": "module",
|
||||||
|
"ecmaVersion": 2019
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2017": true,
|
||||||
|
"node": true
|
||||||
|
}
|
||||||
|
}
|
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
35
.github/CONTRIBUTING.md
vendored
Normal file
35
.github/CONTRIBUTING.md
vendored
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Thanks for ur interest in contributing to Urara! Please take a moment to read this document before submitting a pull request.
|
||||||
|
|
||||||
|
## Pull requests
|
||||||
|
|
||||||
|
pls ask before u start working on any important new feature.
|
||||||
|
|
||||||
|
for minor features and bug fixes: I will accept them as long as I think the code quality is good enough.
|
||||||
|
|
||||||
|
### Commit message
|
||||||
|
|
||||||
|
This is not mandatory at this time, but pls use [gitmoji](https://gitmoji.dev) and [Conventional Commits](https://www.conventionalcommits.org) whenever possible.
|
||||||
|
|
||||||
|
### Check the code
|
||||||
|
|
||||||
|
Run this command to check the code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm check
|
||||||
|
```
|
||||||
|
|
||||||
|
In general, expect to see output like this:
|
||||||
|
|
||||||
|
```text
|
||||||
|
svelte-check found 0 errors, 0 warnings, and 0 hints
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format the code
|
||||||
|
|
||||||
|
run this command to format the code:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm format
|
||||||
|
```
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
custom: ['https://giveth.io/project/urara', 'https://donate.lol/eth/0xaBdB3f715198A4d7e6591b6ebBE8Ccf235e5D752']
|
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/.svelte-kit
|
||||||
|
/package
|
||||||
|
src/routes/**/
|
||||||
|
static/
|
||||||
|
build/
|
||||||
|
.vercel_build_output/
|
||||||
|
.netlify/
|
||||||
|
.env.local
|
||||||
|
.env.**.local
|
||||||
|
myblog/urara/2022-06-12-appwrite.md
|
||||||
|
*.config.js
|
||||||
|
urara.js
|
1
.npmrc
Normal file
1
.npmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
1
.nvmrc
Normal file
1
.nvmrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
v18.4.0
|
7
.prettierignore
Normal file
7
.prettierignore
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.svelte-kit/**
|
||||||
|
static/**
|
||||||
|
build/**
|
||||||
|
node_modules/**
|
||||||
|
pnpm-lock.yaml
|
||||||
|
.netlify/**
|
||||||
|
.vercel_build_output/**
|
13
.prettierrc.json
Normal file
13
.prettierrc.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"printWidth": 128,
|
||||||
|
"useTabs": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"trailingComma": "none",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": true,
|
||||||
|
"htmlWhitespaceSensitivity": "ignore"
|
||||||
|
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"recommendations": ["svelte.svelte-vscode", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||||
|
}
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"files.eol": "\n",
|
||||||
|
"typescript.tsdk": "node_modules\\typescript\\lib",
|
||||||
|
"css.lint.unknownAtRules": "ignore",
|
||||||
|
"svelte.plugin.css.diagnostics.enable": false,
|
||||||
|
"[html]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
|
}
|
26
README.md
Normal file
26
README.md
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
My Tech Blog, base on [Urara](https://github.com/importantimport/urara)
|
||||||
|
|
||||||
|
## Fork from
|
||||||
|
|
||||||
|
- [kwchang083.dev](https://github.com/kwchang0831/kwchang0831.dev)
|
||||||
|
- [kwaa/blog](https://github.com/kwaa/blog)
|
||||||
|
|
||||||
|
### Custom Featrues:
|
||||||
|
|
||||||
|
- Copy code to clipboard
|
||||||
|
- Project page
|
||||||
|
- [zhlint](https://github.com/Jinjiang/zhlint)
|
||||||
|
- Quote Style
|
||||||
|

|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [ ] Note Page
|
||||||
|
- [ ] Archie Page
|
||||||
|
- [ ] Refactoring the atom feed format
|
||||||
|
- [ ] NeoDB component
|
||||||
|
- [ ] ...
|
||||||
|
|
||||||
|
### License:
|
||||||
|
|
||||||
|
[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)
|
121
mdsvex.config.ts
Normal file
121
mdsvex.config.ts
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
// mdsvex config type
|
||||||
|
import type { MdsvexOptions } from 'mdsvex'
|
||||||
|
|
||||||
|
// rehype plugins
|
||||||
|
import rehypeSlug from 'rehype-slug'
|
||||||
|
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
|
||||||
|
import rehypeExternalLinks from 'rehype-external-links'
|
||||||
|
|
||||||
|
// urara remark plugins
|
||||||
|
import type { Node, Data } from 'unist'
|
||||||
|
import { statSync } from 'fs'
|
||||||
|
import { parse, join } from 'path'
|
||||||
|
import { visit } from 'unist-util-visit'
|
||||||
|
import { toString } from 'mdast-util-to-string'
|
||||||
|
import Slugger from 'github-slugger'
|
||||||
|
import remarkFootnotes from 'remark-footnotes'
|
||||||
|
|
||||||
|
// highlighter
|
||||||
|
import { escapeSvelte } from 'mdsvex'
|
||||||
|
import { lex, parse as parseFence } from 'fenceparser'
|
||||||
|
import { renderCodeToHTML, runTwoSlash, createShikiHighlighter } from 'shiki-twoslash'
|
||||||
|
type VALUE = { [key in string | number]: VALUE } | Array<VALUE> | string | boolean | number
|
||||||
|
|
||||||
|
const remarkUraraFm =
|
||||||
|
() =>
|
||||||
|
(tree: Node<Data>, { data, filename }: { data: { fm?: Record<string, unknown> }; filename?: string }) => {
|
||||||
|
const filepath = (filename as string).split('/src/routes')[1]
|
||||||
|
let { dir, name } = parse(filepath)
|
||||||
|
if (!data.fm) data.fm = {}
|
||||||
|
// Generate slug & path
|
||||||
|
data.fm.slug = filepath
|
||||||
|
data.fm.path = join(dir, `/${name}`.replace('/index', '').replace('.svelte', ''))
|
||||||
|
// Auto-set layout as article
|
||||||
|
if (!data.fm.layout) data.fm.layout = 'article'
|
||||||
|
// Generate ToC
|
||||||
|
if (data.fm.toc !== false) {
|
||||||
|
let [slugs, toc]: [slugs: Slugger, toc: { depth: number; title: string; slug: string }[]] = [new Slugger(), []]
|
||||||
|
visit(tree, 'heading', (node: { depth: number }) => {
|
||||||
|
toc.push({
|
||||||
|
depth: node.depth,
|
||||||
|
title: toString(node),
|
||||||
|
slug: slugs.slug(toString(node), false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
data.fm.toc = toc
|
||||||
|
}
|
||||||
|
// Rename
|
||||||
|
if (data.fm.date) data.fm.created = data.fm.date
|
||||||
|
if (data.fm.lastmod) data.fm.updated = data.fm.lastmod
|
||||||
|
if (data.fm.cover) data.fm.photo = data.fm.cover
|
||||||
|
if (data.fm.descr) data.fm.summary = data.fm.descr
|
||||||
|
// Auto-read created & updated
|
||||||
|
if (!data.fm.created || !data.fm.updated) {
|
||||||
|
const { ctime, mtime } = statSync(new URL(`./urara${filepath}`, import.meta.url))
|
||||||
|
if (!data.fm.created) data.fm.created = ctime
|
||||||
|
if (!data.fm.updated) data.fm.updated = mtime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Better type definitions needed
|
||||||
|
const remarkUraraSpoiler = () => (tree: Node<Data>) =>
|
||||||
|
visit(tree, 'paragraph', (node: any) => {
|
||||||
|
const { children } = node
|
||||||
|
const text = children[0].value
|
||||||
|
const re = /\|\|(.{1,}?)\|\|/g
|
||||||
|
if (re.test(children[0].value)) {
|
||||||
|
children[0].type = 'html'
|
||||||
|
children[0].value = text.replace(re, (_match: unknown, p1: string) => `<span class="spoiler">${p1}</span>`)
|
||||||
|
}
|
||||||
|
return node
|
||||||
|
})
|
||||||
|
|
||||||
|
const defineConfig = (config: MdsvexOptions) => config
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
extensions: ['.svelte.md', '.md'],
|
||||||
|
smartypants: {
|
||||||
|
dashes: 'oldschool'
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
article: './src/lib/components/layouts/article.svelte',
|
||||||
|
note: './src/lib/components/layouts/note.svelte',
|
||||||
|
photo: './src/lib/components/layouts/photo.svelte',
|
||||||
|
reply: './src/lib/components/layouts/reply.svelte',
|
||||||
|
_: './src/lib/components/layouts/article.svelte'
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
highlighter: async (code, lang, meta) => {
|
||||||
|
let fence: Record<string, VALUE> | null
|
||||||
|
let twoslash: any
|
||||||
|
try {
|
||||||
|
fence = parseFence(lex([lang, meta].filter(Boolean).join(' ')))
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Could not parse the codefence for this code sample \n${code}`)
|
||||||
|
}
|
||||||
|
if (fence?.twoslash === true) twoslash = runTwoSlash(code, lang as string)
|
||||||
|
return `{@html \`${escapeSvelte(
|
||||||
|
renderCodeToHTML(
|
||||||
|
code,
|
||||||
|
lang as string,
|
||||||
|
fence ?? {},
|
||||||
|
{ themeName: 'material-default' },
|
||||||
|
await createShikiHighlighter({ theme: 'material-default' }),
|
||||||
|
twoslash
|
||||||
|
)
|
||||||
|
)}\` }`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remarkPlugins: [remarkUraraFm, remarkUraraSpoiler, [remarkFootnotes, { inlineNotes: true }]],
|
||||||
|
rehypePlugins: [
|
||||||
|
rehypeSlug,
|
||||||
|
[rehypeAutolinkHeadings, { behavior: 'wrap' }],
|
||||||
|
[
|
||||||
|
rehypeExternalLinks,
|
||||||
|
{
|
||||||
|
rel: ['nofollow', 'noopener', 'noreferrer', 'external'],
|
||||||
|
target: '_blank'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
})
|
8
netlify.toml
Normal file
8
netlify.toml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
[build]
|
||||||
|
command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm build"
|
||||||
|
publish = "build"
|
||||||
|
[build.environment]
|
||||||
|
NPM_FLAGS = "--version"
|
||||||
|
AWS_LAMBDA_JS_RUNTIME = "nodejs16.x"
|
||||||
|
[functions]
|
||||||
|
node_bundler = "esbuild"
|
82
package.json
Normal file
82
package.json
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
{
|
||||||
|
"name": "urara",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"repository": "importantimport/urara",
|
||||||
|
"homepage": "https://github.com/importantimport/urara",
|
||||||
|
"bugs": "https://github.com/importantimport/urara/issues",
|
||||||
|
"author": "藍+85CD",
|
||||||
|
"scripts": {
|
||||||
|
"clean": "node urara.js clean",
|
||||||
|
"tsc": "tsc -p tsconfig.node.json",
|
||||||
|
"tsc:watch": "tsc -w -p tsconfig.node.json",
|
||||||
|
"urara:build": "node urara.js build",
|
||||||
|
"urara:watch": "node urara.js watch",
|
||||||
|
"kit:dev": "export NODE_OPTIONS=--max_old_space_size=7680 && MODE=development vite dev",
|
||||||
|
"kit:build": "export NODE_OPTIONS=--max_old_space_size=7680 && vite build",
|
||||||
|
"dev:parallel": "npm-run-all -p -r tsc:watch urara:watch \"kit:dev {@} \" --",
|
||||||
|
"dev": "npm-run-all -s tsc \"dev:parallel {@} \" --",
|
||||||
|
"build": "npm-run-all -s tsc urara:build kit:build clean",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"dev:urara": "node urara.js watch",
|
||||||
|
"dev:kit": "export NODE_OPTIONS=--max_old_space_size=7680 && MODE=development svelte-kit dev",
|
||||||
|
"build:urara": "node urara.ts build",
|
||||||
|
"build:kit": "export NODE_OPTIONS=--max_old_space_size=8192 && svelte-kit build",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
"lint": "prettier --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
|
||||||
|
"format": "prettier --write --plugin-search-dir=. .",
|
||||||
|
"zhlint": "zhlint urara/*/*.md --fix && zhlint urara/*.md --fix"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@iconify-json/heroicons-outline": "^1.1.2",
|
||||||
|
"@iconify-json/heroicons-solid": "^1.1.2",
|
||||||
|
"@iconify-json/ic": "^1.1.7",
|
||||||
|
"@iconify-json/mdi": "^1.1.29",
|
||||||
|
"@iconify-json/uil": "^1.1.2",
|
||||||
|
"@sveltejs/adapter-auto": "1.0.0-next.63",
|
||||||
|
"@sveltejs/adapter-node": "1.0.0-next.83",
|
||||||
|
"@sveltejs/adapter-static": "1.0.0-next.38",
|
||||||
|
"@sveltejs/kit": "1.0.0-next.392",
|
||||||
|
"@tailwindcss/typography": "^0.5.4",
|
||||||
|
"@types/node": "^18.0.6",
|
||||||
|
"@types/unist": "^2.0.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.30.7",
|
||||||
|
"@typescript-eslint/parser": "^5.30.7",
|
||||||
|
"autoprefixer": "^10.4.7",
|
||||||
|
"chalk": "^5.0.1",
|
||||||
|
"chokidar": "^3.5.3",
|
||||||
|
"cssnano": "^5.1.12",
|
||||||
|
"daisyui": "^2.20.0",
|
||||||
|
"eslint": "^8.20.0",
|
||||||
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-plugin-svelte3": "^4.0.0",
|
||||||
|
"fenceparser": "^2.0.0",
|
||||||
|
"github-slugger": "^1.4.0",
|
||||||
|
"mdast-util-to-string": "^3.1.0",
|
||||||
|
"mdsvex": "^0.10.6",
|
||||||
|
"npm-run-all": "^4.1.5",
|
||||||
|
"postcss": "^8.4.14",
|
||||||
|
"prettier": "^2.7.1",
|
||||||
|
"prettier-plugin-svelte": "^2.7.0",
|
||||||
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
|
"rehype-external-links": "^2.0.0",
|
||||||
|
"rehype-slug": "^5.0.1",
|
||||||
|
"remark": "^14.0.2",
|
||||||
|
"remark-footnotes": "~2.0.0",
|
||||||
|
"shiki-twoslash": "^3.1.0",
|
||||||
|
"svelte": "^3.49.0",
|
||||||
|
"svelte-bricks": "^0.1.7",
|
||||||
|
"svelte-check": "^2.8.0",
|
||||||
|
"svelte-preprocess": "^4.10.7",
|
||||||
|
"svelte-typeahead": "^4.2.4",
|
||||||
|
"tailwindcss": "^3.1.6",
|
||||||
|
"tslib": "^2.4.0",
|
||||||
|
"typescript": "^4.7.4",
|
||||||
|
"unist-util-visit": "^4.1.0",
|
||||||
|
"unocss": "^0.44.7",
|
||||||
|
"vite": "^3.0.2",
|
||||||
|
"vite-plugin-pwa": "^0.12.3",
|
||||||
|
"workbox-window": "^6.5.3"
|
||||||
|
}
|
||||||
|
}
|
6250
pnpm-lock.yaml
Normal file
6250
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
184
src/app.css
Normal file
184
src/app.css
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
/* tailwind */
|
||||||
|
@import url('http://fonts.cdnfonts.com/css/lato');
|
||||||
|
@tailwind base;
|
||||||
|
|
||||||
|
@tailwind components;
|
||||||
|
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* global */
|
||||||
|
|
||||||
|
html {
|
||||||
|
@apply !bg-base-200 scroll-smooth overflow-x-hidden overflow-y-scroll;
|
||||||
|
font-family: Lato, pingfang sc, microsoft yahei, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
@apply bg-primary/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .urara-prose */
|
||||||
|
|
||||||
|
.urara-prose {
|
||||||
|
@apply !max-w-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .urara-prose heading */
|
||||||
|
|
||||||
|
.urara-prose > :is(h1, h2, h3, h4, h5) > a {
|
||||||
|
@apply no-underline font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose > :is(h1, h2, h3, h4, h5) > a::after {
|
||||||
|
@apply pl-2 text-base-200 transition-all content-['#'];
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose > :is(h1, h2, h3, h4, h5):hover > a::after {
|
||||||
|
@apply text-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .urara-prose table */
|
||||||
|
|
||||||
|
.urara-prose div > table > thead {
|
||||||
|
@apply border-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose div > table > thead > tr > th {
|
||||||
|
@apply !relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .urara-prose a */
|
||||||
|
|
||||||
|
.urara-prose :is(p, li) > a {
|
||||||
|
@apply bg-[length:100%_0.2em] hover:bg-[length:100%_100%] bg-[position:0_88%] bg-gradient-to-t from-secondary/50 to-primary/25 bg-no-repeat transition-all ease-in-out !no-underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .urara-prose misc */
|
||||||
|
|
||||||
|
.urara-prose > p img {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose :is(p, li) > code {
|
||||||
|
@apply bg-base-200 px-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose li > input {
|
||||||
|
@apply checkbox checkbox-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose kbd {
|
||||||
|
@apply kbd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose hr {
|
||||||
|
@apply border-none divider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* footer a */
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
@apply !no-underline hover:text-primary hover:!underline transition-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spoiler {
|
||||||
|
@apply blur-sm hover:blur-none active:blur-none transition-all select-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .prose pre */
|
||||||
|
|
||||||
|
.prose pre {
|
||||||
|
@apply mockup-code !bg-neutral min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre:not(.shiki) {
|
||||||
|
@apply bg-neutral text-neutral-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prose pre:not(.shiki)::before {
|
||||||
|
@apply sticky -left-5 -ml-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .urara-prose pre */
|
||||||
|
|
||||||
|
.urara-prose > pre {
|
||||||
|
@apply -mx-8 rounded-none pb-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose > pre > div.code-container {
|
||||||
|
@apply pb-5 overflow-x-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* shiki */
|
||||||
|
|
||||||
|
pre.shiki {
|
||||||
|
@apply px-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki::before {
|
||||||
|
@apply sticky;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki > div.code-title {
|
||||||
|
@apply absolute -mt-10 ml-20 pt-1.5 pl-1.5 opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki .language-id {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki > .code-container {
|
||||||
|
@apply overflow-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(pre.shiki[text='true'], pre.shiki[svelte='true']) > div.code-container {
|
||||||
|
@apply mx-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki:not([text='true'], [svelte='true']) > .code-container > code > div.line > span:first-child {
|
||||||
|
@apply pl-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki:not([text='true'], [svelte='true']) > .code-container > code > div.line > span:last-child {
|
||||||
|
@apply pr-5;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki div.dim {
|
||||||
|
@apply opacity-50 transition-opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki:hover div.dim {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.shiki div.highlight::before {
|
||||||
|
@apply bg-warning/20 absolute content-[''] w-full h-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.twoslash data-lsp {
|
||||||
|
@apply border-b border-dashed border-transparent transition-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.twoslash:hover data-lsp {
|
||||||
|
@apply border-neutral-content/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.twoslash data-lsp:hover::before {
|
||||||
|
@apply content-[attr(lsp)] absolute rounded translate-y-5 bg-neutral-focus text-neutral-content font-mono whitespace-pre-wrap transition-all px-2 py-1 z-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* your code here */
|
||||||
|
|
||||||
|
.urara-prose blockquote {
|
||||||
|
@apply font-normal text-current not-italic before:content-['“'];
|
||||||
|
border-left: 4px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose blockquote:before {
|
||||||
|
vertical-align: -0.4em;
|
||||||
|
@apply mr-2 text-5xl leading-3 italic font-serif opacity-25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.urara-prose blockquote p {
|
||||||
|
@apply inline opacity-80 before:content-[''] after:content-[''];
|
||||||
|
}
|
91
src/app.d.ts
vendored
Normal file
91
src/app.d.ts
vendored
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
/// <reference types="@sveltejs/kit" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv extends Readonly<Record<string, string>> {
|
||||||
|
readonly URARA_SITE_DOMAIN?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
globEager<Module = { [key: string]: unknown }>(pattern: string): Record<string, Module>
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace Urara {
|
||||||
|
namespace Post {
|
||||||
|
interface Article {
|
||||||
|
layout: 'article'
|
||||||
|
/** post photo */
|
||||||
|
photo?: string
|
||||||
|
/** post photo alternative */
|
||||||
|
alt?: string
|
||||||
|
/** table of content - auto generated or set `false` to disable */
|
||||||
|
toc?: false | Article.Toc[]
|
||||||
|
}
|
||||||
|
namespace Article {
|
||||||
|
interface Toc {
|
||||||
|
depth?: number
|
||||||
|
title?: string
|
||||||
|
slug?: string
|
||||||
|
children?: Toc[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface Note {
|
||||||
|
layout: 'note'
|
||||||
|
}
|
||||||
|
interface Photo {
|
||||||
|
layout: 'photo'
|
||||||
|
/** post photo */
|
||||||
|
photo?: string
|
||||||
|
/** post photo alternative */
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
interface Reply {
|
||||||
|
layout: 'reply'
|
||||||
|
/** u-in-reply-to */
|
||||||
|
inReplyTo?: string | string[]
|
||||||
|
}
|
||||||
|
interface Common {
|
||||||
|
/** @deprecated - do not use */
|
||||||
|
priority?: never
|
||||||
|
/** @deprecated - transfer to `created` */
|
||||||
|
date?: string
|
||||||
|
/** @deprecated - transfer to `updated` */
|
||||||
|
lastmod?: string
|
||||||
|
/** @deprecated - transfer to `summary` */
|
||||||
|
descr?: never
|
||||||
|
/** @deprecated - transfer to `photo` */
|
||||||
|
cover?: never
|
||||||
|
/** post path - auto generated */
|
||||||
|
path?: string
|
||||||
|
/** post slug - auto generated */
|
||||||
|
slug?: string
|
||||||
|
/** created time - auto generated or set manually */
|
||||||
|
created?: string
|
||||||
|
/** updated time - auto generated or set manually */
|
||||||
|
updated?: string
|
||||||
|
/** published time */
|
||||||
|
published?: string
|
||||||
|
/** post title */
|
||||||
|
title?: string
|
||||||
|
/** post summary */
|
||||||
|
summary?: string
|
||||||
|
/** post tags */
|
||||||
|
tags?: string[]
|
||||||
|
/** enable some advanced features.
|
||||||
|
* @property hidden - deprecated, transfer to `unlisted`
|
||||||
|
* @property unlisted - hide this post from the homepage and feed.
|
||||||
|
* @property bridgy-fed - add a link to Bridgy Fed in the post. https://fed.brid.gy/
|
||||||
|
* @property bridgy-{target} - add a link to Bridgy in the post. https://brid.gy/publish/{target}
|
||||||
|
*/
|
||||||
|
flags?: string[]
|
||||||
|
}
|
||||||
|
type Metadata = Common & (Article | Note | Photo | Reply)
|
||||||
|
interface Module {
|
||||||
|
default: { render: () => { html: string; head: string; css: { code: string } } }
|
||||||
|
metadata: Metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type Post = Post.Metadata & { html?: string }
|
||||||
|
type Page = { title?: string; path: string }
|
||||||
|
}
|
||||||
|
}
|
17
src/app.html
Normal file
17
src/app.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head prefix="og: https://ogp.me/ns#">
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="generator" content="gh:importantimport/urara" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link rel="manifest" crossorigin="use-credentials" href="/manifest.webmanifest" />
|
||||||
|
<link rel="alternate" type="application/feed+json" href="/feed.json" />
|
||||||
|
<link rel="alternate" type="application/atom+xml" href="/atom.xml" />
|
||||||
|
<link rel="sitemap" type="application/xml" href="/sitemap.xml" />
|
||||||
|
%sveltekit.head%
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body itemscope itemtype="https://schema.org/WebPage">
|
||||||
|
%sveltekit.body%
|
||||||
|
</body>
|
||||||
|
</html>
|
7
src/hooks.ts
Normal file
7
src/hooks.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import type { Handle } from '@sveltejs/kit'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
|
||||||
|
export const handle: Handle = async ({ event, resolve }) =>
|
||||||
|
await resolve(event, {
|
||||||
|
transformPageChunk: ({ html }) => html.replace('<html lang="en">', `<html lang="${site.lang ?? 'en'}">`)
|
||||||
|
})
|
3
src/lib/components/actions/1-reply.svelte
Normal file
3
src/lib/components/actions/1-reply.svelte
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<a href="#post-comment" class="btn btn-lg btn-circle btn-ghost bg-base-100 shadow-lg hover:shadow-xl">
|
||||||
|
<span class="i-heroicons-outline-chat-alt-2" />
|
||||||
|
</a>
|
12
src/lib/components/actions/2-translate.svelte
Normal file
12
src/lib/components/actions/2-translate.svelte
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
export let post: Urara.Post
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`https://translate.google.com/translate?sl=auto&tl=${
|
||||||
|
navigator.languages ? navigator.languages[0] : navigator.language
|
||||||
|
}&u=${site.protocol + site.domain + post.path}`}
|
||||||
|
class="btn btn-lg btn-circle btn-ghost bg-base-100 shadow-lg hover:shadow-xl">
|
||||||
|
<span class="i-heroicons-outline-translate" />
|
||||||
|
</a>
|
12
src/lib/components/actions/3-share.svelte
Normal file
12
src/lib/components/actions/3-share.svelte
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
export let post: Urara.Post
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`https://www.addtoany.com/share#url=${site.protocol + site.domain + post.path}&title=${encodeURI(
|
||||||
|
post.title ?? post.path.slice(1)
|
||||||
|
)}`}
|
||||||
|
class="btn btn-lg btn-circle btn-ghost bg-base-100 shadow-lg hover:shadow-xl">
|
||||||
|
<span class="i-heroicons-outline-share" />
|
||||||
|
</a>
|
33
src/lib/components/comments/-disqus.svelte
Normal file
33
src/lib/components/comments/-disqus.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
import type { DisqusConfig } from '$lib/types/post'
|
||||||
|
export let post: Urara.Post
|
||||||
|
export let config: DisqusConfig
|
||||||
|
onMount(() => {
|
||||||
|
const [c, s] = [document.createElement('script'), document.createElement('script')]
|
||||||
|
c.id = 'disqus_config'
|
||||||
|
c.type = 'application/javascript'
|
||||||
|
c.innerHTML = `
|
||||||
|
const disqus_config = function () {
|
||||||
|
this.page.url = '${post.path}'
|
||||||
|
this.page.identifier = '${post.path}'
|
||||||
|
this.page.title = '${post.title ?? post.path.slice(1)}'
|
||||||
|
${`this.language = '${config.lang}'` ?? ''}
|
||||||
|
}`
|
||||||
|
s.id = 'disqus_script'
|
||||||
|
s.src = `https://${config.shortname}.disqus.com/embed.js`
|
||||||
|
s.setAttribute('data-timestamp', Date.now().toString())
|
||||||
|
if (window['DISQUS']) {
|
||||||
|
window['DISQUS'].reset({
|
||||||
|
reload: true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
document.head.appendChild(c)
|
||||||
|
document.head.appendChild(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => document.querySelectorAll('#disqus_config, #disqus_script').forEach(node => node.remove()))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="disqus_thread" class="-mb-2" />
|
24
src/lib/components/comments/disqus.svelte
Normal file
24
src/lib/components/comments/disqus.svelte
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
export let post: Urara.Post
|
||||||
|
onMount(() => {
|
||||||
|
const [c, s] = [document.createElement('script'), document.createElement('script')]
|
||||||
|
c.id = 'disqus_config'
|
||||||
|
c.type = 'application/javascript'
|
||||||
|
s.id = 'disqus_script'
|
||||||
|
s.src = `https://cdn.commento.io/js/commento.js`
|
||||||
|
s.setAttribute('data-timestamp', Date.now().toString())
|
||||||
|
if (window['DISQUS']) {
|
||||||
|
window['DISQUS'].reset({
|
||||||
|
reload: true
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
document.head.appendChild(c)
|
||||||
|
document.head.appendChild(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => document.querySelectorAll('#disqus_config, #disqus_script').forEach(node => node.remove()))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="commento" class="-mb-2" />
|
40
src/lib/components/comments/giscus.svelte
Normal file
40
src/lib/components/comments/giscus.svelte
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import type { GiscusConfig } from '$lib/types/post'
|
||||||
|
export let config: GiscusConfig
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const giscus = document.createElement('script')
|
||||||
|
Object.entries({
|
||||||
|
src: config.src ?? 'https://giscus.app/client.js',
|
||||||
|
'data-repo': config.repo,
|
||||||
|
'data-repo-id': config.repoID,
|
||||||
|
'data-category': config.category ?? '',
|
||||||
|
'data-category-id': config.categoryID,
|
||||||
|
'data-mapping': 'pathname',
|
||||||
|
'data-reactions-enabled': config.reactionsEnabled === false ? '0' : '1',
|
||||||
|
'data-input-position': config.inputPosition ?? 'bottom',
|
||||||
|
'data-theme': config.theme ?? 'preferred_color_scheme',
|
||||||
|
'data-lang': config.lang ?? site.lang ?? 'en',
|
||||||
|
'data-loading': config.loading ?? '',
|
||||||
|
crossorigin: 'anonymous',
|
||||||
|
async: ''
|
||||||
|
}).forEach(([key, value]) => giscus.setAttribute(key, value))
|
||||||
|
setTimeout(() => {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
document.getElementById('giscus-loading').remove()
|
||||||
|
observer.disconnect()
|
||||||
|
})
|
||||||
|
observer.observe(document.getElementById('giscus'), {
|
||||||
|
childList: true
|
||||||
|
})
|
||||||
|
document.getElementById('giscus-container').appendChild(giscus)
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="giscus-container">
|
||||||
|
<button id="giscus-loading" class="btn btn-lg flex mx-auto my-4 btn-ghost btn-circle loading" />
|
||||||
|
<div id="giscus" class="giscus" />
|
||||||
|
</div>
|
33
src/lib/components/comments/utterances.svelte
Normal file
33
src/lib/components/comments/utterances.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import type { UtterancesConfig } from '$lib/types/post'
|
||||||
|
export let config: UtterancesConfig
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const utterances = document.createElement('script')
|
||||||
|
Object.entries({
|
||||||
|
src: config.src ?? 'https://utteranc.es/client.js',
|
||||||
|
repo: config.repo,
|
||||||
|
'issue-term': 'pathname',
|
||||||
|
label: config.label ?? '',
|
||||||
|
theme: config.theme ?? 'preferred-color-scheme',
|
||||||
|
crossorigin: 'anonymous',
|
||||||
|
async: ''
|
||||||
|
}).forEach(([key, value]) => utterances.setAttribute(key, value))
|
||||||
|
setTimeout(() => {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
document.getElementById('utterances-loading').remove()
|
||||||
|
observer.disconnect()
|
||||||
|
})
|
||||||
|
observer.observe(document.getElementById('utterances'), {
|
||||||
|
childList: true
|
||||||
|
})
|
||||||
|
document.getElementById('utterances-container').appendChild(utterances)
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div id="utterances-container">
|
||||||
|
<button id="utterances-loading" class="btn btn-lg flex mx-auto my-4 btn-ghost btn-circle loading" />
|
||||||
|
<div id="utterances" class="utterances" />
|
||||||
|
</div>
|
117
src/lib/components/comments/waline.svelte
Normal file
117
src/lib/components/comments/waline.svelte
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
<!-- <script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import type { WalineConfig } from '$lib/types/post'
|
||||||
|
|
||||||
|
export let post: Urara.Post
|
||||||
|
export let config: WalineConfig
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
const waline = document.createElement('script')
|
||||||
|
const [c, s] = [document.createElement('script'), document.createElement('script')]
|
||||||
|
c.id = 'disqus_config'
|
||||||
|
c.type = 'application/javascript'
|
||||||
|
console.log(waline)
|
||||||
|
Object.entries({
|
||||||
|
src: 'https://unpkg.com/@waline/client@v2/dist/waline.js',
|
||||||
|
serverURL: config.serverURL,
|
||||||
|
path: config.path ?? post.path ?? window.location.pathname,
|
||||||
|
lang: config.lang ?? 'en',
|
||||||
|
emoji: config.emoji ?? ['//unpkg.com/@waline/emojis@1.0.1/weibo'],
|
||||||
|
dark: config.dark ?? false,
|
||||||
|
meta: config.meta ?? ['nick', 'mail', 'link'],
|
||||||
|
requiredMeta: config.requiredMeta ?? [],
|
||||||
|
login: config.login ?? 'enable',
|
||||||
|
wordLimit: config.wordLimit ?? 0,
|
||||||
|
pageSize: config.pageSize ?? 10,
|
||||||
|
imageUploader: config.imageUploader,
|
||||||
|
highlighter: config.highlighter,
|
||||||
|
texRender: config.texRender,
|
||||||
|
copyright: config.copyright ?? true,
|
||||||
|
crossorigin: 'anonymous',
|
||||||
|
async: ''
|
||||||
|
}).forEach(([key, value]) => waline.setAttribute(key, value))
|
||||||
|
setTimeout(() => {
|
||||||
|
const observer = new MutationObserver(() => {
|
||||||
|
document.getElementById('giscus-loading').remove()
|
||||||
|
observer.disconnect()
|
||||||
|
})
|
||||||
|
observer.observe(document.getElementById('giscus'), {
|
||||||
|
childList: true
|
||||||
|
})
|
||||||
|
document.getElementById('giscus-container').appendChild(waline)
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/@waline/client@v2/dist/waline.css" />
|
||||||
|
<div id="waline" class="waline-container" />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.waline-container {
|
||||||
|
background-color: var(--card-background);
|
||||||
|
border-radius: var(--card-border-radius);
|
||||||
|
box-shadow: var(--shadow-l1);
|
||||||
|
padding: 2%;
|
||||||
|
}
|
||||||
|
.waline-container .vcount {
|
||||||
|
color: var(--card-text-color-main);
|
||||||
|
}
|
||||||
|
.v[data-class='v'] .vcard {
|
||||||
|
flex: 1;
|
||||||
|
width: 0;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
border-bottom: 0; /*删掉回复下面的线*/
|
||||||
|
}
|
||||||
|
.v[data-class='v'] .vcard .vquote {
|
||||||
|
border-left: 1px solid rgba(237, 237, 237, 0.5);
|
||||||
|
}
|
||||||
|
@media (max-width: 580px) {
|
||||||
|
.v[data-class='v'] .vheader .vheader-item:not(:last-child) {
|
||||||
|
border-bottom: 1px solid rgba(237, 237, 237, 0.8); /*输入框分割线*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*日间模式*/
|
||||||
|
:root {
|
||||||
|
--waline-theme-color: #34495e; /*主题色,提交按钮*/
|
||||||
|
--waline-active-color: #246bb1; /*鼠标移到提交按钮上的颜色*/
|
||||||
|
/* 徽章 */
|
||||||
|
--waline-badge-color: #34495e; /*博主徽章色*/
|
||||||
|
--waline-avatar-radius: 5px;
|
||||||
|
--waline-avatar-size: 6rem;
|
||||||
|
--waline-dark-grey: #34495e; /*ID颜色*/
|
||||||
|
--waline-text-color: #34495e; /*字体颜色*/
|
||||||
|
--waline-font-size: 1.7rem; /*字体大小颜色*/
|
||||||
|
}
|
||||||
|
/*夜间模式*/
|
||||||
|
:root[data-scheme='dark'] {
|
||||||
|
--waline-theme-color: #acc6e0;
|
||||||
|
--waline-white: #34495e; /*按键字体颜色*/
|
||||||
|
--waline-active-color: #8ab1d8;
|
||||||
|
--waline-light-grey: #666;
|
||||||
|
--waline-dark-grey: #acc6e0; /*ID颜色*/
|
||||||
|
--waline-badge-color: #acc6e0;
|
||||||
|
/* 布局颜色 */
|
||||||
|
--waline-text-color: rgba(255, 255, 255, 0.7);
|
||||||
|
--waline-bgcolor: #515151;
|
||||||
|
--waline-bgcolor-light: #66696b; /*行内代码块颜色*/
|
||||||
|
--waline-border-color: #9b9c9c;
|
||||||
|
--waline-disable-bgcolor: #444;
|
||||||
|
--waline-disable-color: #272727;
|
||||||
|
/* 特殊颜色 */
|
||||||
|
--waline-bq-color: #9b9c9c; /*quote*/
|
||||||
|
/* 其他颜色 */
|
||||||
|
--waline-info-bgcolor: #acc6e0;
|
||||||
|
--waline-info-color: #9b9c9c;
|
||||||
|
}
|
||||||
|
.v[data-class='v'] .vcontent .vemoji {
|
||||||
|
width: 2.2em; /*表情包大小修改*/
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
.v[data-class='v'] .vheader {
|
||||||
|
border-bottom: 1px solid rgba(237, 237, 237, 0.8); /*输入框分割线*/
|
||||||
|
}
|
||||||
|
.v[data-class='v'] .vpanel {
|
||||||
|
border-radius: 8px; /*输入框圆角*/
|
||||||
|
}
|
||||||
|
</style> -->
|
197
src/lib/components/comments/webmention.svelte
Normal file
197
src/lib/components/comments/webmention.svelte
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import type { WebmentionConfig } from '$lib/types/post'
|
||||||
|
export let config: WebmentionConfig
|
||||||
|
export let post: Urara.Post
|
||||||
|
|
||||||
|
interface WebmentionFeed {
|
||||||
|
type: 'feed'
|
||||||
|
name: 'Webmentions'
|
||||||
|
children: WebmentionEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebmentionEntry {
|
||||||
|
url: string
|
||||||
|
author?: {
|
||||||
|
name?: string
|
||||||
|
photo?: string
|
||||||
|
url?: string
|
||||||
|
}
|
||||||
|
content?: {
|
||||||
|
html?: string
|
||||||
|
}
|
||||||
|
rsvp?: string
|
||||||
|
published?: string
|
||||||
|
'wm-source': string
|
||||||
|
'wm-target': string
|
||||||
|
'wm-id': number
|
||||||
|
'wm-property': 'in-reply-to' | 'like-of' | 'repost-of' | 'bookmark-of' | 'mention-of' | 'rsvp'
|
||||||
|
'wm-private': boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
let [page, loaded, end, mentions, sortDirUp] = [0, false, false, [], config?.sortDir === 'up' ? true : false]
|
||||||
|
|
||||||
|
const load = async () =>
|
||||||
|
await fetch(
|
||||||
|
`https://webmention.io/api/mentions.jf2?page=${page}&per-page=${config?.perPage ?? '20'}&sort-by=${
|
||||||
|
config?.sortBy ?? 'created'
|
||||||
|
}&sort-dir=${sortDirUp ? 'up' : 'down'}${
|
||||||
|
config?.property && config.property.forEach(wmProperty => `&wm-property=${wmProperty}`)
|
||||||
|
}&target[]=${site.protocol + site.domain + post.path}&target[]=${site.protocol + site.domain + post.path}/`
|
||||||
|
)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then((feed: WebmentionFeed) => {
|
||||||
|
if (feed.children.length < 10) end = true
|
||||||
|
feed = {
|
||||||
|
...feed,
|
||||||
|
children: feed.children.filter(
|
||||||
|
(entry: WebmentionEntry) =>
|
||||||
|
!(config?.blockList?.length > 0 && config.blockList.includes(new URL(entry['wm-source']).origin))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (feed.children.length > 0) mentions = [...mentions, ...feed.children]
|
||||||
|
page++
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
|
||||||
|
const reset = async () => {
|
||||||
|
page = 0
|
||||||
|
loaded = false
|
||||||
|
end = false
|
||||||
|
mentions = []
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => load())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-8">
|
||||||
|
<div class="flex">
|
||||||
|
<p class="flex-1 m-auto italic opacity-50">
|
||||||
|
<!-- {`Sort by=${config?.sortBy ?? 'Created'}&sort-dir=${sortDirUp ? 'up' : 'down'}`} -->
|
||||||
|
{`Sort ${sortDirUp ? 'up' : 'down'}`}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-sm float-right"
|
||||||
|
on:click={() => {
|
||||||
|
sortDirUp = !sortDirUp
|
||||||
|
reset()
|
||||||
|
}}>
|
||||||
|
{#if sortDirUp === true}
|
||||||
|
<span class="i-heroicons-outline-sort-ascending" />
|
||||||
|
{:else}
|
||||||
|
<span class="i-heroicons-outline-sort-descending" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{#key mentions}
|
||||||
|
{#each mentions as mention}
|
||||||
|
{@const [wmProperty, borderColor, textColor, tooltipColor] = {
|
||||||
|
'in-reply-to': ['💬 Replied', 'border-primary/50', 'text-primary', 'tooltip-primary'],
|
||||||
|
'like-of': ['❤️ liked', 'border-secondary/50', 'text-secondary', 'tooltip-secondary'],
|
||||||
|
'repost-of': ['🔄 Reposted', 'border-accent/50', 'text-accent', 'tooltip-accent'],
|
||||||
|
'bookmark-of': ['⭐️ bookmarked', 'border-neutral/50', 'text-neutral', 'tooltip-neutral'],
|
||||||
|
'mention-of': ['💬 mentioned', 'border-base-300/50', 'text-base-content', 'tooltip-base-content'],
|
||||||
|
rsvp: [
|
||||||
|
`📅 RSVPed ${
|
||||||
|
{
|
||||||
|
yes: '✅',
|
||||||
|
no: '❌',
|
||||||
|
interested: '💡',
|
||||||
|
maybe: '💭'
|
||||||
|
}[mention.rsvp]
|
||||||
|
}`,
|
||||||
|
'border-warning/50',
|
||||||
|
'text-warning',
|
||||||
|
'tooltip-warning'
|
||||||
|
]
|
||||||
|
}[mention['wm-property']]}
|
||||||
|
{#if mention.url !== null}
|
||||||
|
<div class="{borderColor} border-2 rounded-box p-4">
|
||||||
|
<div class="flex bg-base-200 rounded-btn">
|
||||||
|
{#if mention?.author?.photo}
|
||||||
|
<img
|
||||||
|
class="w-12 h-12 flex-none rounded-btn"
|
||||||
|
src={mention.author.photo}
|
||||||
|
alt={mention.author?.name ?? new URL(mention.url).host}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async" />
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1 px-4 py-2 m-auto">
|
||||||
|
<p>
|
||||||
|
{#if mention?.author?.url}
|
||||||
|
<a class="font-semibold {textColor} hover:underline" href={mention.author.url}>
|
||||||
|
{mention.author?.name ?? new URL(mention.url).host}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
{mention?.author?.name ?? new URL(mention.url).host}
|
||||||
|
{/if}
|
||||||
|
<a class="{textColor} hover:underline" href={mention['wm-source']}>
|
||||||
|
{wmProperty}
|
||||||
|
</a>
|
||||||
|
this post on
|
||||||
|
<span
|
||||||
|
class="tooltip tooltip-bottom xl:tooltip-right {tooltipColor}"
|
||||||
|
data-tip={new Date(mention.published ?? mention['wm-received']).toLocaleString()}>
|
||||||
|
{mention.published ? mention.published.slice(0, 10) : mention['wm-received'].slice(0, 10)}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if mention.content}
|
||||||
|
<div class="prose max-w-none break-words mt-4">
|
||||||
|
<p>{@html mention.content?.html ?? mention.content?.text}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/key}
|
||||||
|
{#if loaded === true}
|
||||||
|
{#if end !== true}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
loaded = false
|
||||||
|
load()
|
||||||
|
}}
|
||||||
|
class="btn btn-primary btn-block">
|
||||||
|
LOAD
|
||||||
|
</button>
|
||||||
|
{:else if config?.form !== true}
|
||||||
|
<div class="divider mt-0 -mb-2">END</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<button id="webmention-loading" class="btn btn-lg btn-block flex btn-ghost loading" />
|
||||||
|
{/if}
|
||||||
|
{#if config?.form === true}
|
||||||
|
<form id="webmention-form" method="post" action="https://webmention.io/{config.username}/webmention">
|
||||||
|
<input type="hidden" name="target" value={site.protocol + site.domain + post.path} />
|
||||||
|
<div class="label gap-4">
|
||||||
|
<span class="label-text">send webmentions here:</span>
|
||||||
|
{#if config?.commentParade === true}
|
||||||
|
<span class="label-text-alt text-right">
|
||||||
|
or <a
|
||||||
|
class="hover:!text-primary"
|
||||||
|
href="https://quill.p3k.io/?dontask=1&me=https://commentpara.de/&reply={encodeURI(
|
||||||
|
site.protocol + site.domain + post.path
|
||||||
|
)}">
|
||||||
|
comment anonymously
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="flex-1">
|
||||||
|
<input
|
||||||
|
class="input input-bordered focus:input-primary w-full"
|
||||||
|
type="text"
|
||||||
|
id="reply-url"
|
||||||
|
name="source"
|
||||||
|
placeholder="https://example.com/my-post" />
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary flex-none mt-auto" type="submit" id="webmention-submit">Send</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
|
</div>
|
35
src/lib/components/extra/alert.svelte
Normal file
35
src/lib/components/extra/alert.svelte
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let title: string = undefined
|
||||||
|
export let description: string = undefined
|
||||||
|
export let status: 'info' | 'success' | 'warning' | 'error' = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class:alert-info={status === 'info'}
|
||||||
|
class:alert-success={status === 'success'}
|
||||||
|
class:alert-warning={status === 'warning'}
|
||||||
|
class:alert-error={status === 'error'}
|
||||||
|
class="alert flex-col shadow-inner my-4">
|
||||||
|
<div class="mr-auto">
|
||||||
|
{#if status === 'success'}
|
||||||
|
<span class="i-heroicons-outline-check-circle" />
|
||||||
|
{:else if status === 'warning'}
|
||||||
|
<span class="i-heroicons-outline-exclamation-circle" />
|
||||||
|
{:else if status === 'error'}
|
||||||
|
<span class="i-heroicons-outline-x-circle" />
|
||||||
|
{:else}
|
||||||
|
<span class="i-heroicons-outline-information-circle" />
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<div class:font-bold={description}>{title}</div>
|
||||||
|
{#if description}
|
||||||
|
<div class="text-xs">{description}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if $$slots.default}
|
||||||
|
<div class="block w-full">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
64
src/lib/components/extra/follow.svelte
Normal file
64
src/lib/components/extra/follow.svelte
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let status: string = ''
|
||||||
|
let statusText: string = ''
|
||||||
|
let input: string = ''
|
||||||
|
const follow = async (event: Event, account = new FormData(event.target as HTMLFormElement).get('account') as string) =>
|
||||||
|
await fetch(
|
||||||
|
account.startsWith('@')
|
||||||
|
? `https://${account.split('@')[2]}/.well-known/webfinger?resource=acct:${account.slice(1)}`
|
||||||
|
: `https://${account.split('@')[1]}/.well-known/webfinger?resource=acct:${account}`,
|
||||||
|
{
|
||||||
|
headers: { Accept: 'application/jrd+json' }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(res => {
|
||||||
|
if (res.ok) {
|
||||||
|
status = 'success'
|
||||||
|
return res.json()
|
||||||
|
} else {
|
||||||
|
status = 'error'
|
||||||
|
statusText = res.status + res.statusText
|
||||||
|
throw Error(res.status + res.statusText)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
({ links }) => links.find((link: { rel: string }) => link.rel === 'http://ostatus.org/schema/1.0/subscribe').template
|
||||||
|
)
|
||||||
|
.then(template => (window.location.href = template.replace('{uri}', `dotdev@kwaa.moe`)))
|
||||||
|
.catch(error => console.error(error))
|
||||||
|
$: if (input)
|
||||||
|
input.length < 5 ? (status = '') : input.includes('@') && input.includes('.') ? (status = 'success') : (status = 'warning')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<input type="checkbox" id="remote-follow" class="modal-toggle" />
|
||||||
|
<label for="remote-follow" class="modal modal-bottom sm:modal-middle cursor-pointer">
|
||||||
|
<div class="modal-box relative" for="">
|
||||||
|
<form on:submit|preventDefault={follow} class="form-control gap-2">
|
||||||
|
<div class="label py-0">
|
||||||
|
<span class="label-text">Your fediverse account ID:</span>
|
||||||
|
</div>
|
||||||
|
<label class="input-group">
|
||||||
|
<input
|
||||||
|
bind:value={input}
|
||||||
|
type="text"
|
||||||
|
id="account"
|
||||||
|
name="account"
|
||||||
|
placeholder="username@instance.tld"
|
||||||
|
class:input-success={status === 'success'}
|
||||||
|
class:input-warning={status === 'warning'}
|
||||||
|
class:input-error={status === 'error'}
|
||||||
|
class="input input-bordered transition-all flex-1" />
|
||||||
|
<button type="submit" class="btn btn-square">
|
||||||
|
<span class="i-heroicons-outline-paper-airplane rotate-90" />
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
{#if statusText}
|
||||||
|
<div class="label py-0">
|
||||||
|
<span class="label-text-alt text-error">
|
||||||
|
{statusText}{#if statusText === '404'}: Couldn't find user{/if}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</label>
|
50
src/lib/components/extra/friend.svelte
Normal file
50
src/lib/components/extra/friend.svelte
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Friend } from '$lib/config/friends'
|
||||||
|
import Footer from '$lib/components/footer.svelte'
|
||||||
|
export let item: unknown
|
||||||
|
let friend = item as unknown as Friend
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if friend.id === 'footer'}
|
||||||
|
<Footer rounded={true} class="p-4 md:p-8" />
|
||||||
|
{:else if friend.html}
|
||||||
|
<a id={friend.id} rel={friend.rel} href={friend.link} class="h-card u-url">
|
||||||
|
{@html friend.html}
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
id={friend.id}
|
||||||
|
rel={friend.rel}
|
||||||
|
href={friend.link}
|
||||||
|
class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow h-card u-url">
|
||||||
|
<div class="absolute text-4xl font-bold opacity-5 rotate-6 leading-tight top-4">
|
||||||
|
{friend.name ?? ''}
|
||||||
|
<br />
|
||||||
|
{friend.title ?? ''}
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
{#if friend.avatar}
|
||||||
|
<div class="avatar {friend.class?.avatar} shrink-0 w-16 mb-auto">
|
||||||
|
<img class="{friend.class?.img ?? 'rounded-xl'} u-photo" src={friend.avatar} alt={friend.title} />
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="avatar {friend.class?.avatar} placeholder mb-auto">
|
||||||
|
<div class="{friend.class?.img ?? 'bg-neutral-focus text-neutral-content shadow-inner rounded-xl'} w-16">
|
||||||
|
<span class="text-3xl">{(friend.name ?? friend.title).charAt(0)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="card-title flex-col gap-0 flex-1 items-end">
|
||||||
|
<span class="text-right p-name">{friend.name ?? ''}</span>
|
||||||
|
<span class="opacity-50 text-right">{friend.title}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if friend.descr}
|
||||||
|
<div class="prose opacity-70 p-note">
|
||||||
|
{friend.descr}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/if}
|
83
src/lib/components/extra/github.svelte
Normal file
83
src/lib/components/extra/github.svelte
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
export const prerender = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let user = undefined
|
||||||
|
export let repo = undefined
|
||||||
|
let info: {
|
||||||
|
html_url: string
|
||||||
|
description: string
|
||||||
|
homepage?: string
|
||||||
|
owner: { avatar_url: string }
|
||||||
|
stargazers_count: any
|
||||||
|
license?: { key?: any }
|
||||||
|
}
|
||||||
|
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
onMount(async () => {
|
||||||
|
info = await (await fetch(`https://api.github.com/repos/${user}/${repo}`)).json()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 !bg-base-200 my-4 ">
|
||||||
|
<div class="p-6">
|
||||||
|
{#key info}
|
||||||
|
{#if info}
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-initial pr-4">
|
||||||
|
<div class="card-title mb-6 !text-3xl font-medium">
|
||||||
|
<a rel="noopener noreferrer external" target="_blank" href={info.html_url}>
|
||||||
|
{user}/
|
||||||
|
<span class="font-semibold">{repo}</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<p class="prose">
|
||||||
|
{info.description}
|
||||||
|
<br />
|
||||||
|
<a rel="noopener noreferrer external" target="_blank" href={info.homepage}>{info.homepage}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<img class="w-20 h-20 ml-auto rounded-xl flex-initial" alt="owner_avatar" src={info.owner.avatar_url} />
|
||||||
|
</div>
|
||||||
|
<div class="card-actions -ml-2">
|
||||||
|
<button class="btn btn-sm btn-ghost">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
version="1.1"
|
||||||
|
data-view-component="true"
|
||||||
|
class="inline-block w-4 h-4 mr-2 fill-current">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25zm0 2.445L6.615 5.5a.75.75 0 01-.564.41l-3.097.45 2.24 2.184a.75.75 0 01.216.664l-.528 3.084 2.769-1.456a.75.75 0 01.698 0l2.77 1.456-.53-3.084a.75.75 0 01.216-.664l2.24-2.183-3.096-.45a.75.75 0 01-.564-.41L8 2.694v.001z" />
|
||||||
|
</svg>
|
||||||
|
{info.stargazers_count}
|
||||||
|
</button>
|
||||||
|
{#if info.license}
|
||||||
|
<a class="btn btn-sm btn-ghost" href="https://choosealicense.com/licenses/{info.license.key}">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
version="1.1"
|
||||||
|
data-view-component="true"
|
||||||
|
class="inline-block w-4 h-4 mr-2 fill-current">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8.75.75a.75.75 0 00-1.5 0V2h-.984c-.305 0-.604.08-.869.23l-1.288.737A.25.25 0 013.984 3H1.75a.75.75 0 000 1.5h.428L.066 9.192a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.514 3.514 0 00.686.45A4.492 4.492 0 003 11c.88 0 1.556-.22 2.023-.454a3.515 3.515 0 00.686-.45l.045-.04.016-.015.006-.006.002-.002.001-.002L5.25 9.5l.53.53a.75.75 0 00.154-.838L3.822 4.5h.162c.305 0 .604-.08.869-.23l1.289-.737a.25.25 0 01.124-.033h.984V13h-2.5a.75.75 0 000 1.5h6.5a.75.75 0 000-1.5h-2.5V3.5h.984a.25.25 0 01.124.033l1.29.736c.264.152.563.231.868.231h.162l-2.112 4.692a.75.75 0 00.154.838l.53-.53-.53.53v.001l.002.002.002.002.006.006.016.015.045.04a3.517 3.517 0 00.686.45A4.492 4.492 0 0013 11c.88 0 1.556-.22 2.023-.454a3.512 3.512 0 00.686-.45l.045-.04.01-.01.006-.005.006-.006.002-.002.001-.002-.529-.531.53.53a.75.75 0 00.154-.838L13.823 4.5h.427a.75.75 0 000-1.5h-2.234a.25.25 0 01-.124-.033l-1.29-.736A1.75 1.75 0 009.735 2H8.75V.75zM1.695 9.227c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L3 6.327l-1.305 2.9zm10 0c.285.135.718.273 1.305.273s1.02-.138 1.305-.273L13 6.327l-1.305 2.9z" />
|
||||||
|
</svg>
|
||||||
|
{info.license.key}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
<button class="btn btn-sm btn-circle btn-ghost ml-auto">
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 16 16" version="1.1" data-view-component="true" class="w-6 h-6 fill-current">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
12
src/lib/components/extra/mockup_code.svelte
Normal file
12
src/lib/components/extra/mockup_code.svelte
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
export const prerender = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export let code: string
|
||||||
|
export let lang = 'text'
|
||||||
|
export let theme = 'material-default'
|
||||||
|
export let highlightedLines = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<p>TODO {code} {lang} {theme} {highlightedLines}</p>
|
18
src/lib/components/extra/picture.svelte
Normal file
18
src/lib/components/extra/picture.svelte
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { dev } from '$app/env'
|
||||||
|
let className = undefined
|
||||||
|
export { className as class }
|
||||||
|
export let src = undefined
|
||||||
|
export let alt = undefined
|
||||||
|
export let loading: 'lazy' | 'eager' = 'lazy'
|
||||||
|
export let decoding: 'async' | 'sync' | 'auto' = 'async'
|
||||||
|
let [name, ext] = [src.split('.').slice(0, -1).join('.'), src.split('.').pop()]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<picture>
|
||||||
|
{#if !dev}
|
||||||
|
<source srcset="{name}_768.{ext} 1x" media="(max-width: 425px)" />
|
||||||
|
<source srcset="{name}_768.{ext} 1x, {src} 2x" media="(min-width: 425px)" />
|
||||||
|
{/if}
|
||||||
|
<img itemprop="image" class={className} {src} {alt} {loading} {decoding} />
|
||||||
|
</picture>
|
7
src/lib/components/extra/post_theme.svelte
Normal file
7
src/lib/components/extra/post_theme.svelte
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let theme = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@html `
|
||||||
|
${theme ?? ''}
|
||||||
|
`}
|
36
src/lib/components/extra/profile.svelte
Normal file
36
src/lib/components/extra/profile.svelte
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
export const prerender = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
export let avatar: string
|
||||||
|
export let name: string
|
||||||
|
export let subname: string
|
||||||
|
export let bio: string
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="relative w-auto min-h-48 rounded-box overflow-hidden bg-gradient-to-b from-primary to-secondary text-primary-content transition-shadow duration-200 shadow-xl hover:shadow-2xl p-4 md:p-8 my-4">
|
||||||
|
<div class="absolute -top-4 opacity-10 text-[12rem] text-neutral leading-tight rotate-[30deg]">
|
||||||
|
{name ?? site.author.name}
|
||||||
|
</div>
|
||||||
|
<div class="avatar mb-4">
|
||||||
|
<div class="rounded-full border-2 border-white shadow-xl w-16 h-16">
|
||||||
|
<img
|
||||||
|
class="hover:rotate-[360deg] transition-transform duration-1000 ease-in-out m-0"
|
||||||
|
src={avatar ?? site.author.photo}
|
||||||
|
alt={name ?? site.author.name}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if subname}
|
||||||
|
<div class="opacity-50">{subname}</div>
|
||||||
|
{/if}
|
||||||
|
<div class="text-2xl mb-2">{name ?? site.author.name}</div>
|
||||||
|
{#if bio || site.author.bio}
|
||||||
|
<div>{@html bio ?? site.author.bio}</div>
|
||||||
|
{/if}
|
||||||
|
<slot />
|
||||||
|
</div>
|
42
src/lib/components/extra/projects.svelte
Normal file
42
src/lib/components/extra/projects.svelte
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Project } from '$lib/config/projects'
|
||||||
|
import Footer from '$lib/components/footer.svelte'
|
||||||
|
export let item: unknown
|
||||||
|
let project = item as unknown as Project
|
||||||
|
let tags = project.tags
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if project.id === 'footer'}
|
||||||
|
<Footer rounded={true} class="max-w-4xl mx-auto p-4 md:p-8" />
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
id={project.id}
|
||||||
|
href={project.link}
|
||||||
|
class="card mx-auto max-w-4xl bg-base-100 shadow-xl transition-shadow mb-7 h-card u-url hover:shadow-2xl">
|
||||||
|
<div class="absolute text-5xl font-bold opacity-5 rotate-6 leading-tight top-2 right-0">
|
||||||
|
{project.feature}
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex flex-col md:flex-row items-start gap-4">
|
||||||
|
<div class="mb-auto max-w-full shrink-0 md:max-w-xs">
|
||||||
|
<img class="rounded-md" src={project.img} alt={project.description} />
|
||||||
|
</div>
|
||||||
|
<div class="card-title flex-1 flex-col items-start gap-4">
|
||||||
|
<div>
|
||||||
|
<h2 class="p-name text-left text-2xl mb-2">{project.name}</h2>
|
||||||
|
<div class="mb-3 text-base font-normal">
|
||||||
|
{#each tags as tag}
|
||||||
|
<span class="btn btn-sm btn-ghost normal-case border-dotted border-base-content/20 border-2 my-1 mr-1">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-left text-base font-normal opacity-70">
|
||||||
|
{@html project.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/if}
|
28
src/lib/components/extra/youtube.svelte
Normal file
28
src/lib/components/extra/youtube.svelte
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let id = undefined
|
||||||
|
export let list = undefined
|
||||||
|
export let playlist = undefined
|
||||||
|
export let start = undefined
|
||||||
|
export let autoplay = false
|
||||||
|
export let disablekb = false
|
||||||
|
export let controls = true
|
||||||
|
export let fs = true
|
||||||
|
export let loop = false
|
||||||
|
|
||||||
|
const src = `https://www.youtube.com/embed/${id}?${list ? `listType=playlist&list=${list}&` : ''}${
|
||||||
|
playlist ? `playlist=${playlist}&` : ''
|
||||||
|
}${start ? `start=${start}` : ''}${autoplay ? 'autoplay=1&' : ''}${disablekb ? 'disablekb=1&' : ''}${
|
||||||
|
controls ? '' : 'controls=0&'
|
||||||
|
}${fs ? '' : 'fs=0&'}${loop ? 'loop=1' : ''}`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative pb-[56.25%] mb-2">
|
||||||
|
<iframe
|
||||||
|
{src}
|
||||||
|
class="absolute w-full h-full"
|
||||||
|
title="YouTube video player"
|
||||||
|
frameborder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
loading="lazy"
|
||||||
|
allowfullscreen />
|
||||||
|
</div>
|
46
src/lib/components/footer.svelte
Normal file
46
src/lib/components/footer.svelte
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import { footer as footerConfig } from '$lib/config/general'
|
||||||
|
let className: string = undefined
|
||||||
|
export { className as class }
|
||||||
|
export let sticky: boolean = false
|
||||||
|
export let rounded: boolean = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<footer
|
||||||
|
id="footer"
|
||||||
|
class="footer footer-center bg-base-300 text-base-content shadow-inner p-8 {rounded
|
||||||
|
? 'rounded-box'
|
||||||
|
: 'md:rounded-box'} {sticky ? 'sticky bottom-0 z-0 md:static' : ''} {className ?? ''}">
|
||||||
|
<div class="prose">
|
||||||
|
<p>
|
||||||
|
{#if footerConfig.nav}
|
||||||
|
{#each footerConfig.nav as { text, link }, i}
|
||||||
|
<a href={link} rel="noopener external" target="_blank">{text}</a>
|
||||||
|
{#if i + 1 < footerConfig.nav.length}
|
||||||
|
<span class="mr-1">·</span>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
<br />
|
||||||
|
{/if}
|
||||||
|
Copyright © {footerConfig.since && footerConfig.since !== new Date().toJSON().substring(0, 4)
|
||||||
|
? `${footerConfig.since} - ${new Date().toJSON().substring(0, 4)}`
|
||||||
|
: new Date().toJSON().substring(0, 4)}
|
||||||
|
{site.author.name}
|
||||||
|
<br />
|
||||||
|
Powered by
|
||||||
|
<a
|
||||||
|
rel="noopener external"
|
||||||
|
target="_blank"
|
||||||
|
class="tooltip tooltip-secondary hover:text-secondary"
|
||||||
|
data-tip="🌸 [γ] - Based on MDsveX & SvelteKit 🌸"
|
||||||
|
href="https://github.com/importantimport/urara">
|
||||||
|
Urara
|
||||||
|
</a>
|
||||||
|
{#if footerConfig.html}
|
||||||
|
<br />
|
||||||
|
{@html footerConfig.html}
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
39
src/lib/components/head.svelte
Normal file
39
src/lib/components/head.svelte
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { dev } from '$app/env'
|
||||||
|
import { head } from '$lib/config/general'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import OpenGraph from '$lib/components/head_opengraph.svelte'
|
||||||
|
export let post: Urara.Post = undefined
|
||||||
|
export let page: Urara.Page = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="author" content={site.author?.name} />
|
||||||
|
{#if post}
|
||||||
|
<link rel="canonical" href={site.protocol + site.domain + post.path} />
|
||||||
|
{#if post.layout === 'article'}
|
||||||
|
<title>{post.title ?? post.path.slice(1)} | {site.title}</title>
|
||||||
|
{:else if post.layout === 'note'}
|
||||||
|
<title>{post.path.slice(1)} | {site.title}</title>
|
||||||
|
{/if}
|
||||||
|
{#if post.tags}<meta name="keywords" content={post.tags.join(', ')} />{/if}
|
||||||
|
{#if post.summary}<meta name="description" content={post.summary} />{/if}
|
||||||
|
{:else}
|
||||||
|
<meta name="description" content={site.description} />
|
||||||
|
<meta name="keywords" content={site.keywords?.join(', ')} />
|
||||||
|
{#if page}
|
||||||
|
<title>{page.title ?? page.path.slice(1)} | {site.title}</title>
|
||||||
|
<link rel="canonical" href={site.protocol + site.domain + page.path} />
|
||||||
|
{:else}
|
||||||
|
<title>{site.subtitle ? `${site.title} - ${site.subtitle}` : site.title}</title>
|
||||||
|
<link rel="canonical" href={site.protocol + site.domain} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if head.custom}
|
||||||
|
{#each head.custom({ dev, post, page }) as tag}
|
||||||
|
{@html tag}
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<OpenGraph {post} {page} />
|
15
src/lib/components/head_icon.svelte
Normal file
15
src/lib/components/head_icon.svelte
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { favicon, any } from '$lib/config/icon'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
{#if favicon}
|
||||||
|
<link rel="shortcut icon" href={favicon.src} sizes={favicon.sizes} type={favicon.type} />
|
||||||
|
{/if}
|
||||||
|
{#if any['180']}
|
||||||
|
<link rel="apple-touch-icon" href={any['180'].src} sizes={any['180'].sizes} type={any['180'].type} />
|
||||||
|
{/if}
|
||||||
|
{#if any['192']}
|
||||||
|
<link rel="icon" href={any['192'].src} sizes={any['192'].sizes} type={any['192'].type} />
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
50
src/lib/components/head_opengraph.svelte
Normal file
50
src/lib/components/head_opengraph.svelte
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import { any, maskable } from '$lib/config/icon'
|
||||||
|
export let post: Urara.Post = undefined
|
||||||
|
export let page: Urara.Page = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta property="og:site_name" content={site.title} />
|
||||||
|
<meta property="og:locale" content={site.lang} />
|
||||||
|
{#if post}
|
||||||
|
<meta property="og:type" content="article" />
|
||||||
|
<meta property="og:url" content={site.protocol + site.domain + post.path} />
|
||||||
|
<meta property="article:author" content={site.author.name} />
|
||||||
|
<meta property="article:published_time" content={post.published ?? post.created} />
|
||||||
|
<meta property="article:modified_time" content={post.updated ?? post.published ?? post.created} />
|
||||||
|
{#if post.layout === 'article'}
|
||||||
|
<meta property="og:title" content={post.title ?? post.path.slice(1)} />
|
||||||
|
<meta property="og:url" content={site.protocol + site.domain + post.path} />
|
||||||
|
{#if post.summary}
|
||||||
|
<meta property="og:description" content={post.summary} />
|
||||||
|
{/if}
|
||||||
|
{#if post.photo}
|
||||||
|
<meta property="og:image" content={site.protocol + site.domain + post.photo} />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
{:else}
|
||||||
|
<meta property="og:image" content={maskable['512'].src ?? any['512'].src ?? any['192'].src} />
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
{/if}
|
||||||
|
{#if post.tags}
|
||||||
|
{#each post.tags as tag}
|
||||||
|
<meta property="article:tag" content={tag} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{:else if post.layout === 'note'}
|
||||||
|
<meta property="og:title" content={post.path} />
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:image" content={maskable['512'].src ?? any['512'].src ?? any['192'].src} />
|
||||||
|
<meta property="og:description" content={site.description} />
|
||||||
|
{#if page}
|
||||||
|
<meta property="og:title" content={page.title ?? page.path.slice(1)} />
|
||||||
|
<meta property="og:url" content={site.protocol + site.domain + page.path} />
|
||||||
|
{:else}
|
||||||
|
<meta property="og:title" content={site.title} />
|
||||||
|
<meta property="og:url" content={site.protocol + site.domain} />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
18
src/lib/components/head_relmeauth.svelte
Normal file
18
src/lib/components/head_relmeauth.svelte
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import { head } from '$lib/config/general'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
{#if site.author.github}
|
||||||
|
<link rel="me" href="https://github.com/{site.author.github}" />
|
||||||
|
{/if}
|
||||||
|
{#if site.author.twitter}
|
||||||
|
<link rel="me" href="https://twitter.com/{site.author.twitter}" />
|
||||||
|
{/if}
|
||||||
|
{#if head.relMe}
|
||||||
|
{#each head.relMe as href}
|
||||||
|
<link rel="me" {href} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
19
src/lib/components/head_static.svelte
Normal file
19
src/lib/components/head_static.svelte
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { head } from '$lib/config/general'
|
||||||
|
import { post } from '$lib/config/post'
|
||||||
|
import Icon from '$lib/components/head_icon.svelte'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
{#if head.me}
|
||||||
|
{#each head.me as href}
|
||||||
|
<link rel="me" {href} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{#if post.comment?.webmention?.username}
|
||||||
|
<link rel="webmention" href="https://webmention.io/{post.comment.webmention.username}/webmention" />
|
||||||
|
<link rel="pingback" href="https://webmention.io/{post.comment.webmention.username}/xmlrpc" />
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<Icon />
|
131
src/lib/components/header.svelte
Normal file
131
src/lib/components/header.svelte
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { browser, dev } from '$app/env'
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import { theme } from '$lib/config/general'
|
||||||
|
import { title as storedTitle } from '$lib/stores/title'
|
||||||
|
import { header as headerConfig } from '$lib/config/general'
|
||||||
|
import { hslToHex } from '$lib/utils/color'
|
||||||
|
import Nav from '$lib/components/header_nav.svelte'
|
||||||
|
import Search from '$lib/components/header_search.svelte'
|
||||||
|
export let path: string
|
||||||
|
let title: string
|
||||||
|
let currentTheme: string
|
||||||
|
let currentThemeColor: string
|
||||||
|
let search: boolean = false
|
||||||
|
let pin: boolean = true
|
||||||
|
let percent: number
|
||||||
|
let [scrollY, lastY] = [0, 0]
|
||||||
|
|
||||||
|
storedTitle.subscribe(storedTitle => (title = storedTitle as string))
|
||||||
|
|
||||||
|
$: if (browser && currentTheme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', currentTheme)
|
||||||
|
currentThemeColor = hslToHex(
|
||||||
|
...(getComputedStyle(document.documentElement)
|
||||||
|
.getPropertyValue('--b1')
|
||||||
|
.slice(dev ? 1 : 0)
|
||||||
|
.replaceAll('%', '')
|
||||||
|
.split(' ')
|
||||||
|
.map(Number) as [number, number, number])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: if (scrollY) {
|
||||||
|
pin = lastY - scrollY > 0 || scrollY === 0 ? true : false
|
||||||
|
lastY = scrollY
|
||||||
|
if (browser)
|
||||||
|
percent =
|
||||||
|
Math.round((scrollY / (document.documentElement.scrollHeight - document.documentElement.clientHeight)) * 10000) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser)
|
||||||
|
currentTheme =
|
||||||
|
localStorage.getItem('theme') ?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'night' : 'lemonade')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="theme-color" content={currentThemeColor} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<svelte:window bind:scrollY />
|
||||||
|
|
||||||
|
<header
|
||||||
|
id="header"
|
||||||
|
class:-translate-y-32={!pin && scrollY > 0}
|
||||||
|
class="fixed z-50 w-screen bg-base-100/30 md:bg-base-200/30 transition-all duration-500 ease-in-out border-b-2 border-transparent max-h-[4.125rem] {scrollY >
|
||||||
|
32 && 'backdrop-blur border-base-content/10'}">
|
||||||
|
<div in:fly={{ x: -50, duration: 300, delay: 300 }} out:fly={{ x: -50, duration: 300 }} class="navbar">
|
||||||
|
<div class="navbar-start">
|
||||||
|
{#if headerConfig.nav}
|
||||||
|
<Nav {path} {title} {pin} {scrollY} nav={headerConfig.nav} />
|
||||||
|
{/if}
|
||||||
|
<a href="/" sveltekit:prefetch class="btn btn-ghost normal-case text-lg">{site.title}</a>
|
||||||
|
</div>
|
||||||
|
<div class="navbar-end">
|
||||||
|
{#if headerConfig.search}
|
||||||
|
<!-- The button to open modal -->
|
||||||
|
<label for="search-modal" class="btn btn-square btn-ghost ml-2"><span class="i-heroicons-outline-search" /></label>
|
||||||
|
<!-- <button
|
||||||
|
on:click={() => {
|
||||||
|
search = !search
|
||||||
|
}}
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-square btn-ghost ml-2">
|
||||||
|
<span class="i-heroicons-outline-search" />
|
||||||
|
</button> -->
|
||||||
|
{/if}
|
||||||
|
<div id="change-theme" class="dropdown dropdown-end">
|
||||||
|
<div tabindex="0" class="btn btn-square btn-ghost">
|
||||||
|
<span class="i-heroicons-outline-color-swatch" />
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabindex="0"
|
||||||
|
class="flex shadow-2xl menu dropdown-content bg-base-100 text-base-content rounded-box w-52 p-2 gap-2 overflow-y-auto max-h-[21.5rem]"
|
||||||
|
class:hidden={!pin}>
|
||||||
|
{#each theme as { name, text }}
|
||||||
|
<button
|
||||||
|
data-theme={name}
|
||||||
|
on:click={() => {
|
||||||
|
currentTheme = name
|
||||||
|
localStorage.setItem('theme', name)
|
||||||
|
}}
|
||||||
|
class:border-2={currentTheme === name}
|
||||||
|
class:border-primary={currentTheme === name}
|
||||||
|
class="btn btn-ghost hover:bg-primary group rounded-lg flex bg-base-100 p-2 transition-all">
|
||||||
|
<p class="flex-1 text-left text-base-content group-hover:text-primary-content transition-color">
|
||||||
|
{text ?? name}
|
||||||
|
</p>
|
||||||
|
<div class="flex-none m-auto flex gap-1">
|
||||||
|
<div class="bg-primary w-2 h-4 rounded" />
|
||||||
|
<div class="bg-secondary w-2 h-4 rounded" />
|
||||||
|
<div class="bg-accent w-2 h-4 rounded" />
|
||||||
|
<div class="bg-neutral w-2 h-4 rounded" />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="totop"
|
||||||
|
on:click={() => window.scrollTo(0, 0)}
|
||||||
|
class:translate-y-24={!pin || scrollY === 0}
|
||||||
|
aria-label="scroll to top"
|
||||||
|
class="fixed grid group btn btn-circle btn-lg border-none backdrop-blur bottom-6 right-6 z-50 duration-500 ease-in-out {percent >
|
||||||
|
95
|
||||||
|
? 'btn-accent shadow-lg'
|
||||||
|
: 'btn-ghost bg-base-100/30 md:bg-base-200/30'}"
|
||||||
|
class:opacity-100={scrollY}>
|
||||||
|
<div
|
||||||
|
class="radial-progress text-accent transition-all duration-500 ease-in-out group-hover:text-accent-focus col-start-1 row-start-1"
|
||||||
|
style={`--size:4rem; --thickness: 0.25rem; --value:${percent};"`} />
|
||||||
|
<div
|
||||||
|
class:border-transparent={percent > 95}
|
||||||
|
class="border-4 border-base-content/10 group-hover:border-transparent col-start-1 row-start-1 rounded-full w-full h-full p-4 grid duration-500 ease-in-out">
|
||||||
|
<span class="i-heroicons-solid-chevron-up" />
|
||||||
|
</div>
|
||||||
|
</button>
|
71
src/lib/components/header_nav.svelte
Normal file
71
src/lib/components/header_nav.svelte
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let nav: { text: string; link?: string; children?: { text: string; link: string }[] }[]
|
||||||
|
export let path: string
|
||||||
|
export let title: string
|
||||||
|
export let scrollY: number
|
||||||
|
export let pin: boolean
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dropdown lg:hidden">
|
||||||
|
<label for="navbar-dropdown" tabindex="0" class="btn btn-square btn-ghost">
|
||||||
|
<span class="i-heroicons-outline-menu-alt-1" />
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
id="navbar-dropdown"
|
||||||
|
tabindex="0"
|
||||||
|
class:hidden={!pin}
|
||||||
|
class="menu menu-compact dropdown-content bg-base-100 text-base-content shadow-lg rounded-box max-w-52 p-2">
|
||||||
|
{#each nav as { text, link, children }}
|
||||||
|
{#if link && !children}
|
||||||
|
<li>
|
||||||
|
<a sveltekit:prefetch class:font-bold={link === path} href={link}>{text}</a>
|
||||||
|
</li>
|
||||||
|
{:else if children}
|
||||||
|
<li tabindex="0">
|
||||||
|
<span class:font-bold={children.some(({ link }) => link === path)} class="justify-between gap-1 max-w-[13rem]">
|
||||||
|
{text}
|
||||||
|
<span class="i-heroicons-solid-chevron-right mr-2" />
|
||||||
|
</span>
|
||||||
|
<ul class="bg-base-100 text-base-content shadow-lg p-2">
|
||||||
|
{#each children as { text, link }}
|
||||||
|
<li>
|
||||||
|
<a sveltekit:prefetch class:font-bold={link === path} href={link}>{text}</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class:swap-active={scrollY > 32 && title} class="swap order-last hidden lg:inline-grid">
|
||||||
|
<button
|
||||||
|
on:click={() => window.scrollTo(0, 0)}
|
||||||
|
class:hidden={scrollY < 32 || !title}
|
||||||
|
class="swap-on btn btn-ghost text-base font-normal normal-case transition-all duration-200">
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
<ul class:hidden={scrollY > 64 && title} class="swap-off menu menu-horizontal p-0">
|
||||||
|
{#each nav as { text, link, children }}
|
||||||
|
{#if link && !children}
|
||||||
|
<li>
|
||||||
|
<a sveltekit:prefetch class:font-bold={link === path} href={link}>{text}</a>
|
||||||
|
</li>
|
||||||
|
{:else if children}
|
||||||
|
<li tabindex="0">
|
||||||
|
<span class:font-bold={children.some(({ link }) => link === path)} class="gap-1">
|
||||||
|
{text}
|
||||||
|
<span class="i-heroicons-solid-chevron-down -mr-1" />
|
||||||
|
</span>
|
||||||
|
<ul class="bg-base-100 text-base-content shadow-lg p-2">
|
||||||
|
{#each children as { text, link }}
|
||||||
|
<li>
|
||||||
|
<a sveltekit:prefetch class:font-bold={link === path} href={link}>{text}</a>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
102
src/lib/components/header_search.svelte
Normal file
102
src/lib/components/header_search.svelte
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, createEventDispatcher } from 'svelte'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import { header as headerConfig } from '$lib/config/general'
|
||||||
|
import Typeahead from 'svelte-typeahead'
|
||||||
|
import { page } from '$app/stores'
|
||||||
|
// ref : https://github.com/saadeghi/daisyui/blob/master/src/docs/src/components/Search.svelte
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<input type="checkbox" id="search-modal" class="modal-toggle " />
|
||||||
|
<label for="search-modal" class="modal cursor-pointer w-full items-start pt-16 ">
|
||||||
|
<label class="modal-box w-11/12 max-w-4xl " for="">
|
||||||
|
<div class="form-control">
|
||||||
|
<span
|
||||||
|
class="i-heroicons-outline-search text-base-content pointer-events-none absolute z-10 my-2 ml-3 stroke-current opacity-60 w-5" />
|
||||||
|
</div>
|
||||||
|
<Typeahead placeholder="Search is not avaible …" limit={8} label="Search" inputAfterSelect="clear">
|
||||||
|
<div class="py-1 text-sm" />
|
||||||
|
</Typeahead>
|
||||||
|
<div class="pointer-events-none absolute right-14 top-8 gap-1 opacity-50 hidden lg:flex">
|
||||||
|
<kbd class="kbd kbd-sm">ESC</kbd>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</label>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- <script lang="ts">
|
||||||
|
import { onMount, createEventDispatcher } from "svelte"
|
||||||
|
import { page } from "$app/stores"
|
||||||
|
import { goto } from "$app/navigation"
|
||||||
|
import Typeahead from "svelte-typeahead"
|
||||||
|
import { pages } from "@src/lib/data.js"
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let searchIndex = []
|
||||||
|
pages.forEach((group) => {
|
||||||
|
group.items.forEach((item) => {
|
||||||
|
searchIndex.push(item)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
let seachboxEl
|
||||||
|
function handleKeydown(e) {
|
||||||
|
if ((e.keyCode === 75 && e.metaKey) || (e.keyCode === 75 && e.ctrlKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
seachboxEl.querySelector("input[type=search]").focus()
|
||||||
|
dispatch("focus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function onSelect({ detail }) {
|
||||||
|
goto(searchIndex[detail.originalIndex].href)
|
||||||
|
dispatch("search", detail)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
-->
|
||||||
|
<style global>
|
||||||
|
.searchbox [data-svelte-typeahead] {
|
||||||
|
background: none;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
[data-svelte-search] label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
[data-svelte-search] input {
|
||||||
|
background-color: transparent;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
padding-left: 2em;
|
||||||
|
}
|
||||||
|
[data-svelte-search] input::placeholder {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
[data-svelte-search] input:focus {
|
||||||
|
outline-color: hsla(var(--bc) / 0.2);
|
||||||
|
background-color: hsl(var(--b1));
|
||||||
|
color: hsla(var(--bc));
|
||||||
|
}
|
||||||
|
[data-svelte-typeahead] .svelte-typeahead-list {
|
||||||
|
transform: translateY(0.5em);
|
||||||
|
background: hsl(var(--b1));
|
||||||
|
border-radius: var(--rounded-btn);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
[data-svelte-typeahead] .svelte-typeahead-list .selected,
|
||||||
|
[data-svelte-typeahead] .svelte-typeahead-list .selected:hover {
|
||||||
|
background: hsl(var(--n));
|
||||||
|
color: hsl(var(--nc));
|
||||||
|
}
|
||||||
|
[data-svelte-typeahead] .svelte-typeahead-list li {
|
||||||
|
color: hsl(var(--bc));
|
||||||
|
}
|
||||||
|
[data-svelte-typeahead] .svelte-typeahead-list li:hover {
|
||||||
|
background: hsl(var(--b2));
|
||||||
|
color: hsl(var(--bc));
|
||||||
|
}
|
||||||
|
[data-svelte-typeahead] .svelte-typeahead-list li:not(:last-of-type) {
|
||||||
|
border-bottom-color: hsla(var(--bc) / 0.1);
|
||||||
|
}
|
||||||
|
</style>
|
73
src/lib/components/index_post.svelte
Normal file
73
src/lib/components/index_post.svelte
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Status from '$lib/components/post_status.svelte'
|
||||||
|
import Reply from '$lib/components/post_reply.svelte'
|
||||||
|
import Image from '$lib/components/prose/img.svelte'
|
||||||
|
export let post: Urara.Post
|
||||||
|
export let loading: 'eager' | 'lazy' = 'lazy'
|
||||||
|
export let decoding: 'async' | 'sync' | 'auto' = 'async'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if post.layout === 'photo'}
|
||||||
|
<article
|
||||||
|
itemscope
|
||||||
|
itemtype="https://schema.org/BlogPosting"
|
||||||
|
itemprop="blogPost"
|
||||||
|
class="h-entry card image-full before:!bg-transparent bg-base-100 rounded-none md:rounded-box">
|
||||||
|
<figure>
|
||||||
|
<Image
|
||||||
|
class="u-photo object-cover object-center h-full w-full"
|
||||||
|
src={post.photo}
|
||||||
|
alt={post.alt ?? post.photo}
|
||||||
|
{loading}
|
||||||
|
{decoding} />
|
||||||
|
{#if post.alt}
|
||||||
|
<figcaption>{@html post.alt}</figcaption>
|
||||||
|
{/if}
|
||||||
|
</figure>
|
||||||
|
<div class="card-body mt-auto">
|
||||||
|
<Status {post} index={true} />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{:else}
|
||||||
|
<article
|
||||||
|
itemscope
|
||||||
|
itemtype="https://schema.org/BlogPosting"
|
||||||
|
itemprop="blogPost"
|
||||||
|
class="h-entry card bg-base-100 rounded-none md:rounded-box group {post.layout === 'article' && post.photo
|
||||||
|
? 'image-full before:!rounded-none'
|
||||||
|
: ''}">
|
||||||
|
{#if post.layout === 'article' && post.photo}
|
||||||
|
<figure class="!block">
|
||||||
|
<Image
|
||||||
|
class="u-featured object-center h-full w-full absolute group-hover:scale-105 transition-transform duration-500 ease-in-out"
|
||||||
|
src={post.photo}
|
||||||
|
alt={post.alt ?? post.photo}
|
||||||
|
{loading}
|
||||||
|
{decoding} />
|
||||||
|
{#if post.alt}
|
||||||
|
<figcaption>{@html post.alt}</figcaption>
|
||||||
|
{/if}
|
||||||
|
</figure>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="card-body {post.layout === 'article' && post.photo
|
||||||
|
? 'md:col-start-1 md:row-start-1 md:text-neutral-content md:z-20'
|
||||||
|
: ''}">
|
||||||
|
{#if post.layout === 'reply'}
|
||||||
|
<Reply inReplyTo={post.inReplyTo} class="-mt-4 -mx-4 mb-4" />
|
||||||
|
{/if}
|
||||||
|
<Status {post} index={true} />
|
||||||
|
{#if post.layout === 'article'}
|
||||||
|
<h1
|
||||||
|
itemprop="name headline"
|
||||||
|
class="card-title text-2xl mr-auto bg-[length:100%_0%] bg-[position:0_88%] underline decoration-4 decoration-transparent group-hover:decoration-primary hover:bg-[length:100%_100%] hover:text-primary-content bg-gradient-to-t from-primary to-primary bg-no-repeat transition-all ease-in-out duration-300">
|
||||||
|
<a itemprop="url" class="u-url p-name" href={post.path}>{post.title ?? post.path.slice(1)}</a>
|
||||||
|
</h1>
|
||||||
|
{#if post.summary}
|
||||||
|
<p itemprop="description" class="p-summary mb-auto">{post.summary}</p>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{@html post.html}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{/if}
|
181
src/lib/components/index_profile.svelte
Normal file
181
src/lib/components/index_profile.svelte
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="sticky flex flex-row gap-4 xl:flex-col top-24 card card-body items-right h-card">
|
||||||
|
<a href={site.protocol + site.domain} class="hidden u-url" rel="me">{site.title}</a>
|
||||||
|
{#if site.author.photo}
|
||||||
|
<img class="hidden u-photo" src={site.author.photo} alt={site.author.name} decoding="async" loading="lazy" />
|
||||||
|
<div class="justify-end flex-none w-32 h-32 my-auto ml-auto avatar">
|
||||||
|
<img
|
||||||
|
class="rounded-full shadow-xl w-32 h-32 hover:rotate-[360deg] transition-transform duration-1000 ease-in-out"
|
||||||
|
src={site.author.photo}
|
||||||
|
alt={site.author.name} />
|
||||||
|
{#if site.author.status}
|
||||||
|
<div class="heart absolute rounded-full w-10 h-10 bottom-0 right-0 bg-base-100 shadow-xl text-xl text-center py-1.5">
|
||||||
|
{site.author.status}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1 my-auto text-right">
|
||||||
|
<h1 class="mt-0 mb-2 text-3xl font-bold p-name">{site.author.name}</h1>
|
||||||
|
<p class="opacity-75 p-note">{@html site.author.bio}</p>
|
||||||
|
<p class="pt-3 space-x-3">
|
||||||
|
<a
|
||||||
|
class="text-sm transition tooltip tooltip-secondary hover:text-secondary"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
data-tip="RSS"
|
||||||
|
aria-label="RSS"
|
||||||
|
href="https://seviche.cc/atom.xml">
|
||||||
|
<span class="i-ic-twotone-rss-feed !w-8 !h-8 mr-1 fill-current inline-block hover:text-lime-500" />
|
||||||
|
</a>
|
||||||
|
{#if site.author.email}
|
||||||
|
<a
|
||||||
|
class="text-sm transition tooltip tooltip-secondary hover:text-secondary"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
data-tip="Mail"
|
||||||
|
aria-label="Mail"
|
||||||
|
href="mailto:{site.author.email}">
|
||||||
|
<span class="i-ic-baseline-mail !w-8 !h-8 mr-1 fill-current inline-block hover:text-lime-500" />
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
{#if site.author.github}
|
||||||
|
<a
|
||||||
|
class="text-sm transition tooltip tooltip-secondary hover:text-secondary"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
data-tip="Github"
|
||||||
|
aria-label="Github"
|
||||||
|
href="https://github.com/{site.author.github}">
|
||||||
|
<span class="i-uil-github !w-8 !h-8 mr-1 fill-current inline-block hover:text-violet-500" />
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
{#if site.author.pgp}
|
||||||
|
<a href={site.author.pgp.link} rel="pgpkey" class="mt-4 font-mono rounded-full btn btn-ghost btn-xs bg-base-300">
|
||||||
|
<span class="i-heroicons-solid-key !w-4 !h-4 mr-1" />
|
||||||
|
{site.author.pgp.text}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.heart {
|
||||||
|
-webkit-animation: heartbeat 2s linear 1s infinite;
|
||||||
|
-o-animation: heartbeat 2s linear 1s infinite;
|
||||||
|
-moz-animation: heartbeat 2s linear 1s infinite;
|
||||||
|
-ms-animation: heartbeat 2s linear 1s infinite;
|
||||||
|
animation: heartbeat 2s linear 1s infinite;
|
||||||
|
}
|
||||||
|
@keyframes heartbeat {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
2% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
4% {
|
||||||
|
-webkit-transform: scale(1.08);
|
||||||
|
-o-transform: scale(1.08);
|
||||||
|
-moz-transform: scale(1.08);
|
||||||
|
-ms-transform: scale(1.08);
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
8% {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
-o-transform: scale(1.1);
|
||||||
|
-moz-transform: scale(1.1);
|
||||||
|
-ms-transform: scale(1.1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
-webkit-transform: scale(0.96);
|
||||||
|
-o-transform: scale(0.96);
|
||||||
|
-moz-transform: scale(0.96);
|
||||||
|
-ms-transform: scale(0.96);
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
24% {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
-o-transform: scale(1.1);
|
||||||
|
-moz-transform: scale(1.1);
|
||||||
|
-ms-transform: scale(1.1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
32% {
|
||||||
|
-webkit-transform: scale(1.08);
|
||||||
|
-o-transform: scale(1.08);
|
||||||
|
-moz-transform: scale(1.08);
|
||||||
|
-ms-transform: scale(1.08);
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-ms-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes heartbeat {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
2% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
4% {
|
||||||
|
-webkit-transform: scale(1.08);
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
8% {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
-webkit-transform: scale(0.96);
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
24% {
|
||||||
|
-webkit-transform: scale(1.1);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
32% {
|
||||||
|
-webkit-transform: scale(1.08);
|
||||||
|
transform: scale(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
99
src/lib/components/layouts/_post.svelte
Normal file
99
src/lib/components/layouts/_post.svelte
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
import { browser } from '$app/env'
|
||||||
|
import { posts as storedPosts } from '$lib/stores/posts'
|
||||||
|
import { title as storedTitle } from '$lib/stores/title'
|
||||||
|
import { post as postConfig } from '$lib/config/post'
|
||||||
|
import Status from '$lib/components/post_status.svelte'
|
||||||
|
import Pagination from '$lib/components/post_pagination.svelte'
|
||||||
|
import Action from '$lib/components/post_action.svelte'
|
||||||
|
import Comment from '$lib/components/post_comment.svelte'
|
||||||
|
import Footer from '$lib/components/footer.svelte'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { copyCode } from '$lib/utils/copyCode'
|
||||||
|
export let post: Urara.Post
|
||||||
|
|
||||||
|
let index: number
|
||||||
|
let prev: Urara.Post
|
||||||
|
let next: Urara.Post
|
||||||
|
|
||||||
|
$: if (browser)
|
||||||
|
storedPosts.subscribe((storedPosts: Urara.Post[]) => {
|
||||||
|
index = storedPosts.findIndex(storedPost => storedPost.path === post.path)
|
||||||
|
prev = storedPosts
|
||||||
|
.slice(0, index)
|
||||||
|
.reverse()
|
||||||
|
.find(post => !post.flags?.includes('unlisted'))
|
||||||
|
next = storedPosts.slice(index + 1).find(post => !post.flags?.includes('unlisted'))
|
||||||
|
storedTitle.set(post.title)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
copyCode()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col flex-nowrap justify-center xl:flex-row xl:flex-wrap">
|
||||||
|
<div
|
||||||
|
in:fly={{ x: 25, duration: 300, delay: 500 }}
|
||||||
|
out:fly={{ x: 25, duration: 300 }}
|
||||||
|
class="flex-1 w-full max-w-screen-md order-first ease-out transform mx-auto xl:mr-0">
|
||||||
|
{#if browser}
|
||||||
|
<Action {post} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
in:fly={{ x: -25, duration: 300, delay: 500 }}
|
||||||
|
out:fly={{ x: -25, duration: 300 }}
|
||||||
|
class="flex-1 w-full max-w-screen-md xl:order-last ease-out transform mx-auto xl:mr-0">
|
||||||
|
<slot name="right" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-none w-full max-w-screen-md mx-auto xl:mx-0">
|
||||||
|
<div class="card bg-base-100 rounded-none md:rounded-box md:shadow-xl md:mb-8 lg:mb-16 z-10">
|
||||||
|
<article itemscope itemtype="https://schema.org/BlogPosting" class="h-entry">
|
||||||
|
{#if postConfig.bridgy}
|
||||||
|
<div id="bridgy" class="hidden">
|
||||||
|
{#each post.flags?.some( flag => flag.startsWith('bridgy') ) ? post.flags.flatMap( flag => (flag.startsWith('bridgy') ? flag.slice(7) : []) ) : [...(postConfig.bridgy.post ?? []), ...(postConfig.bridgy[post.layout] ?? [])] as target}
|
||||||
|
{#if target === 'fed'}
|
||||||
|
<a href="https://fed.brid.gy/">fed</a>
|
||||||
|
{:else}
|
||||||
|
<a href="https://brid.gy/publish/{target}">{target}</a>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<slot name="top" />
|
||||||
|
<div class="card-body gap-0">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#if $$slots.middle}
|
||||||
|
<slot name="middle" />
|
||||||
|
{:else}
|
||||||
|
<!-- legacy fallback -->
|
||||||
|
<slot name="middle-top" />
|
||||||
|
<Status {post} />
|
||||||
|
<slot name="middle-bottom" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<slot name="content" />
|
||||||
|
{#if post.tags}
|
||||||
|
<div class="divider mt-4 mb-0" />
|
||||||
|
<div>
|
||||||
|
{#each post.tags as tag}
|
||||||
|
<a href="/?tags={tag}" class="btn btn-sm btn-ghost normal-case mt-2 mr-2 p-category">
|
||||||
|
#{tag}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{#if (prev || next) && !post.flags?.includes('pagination-disabled') && !post.flags?.includes('unlisted')}
|
||||||
|
<Pagination {next} {prev} />
|
||||||
|
{/if}
|
||||||
|
{#if browser && postConfig.comment && !post.flags?.includes('comment-disabled')}
|
||||||
|
<Comment {post} config={postConfig.comment} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Footer sticky={true} />
|
||||||
|
</div>
|
||||||
|
</div>
|
68
src/lib/components/layouts/article.svelte
Normal file
68
src/lib/components/layouts/article.svelte
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
import Image from '$lib/components/prose/img.svelte'
|
||||||
|
import table from '$lib/components/prose/table.svelte'
|
||||||
|
export { Image as img, table }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { browser } from '$app/env'
|
||||||
|
import Head from '$lib/components/head.svelte'
|
||||||
|
import Post from '$lib/components/layouts/_post.svelte'
|
||||||
|
import Status from '$lib/components/post_status.svelte'
|
||||||
|
import Toc from '$lib/components/post_toc.svelte'
|
||||||
|
|
||||||
|
export let path = undefined
|
||||||
|
export let created = undefined
|
||||||
|
export let updated = undefined
|
||||||
|
export let published = undefined
|
||||||
|
export let tags = undefined
|
||||||
|
export let flags = undefined
|
||||||
|
|
||||||
|
export let title = undefined
|
||||||
|
export let summary = undefined
|
||||||
|
export let photo = undefined
|
||||||
|
export let alt = undefined
|
||||||
|
export let toc = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Head post={{ layout: 'article', path, created, updated, published, tags, flags, title, summary, photo, alt, toc }} />
|
||||||
|
|
||||||
|
<Post post={{ layout: 'article', path, created, updated, published, tags, flags, title, summary, photo, alt, toc }}>
|
||||||
|
<div slot="right" class="h-full hidden xl:block">
|
||||||
|
{#if browser && toc?.length > 1}
|
||||||
|
<Toc {toc} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div slot="middle" class="flex flex-col">
|
||||||
|
{#if photo}
|
||||||
|
<figure class="flex-col gap-2 -mx-4 mb-4 w-auto -mt-8 md:mt-0 md:mb-8 md:order-last">
|
||||||
|
<Image class="u-featured rounded-box w-full shadow-xl" src={photo} alt={alt ?? photo} loading="eager" decoding="auto" />
|
||||||
|
{#if alt}
|
||||||
|
<figcaption>{@html alt}</figcaption>
|
||||||
|
{/if}
|
||||||
|
</figure>
|
||||||
|
{/if}
|
||||||
|
<Status
|
||||||
|
post={{
|
||||||
|
layout: 'article',
|
||||||
|
path,
|
||||||
|
created,
|
||||||
|
updated,
|
||||||
|
published,
|
||||||
|
tags,
|
||||||
|
flags,
|
||||||
|
title,
|
||||||
|
summary,
|
||||||
|
photo,
|
||||||
|
alt,
|
||||||
|
toc
|
||||||
|
}} />
|
||||||
|
<h1 itemprop="name headline" class="card-title text-3xl mt-2 mb-8 p-name">{title ?? path.slice(1)}</h1>
|
||||||
|
{#if summary}
|
||||||
|
<p class="hidden p-summary">{summary}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<main slot="content" itemprop="articleBody" class="urara-prose prose e-content">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</Post>
|
25
src/lib/components/layouts/note.svelte
Normal file
25
src/lib/components/layouts/note.svelte
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
import Image from '$lib/components/prose/img.svelte'
|
||||||
|
import table from '$lib/components/prose/table.svelte'
|
||||||
|
export { Image as img, table }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Head from '$lib/components/head.svelte'
|
||||||
|
import Post from '$lib/components/layouts/_post.svelte'
|
||||||
|
|
||||||
|
export let path = undefined
|
||||||
|
export let created = undefined
|
||||||
|
export let updated = undefined
|
||||||
|
export let published = undefined
|
||||||
|
export let tags = undefined
|
||||||
|
export let flags = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Head post={{ layout: 'note', path, created, updated, published, tags, flags }} />
|
||||||
|
|
||||||
|
<Post post={{ layout: 'note', path, created, updated, published, tags, flags }}>
|
||||||
|
<main slot="content" itemprop="articleBody" class="urara-prose prose p-name p-content">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</Post>
|
34
src/lib/components/layouts/photo.svelte
Normal file
34
src/lib/components/layouts/photo.svelte
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
import Image from '$lib/components/prose/img.svelte'
|
||||||
|
import table from '$lib/components/prose/table.svelte'
|
||||||
|
export { Image as img, table }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Head from '$lib/components/head.svelte'
|
||||||
|
import Post from '$lib/components/layouts/_post.svelte'
|
||||||
|
|
||||||
|
export let path = undefined
|
||||||
|
export let created = undefined
|
||||||
|
export let updated = undefined
|
||||||
|
export let published = undefined
|
||||||
|
export let tags = undefined
|
||||||
|
export let flags = undefined
|
||||||
|
|
||||||
|
export let photo = undefined
|
||||||
|
export let alt = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Head post={{ layout: 'photo', path, created, updated, published, tags, flags, photo, alt }} />
|
||||||
|
|
||||||
|
<Post post={{ layout: 'photo', path, created, updated, published, tags, flags, photo, alt }}>
|
||||||
|
<figure slot="top" class="flex-col gap-2 mx-4 md:mx-0 w-auto">
|
||||||
|
<Image src={photo} alt={alt ?? photo} class="rounded-box w-full shadow-xl" loading="eager" decoding="auto" />
|
||||||
|
{#if alt}
|
||||||
|
<figcaption>{@html alt}</figcaption>
|
||||||
|
{/if}
|
||||||
|
</figure>
|
||||||
|
<main slot="content" itemprop="articleBody" class="urara-prose prose p-name p-content">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</Post>
|
29
src/lib/components/layouts/reply.svelte
Normal file
29
src/lib/components/layouts/reply.svelte
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
import Image from '$lib/components/prose/img.svelte'
|
||||||
|
import table from '$lib/components/prose/table.svelte'
|
||||||
|
export { Image as img, table }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Head from '$lib/components/head.svelte'
|
||||||
|
import Post from '$lib/components/layouts/_post.svelte'
|
||||||
|
import Reply from '$lib/components/post_reply.svelte'
|
||||||
|
|
||||||
|
export let created = undefined
|
||||||
|
export let updated = undefined
|
||||||
|
export let published = undefined
|
||||||
|
export let tags = undefined
|
||||||
|
export let path = undefined
|
||||||
|
export let flags = undefined
|
||||||
|
|
||||||
|
export let inReplyTo = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Head post={{ layout: 'reply', path, created, updated, published, tags, flags, inReplyTo }} />
|
||||||
|
|
||||||
|
<Post post={{ layout: 'reply', path, created, updated, published, tags, flags, inReplyTo }}>
|
||||||
|
<Reply {inReplyTo} slot="top" class="mt-4 mx-4" />
|
||||||
|
<main slot="content" itemprop="articleBody" class="urara-prose prose p-name p-content">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</Post>
|
12
src/lib/components/post_action.svelte
Normal file
12
src/lib/components/post_action.svelte
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let post: Urara.Post
|
||||||
|
const actions = import.meta.globEager('/src/lib/components/actions/*.svelte')
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="sticky top-24 hidden xl:flex flex-col gap-4 w-fit h-[calc(100vh-12rem)] ml-auto mr-8 my-8 justify-center">
|
||||||
|
{#if Object.keys(actions).length}
|
||||||
|
{#each Object.values(actions) as action}
|
||||||
|
<svelte:component this={action.default} {post} />
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
</div>
|
39
src/lib/components/post_comment.svelte
Normal file
39
src/lib/components/post_comment.svelte
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { CommentConfig } from '$lib/types/post'
|
||||||
|
import { toSnake } from '$lib/utils/case'
|
||||||
|
export let post: Urara.Post
|
||||||
|
export let config: CommentConfig = undefined
|
||||||
|
const comments = import.meta.globEager('/src/lib/components/comments/*.svelte')
|
||||||
|
let currentComment: string = undefined
|
||||||
|
currentComment = localStorage.getItem('comment') ?? toSnake(config.use[0])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if config?.use.length > 0}
|
||||||
|
<div id="post-comment" class="card card-body">
|
||||||
|
{#if config.use.length > 1}
|
||||||
|
<div class="tabs w-full mb-8" class:tabs-boxed={config?.['style'] === 'boxed'}>
|
||||||
|
{#each config.use as name}
|
||||||
|
<span
|
||||||
|
on:click={() => {
|
||||||
|
currentComment = toSnake(name)
|
||||||
|
localStorage.setItem('comment', toSnake(name))
|
||||||
|
}}
|
||||||
|
class="flex-1 tab transition-all"
|
||||||
|
class:tab-bordered={config?.['style'] === 'bordered'}
|
||||||
|
class:tab-lifted={config?.['style'] === 'lifted'}
|
||||||
|
class:tab-active={currentComment === toSnake(name)}>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if currentComment}
|
||||||
|
{#key currentComment}
|
||||||
|
<svelte:component
|
||||||
|
this={comments[`/src/lib/components/comments/${currentComment}.svelte`].default}
|
||||||
|
{post}
|
||||||
|
config={config?.[currentComment]} />
|
||||||
|
{/key}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
59
src/lib/components/post_pagination.svelte
Normal file
59
src/lib/components/post_pagination.svelte
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Image from '$lib/components/prose/img.svelte'
|
||||||
|
export let prev: Urara.Post = undefined
|
||||||
|
export let next: Urara.Post = undefined
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="flex flex-col md:flex-row flex-warp justify-evenly">
|
||||||
|
{#if prev}
|
||||||
|
<div
|
||||||
|
href={prev.path}
|
||||||
|
class:image-full={prev['photo']}
|
||||||
|
class:md:rounded-r-box={next && !next['photo']}
|
||||||
|
class="flex-1 card group rounded-none before:!rounded-none">
|
||||||
|
{#if prev['photo']}
|
||||||
|
<figure class="!block">
|
||||||
|
<Image
|
||||||
|
class="object-center h-full w-full absolute group-hover:scale-105 transition-transform duration-500 ease-in-out"
|
||||||
|
src={prev['photo']} />
|
||||||
|
</figure>
|
||||||
|
{/if}
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="i-heroicons-outline-chevron-left opacity-50 group-hover:opacity-100 mr-auto" />
|
||||||
|
<a
|
||||||
|
rel="prev"
|
||||||
|
href={prev.path}
|
||||||
|
class="card-title block text-left mb-0 mr-auto bg-[length:100%_0%] bg-[position:0_88%] underline decoration-3 decoration-transparent group-hover:decoration-primary hover:bg-[length:100%_100%] hover:text-primary-content bg-gradient-to-t from-primary to-primary bg-no-repeat transition-all ease-in-out duration-300">
|
||||||
|
{prev['title'] ?? prev.path.slice(1)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if next && !next['photo'] && !prev['photo']}
|
||||||
|
<div class="flex-0 divider mx-4 md:divider-horizontal md:mx-0 md:my-4" />
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#if next}
|
||||||
|
<div
|
||||||
|
href={next.path}
|
||||||
|
class:image-full={next['photo']}
|
||||||
|
class:md:rounded-l-box={prev && !prev['photo']}
|
||||||
|
class="flex-1 card group rounded-none before:!rounded-none">
|
||||||
|
{#if next['photo']}
|
||||||
|
<figure class="!block">
|
||||||
|
<Image
|
||||||
|
class="object-center h-full w-full absolute group-hover:scale-105 transition-transform duration-500 ease-in-out"
|
||||||
|
src={next['photo']} />
|
||||||
|
</figure>
|
||||||
|
{/if}
|
||||||
|
<div class="card-body">
|
||||||
|
<a
|
||||||
|
rel="next"
|
||||||
|
href={next.path}
|
||||||
|
class="card-title block text-right mb-0 ml-auto bg-[length:100%_0%] bg-[position:0_88%] underline decoration-3 decoration-transparent group-hover:decoration-primary hover:bg-[length:100%_100%] hover:text-primary-content bg-gradient-to-t from-primary to-primary bg-no-repeat transition-all ease-in-out duration-300">
|
||||||
|
{next['title'] ?? next.path.slice(1)}
|
||||||
|
</a>
|
||||||
|
<span class="i-heroicons-outline-chevron-right opacity-50 group-hover:opacity-100 ml-auto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</nav>
|
30
src/lib/components/post_reply.svelte
Normal file
30
src/lib/components/post_reply.svelte
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let className = ''
|
||||||
|
export { className as class }
|
||||||
|
export let inReplyTo: Urara.Post.Reply['inReplyTo']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2 rounded-box outline outline-neutral/10 p-4 {className}">
|
||||||
|
<span class="flex-none font-bold uppercase opacity-30">Reply to: </span>
|
||||||
|
{#if Array.isArray(inReplyTo)}
|
||||||
|
{#each inReplyTo as reply}
|
||||||
|
<a
|
||||||
|
href={reply}
|
||||||
|
rel="noopener external"
|
||||||
|
target="_blank"
|
||||||
|
class="flex-none flex rounded-badge bg-base-200 hover:bg-base-300 transition-all gap-2 px-4 u-in-reply-to">
|
||||||
|
<span class="i-heroicons-outline-reply my-auto !w-4 !h-4" />
|
||||||
|
{reply}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<a
|
||||||
|
href={inReplyTo}
|
||||||
|
rel="noopener external"
|
||||||
|
target="_blank"
|
||||||
|
class="ml-auto flex-none flex rounded-badge bg-base-200 hover:bg-base-300 transition-all gap-2 px-4 u-in-reply-to">
|
||||||
|
<span class="i-heroicons-outline-reply my-auto !w-4 !h-4" />
|
||||||
|
{inReplyTo}
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
</div>
|
35
src/lib/components/post_status.svelte
Normal file
35
src/lib/components/post_status.svelte
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { date } from '$lib/config/general'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
export let post: Urara.Post
|
||||||
|
export let index: boolean = false
|
||||||
|
const stringPublished = new Date(post.published ?? post.created).toLocaleString(date.locales, date.options)
|
||||||
|
const stringUpdated = new Date(post.updated ?? post.published ?? post.created).toLocaleString(date.locales, date.options)
|
||||||
|
const jsonPublished = new Date(post.published ?? post.created).toJSON()
|
||||||
|
const jsonUpdated = new Date(post.updated ?? post.published ?? post.created).toJSON()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex font-semibold gap-1.5">
|
||||||
|
<a
|
||||||
|
class:hidden={index}
|
||||||
|
rel="author"
|
||||||
|
class="opacity-75 hover:opacity-100 hover:text-primary duration-500 ease-in-out p-author h-card"
|
||||||
|
href={site.protocol + site.domain}>
|
||||||
|
{site.author.name}
|
||||||
|
</a>
|
||||||
|
<span class:hidden={index} class="opacity-50">/</span>
|
||||||
|
<a href={post.path} class="swap hover:swap-active u-url u-uid">
|
||||||
|
<time
|
||||||
|
class="swap-off font-semibold opacity-75 duration-500 ease-in-out mr-auto dt-published"
|
||||||
|
datetime={jsonPublished}
|
||||||
|
itemprop="datePublished">
|
||||||
|
{stringPublished}
|
||||||
|
</time>
|
||||||
|
<time
|
||||||
|
class="swap-on font-semibold text-primary duration-500 ease-in-out mr-auto dt-updated"
|
||||||
|
datetime={jsonUpdated}
|
||||||
|
itemprop="dateModified">
|
||||||
|
{stringUpdated}
|
||||||
|
</time>
|
||||||
|
</a>
|
||||||
|
</div>
|
11
src/lib/components/post_tags.svelte
Normal file
11
src/lib/components/post_tags.svelte
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let tags: Urara.Post['tags']
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{#each tags as tag}
|
||||||
|
<a href="/?tags={tag}" class="btn btn-sm btn-ghost normal-case mt-2 mr-2 p-category">
|
||||||
|
#{tag}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
67
src/lib/components/post_toc.svelte
Normal file
67
src/lib/components/post_toc.svelte
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
export const prerender = true
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
import Tree from '$lib/components/post_toc_tree.svelte'
|
||||||
|
export let toc: Urara.Post.Article.Toc[]
|
||||||
|
|
||||||
|
let intersecting: string[] = []
|
||||||
|
let intersectingArticle: boolean = true
|
||||||
|
let bordered: string[] = []
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (window.screen.availWidth >= 1280) {
|
||||||
|
const headingsObserver = new IntersectionObserver(
|
||||||
|
headings =>
|
||||||
|
headings.forEach(heading =>
|
||||||
|
heading.isIntersecting
|
||||||
|
? intersecting.push(heading.target.id)
|
||||||
|
: (intersecting = intersecting.filter(h => h !== heading.target.id))
|
||||||
|
),
|
||||||
|
{ rootMargin: '-64px 0px 0px 0px' }
|
||||||
|
)
|
||||||
|
const articleObserver = new IntersectionObserver(article => (intersectingArticle = article[0].isIntersecting))
|
||||||
|
Array.from(document.querySelectorAll('main h2, main h3, main h4, main h5, main h6')).forEach(element =>
|
||||||
|
headingsObserver.observe(element)
|
||||||
|
)
|
||||||
|
articleObserver.observe(document.getElementsByTagName('main')[0])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
// @ts-ignore: Cannot find name 'headingsObserver'
|
||||||
|
if (typeof headingsObserver !== 'undefined') headingsObserver.disconnect()
|
||||||
|
// @ts-ignore: Cannot find name 'articleObserver'
|
||||||
|
if (typeof headingsObserver !== 'undefined') articleObserver.disconnect()
|
||||||
|
})
|
||||||
|
|
||||||
|
$: if (intersecting.length > 0) bordered = intersecting
|
||||||
|
$: if (intersectingArticle === false) bordered = []
|
||||||
|
$: if (bordered)
|
||||||
|
toc.forEach(heading =>
|
||||||
|
bordered.includes(heading.slug)
|
||||||
|
? document.getElementById(`toc-link-${heading.slug}`)?.classList.add('!border-accent')
|
||||||
|
: document.getElementById(`toc-link-${heading.slug}`)?.classList.remove('!border-accent')
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<aside class="sticky top-16 py-8">
|
||||||
|
<nav
|
||||||
|
id="post-toc"
|
||||||
|
aria-label="TableOfContent"
|
||||||
|
dir="rtl"
|
||||||
|
class="max-h-[calc(100vh-12rem)] overflow-y-hidden hover:overflow-y-auto">
|
||||||
|
<Tree
|
||||||
|
toc={toc.reduce(
|
||||||
|
(acc, heading) => {
|
||||||
|
let parent = acc
|
||||||
|
while (parent.depth + 1 < heading.depth) parent = parent.children.at(-1)
|
||||||
|
parent.children = [...(parent.children ?? []), { ...heading, children: [] }]
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{ depth: toc[0].depth - 1, children: [] }
|
||||||
|
)} />
|
||||||
|
</nav>
|
||||||
|
</aside>
|
31
src/lib/components/post_toc_tree.svelte
Normal file
31
src/lib/components/post_toc_tree.svelte
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
<script lang="ts">
|
||||||
|
export let toc: Urara.Post.Article.Toc
|
||||||
|
const { title, slug, depth, children } = toc
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if title}
|
||||||
|
<span
|
||||||
|
dir="ltr"
|
||||||
|
on:click={() => document.getElementById(slug).scrollIntoView({ behavior: 'smooth' })}
|
||||||
|
id={`toc-link-${slug}`}
|
||||||
|
class="cursor-pointer border-l-4 border-transparent transition-all hover:border-primary hover:bg-base-content hover:bg-opacity-10 active:bg-primary active:text-primary-content active:font-bold pr-4 {depth <=
|
||||||
|
2
|
||||||
|
? 'py-3'
|
||||||
|
: 'py-2'}"
|
||||||
|
class:pl-4={depth <= 2}
|
||||||
|
class:pl-8={depth === 3}
|
||||||
|
class:pl-12={depth === 4}
|
||||||
|
class:pl-16={depth === 5}
|
||||||
|
class:pl-20={depth === 6}>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if children.length > 0}
|
||||||
|
<ul dir="ltr" id={`toc-list-${slug ?? 'root'}`}>
|
||||||
|
{#each children as child}
|
||||||
|
<li id={`toc-item-${child.slug}`} class="flex flex-col">
|
||||||
|
<svelte:self toc={child} />
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
{/if}
|
10
src/lib/components/prose/img.svelte
Normal file
10
src/lib/components/prose/img.svelte
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<script lang="ts">
|
||||||
|
let className = undefined
|
||||||
|
export { className as class }
|
||||||
|
export let src: string
|
||||||
|
export let alt: string = src
|
||||||
|
export let loading: 'eager' | 'lazy' = 'lazy'
|
||||||
|
export let decoding: 'async' | 'sync' | 'auto' = 'async'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img {src} {alt} class="u-photo {className ?? 'rounded-lg my-2'}" {loading} {decoding} />
|
5
src/lib/components/prose/table.svelte
Normal file
5
src/lib/components/prose/table.svelte
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<div class="overflow-x-auto mb-4">
|
||||||
|
<table class="table w-full">
|
||||||
|
<slot />
|
||||||
|
</table>
|
||||||
|
</div>
|
84
src/lib/config/friends.ts
Normal file
84
src/lib/config/friends.ts
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
export interface FriendOld {
|
||||||
|
// hCard+XFN
|
||||||
|
id: string // HTML id
|
||||||
|
rel?: string // XFN, contact / acquaintance / friend
|
||||||
|
link?: string // URL
|
||||||
|
html?: string // HTML
|
||||||
|
title?: string // 标题
|
||||||
|
descr?: string // 描述
|
||||||
|
avatar?: string // 头像
|
||||||
|
name?: string // backwards compatibility
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Friend = {
|
||||||
|
id: string // HTML id
|
||||||
|
rel?: string // XHTML Friends Network
|
||||||
|
link?: string // URL
|
||||||
|
html?: string // Custom HTML
|
||||||
|
|
||||||
|
title?: string // 标题
|
||||||
|
name?: string // 人名
|
||||||
|
avatar?: string // 头像
|
||||||
|
descr?: string // 描述
|
||||||
|
class?: {
|
||||||
|
avatar?: string // 头像类名
|
||||||
|
img?: string // 图片类名
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const friends: Friend[] = [
|
||||||
|
{
|
||||||
|
id: 'mantyke',
|
||||||
|
rel: 'friend',
|
||||||
|
name: '塔塔',
|
||||||
|
title: '小球飞鱼',
|
||||||
|
link: 'https://mantyke.icu/',
|
||||||
|
descr: '我们会一起遇见鲸鱼吗?',
|
||||||
|
avatar: 'https://mantyke.icu/images/logo.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'middleofnowhere',
|
||||||
|
rel: 'friend',
|
||||||
|
name: '陆博学',
|
||||||
|
title: 'Middle of No where',
|
||||||
|
link: 'https://notes.midofnowhere.link/',
|
||||||
|
descr: `Welcome to the middle of nowhere. That's right, absolute nowhere.`,
|
||||||
|
avatar: 'https://github.com/yue2/picbed/blob/main/cutepic.png?raw=true'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'chestnut',
|
||||||
|
rel: 'friend',
|
||||||
|
name: '栗',
|
||||||
|
title: '野生栗子🌰',
|
||||||
|
link: 'https://blog.chestnut.monster/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nekolas',
|
||||||
|
rel: 'friend',
|
||||||
|
name: 'Nic Tian',
|
||||||
|
title: `Nekolas's blog`,
|
||||||
|
link: 'https://blog.nekolas.cafe/',
|
||||||
|
descr: '欢迎加入锈栓抵抗军',
|
||||||
|
avatar: 'https://blogpic-1308403500.cos.ap-shanghai.myqcloud.com/avatar/nic-avatar-tomato.png'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'summerblue',
|
||||||
|
rel: 'friend',
|
||||||
|
name: '夏诤',
|
||||||
|
title: 'SummberBlue',
|
||||||
|
link: 'https://summerblue.space/',
|
||||||
|
descr: '早睡早起身体好'
|
||||||
|
}
|
||||||
|
// {
|
||||||
|
// id: 'test5',
|
||||||
|
// name: '藍',
|
||||||
|
// title: '藍藍藍藍藍',
|
||||||
|
// link: 'https://kwaa.dev',
|
||||||
|
// descr: 'without avatar'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'test6',
|
||||||
|
// title: 'Test6',
|
||||||
|
// name: 'test6'
|
||||||
|
// }
|
||||||
|
]
|
223
src/lib/config/general.ts
Normal file
223
src/lib/config/general.ts
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
import type { ThemeConfig, HeadConfig, HeaderConfig, FooterConfig, DateConfig, FeedConfig } from '$lib/types/general'
|
||||||
|
|
||||||
|
export const theme: ThemeConfig = [
|
||||||
|
{
|
||||||
|
name: 'lemonade',
|
||||||
|
text: 'Light'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'night',
|
||||||
|
text: 'Dark'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cupcake',
|
||||||
|
text: 'Cupcake'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'bumblebee',
|
||||||
|
text: 'Bumblebee'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'emerald',
|
||||||
|
text: 'Emerald'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'corporate',
|
||||||
|
text: 'Corporate'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'valentine',
|
||||||
|
text: 'Valentine'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'synthwave',
|
||||||
|
text: 'Synthwave'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'retro',
|
||||||
|
text: 'Retro'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cyberpunk',
|
||||||
|
text: 'Cyberpunk'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'halloween',
|
||||||
|
text: 'Halloween'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'garden',
|
||||||
|
text: 'Garden'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'forest',
|
||||||
|
text: 'Forest'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'aqua',
|
||||||
|
text: 'Aqua'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'lofi',
|
||||||
|
text: 'Lo-Fi'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'pastel',
|
||||||
|
text: 'Pastel'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'fantasy',
|
||||||
|
text: 'Fantasy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'wirefream',
|
||||||
|
text: 'Wireframe'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'black',
|
||||||
|
text: 'Black'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'luxury',
|
||||||
|
text: 'Luxury'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'dracula',
|
||||||
|
text: 'Dracula'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'cmyk',
|
||||||
|
text: 'CMYK'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'autumn',
|
||||||
|
text: 'Autumn'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'business',
|
||||||
|
text: 'Business'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'acid',
|
||||||
|
text: 'Acid'
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: 'lemonade',
|
||||||
|
// text: 'Lemonade'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'night',
|
||||||
|
// text: '🌃 Night'
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
name: 'coffee',
|
||||||
|
text: 'Coffee'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'winter',
|
||||||
|
text: 'Winter'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const head: HeadConfig = {
|
||||||
|
custom: ({ dev }) =>
|
||||||
|
dev
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
// IndieAuth
|
||||||
|
'<link rel="authorization_endpoint" href="https://indieauth.com/auth">',
|
||||||
|
'<link rel="token_endpoint" href="https://tokens.indieauth.com/token">',
|
||||||
|
'<link rel="me" href="https://github.com/sevichecc" />',
|
||||||
|
// Umami Analytics
|
||||||
|
'<script data-cfasync="false" defer data-do-not-track="true" data-website-id="2403ea30-74ff-4ffa-8264-556b9f3b2897" src="https://hexoverc.vercel.app/umami.js"></script>',
|
||||||
|
// splitbee
|
||||||
|
'<script async data-cfasync="false" src="https://cdn.splitbee.io/sb.js"></script>',
|
||||||
|
// Block Baiduspider
|
||||||
|
'<meta name="baiduspider" content="noindex">',
|
||||||
|
// Microsub
|
||||||
|
'<link rel="microsub" href="https://aperture.p3k.io/microsub/761">'
|
||||||
|
],
|
||||||
|
me: ['https://kongwoo.icu/@seviche']
|
||||||
|
}
|
||||||
|
|
||||||
|
export const header: HeaderConfig = {
|
||||||
|
search: {
|
||||||
|
provider: 'duckduckgo'
|
||||||
|
},
|
||||||
|
nav: [
|
||||||
|
{
|
||||||
|
text: 'Projects',
|
||||||
|
link: '/projects'
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// text: 'Notes',
|
||||||
|
// link: '/notes'
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
text: 'Friends',
|
||||||
|
link: '/friends'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'About',
|
||||||
|
link: '/about'
|
||||||
|
}
|
||||||
|
// ,
|
||||||
|
// {
|
||||||
|
// text: 'Notes',
|
||||||
|
// link: '/notes'
|
||||||
|
// }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const footer: FooterConfig = {
|
||||||
|
nav: [
|
||||||
|
{
|
||||||
|
text: 'Feed',
|
||||||
|
link: '/atom.xml'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Sitemap',
|
||||||
|
link: '/sitemap.xml'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Pravicy',
|
||||||
|
link: '/privacy'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
html: '<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a>'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const date: DateConfig = {
|
||||||
|
// toPublishedString: {
|
||||||
|
// locales: 'en-US',
|
||||||
|
// options: {
|
||||||
|
// year: 'numeric',
|
||||||
|
// weekday: 'short',
|
||||||
|
// month: 'short',
|
||||||
|
// day: 'numeric',
|
||||||
|
// timeZone: 'Asia/Shanghai'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// toUpdatedString: {
|
||||||
|
// locales: 'en-US',
|
||||||
|
// options: {
|
||||||
|
// year: 'numeric',
|
||||||
|
// weekday: 'short',
|
||||||
|
// month: 'short',
|
||||||
|
// day: 'numeric',
|
||||||
|
// timeZone: 'Asia/Shanghai'
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
locales: 'en-US',
|
||||||
|
options: {
|
||||||
|
year: 'numeric',
|
||||||
|
weekday: 'short',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
timeZone: 'Asia/Shanghai'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const feed: FeedConfig = {
|
||||||
|
hubs: ['https://pubsubhubbub.appspot.com', 'https://bridgy-fed.superfeedr.com']
|
||||||
|
}
|
39
src/lib/config/icon.ts
Normal file
39
src/lib/config/icon.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import type { Icon } from '$lib/types/icon'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
|
||||||
|
export const favicon: Icon = {
|
||||||
|
src: site.protocol + site.domain + '/favicon.png',
|
||||||
|
sizes: '48x48',
|
||||||
|
type: 'image/png'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const any: { [key: number]: Icon } = {
|
||||||
|
180: {
|
||||||
|
src: site.protocol + site.domain + '/assets/any@180.png',
|
||||||
|
sizes: '180x180',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
192: {
|
||||||
|
src: site.protocol + site.domain + '/assets/any@192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
512: {
|
||||||
|
src: site.protocol + site.domain + '/assets/any@512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const maskable: { [key: number]: Icon } = {
|
||||||
|
192: {
|
||||||
|
src: site.protocol + site.domain + '/assets/maskable@192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png'
|
||||||
|
},
|
||||||
|
512: {
|
||||||
|
src: site.protocol + site.domain + '/assets/maskable@512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png'
|
||||||
|
}
|
||||||
|
}
|
37
src/lib/config/post.ts
Normal file
37
src/lib/config/post.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import type { PostConfig } from '$lib/types/post'
|
||||||
|
|
||||||
|
export const post: PostConfig = {
|
||||||
|
bridgy: {
|
||||||
|
post: ['mastodon']
|
||||||
|
},
|
||||||
|
comment: {
|
||||||
|
use: ['Webmention', 'Giscus'],
|
||||||
|
style: 'boxed',
|
||||||
|
webmention: {
|
||||||
|
username: 'seviche.cc',
|
||||||
|
sortBy: 'created',
|
||||||
|
sortDir: 'down',
|
||||||
|
form: true,
|
||||||
|
commentParade: true
|
||||||
|
},
|
||||||
|
giscus: {
|
||||||
|
// src: 'https://giscus.kwaa.dev/client.js',
|
||||||
|
repo: 'Sevichecc/urara-giscus',
|
||||||
|
repoID: 'R_kgDOHSra4Q',
|
||||||
|
category: 'General',
|
||||||
|
categoryID: 'DIC_kwDOHSra4c4CO9ua',
|
||||||
|
theme: 'light',
|
||||||
|
lang: 'en'
|
||||||
|
}
|
||||||
|
// waline: {
|
||||||
|
// serverURL: 'https://waline-seviche.vercel.app/',
|
||||||
|
// lang: 'en',
|
||||||
|
// emoji: [
|
||||||
|
// 'https://cdn.jsdelivr.net/gh/norevi/waline-blobcatemojis@1.0/blobs',
|
||||||
|
// 'https://cdn.jsdelivr.net/gh/norevi/blob-emoji-for-waline@2.0/blobs-gif',
|
||||||
|
// 'ttps://cdn.jsdelivr.net/gh/norevi/blob-emoji-for-waline@2.0/blobs-png'
|
||||||
|
// ],
|
||||||
|
// requiredMeta: ['nick', 'mail']
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
70
src/lib/config/projects.ts
Normal file
70
src/lib/config/projects.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
export type Project = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
tags?: string[]
|
||||||
|
feature?: string
|
||||||
|
description?: string
|
||||||
|
img: string
|
||||||
|
link?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const projects: Project[] = [
|
||||||
|
// {
|
||||||
|
// id: 'haibian',
|
||||||
|
// name: '海边小站',
|
||||||
|
// tags: ['Vue 3', 'Vue Router', 'Vuex', 'Axios', 'JWT', 'easyMDE', 'BootStrap'],
|
||||||
|
// feature: 'Vue3 + TypeScript',
|
||||||
|
// description:
|
||||||
|
// '海边小站是一个面向成人英语学习的博文平台,为用户提供丰富的英文文摘、英语词汇教程、英语口语教程。网站有用户注册登录、博文发布编辑、用户资料更新等功能。',
|
||||||
|
// img: 'https://usc1.contabostorage.com/cc0b816231a841b1b0232d5ef0c6deb1:image/2022/06/d4d2489936e4f647c25df6982c6ef924.png',
|
||||||
|
// link: 'https://haibian.seviche.cc'
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
id: 'fokify',
|
||||||
|
name: 'Fokify ',
|
||||||
|
tags: ['MVC', 'Vanilla JS', 'ES6', 'Parcel', 'SCSS', 'HTML5'],
|
||||||
|
feature: 'JavaScript',
|
||||||
|
description:
|
||||||
|
'一个基于Web端的菜谱搜索平台,有菜谱搜索、上传、收藏等功能,并使用 LocalStorage 来存储用户数据,让用户在退出页面后仍能浏览所收藏的菜谱。',
|
||||||
|
img: 'https://usc1.contabostorage.com/cc0b816231a841b1b0232d5ef0c6deb1:image/2022/06/c3f41e397af1e480f57dd75e82334819.png',
|
||||||
|
link: 'https://forkify.seviche.cc'
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// id: 'Coachlist',
|
||||||
|
// name: 'Find a Coach',
|
||||||
|
// tags: ['Vue 3', 'Vite', 'Vuex', 'Vue Router', 'CSS Animation'],
|
||||||
|
// feature: 'Vue3',
|
||||||
|
// description:
|
||||||
|
// '一个基于Vue3组合式API的师生对接的在线平台,有注册登录、筛选老师、联系老师、注册成为老师等功能。使用Vue3的transition组件和CSS动画为页面提供流畅的过渡效果',
|
||||||
|
// img: '/assets/coach.png',
|
||||||
|
// link: 'https://coachlist.seviche.cc'
|
||||||
|
// }
|
||||||
|
// ,
|
||||||
|
{
|
||||||
|
id: 'bankist',
|
||||||
|
name: 'Bankist',
|
||||||
|
tags: ['Lazy-loading'],
|
||||||
|
feature: 'JavaScript',
|
||||||
|
description: '一个模拟的银行官网页面,用原生JS实现了懒加载、平滑滚动,以及幻灯片等组件',
|
||||||
|
img: 'https://usc1.contabostorage.com/cc0b816231a841b1b0232d5ef0c6deb1:image/2022/07/b5e1ff87c3b4ba4cf2f00d4124154472.png',
|
||||||
|
link: 'https://bankist.seviche.cc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'omnifood',
|
||||||
|
name: 'Omnifood',
|
||||||
|
tags: ['CSS5'],
|
||||||
|
feature: 'JavaScript',
|
||||||
|
description: '一个食品订阅APP官网,纯HTML + CSS + JavaScript实现',
|
||||||
|
img: 'https://usc1.contabostorage.com/cc0b816231a841b1b0232d5ef0c6deb1:image/2022/07/89424d0b448d105775c1d60346c57c59.png',
|
||||||
|
link: 'https://omnifood.seviche.cc'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'piggame',
|
||||||
|
name: 'Pig Game',
|
||||||
|
tags: ['Game'],
|
||||||
|
feature: 'JavaScript',
|
||||||
|
description: '一个投骰子的游戏,先累计到20的人输。',
|
||||||
|
img: 'https://usc1.contabostorage.com/cc0b816231a841b1b0232d5ef0c6deb1:image/2022/07/154ae3bc957a478679f1d9b7e0e0dce1.png',
|
||||||
|
link: 'https://pig-game-101.netlify.app/'
|
||||||
|
}
|
||||||
|
]
|
18
src/lib/config/site.ts
Normal file
18
src/lib/config/site.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import type { SiteConfig } from '$lib/types/site'
|
||||||
|
|
||||||
|
export const site: SiteConfig = {
|
||||||
|
protocol: 'https://',
|
||||||
|
domain: 'seviche.cc',
|
||||||
|
title: 'Seviche.cc',
|
||||||
|
subtitle: 'Tech / Code / Random Life',
|
||||||
|
lang: 'zh',
|
||||||
|
descr: 'Tech / Code / Random Life',
|
||||||
|
author: {
|
||||||
|
name: '酸橘汁腌鱼',
|
||||||
|
photo: '/assets/avatar.jpg',
|
||||||
|
status: '🖤',
|
||||||
|
github: 'sevichecc',
|
||||||
|
bio: ' Code / Tech <br> Living a Random Life '
|
||||||
|
},
|
||||||
|
themeColor: '#3D4451'
|
||||||
|
}
|
3
src/lib/stores/posts.ts
Normal file
3
src/lib/stores/posts.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
export const posts = writable({})
|
||||||
|
export const tags = writable({})
|
2
src/lib/stores/title.ts
Normal file
2
src/lib/stores/title.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
export const title = writable({})
|
44
src/lib/types/general.ts
Normal file
44
src/lib/types/general.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
export type ThemeConfig = {
|
||||||
|
text?: string
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
|
||||||
|
export type HeadConfig = {
|
||||||
|
/** @deprecated - use `me` instead */
|
||||||
|
relMe?: never
|
||||||
|
custom?: (params: { dev: boolean; post?: Urara.Post; page?: Urara.Page }) => string[]
|
||||||
|
me?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HeaderConfig = {
|
||||||
|
nav?: {
|
||||||
|
text: string
|
||||||
|
link?: string
|
||||||
|
children?: {
|
||||||
|
text: string
|
||||||
|
link: string
|
||||||
|
}[]
|
||||||
|
}[]
|
||||||
|
search?: {
|
||||||
|
provider: 'google' | 'duckduckgo'
|
||||||
|
colors?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FooterConfig = {
|
||||||
|
nav?: {
|
||||||
|
text: string
|
||||||
|
link: string
|
||||||
|
}[]
|
||||||
|
html?: string
|
||||||
|
since?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DateConfig = { locales: string; options: Intl.DateTimeFormatOptions }
|
||||||
|
|
||||||
|
export type FeedConfig = {
|
||||||
|
/** feed entry limit. */
|
||||||
|
limit?: number
|
||||||
|
/** WebSub (formerly PubSubHubbub) hubs. one per line */
|
||||||
|
hubs?: string[]
|
||||||
|
}
|
5
src/lib/types/icon.ts
Normal file
5
src/lib/types/icon.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type Icon = {
|
||||||
|
src: string
|
||||||
|
sizes?: string
|
||||||
|
type?: string
|
||||||
|
}
|
119
src/lib/types/post.ts
Normal file
119
src/lib/types/post.ts
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
import type { WalineEmojiInfo } from '@waline/client'
|
||||||
|
type WalineImageUploader = (image: File) => Promise<string>
|
||||||
|
|
||||||
|
type WalineHighlighter = (code: string, lang: string) => string
|
||||||
|
|
||||||
|
type WalineTexRenderer = (blockMode: boolean, tex: string) => string
|
||||||
|
|
||||||
|
export type PostConfig = {
|
||||||
|
bridgy?: {
|
||||||
|
[kind: string]: ('fed' | 'mastodon' | 'flickr' | 'github' | 'twitter')[]
|
||||||
|
}
|
||||||
|
comment?: CommentConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CommentConfig = {
|
||||||
|
use: string[]
|
||||||
|
/** tab style for multiple comments, preview at https://daisyui.com/components/tab */
|
||||||
|
style?: 'none' | 'bordered' | 'lifted' | 'boxed'
|
||||||
|
/** Webmention.io config, more at https://github.com/aaronpk/webmention.io#api */
|
||||||
|
webmention?: WebmentionConfig
|
||||||
|
/** Giscus config, more at https://giscus.app */
|
||||||
|
giscus?: GiscusConfig
|
||||||
|
/** Utterances config, more at https://utteranc.es */
|
||||||
|
utterances?: UtterancesConfig
|
||||||
|
/** Waline config, more at https://waline.js.org/en/reference/component.html#texrenderer */
|
||||||
|
waline?: WalineConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WebmentionConfig = {
|
||||||
|
/** username you got when you signed up webmention.io. */
|
||||||
|
username: string
|
||||||
|
/** number of results per page. */
|
||||||
|
perPage?: number
|
||||||
|
/** sorting mechanism to return the list of mentions. */
|
||||||
|
sortBy?: 'created' | 'updated' | 'published' | 'rsvp'
|
||||||
|
/** control the ordering. */
|
||||||
|
sortDir?: 'down' | 'up'
|
||||||
|
/** find links of a specific type. */
|
||||||
|
property?: ('in-reply-to' | 'like-of' | 'repost-of' | 'bookmark-of' | 'mention-of' | 'rsvp')[]
|
||||||
|
/** URL array of a webmention you'd like to block. */
|
||||||
|
blockList?: string[]
|
||||||
|
/** show the form for sending the webmention. */
|
||||||
|
form?: boolean
|
||||||
|
/** show `or comment anonymously` label text. */
|
||||||
|
commentParade?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GiscusConfig = {
|
||||||
|
/** self-hosted giscus url. */
|
||||||
|
src?: string
|
||||||
|
/** a public GitHub repository. this repo is where the discussions will be linked to. */
|
||||||
|
repo: string
|
||||||
|
/** a public GitHub repository. this repo is where the discussions will be linked to. */
|
||||||
|
repoID: string
|
||||||
|
/** fill in here if only search for discussions in this category. */
|
||||||
|
category?: string
|
||||||
|
/** choose the discussion category where new discussions will be created. */
|
||||||
|
categoryID: string
|
||||||
|
/** the reactions for the discussion's main post will be shown before the comments. */
|
||||||
|
reactionsEnabled?: boolean
|
||||||
|
/** discussion metadata will be sent periodically to the parent window (the embedding page). */
|
||||||
|
emitMetadata?: boolean
|
||||||
|
/** the comment input box will be placed above the comments, so that users can leave a comment without scrolling to the bottom of the discussion. */
|
||||||
|
inputPosition?: 'top' | 'bottom'
|
||||||
|
/** choose a theme that matches your website. */
|
||||||
|
theme?: string
|
||||||
|
/** choose the language giscus will be displayed in. */
|
||||||
|
lang?: string
|
||||||
|
/** loading of the comments will be deferred until the user scrolls near the comments container. */
|
||||||
|
loading?: 'lazy'
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UtterancesConfig = {
|
||||||
|
/** self-hosted utterances url. */
|
||||||
|
src?: string
|
||||||
|
/** choose the repository utterances will connect to. */
|
||||||
|
repo: string
|
||||||
|
/** choose the label that will be assigned to issues created by utterances. */
|
||||||
|
label?: string
|
||||||
|
/** choose an utterances theme that matches your blog. */
|
||||||
|
theme?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DisqusConfig = {
|
||||||
|
shortname: string
|
||||||
|
lang?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ref:https://waline.js.org/reference/component.html
|
||||||
|
export type WalineConfig = {
|
||||||
|
/** Waline server address url */
|
||||||
|
serverURL: string
|
||||||
|
/** Article path id*/
|
||||||
|
path?: string
|
||||||
|
/** Display language. */
|
||||||
|
lang?: string
|
||||||
|
/** Emoji settings, for details see https://waline.js.org/en/guide/client/emoji.html */
|
||||||
|
emoji?: (string | WalineEmojiInfo)[] | false
|
||||||
|
/** Darkmode support */
|
||||||
|
dark?: string | boolean
|
||||||
|
/** Reviewer attributes. Optional values: 'nick', 'mail', 'link' */
|
||||||
|
meta?: string[]
|
||||||
|
/** Set required fields*/
|
||||||
|
requiredMeta?: string[]
|
||||||
|
/** login mode status */
|
||||||
|
login?: string
|
||||||
|
/** Comment word s limit. */
|
||||||
|
wordLimit?: number | [number, number]
|
||||||
|
/**number of comments per page. */
|
||||||
|
pageSize?: number
|
||||||
|
/** Custom image upload method. */
|
||||||
|
imageUploader?: WalineImageUploader | false
|
||||||
|
/** Code highlighting, use hanabi by default */
|
||||||
|
highlighter?: WalineHighlighter | false
|
||||||
|
/** Customize \TeX rendering */
|
||||||
|
texRender?: WalineTexRenderer | false
|
||||||
|
/** Whether show copyright and version in footer. */
|
||||||
|
copyright?: boolean
|
||||||
|
}
|
51
src/lib/types/site.ts
Normal file
51
src/lib/types/site.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
export type SiteConfig = {
|
||||||
|
/** @deprecated - use `description` instead */
|
||||||
|
descr?: string
|
||||||
|
/** site protocol. for example: `https://` */
|
||||||
|
protocol: string
|
||||||
|
/** site domain. for example: `example.com` */
|
||||||
|
domain: string
|
||||||
|
/** site title. */
|
||||||
|
title: string
|
||||||
|
/** site subtitle. */
|
||||||
|
subtitle?: string
|
||||||
|
/** site lang. `<html lang={site.lang}>` */
|
||||||
|
lang?: string
|
||||||
|
/** site description. `<meta name="description" content={site.description}>` */
|
||||||
|
description?: string
|
||||||
|
/** site keywords. `<meta name="keywords" content={site.keywords}>` */
|
||||||
|
keywords?: string[]
|
||||||
|
author: {
|
||||||
|
name: string
|
||||||
|
photo?: string
|
||||||
|
status?: string
|
||||||
|
bio?: string
|
||||||
|
/** @deprecated - use `metadata` or `head.me` instead */
|
||||||
|
github?: never
|
||||||
|
/** @deprecated - use `metadata` or `head.me` instead */
|
||||||
|
twitter?: never
|
||||||
|
/** @deprecated - use `metadata` or `head.me` instead */
|
||||||
|
pgp?: never
|
||||||
|
metadata?: (
|
||||||
|
| {
|
||||||
|
text: string
|
||||||
|
icon?: string
|
||||||
|
link?: string
|
||||||
|
rel?: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
text?: string
|
||||||
|
icon: string
|
||||||
|
link?: string
|
||||||
|
rel?: string
|
||||||
|
}
|
||||||
|
)[]
|
||||||
|
}
|
||||||
|
/** for web app manifest only.
|
||||||
|
* ```
|
||||||
|
* "background_color": {site.themeColor},
|
||||||
|
* "theme_color": {site.themeColor}
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
themeColor?: string
|
||||||
|
}
|
8
src/lib/utils/case.ts
Normal file
8
src/lib/utils/case.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const toSnake = (str: string) =>
|
||||||
|
str.charAt(0).toLowerCase() +
|
||||||
|
str
|
||||||
|
.slice(1)
|
||||||
|
.replace(/([A-Z]+)/g, '_$1')
|
||||||
|
.toLowerCase()
|
||||||
|
|
||||||
|
export const toCamel = (str: string) => str.toLowerCase().replace(/([-_][a-z])/g, g => g.slice(-1).toUpperCase())
|
11
src/lib/utils/color.ts
Normal file
11
src/lib/utils/color.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export const hslToHex = (
|
||||||
|
h: number,
|
||||||
|
s: number,
|
||||||
|
l: number,
|
||||||
|
ll = (l /= 100),
|
||||||
|
a = (s * Math.min(ll, 1 - ll)) / 100,
|
||||||
|
f = (n: number, k = (n + h / 30) % 12) =>
|
||||||
|
Math.round(255 * (ll - a * Math.max(Math.min(k - 3, 9 - k, 1), -1)))
|
||||||
|
.toString(16)
|
||||||
|
.padStart(2, '0')
|
||||||
|
) => `#${f(0)}${f(8)}${f(4)}`
|
54
src/lib/utils/copyCode.ts
Normal file
54
src/lib/utils/copyCode.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/** copy code block to clipboard
|
||||||
|
* @todo better transition animate
|
||||||
|
* @todo remove dummy code
|
||||||
|
* @todo typecheck
|
||||||
|
*/
|
||||||
|
export const copyCode = () => {
|
||||||
|
const codeBlocks = document.querySelectorAll('pre')
|
||||||
|
const copyText = 'Copy'
|
||||||
|
const copiedText = 'Copied!'
|
||||||
|
|
||||||
|
//copy funciton
|
||||||
|
const copy = async (el: HTMLElement, btn: HTMLElement) => {
|
||||||
|
//select code
|
||||||
|
const range = document.createRange()
|
||||||
|
const end = el.childNodes.length
|
||||||
|
range.setStart(el, 2)
|
||||||
|
range.setEnd(el, end)
|
||||||
|
const selection = window.getSelection()
|
||||||
|
selection.removeAllRanges()
|
||||||
|
selection.addRange(range)
|
||||||
|
|
||||||
|
// copy to clipboard
|
||||||
|
document.execCommand('copy', false, null)
|
||||||
|
const clip = async () => navigator.clipboard.writeText(selection.toString())
|
||||||
|
|
||||||
|
if (!clip) return
|
||||||
|
btn.textContent = copiedText
|
||||||
|
setTimeout(() => {
|
||||||
|
btn.textContent = copyText
|
||||||
|
}, 1000)
|
||||||
|
selection.removeRange(range)
|
||||||
|
}
|
||||||
|
|
||||||
|
codeBlocks.forEach((block: HTMLElement) => {
|
||||||
|
// add copy button
|
||||||
|
const copyBtn: HTMLElement = document.createElement('button')
|
||||||
|
copyBtn.textContent = copyText
|
||||||
|
copyBtn.classList.add('btn', 'btn-secondary', 'btn-xs', 'absolute', 'right-2', 'top-3', 'hidden')
|
||||||
|
block.prepend(copyBtn)
|
||||||
|
|
||||||
|
block.addEventListener('mouseenter', () => {
|
||||||
|
copyBtn.classList.remove('hidden')
|
||||||
|
})
|
||||||
|
|
||||||
|
block.addEventListener('mouseleave', () => {
|
||||||
|
copyBtn.classList.add('hidden')
|
||||||
|
})
|
||||||
|
|
||||||
|
copyBtn.addEventListener('click', e => {
|
||||||
|
e.preventDefault()
|
||||||
|
copy(block, copyBtn)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
54
src/lib/utils/posts.ts
Normal file
54
src/lib/utils/posts.ts
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
interface GenPostsOptions {
|
||||||
|
/** import.meta.globEager<Urara.Post.Module> https://vitejs.dev/guide/features.html#glob-import */
|
||||||
|
modules?: { [path: string]: Urara.Post.Module }
|
||||||
|
/** set to true to output html */
|
||||||
|
postHtml?: boolean
|
||||||
|
/** limit a certain number of posts */
|
||||||
|
postLimit?: number
|
||||||
|
/** hide posts with 'unlisted' flag */
|
||||||
|
filterUnlisted?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type GenPostsFunction = (options?: GenPostsOptions) => Urara.Post[]
|
||||||
|
|
||||||
|
type GenTagsFunction = (posts: Urara.Post[]) => string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Posts List
|
||||||
|
* @param options - An optional configuration object
|
||||||
|
* @returns - posts list
|
||||||
|
*/
|
||||||
|
export const genPosts: GenPostsFunction = ({
|
||||||
|
modules = import.meta.globEager<Urara.Post.Module>('/src/routes/**/*.{md,svelte.md}'),
|
||||||
|
postHtml = false,
|
||||||
|
postLimit = undefined,
|
||||||
|
filterUnlisted = false
|
||||||
|
} = {}) =>
|
||||||
|
Object.entries(modules)
|
||||||
|
.map(([, module]) => ({
|
||||||
|
...module.metadata,
|
||||||
|
html:
|
||||||
|
postHtml || ['note', 'reply'].includes(module.metadata?.layout)
|
||||||
|
? module.default
|
||||||
|
.render()
|
||||||
|
.html // eslint-disable-next-line no-control-regex
|
||||||
|
.replace(/[\u0000-\u001F]/g, '')
|
||||||
|
.replace(/[\r\n]/g, '')
|
||||||
|
.match(/<main [^>]+>(.*?)<\/main>/gi)[0]
|
||||||
|
// .replace(/( class=")(.*?)(")/gi, '')
|
||||||
|
.replace(/( style=")(.*?)(")/gi, '')
|
||||||
|
.replace(/(<span>)(.*?)(<\/span>)/gi, '$2')
|
||||||
|
.replace(/(<main>)(.*?)(<\/main>)/gi, '$2')
|
||||||
|
: ''
|
||||||
|
}))
|
||||||
|
.filter((post, index) => (!filterUnlisted || !post.flags?.includes('unlisted')) && (!postLimit || index < postLimit))
|
||||||
|
.sort((a: Urara.Post, b: Urara.Post) => Date.parse(b.published ?? b.created) - Date.parse(a.published ?? a.created))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate Tags List
|
||||||
|
* @param posts - posts list
|
||||||
|
* @returns - tags list
|
||||||
|
*/
|
||||||
|
export const genTags: GenTagsFunction = posts => [
|
||||||
|
...new Set(posts.reduce((acc, posts) => (posts.tags ? [...acc, ...posts.tags] : acc), ['']).slice(1))
|
||||||
|
]
|
37
src/routes/__error.svelte
Normal file
37
src/routes/__error.svelte
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
export const load = ({ url: { pathname }, error: { message }, status }) => ({ props: { status, message, pathname } })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Head from '$lib/components/head.svelte'
|
||||||
|
import Footer from '$lib/components/footer.svelte'
|
||||||
|
export let status: string
|
||||||
|
export let message: string
|
||||||
|
export let pathname: string
|
||||||
|
console.error(status, pathname, message)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Head page={{ title: status ?? '404', path: pathname ?? '/404' }} />
|
||||||
|
|
||||||
|
<div class="flex flex-col flex-nowrap justify-center xl:flex-row xl:flex-wrap">
|
||||||
|
<div class="flex-none w-full max-w-screen-md mx-auto xl:mx-0">
|
||||||
|
<article
|
||||||
|
itemscope
|
||||||
|
itemtype="https://schema.org/BlogPosting"
|
||||||
|
class="card bg-base-100 rounded-none md:rounded-box shadow-xl md:mb-8 z-10">
|
||||||
|
<main itemprop="articleBody" class="card-body prose urara-prose">
|
||||||
|
<h1 class="opacity-20 text-6xl md:text-[12rem] -mt-2 mb-0">
|
||||||
|
{status ?? '404'}
|
||||||
|
</h1>
|
||||||
|
<h2 class="-mt-12 md:-mt-24">{message ?? 'Not found'}</h2>
|
||||||
|
<div class="card-actions">
|
||||||
|
<a href="/" class="btn btn-neutral no-underline shadow-xl hover:shadow-2xl mt-8">
|
||||||
|
<span class="i-heroicons-outline-home -ml-1 mr-2" />
|
||||||
|
Back to Home
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</article>
|
||||||
|
<Footer sticky={true} class="flex-1 md:flex-initial" />
|
||||||
|
</div>
|
||||||
|
</div>
|
48
src/routes/__layout.svelte
Normal file
48
src/routes/__layout.svelte
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<script lang="ts" context="module">
|
||||||
|
export const prerender = true
|
||||||
|
export const load = async ({ url, fetch }) => ({
|
||||||
|
props: {
|
||||||
|
path: url.pathname,
|
||||||
|
res: await (await fetch('/posts.json')).json()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { browser, dev } from '$app/env'
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
import { genTags } from '$lib/utils/posts'
|
||||||
|
import { posts, tags } from '$lib/stores/posts'
|
||||||
|
import { registerSW } from 'virtual:pwa-register'
|
||||||
|
import Head from '$lib/components/head_static.svelte'
|
||||||
|
import Header from '$lib/components/header.svelte'
|
||||||
|
import Search from '$lib/components/header_search.svelte'
|
||||||
|
import '../app.css'
|
||||||
|
export let res: Urara.Post[]
|
||||||
|
export let path: string
|
||||||
|
posts.set(res)
|
||||||
|
tags.set(genTags(res))
|
||||||
|
onMount(
|
||||||
|
() =>
|
||||||
|
!dev &&
|
||||||
|
browser &&
|
||||||
|
registerSW({
|
||||||
|
onRegistered: r => r && setInterval(async () => await r.update(), 198964),
|
||||||
|
onRegisterError: error => console.error(error)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Head />
|
||||||
|
<Search />
|
||||||
|
<Header {path} />
|
||||||
|
|
||||||
|
{#key path}
|
||||||
|
<div
|
||||||
|
class="bg-base-100 md:bg-base-200 min-h-screen pt-16 md:pb-8 lg:pb-16"
|
||||||
|
in:fly={{ y: 100, duration: 300, delay: 300 }}
|
||||||
|
out:fly={{ y: -100, duration: 300 }}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/key}
|
50
src/routes/atom.xml.ts
Normal file
50
src/routes/atom.xml.ts
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import { feed } from '$lib/config/general'
|
||||||
|
import { favicon } from '$lib/config/icon'
|
||||||
|
import { genPosts, genTags } from '$lib/utils/posts'
|
||||||
|
|
||||||
|
const render = async (posts = genPosts({ postHtml: true, postLimit: feed.limit, filterUnlisted: true })): Promise<string> =>
|
||||||
|
`<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
|
<id>${site.protocol + site.domain}/</id>
|
||||||
|
<title><![CDATA[${site.title}]]></title>${site.subtitle ? `\n <subtitle><![CDATA[${site.subtitle}]]></subtitle>` : ''}${
|
||||||
|
favicon ? `\n <icon>${favicon.src}</icon>` : ''
|
||||||
|
}
|
||||||
|
<link href="${site.protocol + site.domain}" />
|
||||||
|
<link href="${site.protocol + site.domain}/atom.xml" rel="self" type="application/atom+xml" />${
|
||||||
|
feed.hubs?.map(hub => `\n <link href="${hub}" rel="hub"/>`).join('') ?? ''
|
||||||
|
}
|
||||||
|
<updated>${new Date().toJSON()}</updated>
|
||||||
|
<author>
|
||||||
|
<name><![CDATA[${site.author.name}]]></name>
|
||||||
|
</author>${genTags(posts)
|
||||||
|
.map(tag => `\n <category term="${tag}" scheme="${site.protocol + site.domain}/?tags=${encodeURI(tag)}" />`)
|
||||||
|
.join('')}${posts
|
||||||
|
.map(
|
||||||
|
post => `\n <entry>
|
||||||
|
<title type="html"><![CDATA[${post.title}]]></title>
|
||||||
|
<link href="${site.protocol + site.domain + post.path}" />
|
||||||
|
<id>${site.protocol + site.domain + post.path}</id>
|
||||||
|
<published>${new Date(post.published ?? post.created).toJSON()}</published>
|
||||||
|
<updated>${new Date(post.updated ?? post.published ?? post.created).toJSON()}</updated>${
|
||||||
|
post.layout === 'article' && post.summary
|
||||||
|
? `\n <summary type="html"><![CDATA[${post.summary.toString()}]]></summary>`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
<content type="html">
|
||||||
|
<![CDATA[${post.html}]]>
|
||||||
|
</content>${post.tags
|
||||||
|
?.map(tag => `\n <category term="${tag}" scheme="${site.protocol + site.domain}/?tags=${encodeURI(tag)}" />`)
|
||||||
|
.join('')}
|
||||||
|
</entry>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
</feed>`
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async () => ({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/atom+xml; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: await render()
|
||||||
|
})
|
48
src/routes/feed.json.ts
Normal file
48
src/routes/feed.json.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import { feed } from '$lib/config/general'
|
||||||
|
import { favicon, any } from '$lib/config/icon'
|
||||||
|
import { genPosts } from '$lib/utils/posts'
|
||||||
|
|
||||||
|
const render = async (posts = genPosts({ postHtml: true, postLimit: feed.limit, filterUnlisted: true })) => ({
|
||||||
|
version: 'https://jsonfeed.org/version/1.1',
|
||||||
|
title: site.title,
|
||||||
|
home_page_url: site.protocol + site.domain,
|
||||||
|
feed_url: site.protocol + site.domain + '/feed.json',
|
||||||
|
description: site.description,
|
||||||
|
icon: any['512'].src ?? any['192'].src,
|
||||||
|
favicon: favicon?.src,
|
||||||
|
authors: [
|
||||||
|
{
|
||||||
|
name: site.author.name,
|
||||||
|
url: site.protocol + site.domain,
|
||||||
|
avatar: site.author.photo
|
||||||
|
}
|
||||||
|
],
|
||||||
|
language: site.lang ?? 'en',
|
||||||
|
hubs: feed.hubs?.map(hub => ({
|
||||||
|
type: 'WebSub',
|
||||||
|
url: hub
|
||||||
|
})),
|
||||||
|
items: posts.map(post => ({
|
||||||
|
id: post.path.slice(1),
|
||||||
|
url: site.protocol + site.domain + post.path,
|
||||||
|
title: post.title,
|
||||||
|
content_html: post.html,
|
||||||
|
summary: post['summary'],
|
||||||
|
image: post['photo'],
|
||||||
|
date_published: post.published ?? post.created,
|
||||||
|
date_modified: post.updated ?? post.published ?? post.created,
|
||||||
|
tags: post.tags,
|
||||||
|
_indieweb: {
|
||||||
|
type: post.layout ?? 'article'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async () => ({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/feed+json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(await render(), null, 2)
|
||||||
|
})
|
125
src/routes/index.svelte
Normal file
125
src/routes/index.svelte
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
import { page } from '$app/stores'
|
||||||
|
import { browser } from '$app/env'
|
||||||
|
import { posts as storedPosts, tags as storedTags } from '$lib/stores/posts'
|
||||||
|
import { title as storedTitle } from '$lib/stores/title'
|
||||||
|
import Head from '$lib/components/head.svelte'
|
||||||
|
import Footer from '$lib/components/footer.svelte'
|
||||||
|
import Post from '$lib/components/index_post.svelte'
|
||||||
|
import Profile from '$lib/components/index_profile.svelte'
|
||||||
|
|
||||||
|
let allPosts: Urara.Post[]
|
||||||
|
let allTags: string[]
|
||||||
|
let loaded: boolean
|
||||||
|
let [posts, tags, years] = [[], [], []]
|
||||||
|
|
||||||
|
storedTitle.set('')
|
||||||
|
|
||||||
|
$: storedPosts.subscribe(
|
||||||
|
storedPosts => (allPosts = (storedPosts as Urara.Post[]).filter(post => !post.flags?.includes('unlisted')))
|
||||||
|
)
|
||||||
|
|
||||||
|
$: storedTags.subscribe(storedTags => (allTags = storedTags as string[]))
|
||||||
|
|
||||||
|
$: if (posts.length > 1) years = [new Date(posts[0].published ?? posts[0].created).getFullYear()]
|
||||||
|
|
||||||
|
$: if (tags) {
|
||||||
|
posts = !tags ? allPosts : allPosts.filter(post => tags.every(tag => post.tags?.includes(tag)))
|
||||||
|
if (browser && window.location.pathname === '/')
|
||||||
|
window.history.replaceState({}, '', tags.length > 0 ? `?tags=${tags.toString()}` : `/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (browser) {
|
||||||
|
if ($page.url.searchParams.get('tags')) tags = $page.url.searchParams.get('tags').split(',')
|
||||||
|
loaded = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Head />
|
||||||
|
|
||||||
|
<div class="flex flex-col flex-nowrap justify-center xl:flex-row xl:flex-wrap h-feed">
|
||||||
|
<div
|
||||||
|
in:fly={{ x: 25, duration: 300, delay: 500 }}
|
||||||
|
out:fly={{ x: 25, duration: 300 }}
|
||||||
|
class="flex-1 w-full max-w-screen-md order-first mx-auto xl:mr-0 xl:ml-8 xl:max-w-md">
|
||||||
|
<Profile />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
in:fly={{ x: -25, duration: 300, delay: 500 }}
|
||||||
|
out:fly={{ x: -25, duration: 300 }}
|
||||||
|
class="flex-1 w-full max-w-screen-md xl:order-last mx-auto xl:ml-0 xl:mr-8 xl:max-w-md">
|
||||||
|
{#if allTags && Object.keys(allTags).length > 0}
|
||||||
|
<div
|
||||||
|
class="flex xl:flex-wrap gap-2 overflow-x-auto xl:overflow-x-hidden overflow-y-hidden max-h-24 my-auto xl:max-h-fit max-w-fit xl:max-w-full pl-8 md:px-0 xl:pl-8 xl:pt-8">
|
||||||
|
{#each allTags as tag}
|
||||||
|
<button
|
||||||
|
id={tag}
|
||||||
|
on:click={() => (tags.includes(tag) ? (tags = tags.filter(tagName => tagName != tag)) : (tags = [...tags, tag]))}
|
||||||
|
class:!btn-secondary={tags.includes(tag)}
|
||||||
|
class:shadow-lg={tags.includes(tag)}
|
||||||
|
class="btn btn-sm btn-ghost normal-case border-dotted border-base-content/20 border-2 mt-4 mb-8 xl:m-0">
|
||||||
|
#{tag}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="flex-none w-full max-w-screen-md mx-auto xl:mx-0">
|
||||||
|
{#key posts}
|
||||||
|
<!-- {:else} is not used because there is a problem with the transition -->
|
||||||
|
{#if loaded && posts.length === 0}
|
||||||
|
<div
|
||||||
|
in:fly={{ x: 100, duration: 300, delay: 500 }}
|
||||||
|
out:fly={{ x: -100, duration: 300 }}
|
||||||
|
class="bg-base-300 text-base-content shadow-inner text-center md:rounded-box p-10 -mb-2 md:mb-0 relative z-10">
|
||||||
|
<div class="prose items-center">
|
||||||
|
<h2>
|
||||||
|
Not found: [{#each tags as tag, i}
|
||||||
|
'{tag}'{#if i + 1 < tags.length},{/if}
|
||||||
|
{/each}]
|
||||||
|
</h2>
|
||||||
|
<button on:click={() => (tags = [])} class="btn btn-secondary">
|
||||||
|
<span class="i-heroicons-outline-trash mr-2" />
|
||||||
|
tags = []
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<main
|
||||||
|
class="flex flex-col relative bg-base-100 md:bg-transparent md:gap-8 z-10"
|
||||||
|
itemprop="mainEntityOfPage"
|
||||||
|
itemscope
|
||||||
|
itemtype="https://schema.org/Blog">
|
||||||
|
{#each posts as post, index}
|
||||||
|
{@const year = new Date(post.published ?? post.created).getFullYear()}
|
||||||
|
{#if !years.includes(year)}
|
||||||
|
<div
|
||||||
|
in:fly={{ x: index % 2 ? 100 : -100, duration: 300, delay: 500 }}
|
||||||
|
out:fly={{ x: index % 2 ? -100 : 100, duration: 300 }}
|
||||||
|
class="divider my-4 md:my-0">
|
||||||
|
{years.push(year) && year}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
in:fly={{ x: index % 2 ? 100 : -100, duration: 300, delay: 500 }}
|
||||||
|
out:fly={{ x: index % 2 ? -100 : 100, duration: 300 }}
|
||||||
|
class="rounded-box transition-all duration-500 ease-in-out hover:z-30 hover:shadow-lg md:shadow-xl md:hover:shadow-2xl md:hover:-translate-y-0.5">
|
||||||
|
<Post {post} loading={index < 5 ? 'eager' : 'lazy'} decoding={index < 5 ? 'auto' : 'async'} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</main>
|
||||||
|
<div
|
||||||
|
class:hidden={!loaded}
|
||||||
|
class="sticky bottom-0 md:static md:mt-8"
|
||||||
|
in:fly={{ x: posts.length + (1 % 2) ? 100 : -100, duration: 300, delay: 500 }}
|
||||||
|
out:fly={{ x: posts.length + (1 % 2) ? -100 : 100, duration: 300 }}>
|
||||||
|
<div class="divider mt-0 mb-8 hidden lg:flex" />
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
</div>
|
32
src/routes/manifest.webmanifest.ts
Normal file
32
src/routes/manifest.webmanifest.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import { any, maskable } from '$lib/config/icon'
|
||||||
|
|
||||||
|
export const GET: RequestHandler = () => ({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/manifest+json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(
|
||||||
|
{
|
||||||
|
name: site.title,
|
||||||
|
short_name: site.title,
|
||||||
|
lang: site.lang,
|
||||||
|
description: site.description,
|
||||||
|
id: site.protocol + site.domain + '/',
|
||||||
|
start_url: '/',
|
||||||
|
scope: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
orientation: 'portrait',
|
||||||
|
background_color: site.themeColor,
|
||||||
|
theme_color: site.themeColor,
|
||||||
|
icons: [
|
||||||
|
...Object.values(any)
|
||||||
|
.filter(icon => icon.sizes !== '180x180')
|
||||||
|
.map(icon => ({ ...icon, purpose: 'any' })),
|
||||||
|
...Object.values(maskable).map(icon => ({ ...icon, purpose: 'maskable' }))
|
||||||
|
]
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)
|
||||||
|
})
|
9
src/routes/posts.json.ts
Normal file
9
src/routes/posts.json.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { genPosts } from '$lib/utils/posts'
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async () => ({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(genPosts(), null, 2)
|
||||||
|
})
|
27
src/routes/sitemap.xml.ts
Normal file
27
src/routes/sitemap.xml.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { site } from '$lib/config/site'
|
||||||
|
import { genPosts } from '$lib/utils/posts'
|
||||||
|
|
||||||
|
const render = async (): Promise<string> => `<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>${site.protocol + site.domain}</loc>
|
||||||
|
</url>
|
||||||
|
${genPosts()
|
||||||
|
.map(
|
||||||
|
post => `
|
||||||
|
<url>
|
||||||
|
<loc>${site.protocol + site.domain + post.path}</loc>
|
||||||
|
<lastmod>${new Date(post.updated ?? post.published ?? post.created).toISOString()}</lastmod>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>`
|
||||||
|
)
|
||||||
|
.join('')}
|
||||||
|
</urlset>`
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async () => ({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/xml; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: await render()
|
||||||
|
})
|
9
src/routes/tags.json.ts
Normal file
9
src/routes/tags.json.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
|
import { genPosts, genTags } from '$lib/utils/posts'
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async () => ({
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json; charset=utf-8'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(genTags(genPosts()), null, 2)
|
||||||
|
})
|
29
svelte.config.ts
Normal file
29
svelte.config.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// sveltekit config type
|
||||||
|
import type { Config } from '@sveltejs/kit'
|
||||||
|
// svelte preprocess
|
||||||
|
import preprocess from 'svelte-preprocess'
|
||||||
|
import adapterAuto from '@sveltejs/adapter-auto'
|
||||||
|
import adapterNode from '@sveltejs/adapter-node'
|
||||||
|
import adapterStatic from '@sveltejs/adapter-static'
|
||||||
|
import { mdsvex } from 'mdsvex'
|
||||||
|
import mdsvexConfig from './mdsvex.config.js'
|
||||||
|
|
||||||
|
const defineConfig = (config: Config) => config
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
extensions: ['.svelte', ...(mdsvexConfig.extensions as string[])],
|
||||||
|
preprocess: [mdsvex(mdsvexConfig), preprocess()],
|
||||||
|
kit: {
|
||||||
|
adapter: Object.keys(process.env).some(key => ['VERCEL', 'CF_PAGES', 'NETLIFY'].includes(key))
|
||||||
|
? adapterAuto()
|
||||||
|
: process.env.ADAPTER === 'node'
|
||||||
|
? adapterNode({ out: 'build' })
|
||||||
|
: adapterStatic({
|
||||||
|
pages: 'build',
|
||||||
|
assets: 'build',
|
||||||
|
fallback: undefined
|
||||||
|
}),
|
||||||
|
csp: { mode: 'auto' },
|
||||||
|
prerender: { default: true }
|
||||||
|
}
|
||||||
|
})
|
64
tailwind.config.ts
Normal file
64
tailwind.config.ts
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
// tailwind config type
|
||||||
|
import type { TailwindConfig } from 'tailwindcss/tailwind-config'
|
||||||
|
// @ts-ignore TS2305: Module 'tailwindcss/plugin' has no exported member 'TailwindPluginWithoutOptions'.
|
||||||
|
import type { TailwindPluginWithoutOptions } from 'tailwindcss/plugin'
|
||||||
|
// tailwind plugins
|
||||||
|
import typography from '@tailwindcss/typography'
|
||||||
|
import daisyui from 'daisyui'
|
||||||
|
|
||||||
|
interface Config extends TailwindConfig {
|
||||||
|
daisyui?: {
|
||||||
|
styled?: boolean
|
||||||
|
themes?: boolean | string[]
|
||||||
|
base?: boolean
|
||||||
|
utils?: boolean
|
||||||
|
logs?: boolean
|
||||||
|
rtl?: boolean
|
||||||
|
darkTheme?: string
|
||||||
|
prefix?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defineConfig = (config: Config) => config
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
content: ['./src/**/*.{html,md,js,svelte,ts}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [typography as TailwindPluginWithoutOptions, daisyui as TailwindPluginWithoutOptions],
|
||||||
|
daisyui: {
|
||||||
|
themes: [
|
||||||
|
'light',
|
||||||
|
'dark',
|
||||||
|
'cupcake',
|
||||||
|
'bumblebee',
|
||||||
|
'emerald',
|
||||||
|
'corporate',
|
||||||
|
'synthwave',
|
||||||
|
'retro',
|
||||||
|
'cyberpunk',
|
||||||
|
'valentine',
|
||||||
|
'halloween',
|
||||||
|
'garden',
|
||||||
|
'forest',
|
||||||
|
'aqua',
|
||||||
|
'lofi',
|
||||||
|
'pastel',
|
||||||
|
'fantasy',
|
||||||
|
'wireframe',
|
||||||
|
'black',
|
||||||
|
'luxury',
|
||||||
|
'dracula',
|
||||||
|
'cmyk',
|
||||||
|
'autumn',
|
||||||
|
'business',
|
||||||
|
'acid',
|
||||||
|
'lemonade',
|
||||||
|
'night',
|
||||||
|
'coffee',
|
||||||
|
'winter'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"extends": "./.svelte-kit/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
"checkJs": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": false,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": ["vite/client", "vite-plugin-pwa/client"]
|
||||||
|
},
|
||||||
|
"include": ["src/app.d.ts"]
|
||||||
|
}
|
11
tsconfig.node.json
Normal file
11
tsconfig.node.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"declaration": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["urara.ts", "svelte.config.ts", "mdsvex.config.ts"]
|
||||||
|
}
|
140
urara.ts
Normal file
140
urara.ts
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/**
|
||||||
|
* Urara.TS
|
||||||
|
* Version: Any
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { promises as fs } from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import chokidar from 'chokidar'
|
||||||
|
import chalk from 'chalk'
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
extensions: ['svelte', 'md', 'js', 'ts'],
|
||||||
|
catch: ['ENOENT', 'EEXIST']
|
||||||
|
}
|
||||||
|
|
||||||
|
const check = (ext: string) => (config.extensions.includes(ext) ? 'src/routes' : 'static')
|
||||||
|
|
||||||
|
const log = (color: string, msg: string, dest?: string | Error) =>
|
||||||
|
console.log(
|
||||||
|
chalk.dim(new Date().toLocaleTimeString() + ' ') +
|
||||||
|
chalk.magentaBright.bold('[urara] ') +
|
||||||
|
chalk[color](msg + ' ') +
|
||||||
|
chalk.dim(dest ?? '')
|
||||||
|
)
|
||||||
|
|
||||||
|
const error = (err: { code: string; message: unknown }) => {
|
||||||
|
if (config.catch.includes(err.code)) {
|
||||||
|
console.log(
|
||||||
|
chalk.dim(new Date().toLocaleTimeString() + ' ') +
|
||||||
|
chalk.redBright.bold('[urara] ') +
|
||||||
|
chalk.red('error ') +
|
||||||
|
chalk.dim(err.message)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cpFile = (src: string, { stat = 'copy', dest = path.join(check(path.parse(src).ext.slice(1)), src.slice(6)) } = {}) =>
|
||||||
|
fs
|
||||||
|
.copyFile(src, dest)
|
||||||
|
.then(() => log('green', `${stat} file`, dest))
|
||||||
|
.catch(error)
|
||||||
|
|
||||||
|
const rmFile = (src: string, { dest = path.join(check(path.parse(src).ext.slice(1)), src.slice(6)) } = {}) =>
|
||||||
|
fs
|
||||||
|
.rm(dest)
|
||||||
|
.then(() => log('yellow', 'remove file', dest))
|
||||||
|
.catch(error)
|
||||||
|
|
||||||
|
const cpDir = (src: string) =>
|
||||||
|
fs.readdir(src, { withFileTypes: true }).then(files =>
|
||||||
|
files.forEach(file => {
|
||||||
|
const dest = path.join(src, file.name)
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
mkDir(dest)
|
||||||
|
cpDir(dest)
|
||||||
|
} else if (file.name.startsWith('.')) {
|
||||||
|
log('cyan', 'ignore file', dest)
|
||||||
|
} else {
|
||||||
|
cpFile(dest)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const mkDir = (src: string, { dest = [path.join('src/routes', src.slice(6)), path.join('static', src.slice(6))] } = {}) => {
|
||||||
|
dest.forEach(path =>
|
||||||
|
fs
|
||||||
|
.mkdir(path)
|
||||||
|
.then(() => log('green', 'make dir', path))
|
||||||
|
.catch(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rmDir = (src: string, { dest = [path.join('src/routes', src.slice(6)), path.join('static', src.slice(6))] } = {}) => {
|
||||||
|
dest.forEach(path =>
|
||||||
|
fs
|
||||||
|
.rm(path, { force: true, recursive: true })
|
||||||
|
.then(() => log('yellow', 'remove dir', path))
|
||||||
|
.catch(error)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanDir = (src: string) =>
|
||||||
|
fs.readdir(src, { withFileTypes: true }).then(files => {
|
||||||
|
files.forEach(file => {
|
||||||
|
const dest = path.join(src, file.name)
|
||||||
|
file.isDirectory() ? rmDir(dest) : file.name.startsWith('.') ? log('cyan', 'ignore file', dest) : rmFile(dest)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const build = () => {
|
||||||
|
mkDir('static', { dest: ['static'] })
|
||||||
|
cpDir('urara')
|
||||||
|
}
|
||||||
|
|
||||||
|
const clean = () => {
|
||||||
|
cleanDir('urara')
|
||||||
|
rmDir('static', { dest: ['static'] })
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (process.argv[2]) {
|
||||||
|
case 'watch':
|
||||||
|
{
|
||||||
|
let watcher = chokidar.watch('urara', {
|
||||||
|
ignored: (file: string) => path.basename(file).startsWith('.')
|
||||||
|
})
|
||||||
|
watcher
|
||||||
|
.on('add', file => cpFile(file))
|
||||||
|
.on('change', file => cpFile(file, { stat: 'update' }))
|
||||||
|
.on('unlink', file => rmFile(file))
|
||||||
|
.on('addDir', dir => mkDir(dir))
|
||||||
|
.on('unlinkDir', dir => rmDir(dir))
|
||||||
|
.on('error', error => log('red', 'error', error))
|
||||||
|
.on('ready', () => log('cyan', 'copy complete. ready for changes'))
|
||||||
|
process
|
||||||
|
.on('SIGINT', () => {
|
||||||
|
log('red', 'sigint')
|
||||||
|
clean()
|
||||||
|
watcher?.close()
|
||||||
|
})
|
||||||
|
.on('SIGTERM', () => {
|
||||||
|
log('red', 'sigterm')
|
||||||
|
watcher?.close()
|
||||||
|
})
|
||||||
|
.on('exit', () => {
|
||||||
|
log('red', 'exit')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'build':
|
||||||
|
build()
|
||||||
|
break
|
||||||
|
case 'clean':
|
||||||
|
clean()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
log('red', 'error', 'invalid arguments')
|
||||||
|
break
|
||||||
|
}
|
BIN
urara/2021-11-08-ux1/1.png
Normal file
BIN
urara/2021-11-08-ux1/1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
BIN
urara/2021-11-08-ux1/2.png
Normal file
BIN
urara/2021-11-08-ux1/2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
BIN
urara/2021-11-08-ux1/3.png
Normal file
BIN
urara/2021-11-08-ux1/3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
BIN
urara/2021-11-08-ux1/4.png
Normal file
BIN
urara/2021-11-08-ux1/4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
132
urara/2021-11-08-ux1/index.md
Normal file
132
urara/2021-11-08-ux1/index.md
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
---
|
||||||
|
title: Overview of User Experience Design
|
||||||
|
summary: Introduction to User Experience Design|Week1
|
||||||
|
date: 2021-11-08T10:54:40+08:00
|
||||||
|
# categories:
|
||||||
|
# - UX学习笔记
|
||||||
|
# - Introduction to User Experience Design
|
||||||
|
tags:
|
||||||
|
- UX
|
||||||
|
---
|
||||||
|
|
||||||
|
## 课程简介
|
||||||
|
|
||||||
|
这是一门在 [Coursera](https://www.coursera.org/learn/user-experience-design) 上的 UX 课程,共有五个星期的内容,讲了 UX 的基本概念和实际操作流程,主要侧重用户研究方面,也讲了很多和设计心理学有关的内容,很适合 UX 入门。
|
||||||
|
|
||||||
|
我将按照课程内容的划分,分别发布五个 weeks 的笔记,这是第一篇。
|
||||||
|
|
||||||
|
原笔记是在 Notion 上写的,我喜欢用 Notion 里面的 toggle 效果,用“提问”-“答案”的形式来启发思考,但是博客这里用折叠文本比较麻烦,所以直接列出来了
|
||||||
|
|
||||||
|
## Week 1 笔记
|
||||||
|
|
||||||
|
### 什么是 ux?
|
||||||
|
|
||||||
|
> User Experience design is design that is user centered。The goal is to design artifacts that allow the users to meet their needs in the most effective efficient and satisfying manner。
|
||||||
|
>
|
||||||
|
> **以用户为中心的设计。目标是轻松高效地满足用户的需求。**
|
||||||
|
|
||||||
|
### 用户体验设计的核心概念
|
||||||
|
|
||||||
|
> “用户使用界面来完成任务”
|
||||||
|
|
||||||
|
通过理解用户以及他们要完成的任务,以便设计出最好的界面 (interface)
|
||||||
|
|
||||||
|
### 什么是用户
|
||||||
|
|
||||||
|
这里的“用户”是指使用一些技术来达到目的的个体
|
||||||
|
|
||||||
|
### 什么是界面
|
||||||
|
|
||||||
|
有输入、输出、系统,于此同时,每个输入都会导致一个期望的输出:
|
||||||
|

|
||||||
|
个人使用界面的能力与个人特征、群体、社会有关
|
||||||
|

|
||||||
|
|
||||||
|
### 用户体验设计的目标
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**设计“能用”并且“好用”的界面**
|
||||||
|
|
||||||
|
- “**能用**”的意思是:它可以让用户完成一个任务。在图中,这个系统 (S) 就产生了期望的成果 (Output)
|
||||||
|
- “**好用**”的意思是:用户可以有效高效甚至满意地达成这项任务。在我们的图表中,这需要轻松快速地输入 (Input) 并且输出 (Output) 的结果有效的完成了任务
|
||||||
|
|
||||||
|
### 界面设计环 (四个步骤)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Step1:Requirements Gathering 收集需求
|
||||||
|
|
||||||
|
- Understanding the user and what her goals are,What are the current practices
|
||||||
|
- This step can also be thought of as understanding the**“problem space”**- what is hindering the completion of the task how can the task or process be improved
|
||||||
|
- A whole host of techniques are presented that allow the designer to collect data about the user,her goals and current practices
|
||||||
|
|
||||||
|
#### Step 2:Design Alternatives 设计方案
|
||||||
|
|
||||||
|
- Once you understand the users,their goals,and their current practices (e.i., the problem space) you are able to take this data and **develop various design options** that will improve the user experience
|
||||||
|
|
||||||
|
#### Step 3:Prototyping 原型
|
||||||
|
|
||||||
|
- Techniques for modeling the novel designs before a final version is produced
|
||||||
|
|
||||||
|
#### Step 4:Evaluation 评估测试
|
||||||
|
|
||||||
|
- A set of techniques for ascertaining that your design meets the needs of the user
|
||||||
|
|
||||||
|
### 什么样的界面是“实用”的?
|
||||||
|
|
||||||
|
“实用性”指的是有效、高效并且让用户觉得满意的。
|
||||||
|
|
||||||
|
> 如果用户能够理解怎样的输入 (Input) 会带来所需的输出 (Output) 那么这个界面就是实用的
|
||||||
|
|
||||||
|
### 好设计的三个特征
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### 1 - Affordance (示能)
|
||||||
|
|
||||||
|
> 指一个物理对象与人之间的关系
|
||||||
|
>
|
||||||
|
> (无论是动物还是人类,甚至机器和机器人,他们之间发生的任何交互作用)
|
||||||
|
|
||||||
|
是指可以感知到的事物的实际属性,主要是那些决定事物如何被使用的基本属性 (看看你的智能手机,我们会看到很多种功能,例如按键,可以感知到可以被按。)
|
||||||
|
|
||||||
|
#### 2 - Signifiers (意符)
|
||||||
|
|
||||||
|
> 能告诉人们正确操作方式的任何可感知的标记或声音。
|
||||||
|
|
||||||
|
示能决定可能进行哪些操作,意符则点名操作的位置。
|
||||||
|
|
||||||
|
#### 3 - Feedback (反馈)
|
||||||
|
|
||||||
|
> 一些让你知道系统正在处理你的要求的方式
|
||||||
|
|
||||||
|
反馈需要将用户信息发送回来其中包括哪些系统输入的信息已发生它跟我们沟通操作结果
|
||||||
|
|
||||||
|
- 输入:示能、意符
|
||||||
|
- 输出:示能
|
||||||
|
|
||||||
|
### 如何与“用户”接洽
|
||||||
|
|
||||||
|
注意礼貌、着装
|
||||||
|
|
||||||
|
#### 三个步骤
|
||||||
|
|
||||||
|
**1 - 介绍**
|
||||||
|
|
||||||
|
- 简要概述目标
|
||||||
|
- 坦率:让用户知道该会话的目标是什么。询问他们坦率的意见。
|
||||||
|
- 保密:说明交互内容是保密的。
|
||||||
|
- 自愿:在介绍期间,向用户解释他们的参与完全是自愿的。他们可以随时停止参与。如果他们希望停止参与,这不会消极地影响他们与你的公司或机构的关系。这一部分是很重要的,它能让用户放心并能够不受约束,因为你可能代表一个在社区中具有较高知名度的实体
|
||||||
|
- 开放:让他们知道,没有正确或错误的答案,他们应该简单直接地向你提供反馈和意见
|
||||||
|
|
||||||
|
**2 - 交流**
|
||||||
|
|
||||||
|
- 中立态度、语气轻松专业。
|
||||||
|
- 如果形式允许,鼓励详细阐述意外或有趣的信息
|
||||||
|
- 保持对交流过程的控制,尽量覆盖所有话题
|
||||||
|
|
||||||
|
**3 - 结束**
|
||||||
|
|
||||||
|
- 在会话结束时,提醒他们有关交互的目标和您打算如何处理他们提供的数据。
|
||||||
|
- 询问他们是否还有需要补充的内容。
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue