revert resource

This commit is contained in:
Sevichecc 2022-08-14 14:10:39 +08:00
parent a060f049cf
commit a61cfb5b1f
250 changed files with 19541 additions and 0 deletions

20
.eslintrc.json Normal file
View 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
View file

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

35
.github/CONTRIBUTING.md vendored Normal file
View 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
View file

@ -0,0 +1 @@
custom: ['https://giveth.io/project/urara', 'https://donate.lol/eth/0xaBdB3f715198A4d7e6591b6ebBE8Ccf235e5D752']

14
.gitignore vendored Normal file
View 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
View file

@ -0,0 +1 @@
engine-strict=true

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
v18.4.0

7
.prettierignore Normal file
View file

@ -0,0 +1,7 @@
.svelte-kit/**
static/**
build/**
node_modules/**
pnpm-lock.yaml
.netlify/**
.vercel_build_output/**

13
.prettierrc.json Normal file
View 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
View file

@ -0,0 +1,3 @@
{
"recommendations": ["svelte.svelte-vscode", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
}

10
.vscode/settings.json vendored Normal file
View 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"
}
}

30
README.md Normal file
View file

@ -0,0 +1,30 @@
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
![](https://usc1.contabostorage.com/cc0b816231a841b1b0232d5ef0c6deb1:image/2022/07/637fdb5526081256980d8876bae46c23.png)
## Deploy
- [Netlify]
## 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/)

7
deploy.sh Normal file
View file

@ -0,0 +1,7 @@
#!/bin/bash
echo "1. build locally"
netlify build
echo "2.deploy test"
netlify deploy
echo "3.deploy"
netlify deploy --prod

110
mdsvex.config.ts Normal file
View file

@ -0,0 +1,110 @@
// 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]
const { 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', ''))
// Generate ToC
if (data.fm.toc !== false) {
const [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
}
// 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: {
_: './src/lib/components/post_layout.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'
}
]
]
})

59
netlify.toml Normal file
View file

@ -0,0 +1,59 @@
[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"
[[headers]]
for = "/manifest.webmanifest"
[headers.values]
Content-Type = "application/manifest+json"
[[headers]]
for = "/assets/*"
[headers.values]
cache-control = '''
max-age=31536000,
immutable
'''
[[headers]]
for = "/*"
[headers.values]
Access-Control-Allow-Origin = "https://seviche.cc"
X-Frame-Options = "DENY"
X-Content-Type-Options = "nosniff"
X-XSS-Protection = "1; mode=block"
Content-Security-Policy = "style-src 'self' 'unsafe-inline' https://cdn.commento.io/css/commento.css http://fonts.cdnfonts.com/css/lato ; script-src 'self' 'unsafe-inline' https://*.seviche.cc https://giscus.app https://hexoverc.vercel.app/umami.js https://cdn.splitbee.io/sb.js https://cdn.commento.io/js/commento.js"
Referrer-Policy = "strict-origin-when-cross-origin"
Permissions-Policy = "usb=()"
[[redirects]]
from = "/.well-known/host-meta"
to = "https://fed.brid.gy/.well-known/host-meta"
status = 302
force = true
[[redirects]]
from = "/.well-known/host-meta.xrd"
to = "https://fed.brid.gy/.well-known/host-meta.xrd"
status = 302
force = true
[[redirects]]
from = "/.well-known/host-meta.jrd"
to = "https://fed.brid.gy/.well-known/host-meta.jrd"
status = 302
force = true
[[redirects]]
from = "/.well-known/webfinger"
to = "https://fed.brid.gy/.well-known/webfinger"
status = 302
force = true

86
package.json Normal file
View file

@ -0,0 +1,86 @@
{
"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": "cross-env NODE_OPTIONS=--max_old_space_size=7680 MODE=development vite dev",
"kit:build": "cross-env 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=8192 && 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.9",
"@iconify-json/simple-icons": "1.1.21",
"@iconify-json/material-symbols": "1.1.14",
"@iconify-json/mdi": "^1.1.30",
"@iconify-json/uil": "^1.1.2",
"@sveltejs/adapter-auto": "1.0.0-next.64",
"@sveltejs/adapter-node": "1.0.0-next.86",
"@sveltejs/adapter-static": "1.0.0-next.39",
"@sveltejs/kit": "1.0.0-next.405",
"@tailwindcss/typography": "^0.5.4",
"@types/node": "^18.7.3",
"@types/unist": "^2.0.6",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"autoprefixer": "^10.4.8",
"chalk": "^5.0.1",
"chokidar": "^3.5.3",
"cross-env": "^7.0.3",
"cssnano": "^5.1.13",
"daisyui": "^2.24.0",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"fenceparser": "^2.2.0",
"fff-flavored-frontmatter": "~0.2.1",
"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.16",
"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.8",
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"unist-util-visit": "^4.1.0",
"unocss": "^0.45.6",
"vite": "^3.0.7",
"vite-plugin-pwa": "^0.12.3",
"workbox-window": "^6.5.4"
}
}

6320
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

184
src/app.css Normal file
View 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-[''];
}

88
src/app.d.ts vendored Normal file
View file

@ -0,0 +1,88 @@
/// <reference types="@sveltejs/kit" />
import { FFFBase, FFFExtra } from 'fff-flavored-frontmatter'
interface ImportMetaEnv extends Readonly<Record<string, string>> {
readonly URARA_SITE_DOMAIN?: string
}
interface ImportMeta {
glob<Module = { [key: string]: unknown }>(pattern: string): Record<string, Module>
readonly env: ImportMetaEnv
}
declare global {
namespace Urara {
namespace Post {
type Frontmatter = Omit<FFFBase, 'created' | 'updated' | 'image' | 'audio' | 'video' | 'flags'> &
Pick<FFFExtra, 'in_reply_to'> & {
/**
* post type.
* @remarks auto-generated
*/
type: 'article' | 'note' | 'photo' | 'reply' | 'audio' | 'video' | 'like' | 'repost' | 'bookmark'
/**
* post layout.
*/
layout?: 'article' | 'note' | 'photo' | 'reply'
/**
* post path.
* @remarks auto-generated
*/
path: string
/**
* post slug.
* @remarks auto-generated
*/
slug: string
/**
* table of contents.
* @remarks auto-generated, article-only, set to `false` to disable
*/
toc?: false | Toc[]
/**
* the created date of the post.
* @remarks auto-generated or set manually
*/
created: string
/**
* the updated date of the post.
* @remarks auto-generated or set manually
*/
updated: string
/**
* the featured image for article, or image for "photo" / "multi-photo" posts.
* @remarks currently only supports string
*/
image?: 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 Toc = {
depth: number
title?: string
slug?: string
children?: Toc[]
}
interface Module {
default: {
render: () => {
html: string
head: string
css: {
code: string
}
}
}
metadata: Frontmatter
}
}
type Post = Post.Frontmatter & { html?: string }
type Page = { title?: string; path: string }
}
}

17
src/app.html Normal file
View 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
View 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'}">`)
})

View 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>

View 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>

View 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>

View 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>

View 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>

View 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> -->

View file

@ -0,0 +1,205 @@
<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
text?: string
}
rsvp?: string
published?: string
'wm-received': 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]: [number, boolean, boolean, WebmentionEntry[], boolean] = [
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?.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 ${
mention.rsvp &&
{
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>

View file

@ -0,0 +1,35 @@
<script lang="ts">
export let title: string | undefined = undefined
export let description: string | undefined = undefined
export let status: 'info' | 'success' | 'warning' | 'error' | undefined = 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>

View 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}', `sevichecc@kongwoo.icu`)))
.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>

View 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}

View 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>

View 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>

View 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>

View file

@ -0,0 +1,7 @@
<script lang="ts">
export let theme = undefined
</script>
{@html `
${theme ?? ''}
`}

View 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.avatar}
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>

View 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 aspect-video w-full 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}

View 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>

View 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 = 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>

View 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 = undefined
export let page: Urara.Page | undefined = 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.type === 'article'}
<title>{post.title} | {site.title}</title>
{:else if post.type === 'note'}
<title>{post.summary ?? 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} />

View 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>

View file

@ -0,0 +1,45 @@
<script lang="ts">
import { site } from '$lib/config/site'
import { any, maskable } from '$lib/config/icon'
export let post: Urara.Post | undefined = undefined
export let page: Urara.Page | undefined = 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:title" content={post.title ?? post.summary ?? post.path.slice(1)} />
{#if post.summary}
<meta property="og:description" content={post.summary} />
{/if}
{#if post.image}
<meta property="og:image" content={site.protocol + site.domain + post.image} />
<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}
<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} />
{: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>

View 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>

View 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 />

View 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 transition-all duration-500 ease-in-out border-b-2 border-transparent max-h-[4.125rem] {scrollY >
32 && 'backdrop-blur border-base-content/10 bg-base-100/30 md:bg-base-200/30'}">
<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 !w-6 !h-6" />
</div>
</button>

View 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>

View 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>

View file

@ -0,0 +1,174 @@
<script lang="ts">
import { site } from '$lib/config/site'
</script>
<div
class="h-card flex flex-col gap-4 sticky top-24 card card-body p-4 items-right xl:border-2 xl:py-8 border-base-content/10 xl:ml-auto xl:mr-8 xl:max-w-xs">
<a href={site.protocol + site.domain} class="hidden u-url u-uid">{site.author.name}</a>
<figure class="relative mx-auto group">
<img
class="rounded-full shadow-xl w-32 h-32 hover:rotate-[360deg] transition-transform duration-1000 ease-in-out"
src={site.author.avatar}
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}
</figure>
<div class="text-center flex flex-col gap-2">
<h2 class="text-2xl font-bold mt-0 mb-2 p-name">{site.author.name}</h2>
<p class="opacity-75 p-note">{@html site.author.bio}</p>
<label for="remote-follow" class="btn btn-active btn-outline btn-xs modal-button w-fit mx-auto normal-case mt-4 gap-2 ">
<span class="i-material-symbols-group-add-rounded" />
Remote Follow
</label>
{#if site.author.metadata}
<div class="flex gap-1 flex-wrap justify-center">
{#each site.author.metadata as { text, icon, link, rel }}
{#if link}
<a
href={link}
rel={rel ?? 'me noopener external'}
class:btn-square={!text}
class="btn btn-sm btn-ghost normal-case gap-2 u-url"
target="_blank">
{#if icon}
<span class="{icon} !w-5 !h-5" />
{/if}
{#if text}
{text}
{/if}
</a>
{:else}
<button class:btn-square={!text} class="btn btn-sm btn-ghost normal-case gap-2" {rel}>
{#if icon}
<span class="{icon} !w-5 !h-5" />
{/if}
{#if text}
{text}
{/if}
</button>
{/if}
{/each}
</div>
{/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>

View file

@ -0,0 +1,12 @@
<script lang="ts">
export let post: Urara.Post
const actions = import.meta.glob<{ default: unknown }>('/src/lib/components/actions/*.svelte', { eager: true })
</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>

View file

@ -0,0 +1,129 @@
<script lang="ts">
import { browser } from '$app/env'
import { post as postConfig } from '$lib/config/post'
import { posts as storedPosts } from '$lib/stores/posts'
import { title as storedTitle } from '$lib/stores/title'
import Reply from '$lib/components/post_reply.svelte'
import Status from '$lib/components/post_status.svelte'
import Image from '$lib/components/prose/img.svelte'
import Pagination from '$lib/components/post_pagination.svelte'
import Comment from '$lib/components/post_comment.svelte'
export let post: Urara.Post
export let preview: boolean = false
export let loading: 'eager' | 'lazy' = 'lazy'
export let decoding: 'async' | 'sync' | 'auto' = 'async'
// pagination
let index: number
let prev: Urara.Post | undefined = undefined
let next: Urara.Post | undefined = undefined
if (browser && !preview)
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 ?? post.path.slice(1))
})
</script>
<svelte:element
this={post.type === 'article' ? 'article' : 'div'}
itemscope
itemtype="https://schema.org/BlogPosting"
itemprop="blogPost"
class:md:mb-8={!preview}
class:lg:mb-16={!preview}
class:h-entry={preview}
class:group={preview}
class:image-full={preview && post.type === 'article' && post.image}
class:before:!rounded-none={preview && post.image}
class="card bg-base-100 rounded-none md:rounded-box md:shadow-xl z-10">
{#if !preview && 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.type] ?? [])] 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}
{#if post.in_reply_to}
<Reply in_reply_to={post.in_reply_to} class="mt-4 mx-4" />
{/if}
{#if post.image && preview}
<figure class="!block">
<Image
class={post.type === 'article'
? 'u-featured object-center h-full w-full absolute group-hover:scale-105 transition-transform duration-500 ease-in-out'
: 'u-photo rounded-xl md:rounded-b-none -mb-6 md:-mb-2'}
src={post.image}
alt={post.image}
{loading}
{decoding} />
</figure>
{/if}
<div
class={`card-body gap-0 ${
preview && post.type === 'article' && post.image ? 'md:col-start-1 md:row-start-1 md:text-neutral-content md:z-20' : ''
}`}>
<div class="flex flex-col gap-2">
{#if post.image && !preview}
<figure
class={`md:order-last mb-4 ${post.type === 'article' ? 'flex-col gap-2 -mx-4 -mt-8 md:mt-0' : 'flex-col -mx-8'}`}>
<Image
class={`${post.type === 'article' ? 'u-featured rounded-box shadow-xl' : 'u-photo'}`}
src={post.image}
alt={post.image}
{loading}
{decoding} />
</figure>
{/if}
<Status {post} {preview} />
{#if post.title}
{#if preview}
<h2
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>
</h2>
{:else}
<h1 itemprop="name headline" class="card-title text-3xl mb-8 p-name">{post.title ?? post.path.slice(1)}</h1>
{/if}
{/if}
{#if post.summary}
<p itemprop="description" class:hidden={!preview || post.type !== 'article'} class="p-summary mb-auto">
{post.summary}
</p>
{/if}
</div>
<main itemprop="articleBody" class:mt-4={post.type !== 'article'} class="urara-prose prose e-content">
{#if !preview}
<slot />
{:else if post.html}
{@html post.html}
{/if}
</main>
{#if !preview && 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>
{#if !preview}
{#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}
{/if}
</svelte:element>

View file

@ -0,0 +1,42 @@
<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
const comments = import.meta.glob<{ default: unknown }>('/src/lib/components/comments/*.svelte', { eager: true })
let currentComment: string | undefined = undefined
let currentConfig: unknown | undefined = undefined
currentComment = localStorage.getItem('comment') ?? toSnake(config.use[0])
// @ts-ignore No index signature with a parameter of type 'string' was found on type 'CommentConfig'. ts(7053)
$: if (currentComment) currentConfig = config[currentComment]
</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={currentConfig} />
{/key}
{/if}
</div>
{/if}

View file

@ -0,0 +1,46 @@
<script lang="ts">
import { fly } from 'svelte/transition'
import { browser } from '$app/env'
import Card from '$lib/components/post_card.svelte'
import Head from '$lib/components/head.svelte'
import Toc from '$lib/components/post_toc.svelte'
import Action from '$lib/components/post_action.svelte'
import Footer from '$lib/components/footer.svelte'
export let post: Urara.Post
import { onMount } from 'svelte'
import { copyCode } from '$lib/utils/copyCode'
onMount(() => {
copyCode()
})
</script>
<Head {post} />
<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">
{#if browser && post.toc}
<div class="h-full hidden xl:block">
<Toc toc={post.toc} />
</div>
{/if}
</div>
<div class="flex-none w-full max-w-screen-md mx-auto xl:mx-0">
<Card {post}>
<slot />
</Card>
<Footer sticky={true} />
</div>
</div>

View file

@ -0,0 +1,32 @@
<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 { typeOfPost } from '$lib/utils/posts'
import Container from '$lib/components/post_container.svelte'
// auto-generated
export let path
export let slug
export let toc
// common
export let created
export let updated
export let published
export let summary
export let tags
export let flags
// specify
export let title
export let image
export let in_reply_to
// post
let fm = { path, slug, toc, created, updated, published, summary, tags, flags, title, image, in_reply_to }
let post = { type: typeOfPost(fm), ...fm }
</script>
<Container {post}>
<slot />
</Container>

View file

@ -0,0 +1,59 @@
<script lang="ts">
import Image from '$lib/components/prose/img.svelte'
export let prev: Urara.Post | undefined = undefined
export let next: Urara.Post | undefined = undefined
</script>
<nav class="flex flex-col md:flex-row flex-warp justify-evenly">
{#if prev}
<div
href={prev.path}
class:image-full={prev['image']}
class:md:rounded-r-box={next && !next['image']}
class="flex-1 card group rounded-none before:!rounded-none">
{#if prev['image']}
<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['image']} />
</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['summary'] ?? prev.path.slice(1)}
</a>
</div>
</div>
{#if next && !next['image'] && !prev['image']}
<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['image']}
class:md:rounded-l-box={prev && !prev['image']}
class="flex-1 card group rounded-none before:!rounded-none">
{#if next['image']}
<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['image']} />
</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['summary'] ?? next.path.slice(1)}
</a>
<span class="i-heroicons-outline-chevron-right opacity-50 group-hover:opacity-100 ml-auto" />
</div>
</div>
{/if}
</nav>

View file

@ -0,0 +1,30 @@
<script lang="ts">
let className = ''
export { className as class }
export let in_reply_to: Urara.Post['in_reply_to']
</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:&nbsp;</span>
{#if Array.isArray(in_reply_to)}
{#each in_reply_to 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={in_reply_to}
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" />
{in_reply_to}
</a>
{/if}
</div>

View 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 preview: 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:md:mb-4={!preview && post.type !== 'article'} class="flex font-semibold gap-1.5">
<a
class:hidden={preview}
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={preview} 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>

View file

@ -0,0 +1,68 @@
<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.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
// @ts-ignore Type 'Toc | undefined' is not assignable to type 'Toc.' ts(2322)
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>

View file

@ -0,0 +1,33 @@
<script lang="ts">
export let toc: Urara.Post.Toc
const { title, slug, depth, children } = toc
</script>
{#if title}
<span
dir="ltr"
on:click={() =>
// @ts-ignore Object is possibly 'null'. ts(2531)
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}
<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}

View file

@ -0,0 +1,10 @@
<script lang="ts">
let className: string | undefined = 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={className ?? 'rounded-lg my-2'} {loading} {decoding} />

View file

@ -0,0 +1,5 @@
<div class="overflow-x-auto mb-4">
<table class="table w-full">
<slot />
</table>
</div>

90
src/lib/config/friends.ts Normal file
View file

@ -0,0 +1,90 @@
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: 'loikin',
rel: 'friend',
name: 'Loikin',
title: '此生未命名',
link: 'https://blog.loikein.one/',
descr: '用爱和理性对抗荒谬',
avatar: '/assets/loikin.png'
},
{
id: 'sharktale',
rel: 'friend',
name: '鲨',
title: '一只脆脆鲨',
link: 'http://blog.sharktale.xyz/',
descr: '遇见一只脆脆鲨',
avatar: 'https://s2.loli.net/2022/03/30/xwOzn9G8TIqFPvR.jpg'
}
]

219
src/lib/config/general.ts Normal file
View file

@ -0,0 +1,219 @@
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,noarchive">',
// 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: '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
View 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
View 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']
// }
}
}

View 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/'
}
]

49
src/lib/config/site.ts Normal file
View file

@ -0,0 +1,49 @@
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',
description: 'Tech / Code / Random Life',
author: {
name: '酸橘汁腌鱼',
avatar: '/assets/avatar.jpg',
status: '🖤',
bio: ' Code / Tech <br> Living a Random Life ',
metadata: [
{
text: '',
icon: 'i-mdi-github',
link: 'https://github.com/sevichecc'
},
{
text: '',
icon: 'i-simple-icons-matrix',
link: 'https://matrix.to/#/@seviche:kongwoo.icu'
},
{
text: '',
icon: 'i-heroicons-solid-key',
link: 'https://keys.openpgp.org/vks/v1/by-fingerprint/76DF9F9CC0C3619AA12CB914AFF18B986818D8AD',
rel: 'pgpkey'
},
{
text: '',
icon: 'i-ic-twotone-rss-feed',
link: '/atom.xml',
rel: 'rss'
}
// ,
// {
// text: 'Bookmark',
// icon: 'i-ic-round-bookmark-border',
// link: 'https://airtable.com/shrpftxf6JgRomP2X',
// rel: 'bookmark'
// }
]
},
keywords: ['Tech', 'Code', 'Random Life'],
themeColor: '#3D4451'
}

4
src/lib/stores/posts.ts Normal file
View file

@ -0,0 +1,4 @@
import type { Writable } from 'svelte/store'
import { writable } from 'svelte/store'
export const posts: Writable<Urara.Post[]> = writable([])
export const tags: Writable<string[]> = writable([])

2
src/lib/stores/title.ts Normal file
View file

@ -0,0 +1,2 @@
import { writable } from 'svelte/store'
export const title = writable({})

42
src/lib/types/general.ts Normal file
View file

@ -0,0 +1,42 @@
export type ThemeConfig = {
text?: string
name: string
}[]
export type HeadConfig = {
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
View file

@ -0,0 +1,5 @@
export type Icon = {
src: string
sizes?: string
type?: string
}

119
src/lib/types/post.ts Normal file
View 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
}
// Refhttps://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
}

45
src/lib/types/site.ts Normal file
View file

@ -0,0 +1,45 @@
import type { FFFAuthor } from 'fff-flavored-frontmatter'
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: Omit<FFFAuthor, 'url'> & {
status?: string
bio?: string
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
View 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
View 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)}`

55
src/lib/utils/copyCode.ts Normal file
View file

@ -0,0 +1,55 @@
/** 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()
if (!selection) return
selection.removeAllRanges()
selection.addRange(range)
// copy to clipboard
document.execCommand('copy', false)
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)
})
})
}

84
src/lib/utils/posts.ts Normal file
View file

@ -0,0 +1,84 @@
import type { FFFFlavoredFrontmatter } from 'fff-flavored-frontmatter'
interface GenPostsOptions {
/** import.meta.glob<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[]
/**
* Detect Post Type
* @param fm - post frontmatter
* @returns - post type string
*/
export const typeOfPost = (
fm: FFFFlavoredFrontmatter
): 'note' | 'article' | 'reply' | 'photo' | 'like' | 'video' | 'repost' | 'bookmark' | 'audio' =>
fm.title
? 'article'
: fm.image
? 'photo'
: fm.audio
? 'audio'
: fm.video
? 'video'
: fm.bookmark_of
? 'bookmark'
: fm.like_of
? 'like'
: fm.repost_of
? 'repost'
: fm.in_reply_to
? 'reply'
: 'note'
/**
* Generate Posts List
* @param options - An optional configuration object
* @returns - posts list
*/
export const genPosts: GenPostsFunction = ({
modules = import.meta.glob<Urara.Post.Module>('/src/routes/**/*.{md,svelte.md}', { eager: true }),
postHtml = false,
postLimit = undefined,
filterUnlisted = false
} = {}) =>
Object.entries(modules)
.map(([, module]) => ({
...module.metadata,
type: typeOfPost(module.metadata),
html:
postHtml || typeOfPost(module.metadata) !== 'article'
? 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(/<main [^>]+>(.*?)<\/main>/gi, '$1')
// .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, b) => 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))
]

38
src/routes/__error.svelte Normal file
View file

@ -0,0 +1,38 @@
<script lang="ts" context="module">
import type { Load } from './__types'
export const load: Load = ({ url: { pathname }, error, status }) => ({ props: { pathname, error, status } })
</script>
<script lang="ts">
import Head from '$lib/components/head.svelte'
import Footer from '$lib/components/footer.svelte'
export let pathname: string
export let error: Error
export let status: string
console.error(status, error.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">{error.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>

View file

@ -0,0 +1,49 @@
<script lang="ts" context="module">
import type { Load } from './__types'
export const prerender = true
export const load: Load = async ({ url, fetch }) => ({
props: {
path: url.pathname,
res: await fetch('/posts.json').then(res => res.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 'uno.css'
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 />
<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}

49
src/routes/atom.xml.ts Normal file
View file

@ -0,0 +1,49 @@
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.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()
})

49
src/routes/feed.json.ts Normal file
View file

@ -0,0 +1,49 @@
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.avatar
}
],
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['image'],
date_published: post.published ?? post.created,
date_modified: post.updated ?? post.published ?? post.created,
tags: post.tags,
_indieweb: {
type: post.type,
'in-reply-to': post.in_reply_to
}
}))
})
export const GET: RequestHandler = async () => ({
headers: {
'Content-Type': 'application/feed+json; charset=utf-8'
},
body: JSON.stringify(await render(), null, 2)
})

129
src/routes/index.svelte Normal file
View file

@ -0,0 +1,129 @@
<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/post_card.svelte'
// import Post from '$lib/components/index_post.svelte'
import Profile from '$lib/components/index_profile.svelte'
import RemoteFollow from '$lib/components/extra/follow.svelte'
let allPosts: Urara.Post[]
let allTags: string[]
let loaded: boolean
let [posts, tags, years]: [Urara.Post[], string[], number[]] = [[], [], []]
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} preview={true} 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>
<RemoteFollow />

View 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
View 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
View 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
View 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
View 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 }
}
})

63
tailwind.config.ts Normal file
View file

@ -0,0 +1,63 @@
// 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'
]
}
})

14
tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"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"]
}
}

11
tsconfig.node.json Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

BIN
urara/2021-11-08-ux1/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View file

@ -0,0 +1,132 @@
---
title: Overview of User Experience Design
summary: Introduction to User Experience DesignWeek1
created: 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)
### 什么是用户
这里的“用户”是指使用一些技术来达到目的的个体
### 什么是界面
有输入、输出、系统,于此同时,每个输入都会导致一个期望的输出:
![有输入、输出、系统,于此同时,每个输入都会导致一个期望的输出](/2021-11-08-ux1/1.png)
个人使用界面的能力与个人特征、群体、社会有关
![个人使用界面的能力与个人特征、群体、社会有关](/2021-11-08-ux1/2.png)
### 用户体验设计的目标
![](/2021-11-08-ux1/3.png)
**设计“能用”并且“好用”的界面**
- “**能用**”的意思是:它可以让用户完成一个任务。在图中,这个系统 (S) 就产生了期望的成果 (Output)
- “**好用**”的意思是:用户可以有效高效甚至满意地达成这项任务。在我们的图表中,这需要轻松快速地输入 (Input) 并且输出 (Output) 的结果有效的完成了任务
### 界面设计环 (四个步骤)
![](/2021-11-08-ux1/4.png)
#### Step1Requirements Gathering 收集需求
- Understanding the user and what her goals areWhat 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 userher goals and current practices
#### Step 2Design Alternatives 设计方案
- Once you understand the userstheir goalsand 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 3Prototyping 原型
- Techniques for modeling the novel designs before a final version is produced
#### Step 4Evaluation 评估测试
- A set of techniques for ascertaining that your design meets the needs of the user
### 什么样的界面是“实用”的?
“实用性”指的是有效、高效并且让用户觉得满意的。
> 如果用户能够理解怎样的输入 (Input) 会带来所需的输出 (Output) 那么这个界面就是实用的
### 好设计的三个特征
![](https://i.loli.net/2021/11/08/MtmwlyuXHGgbJPZ.png)
#### 1 - Affordance (示能)
> 指一个物理对象与人之间的关系
>
> (无论是动物还是人类,甚至机器和机器人,他们之间发生的任何交互作用)
是指可以感知到的事物的实际属性,主要是那些决定事物如何被使用的基本属性 (看看你的智能手机,我们会看到很多种功能,例如按键,可以感知到可以被按。)
#### 2 - Signifiers (意符)
> 能告诉人们正确操作方式的任何可感知的标记或声音。
示能决定可能进行哪些操作,意符则点名操作的位置。
#### 3 - Feedback (反馈)
> 一些让你知道系统正在处理你的要求的方式
反馈需要将用户信息发送回来其中包括哪些系统输入的信息已发生它跟我们沟通操作结果
- 输入:示能、意符
- 输出:示能
### 如何与“用户”接洽
注意礼貌、着装
#### 三个步骤
**1 - 介绍**
- 简要概述目标
- 坦率:让用户知道该会话的目标是什么。询问他们坦率的意见。
- 保密:说明交互内容是保密的。
- 自愿:在介绍期间,向用户解释他们的参与完全是自愿的。他们可以随时停止参与。如果他们希望停止参与,这不会消极地影响他们与你的公司或机构的关系。这一部分是很重要的,它能让用户放心并能够不受约束,因为你可能代表一个在社区中具有较高知名度的实体
- 开放:让他们知道,没有正确或错误的答案,他们应该简单直接地向你提供反馈和意见
**2 - 交流**
- 中立态度、语气轻松专业。
- 如果形式允许,鼓励详细阐述意外或有趣的信息
- 保持对交流过程的控制,尽量覆盖所有话题
**3 - 结束**
- 在会话结束时,提醒他们有关交互的目标和您打算如何处理他们提供的数据。
- 询问他们是否还有需要补充的内容。

BIN
urara/2021-11-09-ux2/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

BIN
urara/2021-11-09-ux2/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

BIN
urara/2021-11-09-ux2/11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

BIN
urara/2021-11-09-ux2/12.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

BIN
urara/2021-11-09-ux2/13.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Some files were not shown because too many files have changed in this diff Show more