Finally updated
This took way too long
|
@ -1,6 +0,0 @@
|
|||
.husky
|
||||
.vscode
|
||||
node_modules
|
||||
public
|
||||
dist
|
||||
.yarn
|
23
.eslintrc.js
|
@ -1,23 +0,0 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true,
|
||||
browser: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "plugin:astro/recommended"],
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ["*.astro"],
|
||||
parser: "astro-eslint-parser",
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
extraFileExtensions: [".astro"],
|
||||
},
|
||||
rules: {},
|
||||
},
|
||||
],
|
||||
};
|
3
.gitignore
vendored
|
@ -22,9 +22,6 @@ pnpm-debug.log*
|
|||
# ignore .astro directory
|
||||
.astro
|
||||
|
||||
# ignore Jampack cache files
|
||||
.jampack/
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
|
@ -9,5 +9,5 @@
|
|||
!astro.config.ts
|
||||
!package.json
|
||||
!.prettierrc
|
||||
!.eslintrc.js
|
||||
!eslint.config.mjs
|
||||
!README.md
|
34
.vscode/blog.code-snippets
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"Frontmatter": {
|
||||
"scope": "markdown",
|
||||
"prefix": "frontmatter",
|
||||
"body": [
|
||||
"---",
|
||||
"author: $1",
|
||||
"pubDatetime: $CURRENT_YEAR-$CURRENT_MONTH-${CURRENT_DATE}T$CURRENT_HOUR:$CURRENT_MINUTE:$CURRENT_SECOND.000$CURRENT_TIMEZONE_OFFSET",
|
||||
"modDatetime: $3",
|
||||
"title: $4",
|
||||
"featured: ${5|false,true|}",
|
||||
"draft: ${6|true,false|}",
|
||||
"tags:",
|
||||
" - $7",
|
||||
"description: $8",
|
||||
"---",
|
||||
],
|
||||
"description": "Adds the frontmatter block for the Blog post"
|
||||
},
|
||||
"Blog Template": {
|
||||
"scope": "markdown",
|
||||
"prefix": "template",
|
||||
"body": [
|
||||
"${1:frontmatter}",
|
||||
"",
|
||||
"${2: Introductory Sentence}",
|
||||
"",
|
||||
"## Table of contents",
|
||||
"",
|
||||
"## ${3: heading 1}",
|
||||
],
|
||||
"description": "Adds the template for automating the creation of a Blog post. You will need to trigger the snippet modal on the 'frontmatter' line to insert the other snippet."
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
# Toastie's blog
|
||||
|
||||
The code responsible for making the blog at https://blog.toastiet0ast.com work
|
||||
The code responsible for making the blog at https://blog.elliebot.net work
|
||||
|
|
|
@ -14,7 +14,9 @@ export default defineConfig({
|
|||
applyBaseStyles: false,
|
||||
}),
|
||||
react(),
|
||||
sitemap(),
|
||||
sitemap({
|
||||
filter: page => SITE.showArchives || !page.endsWith("/archives"),
|
||||
}),
|
||||
],
|
||||
markdown: {
|
||||
remarkPlugins: [
|
||||
|
@ -27,7 +29,8 @@ export default defineConfig({
|
|||
],
|
||||
],
|
||||
shikiConfig: {
|
||||
theme: "one-dark-pro",
|
||||
// For more themes, visit https://shiki.style/themes
|
||||
themes: { light: "min-light", dark: "night-owl" },
|
||||
wrap: true,
|
||||
},
|
||||
},
|
||||
|
@ -37,4 +40,7 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
scopedStyleStrategy: "where",
|
||||
experimental: {
|
||||
contentLayer: true,
|
||||
},
|
||||
});
|
||||
|
|
44
eslint.config.mjs
Normal file
|
@ -0,0 +1,44 @@
|
|||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
import astroParser from "astro-eslint-parser";
|
||||
import eslintPluginAstro from "eslint-plugin-astro";
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...eslintPluginAstro.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["*.astro"],
|
||||
languageOptions: {
|
||||
parser: astroParser,
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
extraFileExtensions: [".astro"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["tailwind.config.cjs", "**/*.d.ts"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-require-imports": "off",
|
||||
"@typescript-eslint/triple-slash-reference": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-expressions": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ["dist/**", ".astro"],
|
||||
},
|
||||
];
|
74
package.json
|
@ -1,63 +1,49 @@
|
|||
{
|
||||
"name": "toasite-blog",
|
||||
"version": "3.0.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build && jampack ./dist",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"sync": "astro sync",
|
||||
"astro": "astro",
|
||||
"format:check": "prettier --plugin-search-dir=. --check .",
|
||||
"format": "prettier --plugin-search-dir=. --write .",
|
||||
"cz": "cz",
|
||||
"prepare": "husky install",
|
||||
"format:check": "prettier --check . --plugin=prettier-plugin-astro",
|
||||
"format": "prettier --write . --plugin=prettier-plugin-astro",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.2.0",
|
||||
"@astrojs/rss": "^3.0.0",
|
||||
"@resvg/resvg-js": "^2.4.1",
|
||||
"astro": "^3.1.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"github-slugger": "^2.0.0",
|
||||
"@astrojs/check": "^0.9.3",
|
||||
"@astrojs/rss": "^4.0.7",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"astro": "^4.16.18",
|
||||
"fuse.js": "^7.0.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"remark-collapse": "^0.1.2",
|
||||
"remark-toc": "^9.0.0",
|
||||
"satori": "^0.10.8",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.2.2"
|
||||
"satori": "^0.11.0",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"typescript": "^5.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/react": "^3.0.2",
|
||||
"@astrojs/sitemap": "^3.0.0",
|
||||
"@astrojs/tailwind": "^5.0.0",
|
||||
"@divriots/jampack": "^0.20.2",
|
||||
"@tailwindcss/typography": "^0.5.10",
|
||||
"@astrojs/react": "^3.6.2",
|
||||
"@astrojs/sitemap": "^3.1.6",
|
||||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/react": "^18.2.22",
|
||||
"@typescript-eslint/parser": "^6.7.3",
|
||||
"astro-eslint-parser": "^0.15.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^8.50.0",
|
||||
"eslint-plugin-astro": "^0.29.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^14.0.1",
|
||||
"prettier": "^3.0.3",
|
||||
"prettier-plugin-astro": "^0.12.0",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
"@types/lodash.kebabcase": "^4.1.9",
|
||||
"@types/react": "^18.3.6",
|
||||
"@typescript-eslint/parser": "^8.5.0",
|
||||
"astro-eslint-parser": "^1.0.3",
|
||||
"eslint": "^9.10.0",
|
||||
"eslint-plugin-astro": "^1.2.4",
|
||||
"globals": "^15.9.0",
|
||||
"prettier": "^3.3.3",
|
||||
"prettier-plugin-astro": "^0.14.1",
|
||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"typescript-eslint": "^8.5.0"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md,mdx,json}": [
|
||||
"prettier --plugin-search-dir=. --write"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@8.13.1+sha1.90f9b2bb3ed58632bcb7b13c3902d6873ee9501c"
|
||||
}
|
||||
|
|
9847
pnpm-lock.yaml
|
@ -1,361 +0,0 @@
|
|||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="865.76" height="682.89" viewBox="0 0 865.76 682.89">
|
||||
|
||||
<defs>
|
||||
|
||||
<style xmlns="http://www.w3.org/1999/xhtml">*, body, html { -webkit-font-smoothing: antialiased; }
|
||||
img, svg { max-width: 100%; }
|
||||
</style>
|
||||
|
||||
</defs>
|
||||
|
||||
<path d="M391.82,532.2c-15.44,2.82-87.85,18.09-73.28,55a33.24,33.24,0,0,0,9.74,13.13c18.18,15.25,83.33,52.58,272.06,32.22,10.69-1.15,21.42-1.86,32.17-2.06,49.73-.92,206-9.34,202-78.54,0,0-2.07-38.74-95.7-26.87l-71.21-4.43s-160.55-12.38-268.7,10.11C396.57,531.29,394.2,531.77,391.82,532.2Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M391.82,532.2c-15.44,2.82-87.85,18.09-73.28,55a33.24,33.24,0,0,0,9.74,13.13c18.18,15.25,83.33,52.58,272.06,32.22,10.69-1.15,21.42-1.86,32.17-2.06,49.73-.92,206-9.34,202-78.54,0,0-2.07-38.74-95.7-26.87l-71.21-4.43s-160.55-12.38-268.7,10.11C396.57,531.29,394.2,531.77,391.82,532.2Z" fill="#fff" opacity="0.7"/>
|
||||
|
||||
<path d="M503.08,522.3C179.26,552.79,133.91,359.63,133.91,359.63c-24.79-67.13-3.45-111,27.66-152.68a303.36,303.36,0,0,1,117.77-94.5c74.9-34.06,126.36-41,126.36-41S622.58,15.68,735.64,183.7c0,0,108,135.55,37.54,221.14,0,0-35.14,47.34-127.63,82.89l-69.3,20.46A387.7,387.7,0,0,1,503.08,522.3Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M503.08,522.3C179.26,552.79,133.91,359.63,133.91,359.63c-24.79-67.13-3.45-111,27.66-152.68a303.36,303.36,0,0,1,117.77-94.5c74.9-34.06,126.36-41,126.36-41S622.58,15.68,735.64,183.7c0,0,108,135.55,37.54,221.14,0,0-35.14,47.34-127.63,82.89l-69.3,20.46A387.7,387.7,0,0,1,503.08,522.3Z" fill="#fff" opacity="0.7"/>
|
||||
|
||||
<rect x="104.67" y="206.46" width="463.2" height="348.88" fill="#fff"/>
|
||||
|
||||
<rect x="108.43" y="206.46" width="459.44" height="35.42" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="128.82" y="259.06" width="104.13" height="104.13" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="713.86" y="369.62" width="5.37" height="37.57" fill="#999"/>
|
||||
|
||||
<polygon points="664.89 442.18 664.89 554.44 672.53 554.44 676.93 436.58 664.89 442.18" fill="#ccc"/>
|
||||
|
||||
<polygon points="711.71 420.08 711.71 537.08 719.36 537.08 723.52 414.71 711.71 420.08" fill="#ccc"/>
|
||||
|
||||
<polygon points="668.23 434.1 733.18 405.05 703.86 399.96 670.01 385.44 668.23 434.1" fill="#ccc"/>
|
||||
|
||||
<path d="M656.14,446.25l77-35.83v-5.37L668.23,434.1S660.68,442.36,656.14,446.25Z" fill="#b3b3b3"/>
|
||||
|
||||
<path d="M693.46,271.94H734a4.55,4.55,0,0,1,4.55,4.55v67.37a0,0,0,0,1,0,0H693.46a0,0,0,0,1,0,0V271.94A0,0,0,0,1,693.46,271.94Z" fill="#999"/>
|
||||
|
||||
<rect x="241.54" y="44.36" width="325.8" height="139.55" fill="#787878" data-primary="true"/>
|
||||
|
||||
<rect x="263.01" y="83.01" width="100.91" height="65.48" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M297.36,131.59a1.07,1.07,0,0,1-.76-.32l-14.79-14.76a1.08,1.08,0,0,1,0-1.5l14.79-15.56a1.07,1.07,0,0,1,1.56,1.47l-14.07,14.81,14.05,14a1.07,1.07,0,0,1,0,1.52A1.09,1.09,0,0,1,297.36,131.59Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M328.73,132.66a1.06,1.06,0,0,1-.76-.31,1.07,1.07,0,0,1,0-1.52l14-14L328,102a1.08,1.08,0,1,1,1.56-1.48l14.78,15.56a1.06,1.06,0,0,1,0,1.5l-14.78,14.77A1.07,1.07,0,0,1,328.73,132.66Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M305.56,131.59a1.08,1.08,0,0,1-1-1.56l14.34-28.18a1.08,1.08,0,1,1,1.92,1L306.51,131A1.07,1.07,0,0,1,305.56,131.59Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<path d="M524.39,119.51H454.62a1.08,1.08,0,0,1,0-2.15h69.77a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<path d="M540.5,132.39H454.62a1.08,1.08,0,0,1,0-2.15H540.5a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<rect x="460.52" y="153.86" width="65.48" height="16.1" rx="7.5" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M567.33,44.36V183.91H241.54s54.75-59.1,144.51-74c4.1-.68,8.24-1.12,12.38-1.4C426.41,106.6,557.79,95.18,567.33,44.36Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<rect x="31.14" y="128.09" width="187.86" height="213.62" fill="#787878" data-primary="true"/>
|
||||
|
||||
<rect x="31.14" y="128.09" width="187.86" height="34.35" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<rect x="46.17" y="173.18" width="57.97" height="57.97" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<circle cx="164.78" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<circle cx="184.11" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<circle cx="203.43" cy="145.27" r="3.76" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M170.69,192.5H117a1.07,1.07,0,1,1,0-2.14h53.67a1.07,1.07,0,0,1,0,2.14Z" fill="#fff"/>
|
||||
|
||||
<path d="M186.25,205.38h-68.7a1.07,1.07,0,0,1,0-2.14h68.7a1.07,1.07,0,1,1,0,2.14Z" fill="#fff"/>
|
||||
|
||||
<path d="M203.43,218.27H117.55a1.08,1.08,0,0,1,0-2.15h85.88a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<path d="M168,287H84.28a1.08,1.08,0,1,1,0-2.15H168a1.08,1.08,0,0,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<path d="M194.84,299.85H57.44a1.08,1.08,0,1,1,0-2.15h137.4a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<path d="M168.54,312.73H83.74a1.08,1.08,0,1,1,0-2.15h84.8a1.08,1.08,0,1,1,0,2.15Z" fill="#fff"/>
|
||||
|
||||
<rect x="83.74" y="248.32" width="78.36" height="16.1" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<rect x="256.57" y="259.06" width="66.55" height="17.18" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<path d="M308.78,293.79H256.57a1.08,1.08,0,1,1,0-2.15h52.21a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M325.8,306.67H256.57a1.07,1.07,0,1,1,0-2.14H325.8a1.07,1.07,0,1,1,0,2.14Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M339.76,319.55H256.57a1.07,1.07,0,1,1,0-2.14h83.19a1.07,1.07,0,0,1,0,2.14Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M379.48,332.44H256.57a1.08,1.08,0,1,1,0-2.15H379.48a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="256.57" y="348.15" width="154.58" height="15.03" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<path d="M252.45,400.29h-122a1.08,1.08,0,0,1,0-2.15h122a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M353.18,400.29H268.91a1.08,1.08,0,0,1,0-2.15h84.27a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M417.59,400.29H388.06a1.08,1.08,0,0,1,0-2.15h29.53a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="256.57" y="396.53" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="360.69" y="396.53" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="373.57" y="396.53" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<path d="M223.29,429.16H131a1.08,1.08,0,0,1,0-2.15h92.32a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M289.84,455.37H129.9a1.08,1.08,0,1,1,0-2.15H289.84a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M325.27,429.16H255a1.08,1.08,0,1,1,0-2.15h70.31a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M349.42,455.37h-36a1.08,1.08,0,0,1,0-2.15h36a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="227.58" y="425.4" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="240.46" y="425.4" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="290.92" y="451.61" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="303.8" y="451.61" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<path d="M355.32,512.93H298.43a1.08,1.08,0,0,1,0-2.15h56.89a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M416,512.93H388.06a1.08,1.08,0,0,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="361.77" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="374.65" y="509.17" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<path d="M416,455.37H375.72a1.08,1.08,0,0,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="353.18" y="451.61" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="366.06" y="451.61" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<path d="M205,485H131a1.08,1.08,0,0,1,0-2.15H205a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M349.42,485h-52.6a1.08,1.08,0,0,1,0-2.15h52.6a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<path d="M416,485H363.38a1.08,1.08,0,1,1,0-2.15H416a1.08,1.08,0,1,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="207.19" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="220.07" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="231.88" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<path d="M256.57,512.93H131a1.08,1.08,0,0,1,0-2.15h125.6a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="258.71" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="271.59" y="509.17" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="283.4" y="509.17" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="244.76" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="259.79" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="271.59" y="481.26" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="284.48" y="481.26" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<path d="M417.59,429.16H358a1.08,1.08,0,1,1,0-2.15h59.58a1.08,1.08,0,0,1,0,2.15Z" fill="#e6e6e6"/>
|
||||
|
||||
<rect x="330.63" y="425.4" width="5.37" height="5.37" fill="#787878" opacity="0.29" data-primary="true"/>
|
||||
|
||||
<rect x="343.52" y="425.4" width="5.37" height="5.37" fill="#ccc"/>
|
||||
|
||||
<rect x="51.53" y="436.18" width="103.05" height="64.41" fill="#787878" data-primary="true"/>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M88.5,485.36a1.06,1.06,0,0,1-.74-.3l-15.5-14.83a1.06,1.06,0,0,1,0-1.54l15.49-15a1.07,1.07,0,0,1,1.52,0,1.08,1.08,0,0,1,0,1.52l-14.7,14.25,14.69,14.06a1.07,1.07,0,0,1,0,1.52A1.1,1.1,0,0,1,88.5,485.36Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M119.16,485.36a1.07,1.07,0,0,1-.74-1.84l14.69-14.26L118.42,455.2a1.07,1.07,0,0,1,1.48-1.55l15.5,14.83a1.07,1.07,0,0,1,.33.77,1.08,1.08,0,0,1-.32.78l-15.5,15A1.08,1.08,0,0,1,119.16,485.36Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<g opacity="0.3">
|
||||
|
||||
<path d="M96.62,483.41a1.11,1.11,0,0,1-.5-.12,1.07,1.07,0,0,1-.45-1.45l14-26.83a1.08,1.08,0,1,1,1.91,1l-14,26.83A1.06,1.06,0,0,1,96.62,483.41Z" fill="#fff"/>
|
||||
|
||||
</g>
|
||||
|
||||
<rect x="434.76" y="367.48" width="11.81" height="208.25" fill="#999"/>
|
||||
|
||||
<rect x="441.2" y="367.48" width="5.37" height="208.25" opacity="0.1"/>
|
||||
|
||||
<rect x="471.26" y="368.01" width="11.81" height="172.29" fill="#999"/>
|
||||
|
||||
<rect x="477.7" y="368.01" width="5.37" height="172.29" opacity="0.1"/>
|
||||
|
||||
<rect x="728.89" y="367.48" width="11.81" height="208.25" fill="#999"/>
|
||||
|
||||
<rect x="735.33" y="367.48" width="5.37" height="208.25" opacity="0.1"/>
|
||||
|
||||
<rect x="758.95" y="354.06" width="11.81" height="186.25" fill="#999"/>
|
||||
|
||||
<rect x="765.39" y="354.06" width="5.37" height="186.25" opacity="0.1"/>
|
||||
|
||||
<path d="M688.1,271.94h40.53a4.55,4.55,0,0,1,4.55,4.55v67.37a0,0,0,0,1,0,0H688.1a0,0,0,0,1,0,0V271.94A0,0,0,0,1,688.1,271.94Z" fill="#b3b3b3"/>
|
||||
|
||||
<polygon points="421.88 364.26 477.27 336.37 786.88 336.37 750.36 364.26 421.88 364.26" fill="#ccc"/>
|
||||
|
||||
<path d="M542.11,559.63l-32.5,25.42S496,597.2,507.76,604.71c0,0,17.17,10.74,31.13-7.51l19.37-31.64Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M505.61,596.12c8,8.68,20.58,6.87,28.45-1,3.7-3.79,7-8.33,10.52-12.3,3.08-3.62,7.51-8.79,10.65-12.28-2.8,3.74-7.06,9.09-10,12.81-3.41,4.12-6.73,8.65-10.42,12.54-8.21,8.11-21.45,9.88-29.19.26Z" opacity="0.2"/>
|
||||
|
||||
<path d="M512.32,583.74c6.45-.09,13.31,2.42,17.35,7.63a15.61,15.61,0,0,1,2.79,5.84c-.26-.47-.51-1-.74-1.43a8.51,8.51,0,0,0-.81-1.37c-4-6.39-11.44-9.4-18.59-10.67Z" opacity="0.2"/>
|
||||
|
||||
<path d="M519.56,580c4.83-.65,11.72.93,12.9,6.4-2.62-4.61-8.1-5.41-12.9-6.4Z" opacity="0.2"/>
|
||||
|
||||
<path d="M523.86,575.73c4.82-.65,11.72.93,12.89,6.39-2.61-4.6-8.1-5.4-12.89-6.39Z" opacity="0.2"/>
|
||||
|
||||
<path d="M532.45,569.29c4.82-.65,11.72.93,12.89,6.39-2.61-4.61-8.1-5.4-12.89-6.39Z" opacity="0.2"/>
|
||||
|
||||
<path d="M550.16,544.06l-8,15.57s-3.32,4,1.25,6.48a8.52,8.52,0,0,0,4.06,1h7.9a3.61,3.61,0,0,0,2.94-1.51L568,551.93S554.41,546.7,550.16,544.06Z" fill="#f9b499"/>
|
||||
|
||||
<polygon points="548.32 510.23 551.84 520.86 557.18 505.66 548.32 510.23" fill="#f9b499"/>
|
||||
|
||||
<path d="M710.77,332.4c0,8.24-5.5,8.24-5.5,8.24l-15,5.37c-6.68,2.23-9.44,1.89-10.5,1.46a1.62,1.62,0,0,0-1.36.1.24.24,0,0,1-.08.06,11.71,11.71,0,0,1-3.82,1.75h0c-2.89.58-2.48-2.31-2.48-2.31a12.77,12.77,0,0,0,.2-1.54,9.91,9.91,0,0,0-5.8-9.37,26.59,26.59,0,0,0-4.77-1.68,6.38,6.38,0,0,0-3.07,0l-8.33,1.91H608.39L570,335.27a98.24,98.24,0,0,1,12.58-65.48,86.2,86.2,0,0,1,8.82-12.47c.72-.84,1.14-1.27,1.14-1.27l2.37-3.43s32,2.14,32.37,1.07,22.15-14,22.15-14l30.76,13a10.63,10.63,0,0,1,3.59,2.59c6.62,6.85,11.81,23.17,11.81,23.17l14,46.16A30.89,30.89,0,0,1,710.77,332.4Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M675.8,305s-30.74,5-53.75.22c-.59-.12-1.17-.27-1.75-.43A88.92,88.92,0,0,0,592.56,302l-22.06-.18-1.09,7.3h36.87s12,.39,21.7,3.61c0,0,9.66,3.22,29,1.07l21.82-2.66Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M683.8,255.21c-20.39,2.6-56.89,14.58-56.89,14.58-8.59-6.44-35.49-12.47-35.49-12.47.72-.84,1.14-1.27,1.14-1.27l2.37-3.43s32,2.14,32.37,1.07,22.15-14,22.15-14l30.76,13A10.63,10.63,0,0,1,683.8,255.21Z" opacity="0.2"/>
|
||||
|
||||
<path d="M620.1,254.32a12.38,12.38,0,0,1-1.24.26c-7.26,1.28-14.75-1.87-20.74-8a43,43,0,0,1-10.73-19.86c-4.59-18.58,2.63-36.33,16.12-39.66s28.13,9,32.72,27.59S633.6,251,620.1,254.32Z" fill="#f9b499"/>
|
||||
|
||||
<ellipse cx="639.26" cy="215.05" rx="1.61" ry="3.22" fill="none" stroke="red" stroke-miterlimit="10" stroke-width="0.75"/>
|
||||
|
||||
<path d="M651.6,210.75s17.18,9.45-10.73,18.14a15.44,15.44,0,0,1-.54-3.65,15.8,15.8,0,0,1,.54-4.54l-8.59-4.36s-4.67.85-7.17-4.52c0,0-8.93,0-7.86-6.44,0,0-6.44,4.3-8.59-1.07,0,0-9.69,6.88-19.33-4.62,0,0-4.28,7.84-3,21,0,0-6.34,5.08-9.93.38a6.63,6.63,0,0,1-1.28-3.77,5.58,5.58,0,0,1,3.22-5.49s-7.77-2.89-7.56-9.28a10.2,10.2,0,0,1,1.41-4.67s1.61-4,7.63-2.31h0a19.17,19.17,0,0,1,3.1,1.24s-8.21-17.26,3.4-28.49c0,0,19.14-19.82,26.66,4.87,0,0,6.55-10.14,17-7.62h0a15.76,15.76,0,0,1,2.25.72s7.51,2,6.44,14.1c0,0,9.66-8.28,18.25,1.38,0,0,6.44,7.89,0,16.28C657,198.05,660.19,205.38,651.6,210.75Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M590.41,197.6s-3.22-10.46,6.44-16.91c0,0,6.93-4.51,16.49,1.46a23.89,23.89,0,0,1,2.73,2.07,16.44,16.44,0,0,0,10.59,4.11s11-.47,12.6,12c0,0-12.35-10.72-21.47-5.54,0,0-4.83-15.22-17.72-10.93C600.07,183.91,592.56,186.06,590.41,197.6Z" opacity="0.2"/>
|
||||
|
||||
<path d="M579.79,195.56c-5.23.93-9,7-9,7a10.2,10.2,0,0,1,1.41-4.67S573.77,193.84,579.79,195.56Z" opacity="0.2"/>
|
||||
|
||||
<path d="M651.6,210.75s17.18,9.45-10.73,18.14a7.4,7.4,0,0,1-.54-3.65,7.26,7.26,0,0,1,.54-2.07l4.29-6.83h0c3.22-1.93,3.22-6.66,3.22-6.66a10.45,10.45,0,0,0,4.63-3.63,8.74,8.74,0,0,0,1.09-8.24c-2.23-5.68-8.94-4.09-8.94-4.09,2.15-16-7.52-10.85-7.52-10.85-1-14.52-7.22-17.15-7.61-17.3a15.76,15.76,0,0,1,2.25.72s7.51,2,6.44,14.1c0,0,9.66-8.28,18.25,1.38,0,0,6.44,7.89,0,16.28C657,198.05,660.19,205.38,651.6,210.75Z" opacity="0.2"/>
|
||||
|
||||
<path d="M659.11,241.88l-26.83,22.77c-4.3,3.45-6.44.85-6.44.85l-1.19-1.87-5.79-9c20.4-4.65,18.67-31.41,18.67-31.41h3.34a17.63,17.63,0,0,0,2.14,11.2c3.91-1.57,9-1.42,11.4-1.23a14.8,14.8,0,0,1,2.89.49C665.93,236,659.11,241.88,659.11,241.88Z" fill="#f9b499"/>
|
||||
|
||||
<path d="M659.11,241.88l-26.83,22.77c-4.3,3.45-6.44.85-6.44.85l-1.19-1.87c11.69-5.09,23-18.83,23-18.83,4.06-6-2.64-9.53-4.26-10.28a.16.16,0,0,1,0-.29c3.86-1.42,8.68-1.27,11-1.09a14.8,14.8,0,0,1,2.89.49C665.93,236,659.11,241.88,659.11,241.88Z" fill="#f7a48b"/>
|
||||
|
||||
<path d="M618.86,254.58l3.07,4.81s18.66-10.53,15-26.36C637,233,635.5,251.18,618.86,254.58Z" fill="#f7a48b"/>
|
||||
|
||||
<path d="M599,253.69a55.57,55.57,0,0,1,18.79,6.51" fill="none" stroke="red" stroke-miterlimit="10" stroke-width="0.75"/>
|
||||
|
||||
<path d="M710.77,332.4c0,8.24-5.5,8.24-5.5,8.24l-15,5.37c-6.68,2.23-9.44,1.89-10.5,1.46a1.62,1.62,0,0,0-1.36.1c.36-.24,1.68-1.46-.08-5l-5.63-8.42a1.13,1.13,0,0,1,.39-1.6,1.07,1.07,0,0,1,.55-.14,1.12,1.12,0,0,1,.91.46l7.14,9.93s1.07,4.29,15-2.15C696.69,340.64,708.75,336,710.77,332.4Z" opacity="0.2"/>
|
||||
|
||||
<path d="M674.48,349.38h0c-2.89.58-2.48-2.31-2.48-2.31a12.77,12.77,0,0,0,.2-1.54,9.91,9.91,0,0,0-5.8-9.37,26.59,26.59,0,0,0-4.77-1.68s8.22-3.51,12.51,7.22C674.14,341.71,675.46,346.35,674.48,349.38Z" opacity="0.2"/>
|
||||
|
||||
<path d="M677.36,323.46s-14,5.89-18.8,11l-8.33,1.91H608.39L570,335.27a98.24,98.24,0,0,1,12.58-65.48c.3,0,8.89,2.15,17.47,32.21,0,0,8.53,31.81,22,30.4h23.5s21.08.73,21.08-13.23V297.7s.65-8.42,4.09-3.13L681.66,317S683.8,321.32,677.36,323.46Z" opacity="0.2"/>
|
||||
|
||||
<path d="M680.58,258c-8.42,6.71-12.77,17.28-12.88,27.91-.1-1.33-.27-2.68-.25-4,0-9.45,4.89-19.05,13.13-23.89Z" opacity="0.2"/>
|
||||
|
||||
<path d="M640.87,324h0a.54.54,0,0,1-.52-.55l1.07-32.21c0-21.29,5.35-36.51,5.4-36.66a.54.54,0,0,1,.69-.32.53.53,0,0,1,.32.68c0,.15-5.33,15.2-5.33,36.32l-1.08,32.22A.54.54,0,0,1,640.87,324Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M614,327.22h0a.54.54,0,0,1-.52-.55l1.08-31.13a208.17,208.17,0,0,1,2.69-33.9.53.53,0,0,1,.62-.43.54.54,0,0,1,.43.63,208.45,208.45,0,0,0-2.67,33.71l-1.07,31.15A.55.55,0,0,1,614,327.22Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<g opacity="0.2">
|
||||
|
||||
<path d="M640.87,324h0a.54.54,0,0,1-.52-.55l1.07-32.21c0-21.29,5.35-36.51,5.4-36.66a.54.54,0,0,1,.69-.32.53.53,0,0,1,.32.68c0,.15-5.33,15.2-5.33,36.32l-1.08,32.22A.54.54,0,0,1,640.87,324Z"/>
|
||||
|
||||
<path d="M614,327.22h0a.54.54,0,0,1-.52-.55l1.08-31.13a208.17,208.17,0,0,1,2.69-33.9.53.53,0,0,1,.62-.43.54.54,0,0,1,.43.63,208.45,208.45,0,0,0-2.67,33.71l-1.07,31.15A.55.55,0,0,1,614,327.22Z"/>
|
||||
|
||||
</g>
|
||||
|
||||
<path d="M706.34,371.77c0,.17,0,8.76-2.14,25.76l-.34,2.43a58.67,58.67,0,0,0-.52,7.27c-.06,3.84-2.56,11.15-21.3,8.21.49-.83.94-1.65,1.36-2.49l.35-.68c1.57-3,3.69-7.58,4.35-11.52,0,0-59-5.36-78.37-12.88,0,0-28.3-11.81-38.84-8.05l-8.39,7s-7.51-2.15,3.22-15Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M688.1,400.75s13.8.92,13.29,7a4,4,0,0,1-1.61,2.85c-1.84,1.41-6.25,3.29-16.15,1.89A63,63,0,0,0,688.1,400.75Z" opacity="0.2"/>
|
||||
|
||||
<path d="M564.14,385.44s-6.33-4,6.75-5.62Z" opacity="0.2"/>
|
||||
|
||||
<path d="M688.1,400.75a53.84,53.84,0,0,1-4.35,11.52l-.35.68c-.23.43-.44.81-.63,1.15s-.47.89-.73,1.34c-13.3,23.42-41.56,44.44-53.06,52.37a12.19,12.19,0,0,0-5.27,9.22c-2.38,36.84-19.34,68.64-19.34,68.64-3.61,5.17-9,7.66-15,8.46-19.1,2.58-45.12-11.68-45.12-11.68l14-39.72c11.81-44,23.61-56.89,23.61-56.89l26.54-55.12,1.37-2.85C629.06,395.39,688.1,400.75,688.1,400.75Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M688.1,400.75a53.84,53.84,0,0,1-4.35,11.52l-.35.68c-.23.43-.44.81-.63,1.15s-.47.89-.73,1.34c-13.3,23.42-41.56,44.44-53.06,52.37a12.19,12.19,0,0,0-5.27,9.22c-2.38,36.84-19.34,68.64-19.34,68.64-3.61,5.17-9,7.66-15,8.46,20-25.72,31.27-83,31.27-83-.89-13.33,38.47-47.86,38.47-47.86,15-15-4.29-19.33-4.29-19.33-17.27-2.72-39.92-10.84-46.46-13.25l1.37-2.85C629.06,395.39,688.1,400.75,688.1,400.75Z" opacity="0.2"/>
|
||||
|
||||
<path d="M589.37,430.17l-33.31,13.52,13,26s-5.54,16.19-11.4,35.76c0,0-13.42,9.12-27.37,8.05,0,0-11.81-30.06-18.25-61.19,0,0-8.59-18.25,6.44-30.06L562,387.05l8.93-7.23s6.64-4.83,38.84,8.05Z" fill="#282728" data-secondary="true"/>
|
||||
|
||||
<path d="M547.62,532.88,526.35,549.3a27.56,27.56,0,0,1-6.81,3.93c-3.21,1.25-7.85,4-6.84,9.17a8.45,8.45,0,0,0,5.07,6.09c2.55,1.06,6.59,1.78,12.53.37l11.81-9.23,7.47-14.54-5.33-2.64Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M547.62,532.88,526.35,549.3a27.56,27.56,0,0,1-6.81,3.93c-3.21,1.25-7.85,4-6.84,9.17a8.45,8.45,0,0,0,5.07,6.09c2.55,1.06,6.59,1.78,12.53.37l11.81-9.23,7.47-14.54-5.33-2.64Z" opacity="0.2"/>
|
||||
|
||||
<path d="M589.37,430.17l-33.31,13.52,13,26a370.36,370.36,0,0,1-11.89,36s-12.93,8.88-26.88,7.81c39.72-3.22,20.78-67.22,20.78-67.22-4.38-9.78,2.19-12.65,6.18-13.48a53.87,53.87,0,0,0,7.5-2.12l10.06-3.7a29.52,29.52,0,0,0,16.91-15.53,27.2,27.2,0,0,0,2.23-8.31,14.25,14.25,0,0,0-10.3-15c-8.42-2.48-16.86-1.84-21.7-1.1l8.93-7.23s6.64-4.83,38.84,8.05Z" opacity="0.2"/>
|
||||
|
||||
<path d="M512.56,561c8,4.49,17.84,3.92,26,.25,1.19-.51,2.33-1.14,3.54-1.66-1.08.73-2.18,1.45-3.3,2.13-8,4.53-18.78,5.1-26.25-.72Z" opacity="0.2"/>
|
||||
|
||||
<path d="M524.65,550.52c4.14,1.84,12.18,6.84,12.1,11.9-.72-2.81-3.3-4.59-5.35-6.46s-4.5-3.57-6.75-5.44Z" opacity="0.2"/>
|
||||
|
||||
<path d="M530.3,546.25c3.1.44,5.52,3.24,6.45,6.1-2.23-2.05-4-4.27-6.45-6.1Z" opacity="0.2"/>
|
||||
|
||||
<path d="M534.32,543.14a9.55,9.55,0,0,1,6.62,6.16c-2.17-2.19-4.35-4-6.62-6.16Z" opacity="0.2"/>
|
||||
|
||||
<path d="M538.89,539.61a11.81,11.81,0,0,1,5.36,6.61,29.31,29.31,0,0,1-5.36-6.61Z" opacity="0.2"/>
|
||||
|
||||
<rect x="421.88" y="364.26" width="328.48" height="7.51" fill="#b3b3b3"/>
|
||||
|
||||
<polygon points="750.36 364.26 750.36 371.77 786.86 342.79 786.88 336.37 750.36 364.26" fill="#999"/>
|
||||
|
||||
<path d="M507.76,344.93h98.07l-7.33-63.74a5.61,5.61,0,0,0-5.57-5h-90a3,3,0,0,0-2.93,3.31Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M605.83,344.93H507.76L500,279.54a3,3,0,0,1,2.95-3.31h90a5.61,5.61,0,0,1,5.56,5Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<polygon points="583.53 276.23 507.76 341.71 506.12 329.04 567.52 276.23 583.53 276.23" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M517.07,344.93l79.55-67.31a6,6,0,0,1,1.88,3.57l.38,3.34-71.09,60.4Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<rect x="507.76" y="344.93" width="94.46" height="6.44" fill="#787878" data-primary="true"/>
|
||||
|
||||
<rect x="602.22" y="344.93" width="29.49" height="6.44" fill="#787878" data-primary="true"/>
|
||||
|
||||
<rect x="602.22" y="344.93" width="29.49" height="6.44" opacity="0.2"/>
|
||||
|
||||
<polygon points="419.73 353.52 466.38 353.52 499.97 333.94 459.85 333.94 419.73 353.52" fill="#fff"/>
|
||||
|
||||
<rect x="419.73" y="353.52" width="46.65" height="4.65" fill="#e6e6e6"/>
|
||||
|
||||
<polygon points="499.97 333.94 499.97 339.8 466.38 358.17 466.38 353.52 499.97 333.94" fill="#ccc"/>
|
||||
|
||||
<polygon points="499.97 333.94 499.97 339.8 466.38 358.17 466.38 353.52 499.97 333.94" opacity="0.1"/>
|
||||
|
||||
<path d="M658.56,334.46s-13.47,1.87-20.95,12.08c0,0-10.05,9.15-.18,7.53,0,0,.47,4.68,8.39,1.53,0,0,1.37,3.31,10-1.53,0,0,8.64-4.84,16.16-7C672,347.08,675.17,334.71,658.56,334.46Z" fill="#f9b499"/>
|
||||
|
||||
<path d="M646,343.86a40.12,40.12,0,0,1-8.55,10.21A40.49,40.49,0,0,1,646,343.86Z" fill="#f7a48b"/>
|
||||
|
||||
<path d="M645.82,355.6a24.61,24.61,0,0,1,6.85-7.82,24.71,24.71,0,0,1-6.85,7.82Z" fill="#f7a48b"/>
|
||||
|
||||
<ellipse cx="638.72" cy="215.58" rx="6.44" ry="8.05" fill="#f9b499"/>
|
||||
|
||||
<path d="M640.87,228.89s12.2-4.93,24.24-3.72a26.56,26.56,0,0,1,17.33,9.17c4.85,5.6,11.54,15.1,4.38,18.3a8.59,8.59,0,0,1-7.29-.33c-5-2.49-17.91-6.91-47.79,12.65l27.37-23.08s6.49-6.48-3.5-8.53a13.52,13.52,0,0,0-2.62-.25,46.27,46.27,0,0,0-10,1.27S640.87,230.93,640.87,228.89Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M640.87,228.89s12.2-4.93,24.24-3.72a26.56,26.56,0,0,1,17.33,9.17c4.85,5.6,11.54,15.1,4.38,18.3a8.59,8.59,0,0,1-7.29-.33c-5-2.49-17.91-6.91-47.79,12.65l27.37-23.08s6.49-6.48-3.5-8.53a13.52,13.52,0,0,0-2.62-.25,46.27,46.27,0,0,0-10,1.27S640.87,230.93,640.87,228.89Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M674.14,234.37c-5.73,6.95-13.48,12.06-21.25,16.49-1.15.58-2.28,1.2-3.44,1.76,8.36-5.92,17-11.41,24.69-18.25Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M683.8,238.66C679,244,671.85,246.84,664.89,248c6.47-2.57,13.26-5.24,18.91-9.35Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<path d="M625.84,265.5c-4.44-2.67-21.36-6.8-27.08-8.15a23.81,23.81,0,0,0-3.37-.5c-4.81-.45-3.9-3.16-3.9-3.16,0-4.29,6.63-7.09,6.63-7.09,6,6.11,13.48,9.26,20.74,8Z" fill="#787878" data-primary="true"/>
|
||||
|
||||
<path d="M625.84,265.5c-4.44-2.67-21.36-6.8-27.08-8.15a23.81,23.81,0,0,0-3.37-.5c-4.81-.45-3.9-3.16-3.9-3.16,0-4.29,6.63-7.09,6.63-7.09,6,6.11,13.48,9.26,20.74,8Z" fill="#fff" opacity="0.3"/>
|
||||
|
||||
<circle cx="551.23" cy="311.12" r="8.05" fill="#fff"/>
|
||||
|
||||
</svg>
|
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 273 KiB |
Before Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 145 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
@ -1,6 +1,4 @@
|
|||
import type { SocialIcons } from "../types";
|
||||
|
||||
const socialIcons: SocialIcons = {
|
||||
const socialIcons = {
|
||||
Github: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
|
@ -57,14 +55,14 @@ const socialIcons: SocialIcons = {
|
|||
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
|
||||
<polyline points="3 7 12 13 21 7"></polyline>
|
||||
</svg>`,
|
||||
Twitter: `<svg
|
||||
X: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M22 4.01c-1 .49 -1.98 .689 -3 .99c-1.121 -1.265 -2.783 -1.335 -4.38 -.737s-2.643 2.06 -2.62 3.737v1c-3.245 .083 -6.135 -1.395 -8 -4c0 0 -4.182 7.433 4 11c-1.872 1.247 -3.739 2.088 -6 2c3.308 1.803 6.913 2.423 10.034 1.517c3.58 -1.04 6.522 -3.723 7.651 -7.742a13.84 13.84 0 0 0 .497 -3.753c-.002 -.249 1.51 -2.772 1.818 -4.013z"></path>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M4 4l11.733 16h4.267l-11.733 -16z" /><path d="M4 20l6.768 -6.768m2.46 -2.46l6.772 -6.772" />
|
||||
</svg>`,
|
||||
Twitch: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
@ -10,13 +10,25 @@ const breadcrumbList = currentUrlPath.split("/").slice(1);
|
|||
// replace Posts with Posts (page number)
|
||||
breadcrumbList[0] === "posts" &&
|
||||
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
|
||||
|
||||
// if breadcrumb is Home > Tags > [tag] > [page] <etc>
|
||||
// replace [tag] > [page] with [tag] (page number)
|
||||
breadcrumbList[0] === "tags" &&
|
||||
!isNaN(Number(breadcrumbList[2])) &&
|
||||
breadcrumbList.splice(
|
||||
1,
|
||||
3,
|
||||
`${breadcrumbList[1]} ${
|
||||
Number(breadcrumbList[2]) === 1 ? "" : "(page " + breadcrumbList[2] + ")"
|
||||
}`
|
||||
);
|
||||
---
|
||||
|
||||
<nav class="breadcrumb" aria-label="breadcrumb">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
<span aria-hidden="true"> > </span>
|
||||
<span aria-hidden="true">»</span>
|
||||
</li>
|
||||
{
|
||||
breadcrumbList.map((breadcrumb, index) =>
|
||||
|
@ -27,13 +39,13 @@ breadcrumbList[0] === "posts" &&
|
|||
aria-current="page"
|
||||
>
|
||||
{/* make the last part lowercase in Home > Tags > some-tag */}
|
||||
{breadcrumb}
|
||||
{decodeURIComponent(breadcrumb)}
|
||||
</span>
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
<a href={`/${breadcrumb}`}>{breadcrumb}</a>
|
||||
<span aria-hidden="true"> > </span>
|
||||
<a href={`/${breadcrumb}/`}>{breadcrumb}</a>
|
||||
<span aria-hidden="true">»</span>
|
||||
</li>
|
||||
)
|
||||
)
|
||||
|
|
|
@ -9,7 +9,7 @@ export interface Props {
|
|||
}
|
||||
|
||||
export default function Card({ href, frontmatter, secHeading = true }: Props) {
|
||||
const { title, pubDatetime, description } = frontmatter;
|
||||
const { title, pubDatetime, modDatetime, description } = frontmatter;
|
||||
|
||||
const headerProps = {
|
||||
style: { viewTransitionName: slugifyStr(title) },
|
||||
|
@ -28,7 +28,7 @@ export default function Card({ href, frontmatter, secHeading = true }: Props) {
|
|||
<h3 {...headerProps}>{title}</h3>
|
||||
)}
|
||||
</a>
|
||||
<Datetime datetime={pubDatetime} />
|
||||
<Datetime pubDatetime={pubDatetime} modDatetime={modDatetime} />
|
||||
<p>{description}</p>
|
||||
</li>
|
||||
);
|
||||
|
|
|
@ -1,52 +1,120 @@
|
|||
import { LOCALE } from "@config";
|
||||
import { LOCALE, SITE } from "@config";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export interface Props {
|
||||
datetime: string | Date;
|
||||
interface DatetimesProps {
|
||||
pubDatetime: string | Date;
|
||||
modDatetime: string | Date | undefined | null;
|
||||
}
|
||||
|
||||
interface EditPostProps {
|
||||
editPost?: CollectionEntry<"blog">["data"]["editPost"];
|
||||
postId?: CollectionEntry<"blog">["id"];
|
||||
}
|
||||
|
||||
interface Props extends DatetimesProps, EditPostProps {
|
||||
size?: "sm" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Datetime({ datetime, size = "sm", className }: Props) {
|
||||
export default function Datetime({
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
size = "sm",
|
||||
className = "",
|
||||
editPost,
|
||||
postId,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className={`flex items-center space-x-2 opacity-80 ${className}`}>
|
||||
<div
|
||||
className={`flex items-center space-x-2 opacity-80 ${className}`.trim()}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`${
|
||||
size === "sm" ? "scale-90" : "scale-100"
|
||||
} inline-block h-6 w-6 fill-skin-base`}
|
||||
} inline-block h-6 w-6 min-w-[1.375rem] fill-skin-base`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M7 11h2v2H7zm0 4h2v2H7zm4-4h2v2h-2zm0 4h2v2h-2zm4-4h2v2h-2zm0 4h2v2h-2z"></path>
|
||||
<path d="M5 22h14c1.103 0 2-.897 2-2V6c0-1.103-.897-2-2-2h-2V2h-2v2H9V2H7v2H5c-1.103 0-2 .897-2 2v14c0 1.103.897 2 2 2zM19 8l.001 12H5V8h14z"></path>
|
||||
</svg>
|
||||
<span className="sr-only">Posted on:</span>
|
||||
{modDatetime && modDatetime > pubDatetime ? (
|
||||
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
|
||||
<FormattedDatetime datetime={datetime} />
|
||||
Updated:
|
||||
</span>
|
||||
) : (
|
||||
<span className="sr-only">Published:</span>
|
||||
)}
|
||||
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
|
||||
<FormattedDatetime
|
||||
pubDatetime={pubDatetime}
|
||||
modDatetime={modDatetime}
|
||||
/>
|
||||
{size === "lg" && <EditPost editPost={editPost} postId={postId} />}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FormattedDatetime = ({ datetime }: { datetime: string | Date }) => {
|
||||
const myDatetime = new Date(datetime);
|
||||
const FormattedDatetime = ({ pubDatetime, modDatetime }: DatetimesProps) => {
|
||||
const myDatetime = new Date(
|
||||
modDatetime && modDatetime > pubDatetime ? modDatetime : pubDatetime
|
||||
);
|
||||
|
||||
const date = myDatetime.toLocaleDateString(LOCALE, {
|
||||
const date = myDatetime.toLocaleDateString(LOCALE.langTag, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const time = myDatetime.toLocaleTimeString(LOCALE, {
|
||||
const time = myDatetime.toLocaleTimeString(LOCALE.langTag, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{date}
|
||||
<time dateTime={myDatetime.toISOString()}>{date}</time>
|
||||
<span aria-hidden="true"> | </span>
|
||||
<span className="sr-only"> at </span>
|
||||
{time}
|
||||
<span className="text-nowrap">{time}</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EditPost = ({ editPost, postId }: EditPostProps) => {
|
||||
let editPostUrl = editPost?.url ?? SITE?.editPost?.url ?? "";
|
||||
const showEditPost = !editPost?.disabled && editPostUrl.length > 0;
|
||||
const appendFilePath =
|
||||
editPost?.appendFilePath ?? SITE?.editPost?.appendFilePath ?? false;
|
||||
if (appendFilePath && postId) {
|
||||
editPostUrl += `/${postId}`;
|
||||
}
|
||||
const editPostText = editPost?.text ?? SITE?.editPost?.text ?? "Edit";
|
||||
|
||||
return (
|
||||
showEditPost && (
|
||||
<>
|
||||
<span aria-hidden="true"> | </span>
|
||||
<a
|
||||
className="space-x-1.5 hover:opacity-75"
|
||||
href={editPostUrl}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="icon icon-tabler icons-tabler-outline icon-tabler-edit inline-block !scale-90 fill-skin-base"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1" />
|
||||
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z" />
|
||||
<path d="M16 5l3 3" />
|
||||
</svg>
|
||||
<span className="text-base italic">{editPostText}</span>
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,7 +16,9 @@ const { noMarginTop = false } = Astro.props;
|
|||
<div class="footer-wrapper">
|
||||
<Socials centered />
|
||||
<div class="copyright-wrapper">
|
||||
<span>Copyright © {currentYear}, EllieBotDevs</span>
|
||||
<span>Copyright © {currentYear} EllieBotDevs</span>
|
||||
<span class="separator"> | </span>
|
||||
<span>All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
|
@ -4,7 +4,7 @@ import Hr from "./Hr.astro";
|
|||
import LinkButton from "./LinkButton.astro";
|
||||
|
||||
export interface Props {
|
||||
activeNav?: "posts" | "tags" | "about" | "search";
|
||||
activeNav?: "posts" | "archives" | "tags" | "about" | "search";
|
||||
}
|
||||
|
||||
const { activeNav } = Astro.props;
|
||||
|
@ -56,23 +56,58 @@ const { activeNav } = Astro.props;
|
|||
</button>
|
||||
<ul id="menu-items" class="display-none sm:flex">
|
||||
<li>
|
||||
<a href="/posts" class={activeNav === "posts" ? "active" : ""}>
|
||||
<a href="/posts/" class={activeNav === "posts" ? "active" : ""}>
|
||||
Posts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/tags" class={activeNav === "tags" ? "active" : ""}>
|
||||
<a href="/tags/" class={activeNav === "tags" ? "active" : ""}>
|
||||
Tags
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about" class={activeNav === "about" ? "active" : ""}>
|
||||
<a href="/about/" class={activeNav === "about" ? "active" : ""}>
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
{
|
||||
SITE.showArchives && (
|
||||
<li>
|
||||
<LinkButton
|
||||
href="/search"
|
||||
href="/archives/"
|
||||
className={`focus-outline flex justify-center p-3 sm:p-1`}
|
||||
ariaLabel="archives"
|
||||
title="Archives"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class:list={[
|
||||
"icon icon-tabler icons-tabler-outline !hidden sm:!inline-block",
|
||||
activeNav === "archives" && "!stroke-skin-accent",
|
||||
]}
|
||||
>
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v0a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
|
||||
<path d="M5 8v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2 -2v-10" />
|
||||
<path d="M10 12l4 0" />
|
||||
</>
|
||||
</svg>
|
||||
<span
|
||||
class:list={[
|
||||
"sm:sr-only",
|
||||
activeNav === "archives" && "active",
|
||||
]}
|
||||
>
|
||||
Archives
|
||||
</span>
|
||||
</LinkButton>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
<li>
|
||||
<LinkButton
|
||||
href="/search/"
|
||||
className={`focus-outline p-3 sm:p-1 ${
|
||||
activeNav === "search" ? "active" : ""
|
||||
} flex`}
|
||||
|
@ -86,11 +121,12 @@ const { activeNav } = Astro.props;
|
|||
d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"
|
||||
></path>
|
||||
</svg>
|
||||
<span class="sr-only">Search</span>
|
||||
</LinkButton>
|
||||
</li>
|
||||
<li>
|
||||
{
|
||||
SITE.lightAndDarkMode && (
|
||||
<li>
|
||||
<button
|
||||
id="theme-btn"
|
||||
class="focus-outline"
|
||||
|
@ -105,9 +141,9 @@ const { activeNav } = Astro.props;
|
|||
<path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z" />
|
||||
</svg>
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -154,7 +190,7 @@ const { activeNav } = Astro.props;
|
|||
nav ul li:nth-last-child(2) {
|
||||
@apply col-span-1;
|
||||
}
|
||||
nav a.active {
|
||||
nav .active {
|
||||
@apply underline decoration-wavy decoration-2 underline-offset-4;
|
||||
}
|
||||
nav a.active svg {
|
||||
|
|
|
@ -7,22 +7,32 @@ export interface Props {
|
|||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const { href, className, ariaLabel, title, disabled = false } = Astro.props;
|
||||
const {
|
||||
href,
|
||||
className = "",
|
||||
ariaLabel,
|
||||
title,
|
||||
disabled = false,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
href={disabled ? "#" : href}
|
||||
tabindex={disabled ? "-1" : "0"}
|
||||
class={`group inline-block ${className}`}
|
||||
aria-label={ariaLabel}
|
||||
{
|
||||
disabled ? (
|
||||
<span
|
||||
class:list={["group inline-block", className]}
|
||||
title={title}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
@apply hover:text-skin-accent;
|
||||
}
|
||||
</style>
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
{href}
|
||||
class:list={["group inline-block hover:text-skin-accent", className]}
|
||||
aria-label={ariaLabel}
|
||||
title={title}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
|
59
src/components/Pagination.astro
Normal file
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
import type { Page } from "astro";
|
||||
import LinkButton from "./LinkButton.astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export interface Props {
|
||||
page: Page<CollectionEntry<"blog">>;
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
page.lastPage > 1 && (
|
||||
<nav class="pagination-wrapper" aria-label="Pagination">
|
||||
<LinkButton
|
||||
disabled={!page.url.prev}
|
||||
href={page.url.prev as string}
|
||||
className={`mr-4 select-none ${page.url.prev ? "" : "disabled"}`}
|
||||
ariaLabel="Previous"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class:list={[{ "disabled-svg": !page.url.prev }]}
|
||||
>
|
||||
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
|
||||
</svg>
|
||||
Prev
|
||||
</LinkButton>
|
||||
{page.currentPage} / {page.lastPage}
|
||||
<LinkButton
|
||||
disabled={!page.url.next}
|
||||
href={page.url.next as string}
|
||||
className={`mx-4 select-none ${page.url.next ? "" : "disabled"}`}
|
||||
ariaLabel="Next"
|
||||
>
|
||||
Next
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class:list={[{ "disabled-svg": !page.url.next }]}
|
||||
>
|
||||
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
|
||||
</svg>
|
||||
</LinkButton>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
<style>
|
||||
.pagination-wrapper {
|
||||
@apply mb-8 mt-auto flex justify-center;
|
||||
}
|
||||
.disabled {
|
||||
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
|
||||
}
|
||||
.disabled-svg {
|
||||
@apply group-hover:!fill-skin-base;
|
||||
}
|
||||
</style>
|
|
@ -1,13 +1,13 @@
|
|||
import Fuse from "fuse.js";
|
||||
import { useEffect, useRef, useState, useMemo } from "react";
|
||||
import { useEffect, useRef, useState, useMemo, type FormEvent } from "react";
|
||||
import Card from "@components/Card";
|
||||
import slugify from "@utils/slugify";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export type SearchItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
data: CollectionEntry<"blog">["data"];
|
||||
slug: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
|
@ -26,7 +26,7 @@ export default function SearchBar({ searchList }: Props) {
|
|||
null
|
||||
);
|
||||
|
||||
const handleChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
const handleChange = (e: FormEvent<HTMLInputElement>) => {
|
||||
setInputVal(e.currentTarget.value);
|
||||
};
|
||||
|
||||
|
@ -58,7 +58,7 @@ export default function SearchBar({ searchList }: Props) {
|
|||
useEffect(() => {
|
||||
// Add search result only if
|
||||
// input value is more than one character
|
||||
let inputResult = inputVal.length > 1 ? fuse.search(inputVal) : [];
|
||||
const inputResult = inputVal.length > 1 ? fuse.search(inputVal) : [];
|
||||
setSearchResults(inputResult);
|
||||
|
||||
// Update search string in URL
|
||||
|
@ -73,6 +73,13 @@ export default function SearchBar({ searchList }: Props) {
|
|||
}
|
||||
}, [inputVal]);
|
||||
|
||||
useEffect(() => {
|
||||
// focus on text input when search bar is displayed
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [inputVal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="relative block">
|
||||
|
@ -80,19 +87,17 @@ export default function SearchBar({ searchList }: Props) {
|
|||
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M19.023 16.977a35.13 35.13 0 0 1-1.367-1.384c-.372-.378-.596-.653-.596-.653l-2.8-1.337A6.962 6.962 0 0 0 16 9c0-3.859-3.14-7-7-7S2 5.141 2 9s3.14 7 7 7c1.763 0 3.37-.66 4.603-1.739l1.337 2.8s.275.224.653.596c.387.363.896.854 1.384 1.367l1.358 1.392.604.646 2.121-2.121-.646-.604c-.379-.372-.885-.866-1.391-1.36zM9 14c-2.757 0-5-2.243-5-5s2.243-5 5-5 5 2.243 5 5-2.243 5-5 5z"></path>
|
||||
</svg>
|
||||
<span className="sr-only">Search</span>
|
||||
</span>
|
||||
<input
|
||||
className="block w-full rounded border border-skin-fill
|
||||
border-opacity-40 bg-skin-fill py-3 pl-10
|
||||
pr-3 placeholder:italic placeholder:text-opacity-75
|
||||
focus:border-skin-accent focus:outline-none"
|
||||
className="block w-full rounded border border-skin-fill/40 bg-skin-fill py-3 pl-10 pr-3 placeholder:italic focus:border-skin-accent focus:outline-none"
|
||||
placeholder="Search for anything..."
|
||||
type="text"
|
||||
name="search"
|
||||
value={inputVal}
|
||||
onChange={handleChange}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
// autoFocus
|
||||
ref={inputRef}
|
||||
/>
|
||||
</label>
|
||||
|
@ -111,9 +116,9 @@ export default function SearchBar({ searchList }: Props) {
|
|||
{searchResults &&
|
||||
searchResults.map(({ item, refIndex }) => (
|
||||
<Card
|
||||
href={`/posts/${slugify(item.data)}`}
|
||||
href={`/posts/${item.slug}/`}
|
||||
frontmatter={item.data}
|
||||
key={`${refIndex}-${slugify(item.data)}`}
|
||||
key={`${refIndex}-${item.slug}`}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
|
41
src/components/ShareLinks.astro
Normal file
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import LinkButton from "./LinkButton.astro";
|
||||
import socialIcons from "@assets/socialIcons";
|
||||
|
||||
const URL = Astro.url;
|
||||
|
||||
const shareLinks = [
|
||||
{
|
||||
name: "Mail",
|
||||
href: "mailto:?subject=See%20this%20post&body=",
|
||||
linkTitle: `Share this post via email`,
|
||||
},
|
||||
] as const;
|
||||
---
|
||||
|
||||
<div class={`social-icons`}>
|
||||
<span class="italic">Share this post on:</span>
|
||||
<div class="text-center">
|
||||
{
|
||||
shareLinks.map(social => (
|
||||
<LinkButton
|
||||
href={`${social.href + URL}`}
|
||||
className="link-button"
|
||||
title={social.linkTitle}
|
||||
>
|
||||
<Fragment set:html={socialIcons[social.name]} />
|
||||
<span class="sr-only">{social.linkTitle}</span>
|
||||
</LinkButton>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.social-icons {
|
||||
@apply flex flex-col flex-wrap items-center justify-center gap-1 sm:items-start;
|
||||
}
|
||||
.link-button {
|
||||
@apply scale-90 p-2 hover:rotate-6 sm:p-1;
|
||||
}
|
||||
</style>
|
|
@ -19,6 +19,7 @@ const { centered = false } = Astro.props;
|
|||
title={social.linkTitle}
|
||||
>
|
||||
<Fragment set:html={socialIcons[social.name]} />
|
||||
<span class="sr-only">{social.linkTitle}</span>
|
||||
</LinkButton>
|
||||
))
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
---
|
||||
export interface Props {
|
||||
name: string;
|
||||
tag: string;
|
||||
size?: "sm" | "lg";
|
||||
}
|
||||
|
||||
const { name, size = "sm" } = Astro.props;
|
||||
const { tag, size = "sm" } = Astro.props;
|
||||
---
|
||||
|
||||
<li
|
||||
|
@ -13,8 +13,8 @@ const { name, size = "sm" } = Astro.props;
|
|||
}`}
|
||||
>
|
||||
<a
|
||||
href={`/tags/${name.toLowerCase()}`}
|
||||
transition:name={name.toLowerCase()}
|
||||
href={`/tags/${tag}/`}
|
||||
transition:name={tag}
|
||||
class={`${size === "sm" ? "text-sm" : "text-lg"} pr-2 group`}
|
||||
>
|
||||
<svg
|
||||
|
@ -24,7 +24,7 @@ const { name, size = "sm" } = Astro.props;
|
|||
d="M16.018 3.815 15.232 8h-4.966l.716-3.815-1.964-.37L8.232 8H4v2h3.857l-.751 4H3v2h3.731l-.714 3.805 1.965.369L8.766 16h4.966l-.714 3.805 1.965.369.783-4.174H20v-2h-3.859l.751-4H21V8h-3.733l.716-3.815-1.965-.37zM14.106 14H9.141l.751-4h4.966l-.752 4z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>{name.toLowerCase()}</span>
|
||||
<span>{tag}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
|
|
@ -2,40 +2,52 @@ import type { Site, SocialObjects } from "./types";
|
|||
|
||||
export const SITE: Site = {
|
||||
website: "https://blog.elliebot.net/", // replace this with your deployed domain
|
||||
author: "toastie_t0ast, mai_lanfiel, EllieBot_Team",
|
||||
author: "EllieBot Team",
|
||||
profile: "https://blog.elliebot.net",
|
||||
desc: "The tales of the Ellie dev team.",
|
||||
title: "Elliebot blog",
|
||||
ogImage: "",
|
||||
lightAndDarkMode: true,
|
||||
postPerPage: 3,
|
||||
postPerIndex: 4,
|
||||
postPerPage: 4,
|
||||
scheduledPostMargin: 15 * 60 * 1000, // 15 minutes
|
||||
showArchives: true,
|
||||
editPost: {
|
||||
url: "https://toastielab.dev/EllieBotDevs/Ellie-Blog/_edit/main/src/content/blog",
|
||||
text: "Suggest Changes",
|
||||
appendFilePath: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const LOCALE = ["en-EN"]; // set to [] to use the environment default
|
||||
export const LOCALE = {
|
||||
lang: "en",
|
||||
langTag: ["en-EN"],
|
||||
};
|
||||
|
||||
export const LOGO_IMAGE = {
|
||||
enable: false,
|
||||
svg: true,
|
||||
width: 216,
|
||||
height: 46,
|
||||
svg: false,
|
||||
width: 210,
|
||||
height: 40,
|
||||
};
|
||||
|
||||
export const SOCIALS: SocialObjects = [
|
||||
{
|
||||
name: "Mail",
|
||||
href: "mailto:contact@elliebot.net",
|
||||
linkTitle: `Send an email to me`,
|
||||
linkTitle: `Send an email to us`,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: "Discord",
|
||||
href: "https://discord.gg/etQdZxSyEH",
|
||||
linkTitle: `My Discord server`,
|
||||
linkTitle: `Our Discord server`,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: "Mastodon",
|
||||
href: "https://valkyriecoms.com/@EllieBotDevs",
|
||||
linkTitle: `My profile on Valkyriecoms`,
|
||||
linkTitle: `Our profile on Valkyriecoms`,
|
||||
active: true,
|
||||
},
|
||||
];
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
author: toastie_t0ast
|
||||
pubDatetime: 2022-11-14
|
||||
title: Another update
|
||||
slug: another-update
|
||||
description: Another small update
|
||||
tags:
|
||||
- devlog
|
|
@ -2,6 +2,7 @@
|
|||
author: toastie_t0ast
|
||||
pubDatetime: 2022-11-20
|
||||
title: Ellie bday 2022
|
||||
slug: ellie-bday-2022
|
||||
description: Ellie bot and project birthday
|
||||
tags:
|
||||
- devlog
|
Before Width: | Height: | Size: 118 KiB |
|
@ -2,6 +2,7 @@
|
|||
author: toastie_t0ast
|
||||
pubDatetime: 2022-08-05
|
||||
title: Small update
|
||||
slug: small-update
|
||||
description: small update on things
|
||||
tags:
|
||||
- devlog
|
|
@ -2,6 +2,7 @@
|
|||
author: mai_lanfiel
|
||||
pubDatetime: 2022-04-02
|
||||
title: State of development
|
||||
slug: state-of-development
|
||||
description: EllieBot state of development
|
||||
tags:
|
||||
- project-update
|
|
@ -2,6 +2,7 @@
|
|||
author: toastie_t0ast
|
||||
pubDatetime: 2022-07-02
|
||||
title: Update on things
|
||||
slug: update-on-things
|
||||
description: A small update on what we are up to.
|
||||
tags:
|
||||
- update
|
|
@ -2,6 +2,7 @@
|
|||
author: EllieBot_Team
|
||||
pubDatetime: 2022-07-02
|
||||
title: Updated links
|
||||
slug: updated-links
|
||||
description: About our updated links.
|
||||
tags:
|
||||
- announcement
|
|
@ -2,6 +2,7 @@
|
|||
author: toastie_t0ast, EllieBot_Team
|
||||
pubDatetime: 2023-07-31
|
||||
title: Where toastie has been
|
||||
slug: where-toastie-has-been
|
||||
description: Another small update
|
||||
tags:
|
||||
- devlog
|
28
src/content/blog/where-we-have-been.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
author: toastie_t0ast
|
||||
pubDatetime: 2025-01-22T01:13:22.000+13:00
|
||||
modDatetime:
|
||||
title: Where we have been
|
||||
slug: where-we-have-been
|
||||
featured: true
|
||||
draft: false
|
||||
tags:
|
||||
- project=update
|
||||
description: Where we have been and what we have been up to.
|
||||
---
|
||||
|
||||
Hi there, it has been a while since I last posted to this blog, the last time I posted here was back in 2023 but I am here to let you all know where on earth we have been.
|
||||
|
||||
First of all where the heck we have been. We have been hard at work on some fun projects mostly giving Ellie even more fun features for more details you can check out our [patchnotes site](https://notes.elliebot.net) as well as making our docs site even better.
|
||||
|
||||
On the topic of the docs it has had a massive overhaul with new guides as well as an easy to follow guide on how to make your own marmalade module.
|
||||
|
||||
We have also overhauled the Discord server to make it easier to find information.
|
||||
|
||||
On where I personally have been, I have been studying full time and working on Ellie when I have spare time.
|
||||
|
||||
Thanks for reading this update on what we have been up to all this time.
|
||||
|
||||
Till next time,
|
||||
|
||||
Toastie
|
|
@ -1,14 +1,16 @@
|
|||
import { SITE } from "@config";
|
||||
import { glob } from "astro/loaders";
|
||||
import { defineCollection, z } from "astro:content";
|
||||
|
||||
const blog = defineCollection({
|
||||
type: "content",
|
||||
type: "content_layer",
|
||||
loader: glob({ pattern: "**/*.md", base: "./src/content/blog" }),
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
author: z.string().default(SITE.author),
|
||||
pubDatetime: z.date(),
|
||||
modDatetime: z.date().optional().nullable(),
|
||||
title: z.string(),
|
||||
postSlug: z.string().optional(),
|
||||
featured: z.boolean().optional(),
|
||||
draft: z.boolean().optional(),
|
||||
tags: z.array(z.string()).default(["others"]),
|
||||
|
@ -20,6 +22,14 @@ const blog = defineCollection({
|
|||
.optional(),
|
||||
description: z.string(),
|
||||
canonicalURL: z.string().optional(),
|
||||
editPost: z
|
||||
.object({
|
||||
disabled: z.boolean().optional(),
|
||||
url: z.string().optional(),
|
||||
text: z.string().optional(),
|
||||
appendFilePath: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
import { SITE } from "@config";
|
||||
import { LOCALE, SITE } from "@config";
|
||||
import "@styles/base.css";
|
||||
import { ViewTransitions } from "astro:transitions";
|
||||
|
||||
|
@ -8,31 +8,58 @@ const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION;
|
|||
export interface Props {
|
||||
title?: string;
|
||||
author?: string;
|
||||
profile?: string;
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
canonicalURL?: string;
|
||||
pubDatetime?: Date;
|
||||
modDatetime?: Date | null;
|
||||
scrollSmooth?: boolean;
|
||||
}
|
||||
|
||||
const {
|
||||
title = SITE.title,
|
||||
author = SITE.author,
|
||||
profile = SITE.profile,
|
||||
description = SITE.desc,
|
||||
ogImage = SITE.ogImage,
|
||||
canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
scrollSmooth = false,
|
||||
} = Astro.props;
|
||||
|
||||
const socialImageURL = new URL(
|
||||
ogImage ?? SITE.ogImage ?? "og.png",
|
||||
Astro.url.origin
|
||||
).href;
|
||||
|
||||
const structuredData = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: `${title}`,
|
||||
image: `${socialImageURL}`,
|
||||
datePublished: `${pubDatetime?.toISOString()}`,
|
||||
...(modDatetime && { dateModified: modDatetime.toISOString() }),
|
||||
author: [
|
||||
{
|
||||
"@type": "Person",
|
||||
name: `${author}`,
|
||||
url: `${profile}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html
|
||||
lang=`${LOCALE.lang ?? "en"}`
|
||||
class={`${scrollSmooth && "scroll-smooth"}`}
|
||||
>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
|
@ -49,6 +76,24 @@ const socialImageURL = new URL(
|
|||
<meta property="og:url" content={canonicalURL} />
|
||||
<meta property="og:image" content={socialImageURL} />
|
||||
|
||||
<!-- Article Published/Modified time -->
|
||||
{
|
||||
pubDatetime && (
|
||||
<meta
|
||||
property="article:published_time"
|
||||
content={pubDatetime.toISOString()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
modDatetime && (
|
||||
<meta
|
||||
property="article:modified_time"
|
||||
content={modDatetime.toISOString()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={canonicalURL} />
|
||||
|
@ -56,12 +101,21 @@ const socialImageURL = new URL(
|
|||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={socialImageURL} />
|
||||
|
||||
<!-- Google JSON-LD Structured data -->
|
||||
<script
|
||||
type="application/ld+json"
|
||||
set:html={JSON.stringify(structuredData)}
|
||||
/>
|
||||
|
||||
<!-- Google Font -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,400;0,500;0,600;0,700;1,400;1,600&display=swap"
|
||||
rel="stylesheet"
|
||||
rel="preload"
|
||||
as="style"
|
||||
onload="this.onload=null; this.rel='stylesheet';"
|
||||
crossorigin
|
||||
/>
|
||||
|
||||
<meta name="theme-color" content="" />
|
||||
|
@ -80,7 +134,7 @@ const socialImageURL = new URL(
|
|||
|
||||
<ViewTransitions />
|
||||
|
||||
<script is:inline src="/toggle-theme.js"></script>
|
||||
<script is:inline src="/toggle-theme.js" async></script>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
|
|
|
@ -37,7 +37,7 @@ const { props } = Astro;
|
|||
|
||||
<style>
|
||||
#main-content {
|
||||
@apply mx-auto w-full max-w-3xl px-4 pb-12;
|
||||
@apply mx-auto w-full max-w-3xl px-4 pb-4;
|
||||
}
|
||||
#main-content h1 {
|
||||
@apply text-2xl font-semibold sm:text-3xl;
|
||||
|
|
|
@ -6,15 +6,27 @@ import Tag from "@components/Tag.astro";
|
|||
import Datetime from "@components/Datetime";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { slugifyStr } from "@utils/slugify";
|
||||
import ShareLinks from "@components/ShareLinks.astro";
|
||||
import { SITE } from "@config";
|
||||
|
||||
export interface Props {
|
||||
post: CollectionEntry<"blog">;
|
||||
posts: CollectionEntry<"blog">[];
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { post, posts } = Astro.props;
|
||||
|
||||
const { title, author, description, ogImage, canonicalURL, pubDatetime, tags } =
|
||||
post.data;
|
||||
const {
|
||||
title,
|
||||
author,
|
||||
description,
|
||||
ogImage,
|
||||
canonicalURL,
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
tags,
|
||||
editPost,
|
||||
} = post.data;
|
||||
|
||||
const { Content } = await post.render();
|
||||
|
||||
|
@ -23,20 +35,39 @@ const ogUrl = new URL(
|
|||
ogImageUrl ?? `/posts/${slugifyStr(title)}.png`,
|
||||
Astro.url.origin
|
||||
).href;
|
||||
|
||||
const layoutProps = {
|
||||
title: `${title} | ${SITE.title}`,
|
||||
author,
|
||||
description,
|
||||
pubDatetime,
|
||||
modDatetime,
|
||||
canonicalURL,
|
||||
ogImage: ogUrl,
|
||||
scrollSmooth: true,
|
||||
};
|
||||
|
||||
/* ========== Prev/Next Posts ========== */
|
||||
|
||||
const allPosts = posts.map(({ data: { title }, slug }) => ({
|
||||
slug,
|
||||
title,
|
||||
}));
|
||||
|
||||
const currentPostIndex = allPosts.findIndex(a => a.slug === post.slug);
|
||||
|
||||
const prevPost = currentPostIndex !== 0 ? allPosts[currentPostIndex - 1] : null;
|
||||
const nextPost =
|
||||
currentPostIndex !== allPosts.length ? allPosts[currentPostIndex + 1] : null;
|
||||
---
|
||||
|
||||
<Layout
|
||||
title={title}
|
||||
author={author}
|
||||
description={description}
|
||||
ogImage={ogUrl}
|
||||
canonicalURL={canonicalURL}
|
||||
>
|
||||
<Layout {...layoutProps}>
|
||||
<Header />
|
||||
|
||||
<div class="mx-auto flex w-full max-w-3xl justify-start px-2">
|
||||
<button
|
||||
class="focus-outline mb-2 mt-8 flex hover:opacity-75"
|
||||
onclick="history.back()"
|
||||
onclick="(() => (history.length === 1) ? window.location = '/' : history.back())()"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
|
@ -46,15 +77,108 @@ const ogUrl = new URL(
|
|||
</button>
|
||||
</div>
|
||||
<main id="main-content">
|
||||
<h1 transition:name={slugifyStr(title)} class="post-title">{title}</h1>
|
||||
<Datetime datetime={pubDatetime} size="lg" className="my-2" />
|
||||
<article id="article" role="article" class="prose mx-auto mt-8 max-w-3xl">
|
||||
<h1 transition:name={slugifyStr(title)} class="post-title inline-block">
|
||||
{title}
|
||||
</h1>
|
||||
<Datetime
|
||||
pubDatetime={pubDatetime}
|
||||
modDatetime={modDatetime}
|
||||
size="lg"
|
||||
className="my-2"
|
||||
editPost={editPost}
|
||||
postId={post.id}
|
||||
/>
|
||||
<article id="article" class="prose mx-auto mt-8 max-w-3xl">
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
<ul class="tags-container">
|
||||
{tags.map(tag => <Tag name={slugifyStr(tag)} />)}
|
||||
<ul class="my-8">
|
||||
{tags.map(tag => <Tag tag={slugifyStr(tag)} />)}
|
||||
</ul>
|
||||
|
||||
<div
|
||||
class="flex flex-col-reverse items-center justify-between gap-6 sm:flex-row-reverse sm:items-end sm:gap-4"
|
||||
>
|
||||
<button
|
||||
id="back-to-top"
|
||||
class="focus-outline whitespace-nowrap py-1 hover:opacity-75"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="rotate-90">
|
||||
<path
|
||||
d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>Back to Top</span>
|
||||
</button>
|
||||
|
||||
<ShareLinks />
|
||||
</div>
|
||||
|
||||
<hr class="my-6 border-dashed" />
|
||||
|
||||
<!-- Previous/Next Post Buttons -->
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{
|
||||
prevPost && (
|
||||
<a
|
||||
href={`/posts/${prevPost.slug}`}
|
||||
class="flex w-full gap-1 hover:opacity-75"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-left flex-none"
|
||||
>
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M15 6l-6 6l6 6" />
|
||||
</>
|
||||
</svg>
|
||||
<div>
|
||||
<span>Previous Post</span>
|
||||
<div class="text-sm text-skin-accent/85">{prevPost.title}</div>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
{
|
||||
nextPost && (
|
||||
<a
|
||||
href={`/posts/${nextPost.slug}`}
|
||||
class="flex w-full justify-end gap-1 text-right hover:opacity-75 sm:col-start-2"
|
||||
>
|
||||
<div>
|
||||
<span>Next Post</span>
|
||||
<div class="text-sm text-skin-accent/85">{nextPost.title}</div>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="icon icon-tabler icons-tabler-outline icon-tabler-chevron-right flex-none"
|
||||
>
|
||||
<>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M9 6l6 6l-6 6" />
|
||||
</>
|
||||
</svg>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
@ -66,7 +190,126 @@ const ogUrl = new URL(
|
|||
.post-title {
|
||||
@apply text-2xl font-semibold text-skin-accent;
|
||||
}
|
||||
.tags-container {
|
||||
@apply my-8;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script is:inline data-astro-rerun>
|
||||
/** Create a progress indicator
|
||||
* at the top */
|
||||
function createProgressBar() {
|
||||
// Create the main container div
|
||||
const progressContainer = document.createElement("div");
|
||||
progressContainer.className =
|
||||
"progress-container fixed top-0 z-10 h-1 w-full bg-skin-fill";
|
||||
|
||||
// Create the progress bar div
|
||||
const progressBar = document.createElement("div");
|
||||
progressBar.className = "progress-bar h-1 w-0 bg-skin-accent";
|
||||
progressBar.id = "myBar";
|
||||
|
||||
// Append the progress bar to the progress container
|
||||
progressContainer.appendChild(progressBar);
|
||||
|
||||
// Append the progress container to the document body or any other desired parent element
|
||||
document.body.appendChild(progressContainer);
|
||||
}
|
||||
createProgressBar();
|
||||
|
||||
/** Update the progress bar
|
||||
* when user scrolls */
|
||||
function updateScrollProgress() {
|
||||
document.addEventListener("scroll", () => {
|
||||
const winScroll =
|
||||
document.body.scrollTop || document.documentElement.scrollTop;
|
||||
const height =
|
||||
document.documentElement.scrollHeight -
|
||||
document.documentElement.clientHeight;
|
||||
const scrolled = (winScroll / height) * 100;
|
||||
if (document) {
|
||||
const myBar = document.getElementById("myBar");
|
||||
if (myBar) {
|
||||
myBar.style.width = scrolled + "%";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
updateScrollProgress();
|
||||
|
||||
/** Attaches links to headings in the document,
|
||||
* allowing sharing of sections easily */
|
||||
function addHeadingLinks() {
|
||||
const headings = Array.from(
|
||||
document.querySelectorAll("h2, h3, h4, h5, h6")
|
||||
);
|
||||
for (const heading of headings) {
|
||||
heading.classList.add("group");
|
||||
const link = document.createElement("a");
|
||||
link.className =
|
||||
"heading-link ml-2 opacity-0 group-hover:opacity-100 focus:opacity-100";
|
||||
link.href = "#" + heading.id;
|
||||
|
||||
const span = document.createElement("span");
|
||||
span.ariaHidden = "true";
|
||||
span.innerText = "#";
|
||||
link.appendChild(span);
|
||||
heading.appendChild(link);
|
||||
}
|
||||
}
|
||||
addHeadingLinks();
|
||||
|
||||
/** Attaches copy buttons to code blocks in the document,
|
||||
* allowing users to copy code easily. */
|
||||
function attachCopyButtons() {
|
||||
const copyButtonLabel = "Copy";
|
||||
const codeBlocks = Array.from(document.querySelectorAll("pre"));
|
||||
|
||||
for (const codeBlock of codeBlocks) {
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.style.position = "relative";
|
||||
|
||||
const copyButton = document.createElement("button");
|
||||
copyButton.className =
|
||||
"copy-code absolute right-3 -top-3 rounded bg-skin-card px-2 py-1 text-xs leading-4 text-skin-base font-medium";
|
||||
copyButton.innerHTML = copyButtonLabel;
|
||||
codeBlock.setAttribute("tabindex", "0");
|
||||
codeBlock.appendChild(copyButton);
|
||||
|
||||
// wrap codebock with relative parent element
|
||||
codeBlock?.parentNode?.insertBefore(wrapper, codeBlock);
|
||||
wrapper.appendChild(codeBlock);
|
||||
|
||||
copyButton.addEventListener("click", async () => {
|
||||
await copyCode(codeBlock, copyButton);
|
||||
});
|
||||
}
|
||||
|
||||
async function copyCode(block, button) {
|
||||
const code = block.querySelector("code");
|
||||
const text = code?.innerText;
|
||||
|
||||
await navigator.clipboard.writeText(text ?? "");
|
||||
|
||||
// visual feedback that task is completed
|
||||
button.innerText = "Copied";
|
||||
|
||||
setTimeout(() => {
|
||||
button.innerText = copyButtonLabel;
|
||||
}, 700);
|
||||
}
|
||||
}
|
||||
attachCopyButtons();
|
||||
|
||||
/** Scrolls the document to the top when
|
||||
* the "Back to Top" button is clicked. */
|
||||
function backToTop() {
|
||||
document.querySelector("#back-to-top")?.addEventListener("click", () => {
|
||||
document.body.scrollTop = 0; // For Safari
|
||||
document.documentElement.scrollTop = 0; // For Chrome, Firefox, IE and Opera
|
||||
});
|
||||
}
|
||||
backToTop();
|
||||
|
||||
/* Go to page start after page swap */
|
||||
document.addEventListener("astro:after-swap", () =>
|
||||
window.scrollTo({ left: 0, top: 0, behavior: "instant" })
|
||||
);
|
||||
</script>
|
||||
|
|
|
@ -1,24 +1,19 @@
|
|||
---
|
||||
import { SITE } from "@config";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Pagination from "@components/Pagination.astro";
|
||||
import Card from "@components/Card";
|
||||
import LinkButton from "@components/LinkButton.astro";
|
||||
import slugify from "@utils/slugify";
|
||||
import { SITE } from "@config";
|
||||
import type { Page } from "astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export interface Props {
|
||||
pageNum: number;
|
||||
totalPages: number;
|
||||
posts: CollectionEntry<"blog">[];
|
||||
page: Page<CollectionEntry<"blog">>;
|
||||
}
|
||||
|
||||
const { pageNum, totalPages, posts } = Astro.props;
|
||||
|
||||
const prev = pageNum > 1 ? "" : "disabled";
|
||||
const next = pageNum < totalPages ? "" : "disabled";
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`Posts | ${SITE.title}`}>
|
||||
|
@ -26,52 +21,14 @@ const next = pageNum < totalPages ? "" : "disabled";
|
|||
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
|
||||
<ul>
|
||||
{
|
||||
posts.map(({ data }) => (
|
||||
<Card href={`/posts/${slugify(data)}`} frontmatter={data} />
|
||||
page.data.map(({ data, slug }) => (
|
||||
<Card href={`/posts/${slug}/`} frontmatter={data} />
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Main>
|
||||
|
||||
{
|
||||
totalPages > 1 && (
|
||||
<nav class="pagination-wrapper" aria-label="Pagination">
|
||||
<LinkButton
|
||||
disabled={prev === "disabled"}
|
||||
href={`/posts${pageNum - 1 !== 1 ? "/" + (pageNum - 1) : ""}`}
|
||||
className={`mr-4 select-none ${prev}`}
|
||||
ariaLabel="Previous"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class={`${prev}-svg`}>
|
||||
<path d="M12.707 17.293 8.414 13H18v-2H8.414l4.293-4.293-1.414-1.414L4.586 12l6.707 6.707z" />
|
||||
</svg>
|
||||
Prev
|
||||
</LinkButton>
|
||||
<LinkButton
|
||||
disabled={next === "disabled"}
|
||||
href={`/posts/${pageNum + 1}`}
|
||||
className={`ml-4 select-none ${next}`}
|
||||
ariaLabel="Next"
|
||||
>
|
||||
Next
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class={`${next}-svg`}>
|
||||
<path d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z" />
|
||||
</svg>
|
||||
</LinkButton>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
<Footer noMarginTop={totalPages > 1} />
|
||||
</Layout>
|
||||
<Pagination {page} />
|
||||
|
||||
<style>
|
||||
.pagination-wrapper {
|
||||
@apply mb-8 mt-auto flex justify-center;
|
||||
}
|
||||
.disabled {
|
||||
@apply pointer-events-none select-none opacity-50 hover:text-skin-base group-hover:fill-skin-base;
|
||||
}
|
||||
.disabled-svg {
|
||||
@apply group-hover:!fill-skin-base;
|
||||
}
|
||||
</style>
|
||||
<Footer noMarginTop={page.lastPage > 1} />
|
||||
</Layout>
|
||||
|
|
41
src/layouts/TagPosts.astro
Normal file
|
@ -0,0 +1,41 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Card from "@components/Card";
|
||||
import Pagination from "@components/Pagination.astro";
|
||||
import { SITE } from "@config";
|
||||
import type { Page } from "astro";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export interface Props {
|
||||
page: Page<CollectionEntry<"blog">>;
|
||||
tag: string;
|
||||
tagName: string;
|
||||
}
|
||||
|
||||
const { page, tag, tagName } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`Tag: ${tagName} | ${SITE.title}`}>
|
||||
<Header activeNav="tags" />
|
||||
<Main
|
||||
pageTitle={[`Tag:`, `${tagName}`]}
|
||||
titleTransition={tag}
|
||||
pageDesc={`All the articles with the tag "${tagName}".`}
|
||||
>
|
||||
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
|
||||
<ul>
|
||||
{
|
||||
page.data.map(({ data, slug }) => (
|
||||
<Card href={`/posts/${slug}/`} frontmatter={data} />
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Main>
|
||||
|
||||
<Pagination {page} />
|
||||
|
||||
<Footer noMarginTop={page.lastPage > 1} />
|
||||
</Layout>
|
|
@ -11,12 +11,12 @@ import LinkButton from "@components/LinkButton.astro";
|
|||
|
||||
<main id="main-content">
|
||||
<div class="not-found-wrapper">
|
||||
<h1 aria-label="404 Not Found">404</h1>
|
||||
<h1>404</h1>
|
||||
<span aria-hidden="true">It seems like you are lost</span>
|
||||
<p>Let's get you back home</p>
|
||||
<LinkButton
|
||||
href="/"
|
||||
className="my-6 underline decoration-dashed underline-offset-8 text-lg"
|
||||
className="my-6 text-lg underline decoration-dashed underline-offset-8"
|
||||
>
|
||||
Go back home
|
||||
</LinkButton>
|
||||
|
|
84
src/pages/archives/index.astro
Normal file
|
@ -0,0 +1,84 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Card from "@components/Card";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import { SITE } from "@config";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import getPostsByGroupCondition from "@utils/getPostsByGroupCondition";
|
||||
|
||||
// Redirect to 404 page if `showArchives` config is false
|
||||
if (!SITE.showArchives) {
|
||||
return Astro.redirect("/404");
|
||||
}
|
||||
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
const MonthMap: Record<string, string> = {
|
||||
"1": "January",
|
||||
"2": "February",
|
||||
"3": "March",
|
||||
"4": "April",
|
||||
"5": "May",
|
||||
"6": "June",
|
||||
"7": "July",
|
||||
"8": "August",
|
||||
"9": "September",
|
||||
"10": "October",
|
||||
"11": "November",
|
||||
"12": "December",
|
||||
};
|
||||
---
|
||||
|
||||
<Layout title={`Archives | ${SITE.title}`}>
|
||||
<Header activeNav="archives" />
|
||||
<Main pageTitle="Archives" pageDesc="All the articles I've archived.">
|
||||
{
|
||||
Object.entries(
|
||||
getPostsByGroupCondition(posts, post =>
|
||||
post.data.pubDatetime.getFullYear()
|
||||
)
|
||||
)
|
||||
.sort(([yearA], [yearB]) => Number(yearB) - Number(yearA))
|
||||
.map(([year, yearGroup]) => (
|
||||
<div>
|
||||
<span class="text-2xl font-bold">{year}</span>
|
||||
<sup class="text-sm">{yearGroup.length}</sup>
|
||||
{Object.entries(
|
||||
getPostsByGroupCondition(
|
||||
yearGroup,
|
||||
post => post.data.pubDatetime.getMonth() + 1
|
||||
)
|
||||
)
|
||||
.sort(([monthA], [monthB]) => Number(monthB) - Number(monthA))
|
||||
.map(([month, monthGroup]) => (
|
||||
<div class="flex flex-col sm:flex-row">
|
||||
<div class="mt-6 min-w-36 text-lg sm:my-6">
|
||||
<span class="font-bold">{MonthMap[month]}</span>
|
||||
<sup class="text-xs">{monthGroup.length}</sup>
|
||||
</div>
|
||||
<ul>
|
||||
{monthGroup
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Math.floor(
|
||||
new Date(b.data.pubDatetime).getTime() / 1000
|
||||
) -
|
||||
Math.floor(
|
||||
new Date(a.data.pubDatetime).getTime() / 1000
|
||||
)
|
||||
)
|
||||
.map(({ data, slug }) => (
|
||||
<Card href={`/posts/${slug}`} frontmatter={data} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</Main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
|
@ -8,13 +8,13 @@ import Hr from "@components/Hr.astro";
|
|||
import Card from "@components/Card";
|
||||
import Socials from "@components/Socials.astro";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
import slugify from "@utils/slugify";
|
||||
import { SOCIALS } from "@config";
|
||||
import { SITE, SOCIALS } from "@config";
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
const featuredPosts = sortedPosts.filter(({ data }) => data.featured);
|
||||
const recentPosts = sortedPosts.filter(({ data }) => !data.featured);
|
||||
|
||||
const socialCount = SOCIALS.filter(social => social.active).length;
|
||||
---
|
||||
|
@ -23,8 +23,7 @@ const socialCount = SOCIALS.filter(social => social.active).length;
|
|||
<Header />
|
||||
<main id="main-content">
|
||||
<section id="hero">
|
||||
<h1 class="mr-2">Welcome!
|
||||
</h1>
|
||||
<h1>Welcome!</h1>
|
||||
<a
|
||||
target="_blank"
|
||||
href="/rss.xml"
|
||||
|
@ -39,12 +38,13 @@ const socialCount = SOCIALS.filter(social => social.active).length;
|
|||
d="M12 20.001h2C14 14.486 9.514 10 4 10v2c4.411 0 8 3.589 8 8.001z"
|
||||
></path><circle cx="6" cy="18" r="2"></circle>
|
||||
</svg>
|
||||
<span class="sr-only">RSS Feed</span>
|
||||
</a>
|
||||
|
||||
<p>
|
||||
Hi there, it looks like you have stumbled upon our blog,
|
||||
please go and get something to drink or something to eat
|
||||
while you read the contents of this blog.
|
||||
Hi there, it looks like you have stumbled upon our blog, please go and
|
||||
get something to drink or something to eat while you read the contents
|
||||
of this blog.
|
||||
</p>
|
||||
{
|
||||
// only display if at least one social link is enabled
|
||||
|
@ -65,40 +65,42 @@ const socialCount = SOCIALS.filter(social => social.active).length;
|
|||
<section id="featured">
|
||||
<h2>Featured</h2>
|
||||
<ul>
|
||||
{featuredPosts.map(({ data }) => (
|
||||
{featuredPosts.map(({ data, slug }) => (
|
||||
<Card
|
||||
href={`/posts/${slugify(data)}`}
|
||||
href={`/posts/${slug}/`}
|
||||
frontmatter={data}
|
||||
secHeading={false}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<Hr />
|
||||
{recentPosts.length > 0 && <Hr />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
recentPosts.length > 0 && (
|
||||
<section id="recent-posts">
|
||||
<h2>Recent Posts</h2>
|
||||
<ul>
|
||||
{
|
||||
sortedPosts
|
||||
.filter(({ data }) => !data.featured)
|
||||
.map(
|
||||
({ data }, index) =>
|
||||
index < 4 && (
|
||||
{recentPosts.map(
|
||||
({ data, slug }, index) =>
|
||||
index < SITE.postPerIndex && (
|
||||
<Card
|
||||
href={`/posts/${slugify(data)}`}
|
||||
href={`/posts/${slug}/`}
|
||||
frontmatter={data}
|
||||
secHeading={false}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
|
||||
<div class="all-posts-btn-wrapper">
|
||||
<LinkButton href="/posts">
|
||||
<LinkButton href="/posts/">
|
||||
All Posts
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
|
@ -107,7 +109,6 @@ const socialCount = SOCIALS.filter(social => social.active).length;
|
|||
</svg>
|
||||
</LinkButton>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
|
16
src/pages/posts/[...page].astro
Normal file
|
@ -0,0 +1,16 @@
|
|||
---
|
||||
import { SITE } from "@config";
|
||||
import Posts from "@layouts/Posts.astro";
|
||||
import type { GetStaticPaths } from "astro";
|
||||
import { getCollection } from "astro:content";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
|
||||
export const getStaticPaths = (async ({ paginate }) => {
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
return paginate(getSortedPosts(posts), { pageSize: SITE.postPerPage });
|
||||
}) satisfies GetStaticPaths;
|
||||
|
||||
const { page } = Astro.props;
|
||||
---
|
||||
|
||||
<Posts {page} />
|
|
@ -1,11 +1,7 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import Posts from "@layouts/Posts.astro";
|
||||
import PostDetails from "@layouts/PostDetails.astro";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
import getPageNumbers from "@utils/getPageNumbers";
|
||||
import slugify from "@utils/slugify";
|
||||
import { SITE } from "@config";
|
||||
|
||||
export interface Props {
|
||||
post: CollectionEntry<"blog">;
|
||||
|
@ -15,44 +11,17 @@ export async function getStaticPaths() {
|
|||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
const postResult = posts.map(post => ({
|
||||
params: { slug: slugify(post.data) },
|
||||
params: { slug: post.slug },
|
||||
props: { post },
|
||||
}));
|
||||
|
||||
const pagePaths = getPageNumbers(posts.length).map(pageNum => ({
|
||||
params: { slug: String(pageNum) },
|
||||
}));
|
||||
|
||||
return [...postResult, ...pagePaths];
|
||||
return postResult;
|
||||
}
|
||||
|
||||
const { slug } = Astro.params;
|
||||
const { post } = Astro.props;
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
|
||||
const totalPages = getPageNumbers(sortedPosts.length);
|
||||
|
||||
const currentPage =
|
||||
slug && !isNaN(Number(slug)) && totalPages.includes(Number(slug))
|
||||
? Number(slug)
|
||||
: 0;
|
||||
const lastPost = currentPage * SITE.postPerPage;
|
||||
const startPost = lastPost - SITE.postPerPage;
|
||||
|
||||
const paginatedPosts = sortedPosts.slice(startPost, lastPost);
|
||||
---
|
||||
|
||||
{
|
||||
post ? (
|
||||
<PostDetails post={post} />
|
||||
) : (
|
||||
<Posts
|
||||
posts={paginatedPosts}
|
||||
pageNum={currentPage}
|
||||
totalPages={totalPages.length}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<PostDetails post={post} posts={sortedPosts} />
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
import { SITE } from "@config";
|
||||
import Posts from "@layouts/Posts.astro";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
import getPageNumbers from "@utils/getPageNumbers";
|
||||
|
||||
import { getCollection } from "astro:content";
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
|
||||
const totalPages = getPageNumbers(sortedPosts.length);
|
||||
|
||||
const paginatedPosts = sortedPosts.slice(0, SITE.postPerPage);
|
||||
---
|
||||
|
||||
<Posts posts={paginatedPosts} pageNum={1} totalPages={totalPages.length} />
|
|
@ -1,7 +1,6 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { getCollection } from "astro:content";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
import slugify from "@utils/slugify";
|
||||
import { SITE } from "@config";
|
||||
|
||||
export async function GET() {
|
||||
|
@ -11,11 +10,11 @@ export async function GET() {
|
|||
title: SITE.title,
|
||||
description: SITE.desc,
|
||||
site: SITE.website,
|
||||
items: sortedPosts.map(({ data }) => ({
|
||||
link: `posts/${slugify(data)}`,
|
||||
items: sortedPosts.map(({ data, slug }) => ({
|
||||
link: `posts/${slug}/`,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
pubDate: new Date(data.pubDatetime),
|
||||
pubDate: new Date(data.modDatetime ?? data.pubDatetime),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,15 +6,18 @@ import Main from "@layouts/Main.astro";
|
|||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import SearchBar from "@components/Search";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
|
||||
// Retrieve all articles
|
||||
// Retrieve all published articles
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
|
||||
// List of items to search in
|
||||
const searchList = posts.map(({ data }) => ({
|
||||
const searchList = sortedPosts.map(({ data, slug }) => ({
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
data,
|
||||
slug,
|
||||
}));
|
||||
---
|
||||
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
---
|
||||
import { type CollectionEntry, getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Card from "@components/Card";
|
||||
import getUniqueTags from "@utils/getUniqueTags";
|
||||
import getPostsByTag from "@utils/getPostsByTag";
|
||||
import slugify from "@utils/slugify";
|
||||
import { SITE } from "@config";
|
||||
import getSortedPosts from "@utils/getSortedPosts";
|
||||
|
||||
export interface Props {
|
||||
post: CollectionEntry<"blog">;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
const tags = getUniqueTags(posts);
|
||||
|
||||
return tags.map(tag => {
|
||||
return {
|
||||
params: { tag },
|
||||
props: { tag },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { tag } = Astro.props;
|
||||
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
const tagPosts = getPostsByTag(posts, tag);
|
||||
|
||||
const sortTagsPost = getSortedPosts(tagPosts);
|
||||
---
|
||||
|
||||
<Layout title={`Tag:${tag} | ${SITE.title}`}>
|
||||
<Header activeNav="tags" />
|
||||
<Main
|
||||
pageTitle={[`Tag:`, `${tag}`]}
|
||||
titleTransition={tag}
|
||||
pageDesc={`All the articles with the tag "${tag}".`}
|
||||
>
|
||||
<h1 slot="title" transition:name={tag}>{`Tag:${tag}`}</h1>
|
||||
<ul>
|
||||
{
|
||||
sortTagsPost.map(({ data }) => (
|
||||
<Card href={`/posts/${slugify(data)}`} frontmatter={data} />
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Main>
|
||||
<Footer />
|
||||
</Layout>
|
29
src/pages/tags/[tag]/[...page].astro
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import TagPosts from "@layouts/TagPosts.astro";
|
||||
import getUniqueTags from "@utils/getUniqueTags";
|
||||
import getPostsByTag from "@utils/getPostsByTag";
|
||||
import type { GetStaticPathsOptions } from "astro";
|
||||
import { SITE } from "@config";
|
||||
|
||||
export async function getStaticPaths({ paginate }: GetStaticPathsOptions) {
|
||||
const posts = await getCollection("blog");
|
||||
const tags = getUniqueTags(posts);
|
||||
|
||||
return tags.flatMap(({ tag, tagName }) => {
|
||||
const tagPosts = getPostsByTag(posts, tag);
|
||||
|
||||
return paginate(tagPosts, {
|
||||
params: { tag },
|
||||
props: { tagName },
|
||||
pageSize: SITE.postPerPage,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const params = Astro.params;
|
||||
const { tag } = params;
|
||||
const { page, tagName } = Astro.props;
|
||||
---
|
||||
|
||||
<TagPosts {page} {tag} {tagName} />
|
|
@ -17,7 +17,7 @@ let tags = getUniqueTags(posts);
|
|||
<Header activeNav="tags" />
|
||||
<Main pageTitle="Tags" pageDesc="All the tags used in posts.">
|
||||
<ul>
|
||||
{tags.map(tag => <Tag name={tag} size="lg" />)}
|
||||
{tags.map(({ tag }) => <Tag {tag} size="lg" />)}
|
||||
</ul>
|
||||
</Main>
|
||||
<Footer />
|
||||
|
|
|
@ -5,20 +5,20 @@
|
|||
@layer base {
|
||||
:root,
|
||||
html[data-theme="light"] {
|
||||
--color-fill: 251, 254, 251;
|
||||
--color-text-base: 40, 39, 40;
|
||||
--color-accent: 0, 108, 172;
|
||||
--color-card: 230, 230, 230;
|
||||
--color-card-muted: 205, 205, 205;
|
||||
--color-border: 236, 233, 233;
|
||||
--color-fill: 242, 245, 236;
|
||||
--color-text-base: 53, 53, 56;
|
||||
--color-accent: 17, 88, 209;
|
||||
--color-card: 206, 213, 180;
|
||||
--color-card-muted: 187, 199, 137;
|
||||
--color-border: 124, 173, 255;
|
||||
}
|
||||
html[data-theme="dark"] {
|
||||
--color-fill: 33, 39, 55;
|
||||
--color-fill: 0, 1, 35;
|
||||
--color-accent: 97, 123, 255;
|
||||
--color-text-base: 234, 237, 243;
|
||||
--color-accent: 255, 107, 1;
|
||||
--color-card: 52, 63, 96;
|
||||
--color-card-muted: 138, 51, 2;
|
||||
--color-border: 171, 75, 8;
|
||||
--color-card: 33, 34, 83;
|
||||
--color-card-muted: 12, 14, 79;
|
||||
--color-border: 48, 63, 138;
|
||||
}
|
||||
#sun-svg,
|
||||
html[data-theme="dark"] #moon-svg {
|
||||
|
@ -29,45 +29,23 @@
|
|||
display: block;
|
||||
}
|
||||
body {
|
||||
@apply flex min-h-[100svh] flex-col bg-skin-fill font-mono text-skin-base
|
||||
selection:bg-skin-accent selection:bg-opacity-70 selection:text-skin-inverted;
|
||||
@apply flex min-h-[100svh] flex-col bg-skin-fill font-mono text-skin-base selection:bg-skin-accent/70 selection:text-skin-inverted;
|
||||
}
|
||||
section,
|
||||
footer {
|
||||
@apply mx-auto max-w-3xl px-4;
|
||||
}
|
||||
a {
|
||||
@apply outline-2 outline-offset-1 outline-skin-fill
|
||||
focus-visible:no-underline focus-visible:outline-dashed;
|
||||
@apply outline-2 outline-offset-1 outline-skin-fill focus-visible:no-underline focus-visible:outline-dashed;
|
||||
}
|
||||
svg {
|
||||
@apply inline-block h-6 w-6 fill-skin-base group-hover:fill-skin-accent;
|
||||
}
|
||||
svg.icon-tabler {
|
||||
@apply inline-block h-6 w-6 scale-125 fill-transparent
|
||||
stroke-current stroke-2 opacity-90 group-hover:fill-transparent
|
||||
sm:scale-110;
|
||||
@apply inline-block h-6 w-6 scale-125 fill-transparent stroke-current stroke-2 opacity-90 group-hover:fill-transparent sm:scale-110;
|
||||
}
|
||||
.prose {
|
||||
@apply prose-headings:!mb-3 prose-headings:!text-skin-base
|
||||
prose-h3:italic prose-p:!text-skin-base
|
||||
prose-a:!text-skin-base prose-a:!decoration-dashed prose-a:underline-offset-8
|
||||
hover:prose-a:text-skin-accent prose-blockquote:!border-l-skin-accent
|
||||
prose-blockquote:border-opacity-50 prose-blockquote:opacity-80
|
||||
prose-figcaption:!text-skin-base prose-figcaption:opacity-70
|
||||
prose-strong:!text-skin-base
|
||||
|
||||
|
||||
prose-code:rounded prose-code:bg-skin-card
|
||||
prose-code:bg-opacity-75 prose-code:p-1 prose-code:!text-skin-base
|
||||
prose-code:before:!content-[''] prose-code:after:!content-['']
|
||||
prose-pre:!text-skin-base prose-ol:!text-skin-base
|
||||
prose-ul:overflow-x-clip prose-ul:!text-skin-base prose-li:marker:!text-skin-accent
|
||||
prose-table:text-skin-base prose-th:border
|
||||
prose-th:border-skin-line prose-td:border
|
||||
prose-td:border-skin-line prose-img:mx-auto
|
||||
prose-img:!mt-2 prose-img:border-2
|
||||
prose-img:border-skin-line prose-hr:!border-skin-line;
|
||||
@apply prose-headings:!mb-3 prose-headings:!text-skin-base prose-h3:italic prose-p:!text-skin-base prose-a:!text-skin-base prose-a:!decoration-dashed prose-a:underline-offset-8 hover:prose-a:text-skin-accent prose-blockquote:!border-l-skin-accent/50 prose-blockquote:opacity-80 prose-figcaption:!text-skin-base prose-figcaption:opacity-70 prose-strong:!text-skin-base prose-code:rounded prose-code:bg-skin-card/75 prose-code:p-1 prose-code:before:!content-none prose-code:after:!content-none prose-ol:!text-skin-base prose-ul:overflow-x-clip prose-ul:!text-skin-base prose-li:marker:!text-skin-accent prose-table:text-skin-base prose-th:border prose-th:border-skin-line prose-td:border prose-td:border-skin-line prose-img:!my-2 prose-img:mx-auto prose-img:border-2 prose-img:border-skin-line prose-hr:!border-skin-line;
|
||||
}
|
||||
.prose a {
|
||||
@apply break-words hover:!text-skin-accent;
|
||||
|
@ -115,6 +93,10 @@
|
|||
@apply bg-skin-card-muted;
|
||||
}
|
||||
|
||||
/* ===== Code Blocks & Syntax Highlighting ===== */
|
||||
pre:has(code) {
|
||||
@apply border border-skin-line;
|
||||
}
|
||||
code,
|
||||
blockquote {
|
||||
word-wrap: break-word;
|
||||
|
@ -123,6 +105,16 @@
|
|||
pre > code {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
/* Apply Dark Theme (if multi-theme specified) */
|
||||
html[data-theme="dark"] pre:has(code),
|
||||
html[data-theme="dark"] pre:has(code) span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--shiki-dark-bg) !important;
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
|
|
39
src/types.ts
|
@ -1,42 +1,27 @@
|
|||
import type socialIcons from "@assets/socialIcons";
|
||||
|
||||
export type Site = {
|
||||
website: string;
|
||||
author: string;
|
||||
profile: string;
|
||||
desc: string;
|
||||
title: string;
|
||||
ogImage?: string;
|
||||
lightAndDarkMode: boolean;
|
||||
postPerIndex: number;
|
||||
postPerPage: number;
|
||||
scheduledPostMargin: number;
|
||||
showArchives?: boolean;
|
||||
editPost?: {
|
||||
url?: URL["href"];
|
||||
text?: string;
|
||||
appendFilePath?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type SocialObjects = {
|
||||
name: SocialMedia;
|
||||
name: keyof typeof socialIcons;
|
||||
href: string;
|
||||
active: boolean;
|
||||
linkTitle: string;
|
||||
}[];
|
||||
|
||||
export type SocialIcons = {
|
||||
[social in SocialMedia]: string;
|
||||
};
|
||||
|
||||
export type SocialMedia =
|
||||
| "Github"
|
||||
| "Facebook"
|
||||
| "Instagram"
|
||||
| "LinkedIn"
|
||||
| "Mail"
|
||||
| "Twitter"
|
||||
| "Twitch"
|
||||
| "YouTube"
|
||||
| "WhatsApp"
|
||||
| "Snapchat"
|
||||
| "Pinterest"
|
||||
| "TikTok"
|
||||
| "CodePen"
|
||||
| "Discord"
|
||||
| "GitLab"
|
||||
| "Reddit"
|
||||
| "Skype"
|
||||
| "Steam"
|
||||
| "Telegram"
|
||||
| "Mastodon";
|
||||
|
|
|
@ -1,47 +1,8 @@
|
|||
import satori, { type SatoriOptions } from "satori";
|
||||
import { Resvg } from "@resvg/resvg-js";
|
||||
import { type CollectionEntry } from "astro:content";
|
||||
import postOgImage from "./og-templates/post";
|
||||
import siteOgImage from "./og-templates/site";
|
||||
|
||||
const fetchFonts = async () => {
|
||||
// Regular Font
|
||||
const fontFileRegular = await fetch(
|
||||
"https://www.1001fonts.com/download/font/ibm-plex-mono.regular.ttf"
|
||||
);
|
||||
const fontRegular: ArrayBuffer = await fontFileRegular.arrayBuffer();
|
||||
|
||||
// Bold Font
|
||||
const fontFileBold = await fetch(
|
||||
"https://www.1001fonts.com/download/font/ibm-plex-mono.bold.ttf"
|
||||
);
|
||||
const fontBold: ArrayBuffer = await fontFileBold.arrayBuffer();
|
||||
|
||||
return { fontRegular, fontBold };
|
||||
};
|
||||
|
||||
const { fontRegular, fontBold } = await fetchFonts();
|
||||
|
||||
const options: SatoriOptions = {
|
||||
width: 1200,
|
||||
height: 630,
|
||||
embedFont: true,
|
||||
fonts: [
|
||||
{
|
||||
name: "IBM Plex Mono",
|
||||
data: fontRegular,
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "IBM Plex Mono",
|
||||
data: fontBold,
|
||||
weight: 600,
|
||||
style: "normal",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function svgBufferToPngBuffer(svg: string) {
|
||||
const resvg = new Resvg(svg);
|
||||
const pngData = resvg.render();
|
||||
|
@ -49,11 +10,11 @@ function svgBufferToPngBuffer(svg: string) {
|
|||
}
|
||||
|
||||
export async function generateOgImageForPost(post: CollectionEntry<"blog">) {
|
||||
const svg = await satori(postOgImage(post), options);
|
||||
const svg = await postOgImage(post);
|
||||
return svgBufferToPngBuffer(svg);
|
||||
}
|
||||
|
||||
export async function generateOgImageForSite() {
|
||||
const svg = await satori(siteOgImage(), options);
|
||||
const svg = await siteOgImage();
|
||||
return svgBufferToPngBuffer(svg);
|
||||
}
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import { SITE } from "@config";
|
||||
|
||||
const getPageNumbers = (numberOfPosts: number) => {
|
||||
const numberOfPages = numberOfPosts / Number(SITE.postPerPage);
|
||||
|
||||
let pageNumbers: number[] = [];
|
||||
for (let i = 1; i <= Math.ceil(numberOfPages); i++) {
|
||||
pageNumbers = [...pageNumbers, i];
|
||||
}
|
||||
|
||||
return pageNumbers;
|
||||
};
|
||||
|
||||
export default getPageNumbers;
|
25
src/utils/getPostsByGroupCondition.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
type GroupKey = string | number | symbol;
|
||||
|
||||
interface GroupFunction<T> {
|
||||
(item: T, index?: number): GroupKey;
|
||||
}
|
||||
|
||||
const getPostsByGroupCondition = (
|
||||
posts: CollectionEntry<"blog">[],
|
||||
groupFunction: GroupFunction<CollectionEntry<"blog">>
|
||||
) => {
|
||||
const result: Record<GroupKey, CollectionEntry<"blog">[]> = {};
|
||||
for (let i = 0; i < posts.length; i++) {
|
||||
const item = posts[i];
|
||||
const groupKey = groupFunction(item, i);
|
||||
if (!result[groupKey]) {
|
||||
result[groupKey] = [];
|
||||
}
|
||||
result[groupKey].push(item);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export default getPostsByGroupCondition;
|
|
@ -1,7 +1,10 @@
|
|||
import { slugifyAll } from "./slugify";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import getSortedPosts from "./getSortedPosts";
|
||||
import { slugifyAll } from "./slugify";
|
||||
|
||||
const getPostsByTag = (posts: CollectionEntry<"blog">[], tag: string) =>
|
||||
posts.filter(post => slugifyAll(post.data.tags).includes(tag));
|
||||
getSortedPosts(
|
||||
posts.filter(post => slugifyAll(post.data.tags).includes(tag))
|
||||
);
|
||||
|
||||
export default getPostsByTag;
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import type { CollectionEntry } from "astro:content";
|
||||
import postFilter from "./postFilter";
|
||||
|
||||
const getSortedPosts = (posts: CollectionEntry<"blog">[]) =>
|
||||
posts
|
||||
.filter(({ data }) => !data.draft)
|
||||
const getSortedPosts = (posts: CollectionEntry<"blog">[]) => {
|
||||
return posts
|
||||
.filter(postFilter)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Math.floor(new Date(b.data.pubDatetime).getTime() / 1000) -
|
||||
Math.floor(new Date(a.data.pubDatetime).getTime() / 1000)
|
||||
Math.floor(
|
||||
new Date(b.data.modDatetime ?? b.data.pubDatetime).getTime() / 1000
|
||||
) -
|
||||
Math.floor(
|
||||
new Date(a.data.modDatetime ?? a.data.pubDatetime).getTime() / 1000
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default getSortedPosts;
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
import { slugifyStr } from "./slugify";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import postFilter from "./postFilter";
|
||||
|
||||
interface Tag {
|
||||
tag: string;
|
||||
tagName: string;
|
||||
}
|
||||
|
||||
const getUniqueTags = (posts: CollectionEntry<"blog">[]) => {
|
||||
const filteredPosts = posts.filter(({ data }) => !data.draft);
|
||||
const tags: string[] = filteredPosts
|
||||
const tags: Tag[] = posts
|
||||
.filter(postFilter)
|
||||
.flatMap(post => post.data.tags)
|
||||
.map(tag => slugifyStr(tag))
|
||||
.map(tag => ({ tag: slugifyStr(tag), tagName: tag }))
|
||||
.filter(
|
||||
(value: string, index: number, self: string[]) =>
|
||||
self.indexOf(value) === index
|
||||
(value, index, self) =>
|
||||
self.findIndex(tag => tag.tag === value.tag) === index
|
||||
)
|
||||
.sort((tagA: string, tagB: string) => tagA.localeCompare(tagB));
|
||||
.sort((tagA, tagB) => tagA.tag.localeCompare(tagB.tag));
|
||||
return tags;
|
||||
};
|
||||
|
||||
|
|
71
src/utils/loadGoogleFont.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import type { FontStyle, FontWeight } from "satori";
|
||||
|
||||
export type FontOptions = {
|
||||
name: string;
|
||||
data: ArrayBuffer;
|
||||
weight: FontWeight | undefined;
|
||||
style: FontStyle | undefined;
|
||||
};
|
||||
|
||||
async function loadGoogleFont(
|
||||
font: string,
|
||||
text: string
|
||||
): Promise<ArrayBuffer> {
|
||||
const API = `https://fonts.googleapis.com/css2?family=${font}&text=${encodeURIComponent(text)}`;
|
||||
|
||||
const css = await (
|
||||
await fetch(API, {
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; de-at) AppleWebKit/533.21.1 (KHTML, like Gecko) Version/5.0.5 Safari/533.21.1",
|
||||
},
|
||||
})
|
||||
).text();
|
||||
|
||||
const resource = css.match(
|
||||
/src: url\((.+)\) format\('(opentype|truetype)'\)/
|
||||
);
|
||||
|
||||
if (!resource) throw new Error("Failed to download dynamic font");
|
||||
|
||||
const res = await fetch(resource[1]);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error("Failed to download dynamic font. Status: " + res.status);
|
||||
}
|
||||
|
||||
const fonts: ArrayBuffer = await res.arrayBuffer();
|
||||
return fonts;
|
||||
}
|
||||
|
||||
async function loadGoogleFonts(
|
||||
text: string
|
||||
): Promise<
|
||||
Array<{ name: string; data: ArrayBuffer; weight: number; style: string }>
|
||||
> {
|
||||
const fontsConfig = [
|
||||
{
|
||||
name: "IBM Plex Mono",
|
||||
font: "IBM+Plex+Mono",
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
},
|
||||
{
|
||||
name: "IBM Plex Mono",
|
||||
font: "IBM+Plex+Mono:wght@700",
|
||||
weight: 700,
|
||||
style: "bold",
|
||||
},
|
||||
];
|
||||
|
||||
const fonts = await Promise.all(
|
||||
fontsConfig.map(async ({ name, font, weight, style }) => {
|
||||
const data = await loadGoogleFont(font, text);
|
||||
return { name, data, weight, style };
|
||||
})
|
||||
);
|
||||
|
||||
return fonts;
|
||||
}
|
||||
|
||||
export default loadGoogleFonts;
|
|
@ -1,8 +1,10 @@
|
|||
import { SITE } from "@config";
|
||||
import satori from "satori";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { SITE } from "@config";
|
||||
import loadGoogleFonts, { type FontOptions } from "../loadGoogleFont";
|
||||
|
||||
export default (post: CollectionEntry<"blog">) => {
|
||||
return (
|
||||
export default async (post: CollectionEntry<"blog">) => {
|
||||
return satori(
|
||||
<div
|
||||
style={{
|
||||
background: "#fefbfb",
|
||||
|
@ -91,6 +93,14 @@ export default (post: CollectionEntry<"blog">) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
embedFont: true,
|
||||
fonts: (await loadGoogleFonts(
|
||||
post.data.title + post.data.author + SITE.title + "by"
|
||||
)) as FontOptions[],
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import satori from "satori";
|
||||
import { SITE } from "@config";
|
||||
import loadGoogleFonts, { type FontOptions } from "../loadGoogleFont";
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
export default async () => {
|
||||
return satori(
|
||||
<div
|
||||
style={{
|
||||
background: "#fefbfb",
|
||||
|
@ -82,6 +84,14 @@ export default () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
embedFont: true,
|
||||
fonts: (await loadGoogleFonts(
|
||||
SITE.title + SITE.desc + SITE.website
|
||||
)) as FontOptions[],
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
11
src/utils/postFilter.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { SITE } from "@config";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
const postFilter = ({ data }: CollectionEntry<"blog">) => {
|
||||
const isPublishTimePassed =
|
||||
Date.now() >
|
||||
new Date(data.pubDatetime).getTime() - SITE.scheduledPostMargin;
|
||||
return !data.draft && (import.meta.env.DEV || isPublishTimePassed);
|
||||
};
|
||||
|
||||
export default postFilter;
|
|
@ -1,11 +1,5 @@
|
|||
import { slug as slugger } from "github-slugger";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import kebabCase from "lodash.kebabcase";
|
||||
|
||||
export const slugifyStr = (str: string) => slugger(str);
|
||||
|
||||
const slugify = (post: CollectionEntry<"blog">["data"]) =>
|
||||
post.postSlug ? slugger(post.postSlug) : slugger(post.title);
|
||||
export const slugifyStr = (str: string) => kebabCase(str);
|
||||
|
||||
export const slugifyAll = (arr: string[]) => arr.map(str => slugifyStr(str));
|
||||
|
||||
export default slugify;
|
||||
|
|
|
@ -9,6 +9,7 @@ function withOpacity(variableName) {
|
|||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: ["selector", "[data-theme='dark']"],
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||
theme: {
|
||||
// Remove the following screen breakpoint or add other breakpoints
|
||||
|
@ -17,10 +18,7 @@ module.exports = {
|
|||
sm: "640px",
|
||||
},
|
||||
|
||||
// Uncomment the following extend
|
||||
// if existing Tailwind color palette will be used
|
||||
|
||||
// extend: {
|
||||
extend: {
|
||||
textColor: {
|
||||
skin: {
|
||||
base: withOpacity("--color-text-base"),
|
||||
|
@ -56,10 +54,28 @@ module.exports = {
|
|||
},
|
||||
transparent: "transparent",
|
||||
},
|
||||
stroke: {
|
||||
skin: {
|
||||
accent: withOpacity("--color-accent")
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ["IBM Plex Mono", "monospace"],
|
||||
},
|
||||
// },
|
||||
|
||||
typography: {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
pre: {
|
||||
color: false,
|
||||
},
|
||||
code: {
|
||||
color: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|