migrate to astro
This commit is contained in:
commit
129d2008fe
|
@ -0,0 +1,6 @@
|
|||
.husky
|
||||
.vscode
|
||||
node_modules
|
||||
public
|
||||
dist
|
||||
.yarn
|
|
@ -0,0 +1,23 @@
|
|||
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: {},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
# build output
|
||||
dist/
|
||||
.output/
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# logs
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
|
||||
# environment variables
|
||||
.env
|
||||
.env.production
|
||||
|
||||
# macOS-specific files
|
||||
.DS_Store
|
||||
|
||||
# ignore .astro directory
|
||||
.astro
|
||||
|
||||
# ignore Jampack cache files
|
||||
.jampack/
|
||||
|
||||
# yarn
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
.pnp.*
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"MD033": false,
|
||||
"MD013": false
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
# Expose Astro dependencies for `pnpm` users
|
||||
shamefully-hoist=true
|
|
@ -0,0 +1,13 @@
|
|||
# Ignore everything
|
||||
/*
|
||||
|
||||
# Except these files & folders
|
||||
!/src
|
||||
!/public
|
||||
!/.github
|
||||
!tsconfig.json
|
||||
!astro.config.mjs
|
||||
!package.json
|
||||
!.prettierrc
|
||||
!.eslintrc.js
|
||||
!README.md
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"arrowParens": "avoid",
|
||||
"semi": true,
|
||||
"tabWidth": 2,
|
||||
"printWidth": 80,
|
||||
"singleQuote": false,
|
||||
"jsxSingleQuote": false,
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"recommendations": ["astro-build.astro-vscode"],
|
||||
"unwantedRecommendations": []
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"command": "./node_modules/.bin/astro dev",
|
||||
"name": "Development server",
|
||||
"request": "launch",
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 Sat Naing
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
|
@ -0,0 +1,41 @@
|
|||
import { defineConfig } from "astro/config";
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
import react from "@astrojs/react";
|
||||
import remarkToc from "remark-toc";
|
||||
import remarkCollapse from "remark-collapse";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://astro-paper.pages.dev/", // replace this with your deployed domain
|
||||
integrations: [
|
||||
tailwind({
|
||||
config: {
|
||||
applyBaseStyles: false,
|
||||
},
|
||||
}),
|
||||
react(),
|
||||
sitemap(),
|
||||
],
|
||||
markdown: {
|
||||
remarkPlugins: [
|
||||
remarkToc,
|
||||
[
|
||||
remarkCollapse,
|
||||
{
|
||||
test: "Table of contents",
|
||||
},
|
||||
],
|
||||
],
|
||||
shikiConfig: {
|
||||
theme: "one-dark-pro",
|
||||
wrap: true,
|
||||
},
|
||||
extendDefaultPlugins: true,
|
||||
},
|
||||
vite: {
|
||||
optimizeDeps: {
|
||||
exclude: ["@resvg/resvg-js"],
|
||||
},
|
||||
},
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,59 @@
|
|||
{
|
||||
"name": "watzon-tech",
|
||||
"version": "2.3.0",
|
||||
"scripts": {
|
||||
"dev": "astro check --watch & astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build && jampack ./dist",
|
||||
"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",
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/rss": "^2.4.1",
|
||||
"@resvg/resvg-js": "^2.4.1",
|
||||
"astro": "^2.4.5",
|
||||
"fuse.js": "^6.6.2",
|
||||
"github-slugger": "^2.0.0",
|
||||
"remark-collapse": "^0.1.2",
|
||||
"remark-toc": "^8.0.1",
|
||||
"satori": "^0.8.1",
|
||||
"tailwindcss": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/react": "^2.1.3",
|
||||
"@astrojs/sitemap": "^1.3.1",
|
||||
"@astrojs/tailwind": "^3.1.2",
|
||||
"@divriots/jampack": "^0.11.2",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/github-slugger": "^1.3.0",
|
||||
"@types/react": "^18.2.6",
|
||||
"@typescript-eslint/parser": "^5.59.5",
|
||||
"astro-eslint-parser": "^0.14.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"cz-conventional-changelog": "^3.3.0",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-plugin-astro": "^0.27.0",
|
||||
"husky": "^8.0.3",
|
||||
"lint-staged": "^13.2.2",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.2.8",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx,md,mdx,json}": [
|
||||
"prettier --plugin-search-dir=. --write"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,361 @@
|
|||
<!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>
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 145 KiB |
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 36 36">
|
||||
<path fill="#000" d="M22.25 4h-8.5a1 1 0 0 0-.96.73l-5.54 19.4a.5.5 0 0 0 .62.62l5.05-1.44a2 2 0 0 0 1.38-1.4l3.22-11.66a.5.5 0 0 1 .96 0l3.22 11.67a2 2 0 0 0 1.38 1.39l5.05 1.44a.5.5 0 0 0 .62-.62l-5.54-19.4a1 1 0 0 0-.96-.73Z"/>
|
||||
<path fill="url(#gradient)" d="M18 28a7.63 7.63 0 0 1-5-2c-1.4 2.1-.35 4.35.6 5.55.14.17.41.07.47-.15.44-1.8 2.93-1.22 2.93.6 0 2.28.87 3.4 1.72 3.81.34.16.59-.2.49-.56-.31-1.05-.29-2.46 1.29-3.25 3-1.5 3.17-4.83 2.5-6-.67.67-2.6 2-5 2Z"/>
|
||||
<defs>
|
||||
<linearGradient id="gradient" x1="16" x2="16" y1="32" y2="24" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#000"/>
|
||||
<stop offset="1" stop-color="#000" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<style>
|
||||
@media (prefers-color-scheme:dark){:root{filter:invert(100%)}}
|
||||
</style>
|
||||
</svg>
|
After Width: | Height: | Size: 873 B |
|
@ -0,0 +1,5 @@
|
|||
User-agent: Googlebot
|
||||
Disallow: /nogooglebot/
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
|
@ -0,0 +1,52 @@
|
|||
const primaryColorScheme = ""; // "light" | "dark"
|
||||
|
||||
// Get theme data from local storage
|
||||
const currentTheme = localStorage.getItem("theme");
|
||||
|
||||
function getPreferTheme() {
|
||||
// return theme value in local storage if it is set
|
||||
if (currentTheme) return currentTheme;
|
||||
|
||||
// return primary color scheme if it is set
|
||||
if (primaryColorScheme) return primaryColorScheme;
|
||||
|
||||
// return user device's prefer color scheme
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
}
|
||||
|
||||
let themeValue = getPreferTheme();
|
||||
|
||||
function setPreference() {
|
||||
localStorage.setItem("theme", themeValue);
|
||||
reflectPreference();
|
||||
}
|
||||
|
||||
function reflectPreference() {
|
||||
document.firstElementChild.setAttribute("data-theme", themeValue);
|
||||
|
||||
document.querySelector("#theme-btn")?.setAttribute("aria-label", themeValue);
|
||||
}
|
||||
|
||||
// set early so no page flashes / CSS is made aware
|
||||
reflectPreference();
|
||||
|
||||
window.onload = () => {
|
||||
// set on load so screen readers can get the latest value on the button
|
||||
reflectPreference();
|
||||
|
||||
// now this script can find and listen for clicks on the control
|
||||
document.querySelector("#theme-btn")?.addEventListener("click", () => {
|
||||
themeValue = themeValue === "light" ? "dark" : "light";
|
||||
setPreference();
|
||||
});
|
||||
};
|
||||
|
||||
// sync with system changes
|
||||
window
|
||||
.matchMedia("(prefers-color-scheme: dark)")
|
||||
.addEventListener("change", ({ matches: isDark }) => {
|
||||
themeValue = isDark ? "dark" : "light";
|
||||
setPreference();
|
||||
});
|
|
@ -0,0 +1,213 @@
|
|||
import type { SocialIcons } from "../types";
|
||||
|
||||
const socialIcons: SocialIcons = {
|
||||
Github: `<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="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5"
|
||||
></path>
|
||||
</svg>`,
|
||||
Facebook: `<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="M7 10v4h3v7h4v-7h3l1 -4h-4v-2a1 1 0 0 1 1 -1h3v-4h-3a5 5 0 0 0 -5 5v2h-3"
|
||||
></path>
|
||||
</svg>`,
|
||||
Instagram: `<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>
|
||||
<rect x="4" y="4" width="16" height="16" rx="4"></rect>
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<line x1="16.5" y1="7.5" x2="16.5" y2="7.501"></line>
|
||||
</svg>`,
|
||||
LinkedIn: `<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>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
|
||||
<line x1="8" y1="11" x2="8" y2="16"></line>
|
||||
<line x1="8" y1="8" x2="8" y2="8.01"></line>
|
||||
<line x1="12" y1="16" x2="12" y2="11"></line>
|
||||
<path d="M16 16v-3a2 2 0 0 0 -4 0"></path>
|
||||
</svg>`,
|
||||
Mail: `<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>
|
||||
<rect x="3" y="5" width="18" height="14" rx="2"></rect>
|
||||
<polyline points="3 7 12 13 21 7"></polyline>
|
||||
</svg>`,
|
||||
Twitter: `<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>
|
||||
</svg>`,
|
||||
Twitch: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 2H3v16h5v4l4-4h5l4-4V2zm-10 9V7m5 4V7"></path>
|
||||
</svg>`,
|
||||
YouTube: `<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-tabler"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z"></path>
|
||||
<polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02"></polygon>
|
||||
</svg>`,
|
||||
WhatsApp: `<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="M3 21l1.65 -3.8a9 9 0 1 1 3.4 2.9l-5.05 .9"></path>
|
||||
<path d="M9 10a0.5 .5 0 0 0 1 0v-1a0.5 .5 0 0 0 -1 0v1a5 5 0 0 0 5 5h1a0.5 .5 0 0 0 0 -1h-1a0.5 .5 0 0 0 0 1"></path>
|
||||
</svg>`,
|
||||
Snapchat: `<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="M16.882 7.842a4.882 4.882 0 0 0 -9.764 0c0 4.273 -.213 6.409 -4.118 8.118c2 .882 2 .882 3 3c3 0 4 2 6 2s3 -2 6 -2c1 -2.118 1 -2.118 3 -3c-3.906 -1.709 -4.118 -3.845 -4.118 -8.118zm-13.882 8.119c4 -2.118 4 -4.118 1 -7.118m17 7.118c-4 -2.118 -4 -4.118 -1 -7.118"></path>
|
||||
</svg>`,
|
||||
Pinterest: `<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>
|
||||
<line x1="8" y1="20" x2="12" y2="11"></line>
|
||||
<path d="M10.7 14c.437 1.263 1.43 2 2.55 2c2.071 0 3.75 -1.554 3.75 -4a5 5 0 1 0 -9.7 1.7"></path>
|
||||
<circle cx="12" cy="12" r="9"></circle>
|
||||
</svg>`,
|
||||
TikTok: `<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="M9 12a4 4 0 1 0 4 4v-12a5 5 0 0 0 5 5"></path>
|
||||
</svg>`,
|
||||
CodePen: `<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="M3 15l9 6l9 -6l-9 -6l-9 6"></path>
|
||||
<path d="M3 9l9 6l9 -6l-9 -6l-9 6"></path>
|
||||
<line x1="3" y1="9" x2="3" y2="15"></line>
|
||||
<line x1="21" y1="9" x2="21" y2="15"></line>
|
||||
<line x1="12" y1="3" x2="12" y2="9"></line>
|
||||
<line x1="12" y1="15" x2="12" y2="21"></line>
|
||||
</svg>`,
|
||||
Discord: `<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>
|
||||
<circle cx="9" cy="12" r="1"></circle>
|
||||
<circle cx="15" cy="12" r="1"></circle>
|
||||
<path d="M7.5 7.5c3.5 -1 5.5 -1 9 0"></path>
|
||||
<path d="M7 16.5c3.5 1 6.5 1 10 0"></path>
|
||||
<path d="M15.5 17c0 1 1.5 3 2 3c1.5 0 2.833 -1.667 3.5 -3c.667 -1.667 .5 -5.833 -1.5 -11.5c-1.457 -1.015 -3 -1.34 -4.5 -1.5l-1 2.5"></path>
|
||||
<path d="M8.5 17c0 1 -1.356 3 -1.832 3c-1.429 0 -2.698 -1.667 -3.333 -3c-.635 -1.667 -.476 -5.833 1.428 -11.5c1.388 -1.015 2.782 -1.34 4.237 -1.5l1 2.5"></path>
|
||||
</svg>`,
|
||||
GitLab: `<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="M21 14l-9 7l-9 -7l3 -11l3 7h6l3 -7z"></path>
|
||||
</svg>`,
|
||||
Reddit: `<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="M12 8c2.648 0 5.028 .826 6.675 2.14a2.5 2.5 0 0 1 2.326 4.36c0 3.59 -4.03 6.5 -9 6.5c-4.875 0 -8.845 -2.8 -9 -6.294l-1 -.206a2.5 2.5 0 0 1 2.326 -4.36c1.646 -1.313 4.026 -2.14 6.674 -2.14z"></path>
|
||||
<path d="M12 8l1 -5l6 1"></path>
|
||||
<circle cx="19" cy="4" r="1"></circle>
|
||||
<circle cx="9" cy="13" r=".5" fill="currentColor"></circle>
|
||||
<circle cx="15" cy="13" r=".5" fill="currentColor"></circle>
|
||||
<path d="M10 17c.667 .333 1.333 .5 2 .5s1.333 -.167 2 -.5"></path>
|
||||
</svg>`,
|
||||
Skype: `<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="M12 3a9 9 0 0 1 8.603 11.65a4.5 4.5 0 0 1 -5.953 5.953a9 9 0 0 1 -11.253 -11.253a4.5 4.5 0 0 1 5.953 -5.954a8.987 8.987 0 0 1 2.65 -.396z"></path>
|
||||
<path d="M8 14.5c.5 2 2.358 2.5 4 2.5c2.905 0 4 -1.187 4 -2.5c0 -1.503 -1.927 -2.5 -4 -2.5s-4 -.997 -4 -2.5c0 -1.313 1.095 -2.5 4 -2.5c1.642 0 3.5 .5 4 2.5"></path>
|
||||
</svg>`,
|
||||
Steam: `<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="M16.5 5a4.5 4.5 0 1 1 -.653 8.953l-4.347 3.009l0 .038a3 3 0 0 1 -2.824 2.995l-.176 .005a3 3 0 0 1 -2.94 -2.402l-2.56 -1.098v-3.5l3.51 1.755a2.989 2.989 0 0 1 2.834 -.635l2.727 -3.818a4.5 4.5 0 0 1 4.429 -5.302z"></path>
|
||||
<circle fill="currentColor" cx="16.5" cy="9.5" r="1"></circle>
|
||||
</svg>`,
|
||||
Telegram: `<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="M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4"></path>
|
||||
</svg>`,
|
||||
Mastodon: `<svg class="icon-tabler" viewBox="-10 -5 1034 1034" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
|
||||
<path fill="currentColor"
|
||||
d="M499 112q-93 1 -166 11q-81 11 -128 33l-14 8q-16 10 -32 25q-22 21 -38 47q-21 33 -32 73q-14 47 -14 103v37q0 77 1 119q3 113 18 188q19 95 62 154q50 67 134 89q109 29 210 24q46 -3 88 -12q30 -7 55 -17l19 -8l-4 -75l-22 6q-28 6 -57 10q-41 6 -78 4q-53 -1 -80 -7
|
||||
q-43 -8 -67 -30q-29 -25 -35 -72q-2 -14 -2 -29l25 6q31 6 65 10q48 7 93 9q42 2 92 -2q32 -2 88 -9t107 -30q49 -23 81.5 -54.5t38.5 -63.5q9 -45 13 -109q4 -46 5 -97v-41q0 -56 -14 -103q-11 -40 -32 -73q-16 -26 -38 -47q-15 -15 -32 -25q-12 -8 -14 -8
|
||||
q-46 -22 -127 -33q-74 -10 -166 -11h-3zM367 267q73 0 109 56l24 39l24 -39q36 -56 109 -56q63 0 101 43t38 117v239h-95v-232q0 -74 -61 -74q-69 0 -69 88v127h-94v-127q0 -88 -69 -88q-61 0 -61 74v232h-95v-239q0 -74 38 -117t101 -43z" />
|
||||
</svg>`,
|
||||
};
|
||||
|
||||
export default socialIcons;
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
// Remove current url path and remove trailing slash if exists
|
||||
const currentUrlPath = Astro.url.pathname.replace(/\/+$/, "");
|
||||
|
||||
// Get url array from path
|
||||
// eg: /tags/tailwindcss => ['tags', 'tailwindcss']
|
||||
const breadcrumbList = currentUrlPath.split("/").slice(1);
|
||||
|
||||
// if breadcrumb is Home > Posts > 1 <etc>
|
||||
// replace Posts with Posts (page number)
|
||||
breadcrumbList[0] === "posts" &&
|
||||
breadcrumbList.splice(0, 2, `Posts (page ${breadcrumbList[1] || 1})`);
|
||||
---
|
||||
|
||||
<nav class="breadcrumb" aria-label="breadcrumb">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
<span aria-hidden="true">></span>
|
||||
</li>
|
||||
{
|
||||
breadcrumbList.map((breadcrumb, index) =>
|
||||
index + 1 === breadcrumbList.length ? (
|
||||
<li>
|
||||
<span
|
||||
class={`${index > 0 ? "lowercase" : "capitalize"}`}
|
||||
aria-current="page"
|
||||
>
|
||||
{/* make the last part lowercase in Home > Tags > some-tag */}
|
||||
{breadcrumb}
|
||||
</span>
|
||||
</li>
|
||||
) : (
|
||||
<li>
|
||||
<a href={`/${breadcrumb}`}>{breadcrumb}</a>
|
||||
<span aria-hidden="true">></span>
|
||||
</li>
|
||||
)
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.breadcrumb {
|
||||
@apply mx-auto mb-1 mt-8 w-full max-w-3xl px-4;
|
||||
}
|
||||
.breadcrumb ul li {
|
||||
@apply inline;
|
||||
}
|
||||
.breadcrumb ul li a {
|
||||
@apply capitalize opacity-70;
|
||||
}
|
||||
.breadcrumb ul li span {
|
||||
@apply opacity-70;
|
||||
}
|
||||
.breadcrumb ul li:not(:last-child) a {
|
||||
@apply hover:opacity-100;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,32 @@
|
|||
import Datetime from "./Datetime";
|
||||
import type { BlogFrontmatter } from "@content/_schemas";
|
||||
|
||||
export interface Props {
|
||||
href?: string;
|
||||
frontmatter: BlogFrontmatter;
|
||||
secHeading?: boolean;
|
||||
}
|
||||
|
||||
export default function Card({ href, frontmatter, secHeading = true }: Props) {
|
||||
const { title, pubDatetime, description } = frontmatter;
|
||||
return (
|
||||
<li className="my-6">
|
||||
<a
|
||||
href={href}
|
||||
className="inline-block text-lg font-medium text-skin-accent decoration-dashed underline-offset-4 focus-visible:no-underline focus-visible:underline-offset-0"
|
||||
>
|
||||
{secHeading ? (
|
||||
<h2 className="text-lg font-medium decoration-dashed hover:underline">
|
||||
{title}
|
||||
</h2>
|
||||
) : (
|
||||
<h3 className="text-lg font-medium decoration-dashed hover:underline">
|
||||
{title}
|
||||
</h3>
|
||||
)}
|
||||
</a>
|
||||
<Datetime datetime={pubDatetime} />
|
||||
<p>{description}</p>
|
||||
</li>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
import { LOCALE } from "@config";
|
||||
|
||||
export interface Props {
|
||||
datetime: string | Date;
|
||||
size?: "sm" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Datetime({ datetime, size = "sm", className }: Props) {
|
||||
return (
|
||||
<div className={`flex items-center space-x-2 opacity-80 ${className}`}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={`${
|
||||
size === "sm" ? "scale-90" : "scale-100"
|
||||
} inline-block h-6 w-6 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>
|
||||
<span className={`italic ${size === "sm" ? "text-sm" : "text-base"}`}>
|
||||
<FormattedDatetime datetime={datetime} />
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const FormattedDatetime = ({ datetime }: { datetime: string | Date }) => {
|
||||
const myDatetime = new Date(datetime);
|
||||
|
||||
const date = myDatetime.toLocaleDateString(LOCALE, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
const time = myDatetime.toLocaleTimeString(LOCALE, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{date}
|
||||
<span aria-hidden="true"> | </span>
|
||||
<span className="sr-only"> at </span>
|
||||
{time}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
import Hr from "./Hr.astro";
|
||||
import Socials from "./Socials.astro";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
||||
export interface Props {
|
||||
noMarginTop?: boolean;
|
||||
}
|
||||
|
||||
const { noMarginTop = false } = Astro.props;
|
||||
---
|
||||
|
||||
<footer class={`${noMarginTop ? "" : "mt-auto"}`}>
|
||||
<Hr noPadding />
|
||||
<div class="footer-wrapper">
|
||||
<Socials centered />
|
||||
<div class="copyright-wrapper">
|
||||
<span>Copyright © {currentYear}</span>
|
||||
<span class="separator"> | </span>
|
||||
<span>All rights reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
footer {
|
||||
@apply w-full;
|
||||
}
|
||||
.footer-wrapper {
|
||||
@apply flex flex-col items-center justify-between py-6 sm:flex-row-reverse sm:py-4;
|
||||
}
|
||||
.link-button {
|
||||
@apply my-1 p-2 hover:rotate-6;
|
||||
}
|
||||
.link-button svg {
|
||||
@apply scale-125;
|
||||
}
|
||||
.copyright-wrapper {
|
||||
@apply my-2 flex flex-col items-center whitespace-nowrap sm:flex-row;
|
||||
}
|
||||
.separator {
|
||||
@apply hidden sm:inline;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,210 @@
|
|||
---
|
||||
import { LOGO_IMAGE, SITE } from "@config";
|
||||
import Hr from "./Hr.astro";
|
||||
import LinkButton from "./LinkButton.astro";
|
||||
|
||||
import logoPNG from "/assets/logo.png";
|
||||
import logoSVG from "/assets/logo.svg";
|
||||
|
||||
export interface Props {
|
||||
activeNav?: "posts" | "tags" | "about" | "search";
|
||||
}
|
||||
|
||||
const { activeNav } = Astro.props;
|
||||
---
|
||||
|
||||
<header>
|
||||
<a id="skip-to-content" href="#main-content">Skip to content</a>
|
||||
<div class="nav-container">
|
||||
<div class="top-nav-wrap">
|
||||
<a href="/" class="logo">
|
||||
{
|
||||
LOGO_IMAGE.enable ? (
|
||||
<img
|
||||
src={LOGO_IMAGE.svg ? logoSVG : logoPNG}
|
||||
alt="AstroPaper Logo"
|
||||
width={LOGO_IMAGE.width}
|
||||
height={LOGO_IMAGE.height}
|
||||
/>
|
||||
) : (
|
||||
SITE.title
|
||||
)
|
||||
}
|
||||
</a>
|
||||
<nav id="nav-menu">
|
||||
<button
|
||||
class="hamburger-menu focus-outline"
|
||||
aria-label="Open Menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="menu-items"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="menu-icon"
|
||||
>
|
||||
<line x1="7" y1="12" x2="21" y2="12" class="line"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6" class="line"></line>
|
||||
<line x1="12" y1="18" x2="21" y2="18" class="line"></line>
|
||||
<line x1="18" y1="6" x2="6" y2="18" class="close"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18" class="close"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<ul id="menu-items" class="display-none sm:flex">
|
||||
<li>
|
||||
<a href="/posts" class={activeNav === "posts" ? "active" : ""}>
|
||||
Posts
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/tags" class={activeNav === "tags" ? "active" : ""}>
|
||||
Tags
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about" class={activeNav === "about" ? "active" : ""}>
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<LinkButton
|
||||
href="/search"
|
||||
className={`focus-outline p-3 sm:p-1 ${
|
||||
activeNav === "search" ? "active" : ""
|
||||
}`}
|
||||
ariaLabel="search"
|
||||
title="Search"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="scale-125 sm:scale-100"
|
||||
><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>
|
||||
</LinkButton>
|
||||
</li>
|
||||
<li>
|
||||
{
|
||||
SITE.lightAndDarkMode && (
|
||||
<button
|
||||
id="theme-btn"
|
||||
class="focus-outline"
|
||||
title="Toggles light & dark"
|
||||
aria-label="auto"
|
||||
aria-live="polite"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="moon-svg">
|
||||
<path d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="sun-svg">
|
||||
<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>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<Hr />
|
||||
</header>
|
||||
|
||||
<style>
|
||||
#skip-to-content {
|
||||
@apply absolute -top-full left-16 z-50 bg-skin-accent px-3 py-2 text-skin-inverted transition-all focus:top-4;
|
||||
}
|
||||
.nav-container {
|
||||
@apply mx-auto flex max-w-3xl flex-col items-center justify-between sm:flex-row;
|
||||
}
|
||||
.top-nav-wrap {
|
||||
@apply relative flex w-full items-start justify-between p-4 sm:items-center sm:py-8;
|
||||
}
|
||||
.logo {
|
||||
@apply absolute py-1 text-xl font-semibold sm:static sm:text-2xl;
|
||||
}
|
||||
.hamburger-menu {
|
||||
@apply self-end p-2 sm:hidden;
|
||||
}
|
||||
.hamburger-menu svg {
|
||||
@apply h-6 w-6 scale-125 fill-skin-base;
|
||||
}
|
||||
|
||||
nav {
|
||||
@apply flex w-full flex-col items-center bg-skin-fill sm:ml-2 sm:flex-row sm:justify-end sm:space-x-4 sm:py-0;
|
||||
}
|
||||
nav ul {
|
||||
@apply mt-4 grid w-44 grid-cols-2 grid-rows-4 gap-x-2 gap-y-2 sm:ml-0 sm:mt-0 sm:w-auto sm:gap-x-5 sm:gap-y-0;
|
||||
}
|
||||
nav ul li {
|
||||
@apply col-span-2 flex items-center justify-center;
|
||||
}
|
||||
nav ul li a {
|
||||
@apply w-full px-4 py-3 text-center font-medium hover:text-skin-accent sm:my-0 sm:px-2 sm:py-1;
|
||||
}
|
||||
nav ul li:nth-child(4) a {
|
||||
@apply w-auto;
|
||||
}
|
||||
nav ul li:nth-child(4),
|
||||
nav ul li:nth-child(5) {
|
||||
@apply col-span-1;
|
||||
}
|
||||
nav a.active {
|
||||
@apply underline decoration-wavy decoration-2 underline-offset-4;
|
||||
}
|
||||
nav a.active svg {
|
||||
@apply fill-skin-accent;
|
||||
}
|
||||
|
||||
nav button {
|
||||
@apply p-1;
|
||||
}
|
||||
nav button svg {
|
||||
@apply h-6 w-6 fill-skin-base hover:fill-skin-accent;
|
||||
}
|
||||
#theme-btn {
|
||||
@apply p-3 sm:p-1;
|
||||
}
|
||||
#theme-btn svg {
|
||||
@apply scale-125 hover:rotate-12 sm:scale-100;
|
||||
}
|
||||
|
||||
.menu-icon line {
|
||||
@apply transition-opacity duration-75 ease-in-out;
|
||||
}
|
||||
.menu-icon .close {
|
||||
opacity: 0;
|
||||
}
|
||||
.menu-icon.is-active .line {
|
||||
@apply opacity-0;
|
||||
}
|
||||
.menu-icon.is-active .close {
|
||||
@apply opacity-100;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Toggle menu
|
||||
const menuBtn = document.querySelector(".hamburger-menu");
|
||||
const menuIcon = document.querySelector(".menu-icon");
|
||||
const menuItems = document.querySelector("#menu-items");
|
||||
|
||||
menuBtn?.addEventListener("click", () => {
|
||||
const menuExpanded = menuBtn.getAttribute("aria-expanded") === "true";
|
||||
menuIcon?.classList.toggle("is-active");
|
||||
menuBtn.setAttribute("aria-expanded", menuExpanded ? "false" : "true");
|
||||
menuBtn.setAttribute(
|
||||
"aria-label",
|
||||
menuExpanded ? "Open Menu" : "Close Menu"
|
||||
);
|
||||
menuItems?.classList.toggle("display-none");
|
||||
});
|
||||
</script>
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
export interface Props {
|
||||
noPadding?: boolean;
|
||||
ariaHidden?: boolean;
|
||||
}
|
||||
|
||||
const { noPadding = false, ariaHidden = true } = Astro.props;
|
||||
---
|
||||
|
||||
<div class={`max-w-3xl mx-auto ${noPadding ? "px-0" : "px-4"}`}>
|
||||
<hr class="border-skin-line" aria-hidden={ariaHidden} />
|
||||
</div>
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
export interface Props {
|
||||
href: string;
|
||||
className?: string;
|
||||
ariaLabel?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
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}
|
||||
title={title}
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
|
||||
<style>
|
||||
a {
|
||||
@apply hover:text-skin-accent;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,122 @@
|
|||
import Fuse from "fuse.js";
|
||||
import { useEffect, useRef, useState, useMemo } from "react";
|
||||
import Card from "@components/Card";
|
||||
import slugify from "@utils/slugify";
|
||||
import type { BlogFrontmatter } from "@content/_schemas";
|
||||
|
||||
export type SearchItem = {
|
||||
title: string;
|
||||
description: string;
|
||||
data: BlogFrontmatter;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
searchList: SearchItem[];
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
item: SearchItem;
|
||||
refIndex: number;
|
||||
}
|
||||
|
||||
export default function SearchBar({ searchList }: Props) {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [inputVal, setInputVal] = useState("");
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[] | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const handleChange = (e: React.FormEvent<HTMLInputElement>) => {
|
||||
setInputVal(e.currentTarget.value);
|
||||
};
|
||||
|
||||
const fuse = useMemo(
|
||||
() =>
|
||||
new Fuse(searchList, {
|
||||
keys: ["title", "description"],
|
||||
includeMatches: true,
|
||||
minMatchCharLength: 2,
|
||||
threshold: 0.5,
|
||||
}),
|
||||
[searchList]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// if URL has search query,
|
||||
// insert that search query in input field
|
||||
const searchUrl = new URLSearchParams(window.location.search);
|
||||
const searchStr = searchUrl.get("q");
|
||||
if (searchStr) setInputVal(searchStr);
|
||||
|
||||
// put focus cursor at the end of the string
|
||||
setTimeout(function () {
|
||||
inputRef.current!.selectionStart = inputRef.current!.selectionEnd =
|
||||
searchStr?.length || 0;
|
||||
}, 50);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Add search result only if
|
||||
// input value is more than one character
|
||||
let inputResult = inputVal.length > 1 ? fuse.search(inputVal) : [];
|
||||
setSearchResults(inputResult);
|
||||
|
||||
// Update search string in URL
|
||||
if (inputVal.length > 0) {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
searchParams.set("q", inputVal);
|
||||
const newRelativePathQuery =
|
||||
window.location.pathname + "?" + searchParams.toString();
|
||||
history.replaceState(null, "", newRelativePathQuery);
|
||||
} else {
|
||||
history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
}, [inputVal]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label className="relative block">
|
||||
<span className="absolute inset-y-0 left-0 flex items-center pl-2 opacity-75">
|
||||
<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>
|
||||
<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"
|
||||
placeholder="Search for anything..."
|
||||
type="text"
|
||||
name="search"
|
||||
value={inputVal}
|
||||
onChange={handleChange}
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{inputVal.length > 1 && (
|
||||
<div className="mt-8">
|
||||
Found {searchResults?.length}
|
||||
{searchResults?.length && searchResults?.length === 1
|
||||
? " result"
|
||||
: " results"}{" "}
|
||||
for '{inputVal}'
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul>
|
||||
{searchResults &&
|
||||
searchResults.map(({ item, refIndex }) => (
|
||||
<Card
|
||||
href={`/posts/${slugify(item.data)}`}
|
||||
frontmatter={item.data}
|
||||
key={`${refIndex}-${slugify(item.data)}`}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
import { SOCIALS } from "@config";
|
||||
import LinkButton from "./LinkButton.astro";
|
||||
import socialIcons from "@assets/socialIcons";
|
||||
|
||||
export interface Props {
|
||||
centered?: boolean;
|
||||
withText?: boolean;
|
||||
}
|
||||
|
||||
const { centered = false, withText = false } = Astro.props;
|
||||
---
|
||||
|
||||
<div class={`social-icons ${centered && "flex"} ${withText && "flex-col"}`}>
|
||||
{
|
||||
SOCIALS.filter(social => social.active).map(social => (
|
||||
<LinkButton
|
||||
href={social.href}
|
||||
className="link-button flex flex-row items-center gap-2"
|
||||
title={social.linkTitle}
|
||||
>
|
||||
<Fragment set:html={socialIcons[social.name]} />
|
||||
{
|
||||
withText && (
|
||||
<span>
|
||||
{social.name}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</LinkButton>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.social-icons {
|
||||
@apply flex-wrap justify-center gap-1;
|
||||
}
|
||||
.link-button {
|
||||
@apply p-2 sm:p-1;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
export interface Props {
|
||||
name: string;
|
||||
size?: "sm" | "lg";
|
||||
}
|
||||
|
||||
const { name, size = "sm" } = Astro.props;
|
||||
---
|
||||
|
||||
<li
|
||||
class={`inline-block ${
|
||||
size === "sm" ? "my-1 underline-offset-4" : "my-3 mx-1 underline-offset-8"
|
||||
}`}
|
||||
>
|
||||
<a
|
||||
href={`/tags/${name.toLowerCase()}`}
|
||||
class={`${size === "sm" ? "text-sm" : "text-lg"} pr-2 group`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class={`${size === "sm" ? " scale-75" : "scale-110"}`}
|
||||
><path
|
||||
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>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
a {
|
||||
@apply relative underline decoration-dashed hover:-top-0.5 hover:text-skin-accent focus-visible:p-1;
|
||||
}
|
||||
a svg {
|
||||
@apply -mr-5 h-6 w-6 scale-95 text-skin-base opacity-80 group-hover:fill-skin-accent;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,41 @@
|
|||
import type { Site, SocialObjects } from "./types";
|
||||
|
||||
export const SITE: Site = {
|
||||
website: "https://watzon.tech/",
|
||||
author: "Chris Watson",
|
||||
desc: "Personal website and blog of Chris Watson",
|
||||
title: "Watzon Does Tech",
|
||||
ogImage: "astropaper-og.jpg",
|
||||
lightAndDarkMode: true,
|
||||
postPerPage: 5,
|
||||
};
|
||||
|
||||
export const LOCALE = ["en-US"]; // set to [] to use the environment default
|
||||
|
||||
export const LOGO_IMAGE = {
|
||||
enable: false,
|
||||
svg: true,
|
||||
width: 216,
|
||||
height: 46,
|
||||
};
|
||||
|
||||
export const SOCIALS: SocialObjects = [
|
||||
{
|
||||
name: "Github",
|
||||
href: "https://github.com/watzon",
|
||||
linkTitle: ` Watzon on Github`,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: "Mastodon",
|
||||
href: "https://watzonmanor.com/@watzon",
|
||||
linkTitle: `Watzon on Mastodon`,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
name: "TikTok",
|
||||
href: "https://www.tiktok.com/@3dprintifer",
|
||||
linkTitle: `Watzon on TikTok`,
|
||||
active: true,
|
||||
}
|
||||
];
|
|
@ -0,0 +1,18 @@
|
|||
import { z } from "astro:content";
|
||||
|
||||
export const blogSchema = z
|
||||
.object({
|
||||
author: z.string().optional(),
|
||||
pubDatetime: z.date(),
|
||||
title: z.string(),
|
||||
postSlug: z.string().optional(),
|
||||
featured: z.boolean().optional(),
|
||||
draft: z.boolean().optional(),
|
||||
tags: z.array(z.string()).default(["others"]),
|
||||
ogImage: z.string().optional(),
|
||||
description: z.string(),
|
||||
canonicalURL: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type BlogFrontmatter = z.infer<typeof blogSchema>;
|
|
@ -0,0 +1,149 @@
|
|||
---
|
||||
author: Chris W
|
||||
pubDatetime: 2022-12-07
|
||||
title: An introduction to the Crystal standard library and its core modules
|
||||
description:
|
||||
Introducing newcomers to the Crystal standard library and some of the most useful modules contained therein
|
||||
tags:
|
||||
- crystal
|
||||
- programming
|
||||
---
|
||||
|
||||
## Table of contents
|
||||
|
||||
Crystal is a modern, high-performance programming language that combines the expressiveness of Ruby with the efficiency of low-level languages like C. One of the key features of Crystal is its rich and comprehensive standard library, which provides a wide range of tools and modules for common tasks and operations. In this blog post, I'll introduce the Crystal standard library and highlight some of its core modules and functions. Whether you're new to Crystal or an experienced user, this article will give you a better understanding of the power and flexibility of the language.
|
||||
|
||||
One of the greatest strengths of Crystal lies in its standard library, or the modules, classes, and functions that come baked in to the language. From the beginning, Crystal has pulled inspiration from Ruby. This can be seen in the syntax most obviously, but it's also very apparent in its standard library. Glancing through the [API docs](https://crystal-lang.org/api) you'll see a lot of familiar modules (assuming you're familiar with Ruby that is). While there are way too many modules to go over individually, I want to highlight some of the ones that stand out the most to me when coming from other languages.
|
||||
|
||||
## HTTP
|
||||
|
||||
One of Crystal's biggest strengths is its ability to bridge the gap between dynamic languages like Ruby and Python, and typed languages like Rust and C when it comes to web development. The reason this is possible is because of its amazing HTTP module, which comes with a [Client](https://crystal-lang.org/api/1.6.2/HTTP/Client.html) for making requests, and a [Server](https://crystal-lang.org/api/1.6.2/HTTP/Server.html) for receiving them. As a matter of fact, the home page of Crystal's website shows a very simple HTTP server in action, and it really could not be simpler.
|
||||
|
||||
``` crystal
|
||||
# A very basic HTTP server
|
||||
require "http/server"
|
||||
|
||||
server = HTTP::Server.new do |context|
|
||||
context.response.content_type = "text/plain"
|
||||
context.response.print "Hello world, got #{context.request.path}!"
|
||||
end
|
||||
|
||||
puts "Listening on http://127.0.0.1:8080"
|
||||
server.listen(8080)
|
||||
```
|
||||
|
||||
Running that code will leave you with a very basic server running on port `8080` which will return "Hello world, got _\[some path\]_!" for every path you hit. Of course, using the built-in HTTP server isn't the only way to use Crystal for a website. There are a myriad of options from [Kemal]((https://kemalcr.com/)) and [Grip](https://github.com/grip-framework/grip) which are both very Sinatra/Flask like, to [Lucky](https://luckyframework.org/) which is closer to Rails (not at all in design, but it takes the batteries included approach).
|
||||
|
||||
Of course, the HTTP module would be nothing without
|
||||
|
||||
## IO
|
||||
|
||||
Some may see this as one of the more boring classes, but I have used IO so much in my work with Crystal that I'd be remiss if I didn't give it a shoutout here. IO makes it possible to deal with streams of data, whether those are coming from a TCP connection, a parser, or something else. As the name implies, IO deals with input/output and is used by other classes within the standard library such as [File](https://crystal-lang.org/api/1.6.2/File.html) and [Socket](https://crystal-lang.org/api/1.6.2/Socket.html).
|
||||
|
||||
`IO::Memory` can be used as a sort of in-memory file descriptor, but what I've found to be one of the most useful (albeit niche) parts of IO is `IO::ByteFormat` which allows you to encode and decode integers to/from `Bytes` and `IO`. This is extremely useful when implementing things such as protocol buffers and RPC. And look how easy it is:
|
||||
|
||||
``` crystal
|
||||
io = IO::Memory.new
|
||||
io.write_bytes(0x1234_i16, IO::ByteFormat::LittleEndian)
|
||||
io.to_slice # => Bytes[0x34, 0x12]
|
||||
|
||||
int16 = io.read_bytes(Int16, IO::ByteFormat::LittleEndian)
|
||||
int16 # => 0x1234_i16
|
||||
```
|
||||
|
||||
A bit more verbose than I'd prefer, but you can't argue with results.
|
||||
|
||||
## JSON
|
||||
|
||||
What would a web-centric language be without support for JSON. Now I'm not saying that Crystal is intentionally web-centric, but it is filling a hole that Ruby leaves by being slow as molasses, and Ruby is used *heavily* for web development.
|
||||
|
||||
Working with JSON in statically typed languages can be a massive pain, because JSON is, by its very nature, untyped and kind of unsafe to deal with. Before finding Crystal I loved the way Go handled JSON (de)serialization. As an example for those unfamiliar:
|
||||
|
||||
``` go
|
||||
type User struct {
|
||||
Name string `json:"full_name"`
|
||||
Age int `json:"age,omitempty"`
|
||||
Active bool `json:"-"`
|
||||
lastLoginAt string
|
||||
}
|
||||
```
|
||||
|
||||
As you can see Go uses "tags" to change how the JSON data is transformed when it's marshaled into the `User` struct. In this example the JSON key `full_name` will become `Name`, `age` will become `Age` and an empty value will be discarded, `Active` will be removed entirely thanks to the `"-"` and `lastLoginAt` will be read in as is.
|
||||
|
||||
Now I'll show a similar example using Crystal:
|
||||
|
||||
``` crystal
|
||||
struct User
|
||||
include JSON::Serializable
|
||||
|
||||
@[JSON::Field(key: "full_name")]
|
||||
getter name : String
|
||||
|
||||
getter age : Int32?
|
||||
|
||||
@[JSON::Field(ignore: true)]
|
||||
getter active : Bool = false
|
||||
|
||||
@[JSON::Field(key: "lastLoginAt")]
|
||||
getter last_login_at : String
|
||||
end
|
||||
```
|
||||
|
||||
Things are a bit different here, partially because different assumptions have to be made. The first field, `name`, is doing the same thing; we're taking the `full_name` JSON field and calling it `name` in this struct. `age` is defined as a nilable field, and will be nil by default if the `age` property doesn't show up in the JSON. `active` is being ignored, so we set a default value for it to keep the compiler from yelling at us. Lastly we're going to rename `lastLoginAt` to `last_login_at` to keep things in line with Crystal conventions.
|
||||
|
||||
Parsing an incoming JSON object as a `User` would then be as simple as:
|
||||
|
||||
``` crystal
|
||||
user = User.from_json(json_string)
|
||||
```
|
||||
|
||||
And assuming the JSON matches the schema, you'll be left with a `User` object to work with.
|
||||
|
||||
There is also a `YAML` module which functions almost identically, and I'm working on a TOML shard right now which I intend to have the same API to keep things smooth.
|
||||
|
||||
## Time
|
||||
|
||||
I'm going to end with Time, because time is another one of those things that's typically a pain in the butt to work with, but Crystal makes it easy. For anyone coming from Ruby, none of this will be new to you, other than the fact that Crystal just has a `Time` module, no `Date` and no `DateTime`. This drastically simplifies things.
|
||||
|
||||
One thing that's important to note is that Crystal does use an `Int64` for representing time and the supported date range is `0001-01-01 00:00:00.0` to `9999-12-31 23:59:59.999_999_999` in any local time zone. That means we don't have to worry about running out of dates for 7,676 years or so. I think most languages have started switching to a 64-bit time representation if possible, but I just thought it would be good to point that out.
|
||||
|
||||
The entire `Time` module is full of so much syntactic sugar you might leave with a toothache, but I find it to be extremely useful if you want to work with time in an idiomatic and easy to understand manner. For instance:
|
||||
|
||||
``` crystal
|
||||
time = Time.utc(2016, 2, 15, 10, 20, 30)
|
||||
time.year # => 2016
|
||||
time.month # => 2
|
||||
time.day # => 15
|
||||
time.hour # => 10
|
||||
time.minute # => 20
|
||||
time.second # => 30
|
||||
time.millisecond # => 0
|
||||
time.nanosecond # => 0
|
||||
time.day_of_week # => Time::DayOfWeek::Monday
|
||||
time.day_of_year # => 46
|
||||
time.monday? # => true
|
||||
time.time_of_day # => 10:20:30
|
||||
```
|
||||
|
||||
You can also do math with time.
|
||||
|
||||
``` crystal
|
||||
Time.utc + 3.days
|
||||
Time.utc - 14.years
|
||||
# etc etc
|
||||
```
|
||||
|
||||
Need to localize things to a specific timezone? No problem.
|
||||
|
||||
``` crystal
|
||||
time = Time.local(2016, 2, 15, 10, 20, 30, location: Time::Location.load("Europe/Berlin"))
|
||||
time.inspect # => "2016-02-15 10:20:30.0 +01:00 Europe/Berlin"
|
||||
```
|
||||
|
||||
This isn't unique to the `Time` module. There is syntactic sugar all over the standard library that's there to make your life easier. Which leads me to my last section.
|
||||
|
||||
## How much is too much?
|
||||
|
||||
If there's anything I wanted to convey in this post, it's that Crystal's standard library is awesome. I barely scratched the surface of the useful classes and modules that exist within. But with great power... No scratch that. With large standard libraries, come the pain of forgetting about all of those useful tools when you need them most. Some, the ones you use most often, you'll of course remember, but what about the ones you only find yourself needing once in a while? You're probably going to forget they even exist.
|
||||
|
||||
Is this a problem with the language? Or even the standard library itself? I don't think so. The human brain only has so much capacity for standard library documentation, and even with a smaller library, you'd probably still have trouble remembering the useful stuff when you need it. In the end, this is why we have documentation in the first place. Sometimes I'll just pick a random class or module and read through the API documentation to learn about some of the useful tools I didn't know exist. I'm just grateful that so much time and effort has been put into developer happiness when it comes to the standard library, and Crystal itself.
|
|
@ -0,0 +1,8 @@
|
|||
import { defineCollection } from "astro:content";
|
||||
import { blogSchema } from "./_schemas";
|
||||
|
||||
const blog = defineCollection({
|
||||
schema: blogSchema,
|
||||
});
|
||||
|
||||
export const collections = { blog };
|
|
@ -0,0 +1,2 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
import { SITE } from "@config";
|
||||
import Breadcrumbs from "@components/Breadcrumbs.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Layout from "./Layout.astro";
|
||||
|
||||
export interface Props {
|
||||
frontmatter: {
|
||||
title: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const { frontmatter } = Astro.props;
|
||||
---
|
||||
|
||||
<Layout title={`${frontmatter.title} | ${SITE.title}`}>
|
||||
<Header activeNav="about" />
|
||||
<Breadcrumbs />
|
||||
<main id="main-content">
|
||||
<section id="about" class="prose mb-28 max-w-3xl prose-img:border-0">
|
||||
<h1 class="text-2xl tracking-wider sm:text-3xl">{frontmatter.title}</h1>
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
<Footer />
|
||||
</Layout>
|
|
@ -0,0 +1,83 @@
|
|||
---
|
||||
import { SITE } from "@config";
|
||||
import "@styles/base.css";
|
||||
|
||||
const googleSiteVerification = import.meta.env.PUBLIC_GOOGLE_SITE_VERIFICATION;
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
author?: string;
|
||||
description?: string;
|
||||
ogImage?: string;
|
||||
canonicalURL?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
title = SITE.title,
|
||||
author = SITE.author,
|
||||
description = SITE.desc,
|
||||
ogImage = SITE.ogImage,
|
||||
canonicalURL = new URL(Astro.url.pathname, Astro.site).href,
|
||||
} = Astro.props;
|
||||
|
||||
const socialImageURL = new URL(
|
||||
ogImage ? ogImage : SITE.ogImage,
|
||||
Astro.url.origin
|
||||
).href;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="canonical" href={canonicalURL} />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
<!-- General Meta Tags -->
|
||||
<title>{title}</title>
|
||||
<meta name="title" content={title} />
|
||||
<meta name="description" content={description} />
|
||||
<meta name="author" content={author} />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:url" content={canonicalURL} />
|
||||
<meta property="og:image" content={socialImageURL} />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content={canonicalURL} />
|
||||
<meta property="twitter:title" content={title} />
|
||||
<meta property="twitter:description" content={description} />
|
||||
<meta property="twitter:image" content={socialImageURL} />
|
||||
|
||||
<!-- 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"
|
||||
/>
|
||||
|
||||
{
|
||||
// If PUBLIC_GOOGLE_SITE_VERIFICATION is set in the environment variable,
|
||||
// include google-site-verification tag in the heading
|
||||
// Learn more: https://support.google.com/webmasters/answer/9008080#meta_tag_verification&zippy=%2Chtml-tag
|
||||
googleSiteVerification && (
|
||||
<meta
|
||||
name="google-site-verification"
|
||||
content={googleSiteVerification}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
<script is:inline src="/toggle-theme.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
import Breadcrumbs from "@components/Breadcrumbs.astro";
|
||||
|
||||
export interface Props {
|
||||
pageTitle: string;
|
||||
pageDesc?: string;
|
||||
}
|
||||
|
||||
const { pageTitle, pageDesc } = Astro.props;
|
||||
---
|
||||
|
||||
<Breadcrumbs />
|
||||
<main id="main-content">
|
||||
<h1>{pageTitle}</h1>
|
||||
<p>{pageDesc}</p>
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#main-content {
|
||||
@apply mx-auto w-full max-w-3xl px-4 pb-12;
|
||||
}
|
||||
#main-content h1 {
|
||||
@apply text-2xl font-semibold sm:text-3xl;
|
||||
}
|
||||
#main-content p {
|
||||
@apply mb-6 mt-2 italic;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,62 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Tag from "@components/Tag.astro";
|
||||
import Datetime from "@components/Datetime";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { slugifyStr } from "@utils/slugify";
|
||||
|
||||
export interface Props {
|
||||
post: CollectionEntry<"blog">;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
const { title, author, description, ogImage, canonicalURL, pubDatetime, tags } = post.data;
|
||||
|
||||
const { Content } = await post.render();
|
||||
|
||||
const ogUrl = new URL(ogImage ? ogImage : `${title}.png`, Astro.url.origin)
|
||||
.href;
|
||||
---
|
||||
|
||||
<Layout title={title} author={author} description={description} ogImage={ogUrl} canonicalURL={canonicalURL}>
|
||||
<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()"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M13.293 6.293 7.586 12l5.707 5.707 1.414-1.414L10.414 12l4.293-4.293z"
|
||||
></path>
|
||||
</svg><span>Go back</span>
|
||||
</button>
|
||||
</div>
|
||||
<main id="main-content">
|
||||
<h1 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">
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
<ul class="tags-container">
|
||||
{tags.map(tag => <Tag name={slugifyStr(tag)} />)}
|
||||
</ul>
|
||||
</main>
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
main {
|
||||
@apply mx-auto w-full max-w-3xl px-4 pb-12;
|
||||
}
|
||||
.post-title {
|
||||
@apply text-2xl font-semibold text-skin-accent;
|
||||
}
|
||||
.tags-container {
|
||||
@apply my-8;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
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 Card from "@components/Card";
|
||||
import LinkButton from "@components/LinkButton.astro";
|
||||
import slugify from "@utils/slugify";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
export interface Props {
|
||||
pageNum: number;
|
||||
totalPages: number;
|
||||
posts: CollectionEntry<"blog">[];
|
||||
}
|
||||
|
||||
const { pageNum, totalPages, posts } = Astro.props;
|
||||
|
||||
const prev = pageNum > 1 ? "" : "disabled";
|
||||
const next = pageNum < totalPages ? "" : "disabled";
|
||||
---
|
||||
|
||||
<Layout title={`Posts | ${SITE.title}`}>
|
||||
<Header activeNav="posts" />
|
||||
<Main pageTitle="Posts" pageDesc="All the articles I've posted.">
|
||||
<ul>
|
||||
{
|
||||
posts.map(({ data }) => (
|
||||
<Card href={`/posts/${slugify(data)}`} 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>
|
||||
|
||||
<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>
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
import { SITE } from "@config";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import LinkButton from "@components/LinkButton.astro";
|
||||
---
|
||||
|
||||
<Layout title={`404 Not Found | ${SITE.title}`}>
|
||||
<Header />
|
||||
|
||||
<main id="main-content">
|
||||
<div class="not-found-wrapper">
|
||||
<h1 aria-label="404 Not Found">404</h1>
|
||||
<span aria-hidden="true">¯\_(ツ)_/¯</span>
|
||||
<p>Page Not Found</p>
|
||||
<LinkButton
|
||||
href="/"
|
||||
className="my-6 underline decoration-dashed underline-offset-8 text-lg"
|
||||
>
|
||||
Go back home
|
||||
</LinkButton>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
#main-content {
|
||||
@apply mx-auto flex max-w-3xl flex-1 items-center justify-center;
|
||||
}
|
||||
.not-found-wrapper {
|
||||
@apply mb-14 flex flex-col items-center justify-center;
|
||||
}
|
||||
.not-found-wrapper h1 {
|
||||
@apply text-9xl font-bold text-skin-accent;
|
||||
}
|
||||
.not-found-wrapper p {
|
||||
@apply mt-4 text-2xl sm:text-3xl;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,18 @@
|
|||
import { getCollection } from "astro:content";
|
||||
import generateOgImage from "@utils/generateOgImage";
|
||||
import type { APIRoute } from "astro";
|
||||
|
||||
export const get: APIRoute = async ({ params }) => ({
|
||||
body: await generateOgImage(params.ogTitle),
|
||||
});
|
||||
|
||||
const postImportResult = await getCollection("blog", ({ data }) => !data.draft);
|
||||
const posts = Object.values(postImportResult);
|
||||
|
||||
export function getStaticPaths() {
|
||||
return posts
|
||||
.filter(({ data }) => !data.ogImage)
|
||||
.map(({ data }) => ({
|
||||
params: { ogTitle: data.title },
|
||||
}));
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
layout: ../layouts/AboutLayout.astro
|
||||
title: "About"
|
||||
---
|
||||
|
||||
Hi, I'm Chris. I'm a software engineer, amateur photographer, 3d printing enthusiast, husband, and father from the ~great~ _mostly ok_ state of Utah. I have (sometiems crippling) ADHD and Autism, so don't expect great things from this blog, but occasionally I will post articles that, if I'm lucky, might make you think.
|
||||
|
||||
## Personal Life
|
||||
|
||||
I am 30 years old, have been married to the same wonderful woman for the past 8 years, and have 2 beautiful children who were both born prematurely. When I'm not doing software development of some kind I'm probably either 3d printing, taking pictures, or playing with my kids. I also run a 3d printing focused TikTok with a decent number of followers.
|
||||
|
||||
## Experience
|
||||
|
||||
I've been a software engineer for about 14 years now, with about 9 of those years being in a professional context. Throughout the years I've used everything from PHP to Ruby to Java and Zig, and in that time I have picked up a lot of opinions about software, programming languages, best practices, and anything else you could imagine.
|
||||
|
||||
I would like to say that I have made it through the majority of my hatred for various languages, and have instead chosen to appreciate when something just might be the best tool for the job. I would like to say that, but Java still exists.
|
||||
|
||||
Anyway, here is a list of languages that I currently use and may well write about at some point:
|
||||
|
||||
- **Crystal** - I actually use this one for work, and despite its many issues it still holds the position as one of my favorite languages.
|
||||
- **TypeScript** - This also encompasses JavaScript as well, but I much prefer TypeScript in my day-to-day.
|
||||
- **Zig** - For systems programming there is no better language currently in development.
|
||||
- **Nim** - Close runner up to Zig.
|
||||
- **Ruby** - One of the first languages I learned, and one I still work with often enough. I don't love Ruby as much as I used to though.
|
||||
- **Python** - Yeah, I use it sometimes.
|
|
@ -0,0 +1,149 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import LinkButton from "@components/LinkButton.astro";
|
||||
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";
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
const featuredPosts = sortedPosts.filter(({ data }) => data.featured);
|
||||
|
||||
const socialCount = SOCIALS.filter(social => social.active).length;
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Header />
|
||||
<main id="main-content">
|
||||
<section id="hero">
|
||||
<h1>Watzon Does Tech</h1>
|
||||
<a
|
||||
target="_blank"
|
||||
href="/rss.xml"
|
||||
class="rss-link"
|
||||
aria-label="rss feed"
|
||||
title="RSS Feed"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="rss-icon"
|
||||
><path
|
||||
d="M19 20.001C19 11.729 12.271 5 4 5v2c7.168 0 13 5.832 13 13.001h2z"
|
||||
></path><path
|
||||
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>
|
||||
</a>
|
||||
|
||||
<p>
|
||||
Hello, and welcome to my blog. I am Chris, a software engineer from Utah. As such, the prevailing theme
|
||||
of this blog will be software development and technology, but will also be open to other topics on
|
||||
things that interest me such as 3d printing, photography, gaming, and more.
|
||||
</p>
|
||||
{
|
||||
// only display if at least one social link is enabled
|
||||
socialCount > 0 && (
|
||||
<div class="social-wrapper">
|
||||
<div class="social-links">You can also find me on:</div>
|
||||
<Socials withText />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<Hr />
|
||||
|
||||
{
|
||||
featuredPosts.length > 0 && (
|
||||
<>
|
||||
<section id="featured">
|
||||
<h2>Featured</h2>
|
||||
<ul>
|
||||
{featuredPosts.map(({ data }) => (
|
||||
<Card
|
||||
href={`/posts/${slugify(data)}`}
|
||||
frontmatter={data}
|
||||
secHeading={false}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
<Hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<section id="recent-posts">
|
||||
<h2>Recent Posts</h2>
|
||||
<ul>
|
||||
{
|
||||
sortedPosts.map(
|
||||
({ data }, index) =>
|
||||
index < 4 && (
|
||||
<Card
|
||||
href={`/posts/${slugify(data)}`}
|
||||
frontmatter={data}
|
||||
secHeading={false}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
<div class="all-posts-btn-wrapper">
|
||||
<LinkButton href="/posts">
|
||||
All Posts
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="m11.293 17.293 1.414 1.414L19.414 12l-6.707-6.707-1.414 1.414L15.586 11H6v2h9.586z"
|
||||
></path>
|
||||
</svg>
|
||||
</LinkButton>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
/* ===== Hero Section ===== */
|
||||
#hero {
|
||||
@apply pb-6 pt-8;
|
||||
}
|
||||
#hero h1 {
|
||||
@apply my-4 inline-block text-3xl font-bold sm:my-8 sm:text-5xl;
|
||||
}
|
||||
#hero .rss-link {
|
||||
@apply mb-6;
|
||||
}
|
||||
#hero .rss-icon {
|
||||
@apply mb-2 h-6 w-6 scale-110 fill-skin-accent sm:mb-3 sm:scale-125;
|
||||
}
|
||||
#hero p {
|
||||
@apply my-2;
|
||||
}
|
||||
.social-wrapper {
|
||||
@apply mt-4;
|
||||
}
|
||||
.social-links {
|
||||
@apply mb-1 mr-2 whitespace-nowrap;
|
||||
}
|
||||
|
||||
/* ===== Featured & Recent Posts Sections ===== */
|
||||
#featured,
|
||||
#recent-posts {
|
||||
@apply pb-6 pt-12;
|
||||
}
|
||||
#featured h2,
|
||||
#recent-posts h2 {
|
||||
@apply text-2xl font-semibold tracking-wide;
|
||||
}
|
||||
.all-posts-btn-wrapper {
|
||||
@apply my-8 text-center;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,58 @@
|
|||
---
|
||||
import { 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">;
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
const postResult = posts.map(post => ({
|
||||
params: { slug: slugify(post.data) },
|
||||
props: { post },
|
||||
}));
|
||||
|
||||
const pagePaths = getPageNumbers(posts.length).map(pageNum => ({
|
||||
params: { slug: String(pageNum) },
|
||||
}));
|
||||
|
||||
return [...postResult, ...pagePaths];
|
||||
}
|
||||
|
||||
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}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
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} />
|
|
@ -0,0 +1,21 @@
|
|||
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() {
|
||||
const posts = await getCollection("blog");
|
||||
const sortedPosts = getSortedPosts(posts);
|
||||
return rss({
|
||||
title: SITE.title,
|
||||
description: SITE.desc,
|
||||
site: SITE.website,
|
||||
items: sortedPosts.map(({ data }) => ({
|
||||
link: `posts/${slugify(data)}`,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
pubDate: new Date(data.pubDatetime),
|
||||
})),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
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 Search from "@components/Search";
|
||||
|
||||
// Retrieve all articles
|
||||
const posts = await getCollection("blog", ({ data }) => !data.draft);
|
||||
|
||||
// List of items to search in
|
||||
const searchList = posts.map(({ data }) => ({
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
data,
|
||||
}));
|
||||
---
|
||||
|
||||
<Layout title={`Search | ${SITE.title}`}>
|
||||
<Header activeNav="search" />
|
||||
<Main pageTitle="Search" pageDesc="Search any article ...">
|
||||
<Search client:load searchList={searchList} />
|
||||
</Main>
|
||||
<Footer />
|
||||
</Layout>
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
import { 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}`}
|
||||
pageDesc={`All the articles with the tag "${tag}".`}
|
||||
>
|
||||
<ul>
|
||||
{
|
||||
sortTagsPost.map(({ data }) => (
|
||||
<Card href={`/posts/${slugify(data)}`} frontmatter={data} />
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</Main>
|
||||
<Footer />
|
||||
</Layout>
|
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Main from "@layouts/Main.astro";
|
||||
import Tag from "@components/Tag.astro";
|
||||
import getUniqueTags from "@utils/getUniqueTags";
|
||||
import { SITE } from "@config";
|
||||
|
||||
const posts = await getCollection("blog");
|
||||
|
||||
let tags = getUniqueTags(posts);
|
||||
---
|
||||
|
||||
<Layout title={`Tags | ${SITE.title}`}>
|
||||
<Header activeNav="tags" />
|
||||
<Main pageTitle="Tags" pageDesc="All the tags used in posts.">
|
||||
<ul>
|
||||
{tags.map(tag => <Tag name={tag} size="lg" />)}
|
||||
</ul>
|
||||
</Main>
|
||||
<Footer />
|
||||
</Layout>
|
|
@ -0,0 +1,138 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root,
|
||||
/* Catppuccin Latte */
|
||||
html[data-theme="light"] {
|
||||
--color-fill: 239, 241, 245;
|
||||
--color-text-base: 76, 79, 105;
|
||||
--color-accent: 0, 108, 172;
|
||||
--color-card: 230, 230, 230;
|
||||
--color-card-muted: 205, 205, 205;
|
||||
--color-border: 236, 233, 233;
|
||||
}
|
||||
/* Catppuccin Mocha */
|
||||
html[data-theme="dark"] {
|
||||
--color-fill: 30, 30, 46;
|
||||
--color-text-base: 205, 214, 244;
|
||||
--color-accent: 23, 146, 153;
|
||||
--color-card: 49, 50, 68;
|
||||
--color-card-muted: 69, 71, 90;
|
||||
--color-border: 234, 118, 203;
|
||||
}
|
||||
#sun-svg,
|
||||
html[data-theme="dark"] #moon-svg {
|
||||
display: none;
|
||||
}
|
||||
#moon-svg,
|
||||
html[data-theme="dark"] #sun-svg {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
@apply flex min-h-screen flex-col bg-skin-fill font-mono text-skin-base
|
||||
selection:bg-skin-accent selection:bg-opacity-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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.prose a {
|
||||
@apply hover:!text-skin-accent;
|
||||
}
|
||||
.prose thead th:first-child,
|
||||
tbody td:first-child,
|
||||
tfoot td:first-child {
|
||||
padding-left: 0.5714286em;
|
||||
}
|
||||
.prose h2#table-of-contents {
|
||||
@apply mb-2;
|
||||
}
|
||||
.prose details {
|
||||
@apply inline-block cursor-pointer select-none text-skin-base;
|
||||
}
|
||||
.prose summary {
|
||||
@apply focus-outline;
|
||||
}
|
||||
.prose h2#table-of-contents + p {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
/* ===== scrollbar ===== */
|
||||
html {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
/* width */
|
||||
::-webkit-scrollbar {
|
||||
@apply w-3;
|
||||
}
|
||||
|
||||
/* Track */
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-skin-fill;
|
||||
}
|
||||
|
||||
/* Handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply bg-skin-card;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
@apply bg-skin-card-muted;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: pre;
|
||||
overflow: scroll;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.display-none {
|
||||
@apply hidden;
|
||||
}
|
||||
.focus-outline {
|
||||
@apply outline-2 outline-offset-1 outline-skin-fill focus-visible:no-underline focus-visible:outline-dashed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
export type Site = {
|
||||
website: string;
|
||||
author: string;
|
||||
desc: string;
|
||||
title: string;
|
||||
ogImage: string;
|
||||
lightAndDarkMode: boolean;
|
||||
postPerPage: number;
|
||||
};
|
||||
|
||||
export type SocialObjects = {
|
||||
name: SocialMedia;
|
||||
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";
|
|
@ -0,0 +1,155 @@
|
|||
import satori, { SatoriOptions } from "satori";
|
||||
import { SITE } from "@config";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { Resvg } from "@resvg/resvg-js";
|
||||
|
||||
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 ogImage = (text: string) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: "#fefbfb",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-1px",
|
||||
right: "-1px",
|
||||
border: "4px solid #000",
|
||||
background: "#ecebeb",
|
||||
opacity: "0.9",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: "2.5rem",
|
||||
width: "88%",
|
||||
height: "80%",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
style={{
|
||||
border: "4px solid #000",
|
||||
background: "#fefbfb",
|
||||
borderRadius: "4px",
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
margin: "2rem",
|
||||
width: "88%",
|
||||
height: "80%",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
margin: "20px",
|
||||
width: "90%",
|
||||
height: "90%",
|
||||
}}
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 72,
|
||||
fontWeight: "bold",
|
||||
maxHeight: "84%",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</p>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
marginBottom: "8px",
|
||||
fontSize: 28,
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
by{" "}
|
||||
<span
|
||||
style={{
|
||||
color: "transparent",
|
||||
}}
|
||||
>
|
||||
"
|
||||
</span>
|
||||
<span style={{ overflow: "hidden", fontWeight: "bold" }}>
|
||||
{SITE.author}
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span style={{ overflow: "hidden", fontWeight: "bold" }}>
|
||||
{SITE.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const generateOgImage = async (mytext = SITE.title) => {
|
||||
const svg = await satori(ogImage(mytext), options);
|
||||
|
||||
// render png in production mode
|
||||
if (import.meta.env.MODE === "production") {
|
||||
const resvg = new Resvg(svg);
|
||||
const pngData = resvg.render();
|
||||
const pngBuffer = pngData.asPng();
|
||||
|
||||
console.info("Output PNG Image :", `${mytext}.png`);
|
||||
|
||||
await writeFile(`./dist/${mytext}.png`, pngBuffer);
|
||||
}
|
||||
|
||||
return svg;
|
||||
};
|
||||
|
||||
export default generateOgImage;
|
|
@ -0,0 +1,14 @@
|
|||
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;
|
|
@ -0,0 +1,7 @@
|
|||
import { slugifyAll } from "./slugify";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
const getPostsByTag = (posts: CollectionEntry<"blog">[], tag: string) =>
|
||||
posts.filter(post => slugifyAll(post.data.tags).includes(tag));
|
||||
|
||||
export default getPostsByTag;
|
|
@ -0,0 +1,12 @@
|
|||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
const getSortedPosts = (posts: CollectionEntry<"blog">[]) =>
|
||||
posts
|
||||
.filter(({ data }) => !data.draft)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
Math.floor(new Date(b.data.pubDatetime).getTime() / 1000) -
|
||||
Math.floor(new Date(a.data.pubDatetime).getTime() / 1000)
|
||||
);
|
||||
|
||||
export default getSortedPosts;
|
|
@ -0,0 +1,17 @@
|
|||
import { slugifyStr } from "./slugify";
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
|
||||
const getUniqueTags = (posts: CollectionEntry<"blog">[]) => {
|
||||
const filteredPosts = posts.filter(({ data }) => !data.draft);
|
||||
const tags: string[] = filteredPosts
|
||||
.flatMap(post => post.data.tags)
|
||||
.map(tag => slugifyStr(tag))
|
||||
.filter(
|
||||
(value: string, index: number, self: string[]) =>
|
||||
self.indexOf(value) === index
|
||||
)
|
||||
.sort((tagA: string, tagB: string) => tagA.localeCompare(tagB));
|
||||
return tags;
|
||||
};
|
||||
|
||||
export default getUniqueTags;
|
|
@ -0,0 +1,11 @@
|
|||
import { slug as slugger } from "github-slugger";
|
||||
import type { BlogFrontmatter } from "@content/_schemas";
|
||||
|
||||
export const slugifyStr = (str: string) => slugger(str);
|
||||
|
||||
const slugify = (post: BlogFrontmatter) =>
|
||||
post.postSlug ? slugger(post.postSlug) : slugger(post.title);
|
||||
|
||||
export const slugifyAll = (arr: string[]) => arr.map(str => slugifyStr(str));
|
||||
|
||||
export default slugify;
|
|
@ -0,0 +1,65 @@
|
|||
function withOpacity(variableName) {
|
||||
return ({ opacityValue }) => {
|
||||
if (opacityValue !== undefined) {
|
||||
return `rgba(var(${variableName}), ${opacityValue})`;
|
||||
}
|
||||
return `rgb(var(${variableName}))`;
|
||||
};
|
||||
}
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||
theme: {
|
||||
// Remove the following screen breakpoint or add other breakpoints
|
||||
// if one breakpoint is not enough for you
|
||||
screens: {
|
||||
sm: "640px",
|
||||
},
|
||||
|
||||
// Uncomment the following extend
|
||||
// if existing Tailwind color palette will be used
|
||||
|
||||
// extend: {
|
||||
textColor: {
|
||||
skin: {
|
||||
base: withOpacity("--color-text-base"),
|
||||
accent: withOpacity("--color-accent"),
|
||||
inverted: withOpacity("--color-fill"),
|
||||
},
|
||||
},
|
||||
backgroundColor: {
|
||||
skin: {
|
||||
fill: withOpacity("--color-fill"),
|
||||
accent: withOpacity("--color-accent"),
|
||||
inverted: withOpacity("--color-text-base"),
|
||||
card: withOpacity("--color-card"),
|
||||
"card-muted": withOpacity("--color-card-muted"),
|
||||
},
|
||||
},
|
||||
outlineColor: {
|
||||
skin: {
|
||||
fill: withOpacity("--color-accent"),
|
||||
},
|
||||
},
|
||||
borderColor: {
|
||||
skin: {
|
||||
line: withOpacity("--color-border"),
|
||||
fill: withOpacity("--color-text-base"),
|
||||
accent: withOpacity("--color-accent"),
|
||||
},
|
||||
},
|
||||
fill: {
|
||||
skin: {
|
||||
base: withOpacity("--color-text-base"),
|
||||
accent: withOpacity("--color-accent"),
|
||||
},
|
||||
transparent: "transparent",
|
||||
},
|
||||
fontFamily: {
|
||||
mono: ["IBM Plex Mono", "monospace"],
|
||||
},
|
||||
// },
|
||||
},
|
||||
plugins: [require("@tailwindcss/typography")],
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@assets/*": [
|
||||
"assets/*"
|
||||
],
|
||||
"@config": [
|
||||
"config.ts"
|
||||
],
|
||||
"@components/*": [
|
||||
"components/*"
|
||||
],
|
||||
"@content/*": [
|
||||
"content/*"
|
||||
],
|
||||
"@layouts/*": [
|
||||
"layouts/*"
|
||||
],
|
||||
"@pages/*": [
|
||||
"pages/*"
|
||||
],
|
||||
"@styles/*": [
|
||||
"styles/*"
|
||||
],
|
||||
"@utils/*": [
|
||||
"utils/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue