Browse Source

feat: initial blog implementation

Signed-off-by: Muthu Kumar <muthukumar@thefeathers.in>
pull/1/head
Muthu Kumar 2 years ago
parent
commit
a22631ac3b
Signed by: mkrhere GPG Key ID: 3FD688398897097E
  1. 2
      .prettierrc
  2. 30
      blog.html
  3. 8
      package.json
  4. 225
      pnpm-lock.yaml
  5. 33
      public/blog/2017/convergence-of-world-lines.md
  6. BIN
      public/blog/assets/convergence.png
  7. BIN
      public/blog/assets/kurisu-okabe-crossing.jpg
  8. BIN
      public/blog/assets/threads-of-time.jpg
  9. 50
      scripts/blog.js
  10. 3
      src/assets/arrow-thin.svg
  11. 17
      src/blog.css
  12. 32
      src/blog.json
  13. 15
      src/blog.tsx
  14. 4
      src/components/Menu.tsx
  15. 23
      src/components/Spacer.tsx
  16. 34
      src/data.ts
  17. 7
      src/index.css
  18. 3
      src/index.tsx
  19. 142
      src/pages/blog/Home.tsx
  20. 21
      src/pages/blog/components/ArticleSubHeader.tsx
  21. 210
      src/pages/blog/components/BlogContent.tsx
  22. 15
      src/util/index.ts
  23. 13
      vite.config.ts

2
.prettierrc

@ -8,5 +8,5 @@
"bracketSpacing": true,
"jsxBracketSameLine": true,
"arrowParens": "avoid",
"printWidth": 100
"printWidth": 80
}

30
blog.html

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Personal website of MKRhere" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="192x192" href="/favicon/android-chrome-192x192.png">
<link rel="manifest" href="/favicon/site.webmanifest">
<meta name="theme-color" content="#ff5555">
<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="/favicon/favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png">
<meta name="msapplication-config" content="/favicon/browserconfig.xml">
<meta name="msapplication-TileColor" content="#ff5555">
<link rel="stylesheet" href="/fonts.css" />
<title>MKRhere blog</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/blog.tsx"></script>
</body>
</html>

8
package.json

@ -2,10 +2,12 @@
"name": "pw2",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start": "vite",
"build": "tsc && vite build",
"serve": "vite preview"
"serve": "vite preview",
"blog": "node scripts/blog.js"
},
"eslintConfig": {
"extends": [
@ -16,17 +18,19 @@
"@emotion/css": "^11.9.0",
"date-fns": "^2.28.0",
"framer-motion": "^6.3.15",
"marked": "^4.0.17",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0"
},
"devDependencies": {
"@svgr/rollup": "^6.2.1",
"@types/marked": "^4.0.3",
"@types/node": "^18.0.0",
"@types/react": "^18.0.14",
"@types/react-dom": "^18.0.5",
"@types/react-router-dom": "^5.3.3",
"@vitejs/plugin-react": "^1.3.2",
"@vitejs/plugin-react-refresh": "^1.3.6",
"react-scripts": "5.0.1",
"typescript": "^4.7.4",
"vite": "^2.9.12"

225
pnpm-lock.yaml

@ -3,13 +3,15 @@ lockfileVersion: 5.4
specifiers:
'@emotion/css': ^11.9.0
'@svgr/rollup': ^6.2.1
'@types/marked': ^4.0.3
'@types/node': ^18.0.0
'@types/react': ^18.0.14
'@types/react-dom': ^18.0.5
'@types/react-router-dom': ^5.3.3
'@vitejs/plugin-react': ^1.3.2
'@vitejs/plugin-react-refresh': ^1.3.6
date-fns: ^2.28.0
framer-motion: ^6.3.15
marked: ^4.0.17
react: ^18.2.0
react-dom: ^18.2.0
react-router-dom: ^6.3.0
@ -21,17 +23,19 @@ dependencies:
'@emotion/css': 11.9.0
date-fns: 2.28.0
framer-motion: 6.3.15_biqbaboplfbrettd7655fr4n2y
marked: 4.0.17
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-router-dom: 6.3.0_biqbaboplfbrettd7655fr4n2y
devDependencies:
'@svgr/rollup': 6.2.1
'@types/marked': 4.0.3
'@types/node': 18.0.0
'@types/react': 18.0.14
'@types/react-dom': 18.0.5
'@types/react-router-dom': 5.3.3
'@vitejs/plugin-react': 1.3.2
'@vitejs/plugin-react-refresh': 1.3.6
react-scripts: 5.0.1_qtbnez4q7bzoc4eqybg3efzzxe
typescript: 4.7.4
vite: 2.9.12
@ -69,29 +73,6 @@ packages:
engines: {node: '>=6.9.0'}
dev: true
/@babel/core/7.14.8:
resolution: {integrity: sha512-/AtaeEhT6ErpDhInbXmjHcUQXH0L0TEgscfcxk1qbOvLuKCa5aZT0SOOtDKFY96/CLROwbLSKyFor6idgNaU4Q==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/code-frame': 7.16.7
'@babel/generator': 7.18.2
'@babel/helper-compilation-targets': 7.14.5_@babel+core@7.14.8
'@babel/helper-module-transforms': 7.14.8
'@babel/helpers': 7.14.8
'@babel/parser': 7.18.5
'@babel/template': 7.16.7
'@babel/traverse': 7.18.5
'@babel/types': 7.18.4
convert-source-map: 1.8.0
debug: 4.3.2
gensync: 1.0.0-beta.2
json5: 2.2.0
semver: 6.3.0
source-map: 0.5.7
transitivePeerDependencies:
- supports-color
dev: true
/@babel/core/7.18.5:
resolution: {integrity: sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ==}
engines: {node: '>=6.9.0'}
@ -153,19 +134,6 @@ packages:
'@babel/types': 7.18.4
dev: true
/@babel/helper-compilation-targets/7.14.5_@babel+core@7.14.8:
resolution: {integrity: sha512-v+QtZqXEiOnpO6EYvlImB6zCD2Lel06RzOPzmkz/D/XgQiUu3C/Jb1LOqSt/AIA34TYi/Q+KlT8vTQrgdxkbLw==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
dependencies:
'@babel/compat-data': 7.18.5
'@babel/core': 7.14.8
'@babel/helper-validator-option': 7.16.7
browserslist: 4.21.0
semver: 6.3.0
dev: true
/@babel/helper-compilation-targets/7.18.2_@babel+core@7.18.5:
resolution: {integrity: sha512-s1jnPotJS9uQnzFtiZVBUxe67CuBa679oWFHpxYYnTpRL/1ffhyX44R9uYiXoa/pLXcY9H2moJta0iaanlk/rQ==}
engines: {node: '>=6.9.0'}
@ -266,22 +234,6 @@ packages:
dependencies:
'@babel/types': 7.18.4
/@babel/helper-module-transforms/7.14.8:
resolution: {integrity: sha512-RyE+NFOjXn5A9YU1dkpeBaduagTlZ0+fccnIcAGbv1KGUlReBj7utF7oEth8IdIBQPcux0DDgW5MFBH2xu9KcA==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-module-imports': 7.16.7
'@babel/helper-replace-supers': 7.18.2
'@babel/helper-simple-access': 7.14.8
'@babel/helper-split-export-declaration': 7.16.7
'@babel/helper-validator-identifier': 7.16.7
'@babel/template': 7.16.7
'@babel/traverse': 7.18.5
'@babel/types': 7.18.4
transitivePeerDependencies:
- supports-color
dev: true
/@babel/helper-module-transforms/7.18.0:
resolution: {integrity: sha512-kclUYSUBIjlvnzN2++K9f2qzYKFgjmnmjwL4zlmU5f8ZtzgWe8s0rUPSTGy2HmK4P8T52MQsS+HTQAgZd3dMEA==}
engines: {node: '>=6.9.0'}
@ -333,13 +285,6 @@ packages:
- supports-color
dev: true
/@babel/helper-simple-access/7.14.8:
resolution: {integrity: sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.18.4
dev: true
/@babel/helper-simple-access/7.18.2:
resolution: {integrity: sha512-7LIrjYzndorDY88MycupkpQLKS1AFfsVRm2k/9PtKScSy5tZq0McZTj+DiMRynboZfIqOKvo03pmhTaUgiD6fQ==}
engines: {node: '>=6.9.0'}
@ -387,17 +332,6 @@ packages:
- supports-color
dev: true
/@babel/helpers/7.14.8:
resolution: {integrity: sha512-ZRDmI56pnV+p1dH6d+UN6GINGz7Krps3+270qqI9UJ4wxYThfAIcI5i7j5vXC4FJ3Wap+S9qcebxeYiqn87DZw==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/template': 7.16.7
'@babel/traverse': 7.18.5
'@babel/types': 7.18.4
transitivePeerDependencies:
- supports-color
dev: true
/@babel/helpers/7.18.2:
resolution: {integrity: sha512-j+d+u5xT5utcQSzrh9p+PaJX94h++KN+ng9b9WEJq7pkUPAd61FGqhjuUEdfknb3E/uDBb7ruwEeKkIxNJPIrg==}
engines: {node: '>=6.9.0'}
@ -1183,16 +1117,6 @@ packages:
'@babel/plugin-transform-react-jsx': 7.17.12_@babel+core@7.18.5
dev: true
/@babel/plugin-transform-react-jsx-self/7.14.5_@babel+core@7.14.8:
resolution: {integrity: sha512-M/fmDX6n0cfHK/NLTcPmrfVAORKDhK8tyjDhyxlUjYyPYYO8FRWwuxBA3WBx8kWN/uBUuwGa3s/0+hQ9JIN3Tg==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.14.8
'@babel/helper-plugin-utils': 7.17.12
dev: true
/@babel/plugin-transform-react-jsx-self/7.17.12_@babel+core@7.18.5:
resolution: {integrity: sha512-7S9G2B44EnYOx74mue02t1uD8ckWZ/ee6Uz/qfdzc35uWHX5NgRy9i+iJSb2LFRgMd+QV9zNcStQaazzzZ3n3Q==}
engines: {node: '>=6.9.0'}
@ -1203,16 +1127,6 @@ packages:
'@babel/helper-plugin-utils': 7.17.12
dev: true
/@babel/plugin-transform-react-jsx-source/7.14.5_@babel+core@7.14.8:
resolution: {integrity: sha512-1TpSDnD9XR/rQ2tzunBVPThF5poaYT9GqP+of8fAtguYuI/dm2RkrMBDemsxtY0XBzvW7nXjYM0hRyKX9QYj7Q==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0-0
dependencies:
'@babel/core': 7.14.8
'@babel/helper-plugin-utils': 7.17.12
dev: true
/@babel/plugin-transform-react-jsx-source/7.16.7_@babel+core@7.18.5:
resolution: {integrity: sha512-rONFiQz9vgbsnaMtQlZCjIRwhJvlrPET8TabIUK2hzlXw9B9s2Ieaxte1SCOOXMbWRHodbKixNf3BLcWVOQ8Bw==}
engines: {node: '>=6.9.0'}
@ -1887,7 +1801,7 @@ packages:
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
chalk: 4.1.2
jest-message-util: 27.5.1
jest-util: 27.5.1
@ -1899,7 +1813,7 @@ packages:
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
dependencies:
'@jest/types': 28.1.1
'@types/node': 16.4.0
'@types/node': 18.0.0
chalk: 4.1.2
jest-message-util: 28.1.1
jest-util: 28.1.1
@ -1920,7 +1834,7 @@ packages:
'@jest/test-result': 27.5.1
'@jest/transform': 27.5.1
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.8.1
@ -1957,7 +1871,7 @@ packages:
dependencies:
'@jest/fake-timers': 27.5.1
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
jest-mock: 27.5.1
dev: true
@ -1967,7 +1881,7 @@ packages:
dependencies:
'@jest/types': 27.5.1
'@sinonjs/fake-timers': 8.1.0
'@types/node': 16.4.0
'@types/node': 18.0.0
jest-message-util: 27.5.1
jest-mock: 27.5.1
jest-util: 27.5.1
@ -1996,7 +1910,7 @@ packages:
'@jest/test-result': 27.5.1
'@jest/transform': 27.5.1
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
chalk: 4.1.2
collect-v8-coverage: 1.0.1
exit: 0.1.2
@ -2097,7 +2011,7 @@ packages:
dependencies:
'@types/istanbul-lib-coverage': 2.0.3
'@types/istanbul-reports': 3.0.1
'@types/node': 16.4.0
'@types/node': 18.0.0
'@types/yargs': 16.0.4
chalk: 4.1.2
dev: true
@ -2109,7 +2023,7 @@ packages:
'@jest/schemas': 28.0.2
'@types/istanbul-lib-coverage': 2.0.3
'@types/istanbul-reports': 3.0.1
'@types/node': 16.4.0
'@types/node': 18.0.0
'@types/yargs': 17.0.10
chalk: 4.1.2
dev: true
@ -2278,14 +2192,6 @@ packages:
rollup: 2.75.7
dev: true
/@rollup/pluginutils/4.1.1:
resolution: {integrity: sha512-clDjivHqWGXi7u+0d2r2sBi4Ie6VLEAzWMIkvJLnDmxoOhBYOTfzGbOQBA32THHm11/LiJbd01tJUpJsbshSWQ==}
engines: {node: '>= 8.0.0'}
dependencies:
estree-walker: 2.0.2
picomatch: 2.3.0
dev: true
/@rollup/pluginutils/4.2.1:
resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
engines: {node: '>= 8.0.0'}
@ -2627,26 +2533,26 @@ packages:
resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==}
dependencies:
'@types/connect': 3.4.35
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/bonjour/3.5.10:
resolution: {integrity: sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/connect-history-api-fallback/1.3.5:
resolution: {integrity: sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==}
dependencies:
'@types/express-serve-static-core': 4.17.29
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/connect/3.4.35:
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/eslint-scope/3.7.3:
@ -2674,7 +2580,7 @@ packages:
/@types/express-serve-static-core/4.17.29:
resolution: {integrity: sha512-uMd++6dMKS32EOuw1Uli3e3BPgdLIXmezcfHv7N4c1s3gkhikBplORPpMq3fuWkxncZN1reb16d5n8yhQ80x7Q==}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
'@types/qs': 6.9.7
'@types/range-parser': 1.2.4
dev: true
@ -2691,7 +2597,7 @@ packages:
/@types/graceful-fs/4.1.5:
resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/history/4.7.11:
@ -2705,7 +2611,7 @@ packages:
/@types/http-proxy/1.17.9:
resolution: {integrity: sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/istanbul-lib-coverage/2.0.3:
@ -2736,12 +2642,16 @@ packages:
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
dev: true
/@types/marked/4.0.3:
resolution: {integrity: sha512-HnMWQkLJEf/PnxZIfbm0yGJRRZYYMhb++O9M36UCTA9z53uPvVoSlAwJr3XOpDEryb7Hwl1qAx/MV6YIW1RXxg==}
dev: true
/@types/mime/1.3.2:
resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==}
dev: true
/@types/node/16.4.0:
resolution: {integrity: sha512-HrJuE7Mlqcjj+00JqMWpZ3tY8w7EUd+S0U3L1+PQSWiXZbOgyQDvi+ogoUxaHApPJq5diKxYBQwA3iIlNcPqOg==}
/@types/node/18.0.0:
resolution: {integrity: sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==}
dev: true
/@types/parse-json/4.0.0:
@ -2799,7 +2709,7 @@ packages:
/@types/resolve/1.17.1:
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/retry/0.12.0:
@ -2820,13 +2730,13 @@ packages:
resolution: {integrity: sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==}
dependencies:
'@types/mime': 1.3.2
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/sockjs/0.3.33:
resolution: {integrity: sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/stack-utils/2.0.1:
@ -2840,7 +2750,7 @@ packages:
/@types/ws/8.5.3:
resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/@types/yargs-parser/20.2.1:
@ -2998,20 +2908,6 @@ packages:
eslint-visitor-keys: 3.3.0
dev: true
/@vitejs/plugin-react-refresh/1.3.6:
resolution: {integrity: sha512-iNR/UqhUOmFFxiezt0em9CgmiJBdWR+5jGxB2FihaoJfqGt76kiwaKoVOJVU5NYcDWMdN06LbyN2VIGIoYdsEA==}
engines: {node: '>=12.0.0'}
deprecated: This package has been deprecated in favor of @vitejs/plugin-react
dependencies:
'@babel/core': 7.14.8
'@babel/plugin-transform-react-jsx-self': 7.14.5_@babel+core@7.14.8
'@babel/plugin-transform-react-jsx-source': 7.14.5_@babel+core@7.14.8
'@rollup/pluginutils': 4.1.1
react-refresh: 0.10.0
transitivePeerDependencies:
- supports-color
dev: true
/@vitejs/plugin-react/1.3.2:
resolution: {integrity: sha512-aurBNmMo0kz1O4qRoY+FM4epSA39y3ShWGuqfLRA/3z0oEJAdtoSfgA3aO98/PCCHAqMaduLxIxErWrVKIFzXA==}
engines: {node: '>=12.0.0'}
@ -6374,7 +6270,7 @@ packages:
'@jest/environment': 27.5.1
'@jest/test-result': 27.5.1
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
chalk: 4.1.2
co: 4.6.0
dedent: 0.7.0
@ -6499,7 +6395,7 @@ packages:
'@jest/environment': 27.5.1
'@jest/fake-timers': 27.5.1
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
jest-mock: 27.5.1
jest-util: 27.5.1
jsdom: 16.6.0
@ -6517,7 +6413,7 @@ packages:
'@jest/environment': 27.5.1
'@jest/fake-timers': 27.5.1
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
jest-mock: 27.5.1
jest-util: 27.5.1
dev: true
@ -6533,7 +6429,7 @@ packages:
dependencies:
'@jest/types': 27.5.1
'@types/graceful-fs': 4.1.5
'@types/node': 16.4.0
'@types/node': 18.0.0
anymatch: 3.1.2
fb-watchman: 2.0.1
graceful-fs: 4.2.10
@ -6555,7 +6451,7 @@ packages:
'@jest/source-map': 27.5.1
'@jest/test-result': 27.5.1
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
chalk: 4.1.2
co: 4.6.0
expect: 27.5.1
@ -6625,7 +6521,7 @@ packages:
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
dev: true
/jest-pnp-resolver/1.2.2_jest-resolve@27.5.1:
@ -6686,7 +6582,7 @@ packages:
'@jest/test-result': 27.5.1
'@jest/transform': 27.5.1
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
chalk: 4.1.2
emittery: 0.8.1
graceful-fs: 4.2.10
@ -6743,7 +6639,7 @@ packages:
resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
graceful-fs: 4.2.10
dev: true
@ -6782,7 +6678,7 @@ packages:
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
chalk: 4.1.2
ci-info: 3.3.2
graceful-fs: 4.2.10
@ -6794,7 +6690,7 @@ packages:
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
dependencies:
'@jest/types': 28.1.1
'@types/node': 16.4.0
'@types/node': 18.0.0
chalk: 4.1.2
ci-info: 3.3.2
graceful-fs: 4.2.10
@ -6835,7 +6731,7 @@ packages:
dependencies:
'@jest/test-result': 27.5.1
'@jest/types': 27.5.1
'@types/node': 16.4.0
'@types/node': 18.0.0
ansi-escapes: 4.3.2
chalk: 4.1.2
jest-util: 27.5.1
@ -6848,7 +6744,7 @@ packages:
dependencies:
'@jest/test-result': 28.1.1
'@jest/types': 28.1.1
'@types/node': 16.4.0
'@types/node': 18.0.0
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.10.2
@ -6860,7 +6756,7 @@ packages:
resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
merge-stream: 2.0.0
supports-color: 7.2.0
dev: true
@ -6869,7 +6765,7 @@ packages:
resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==}
engines: {node: '>= 10.13.0'}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
merge-stream: 2.0.0
supports-color: 8.1.1
dev: true
@ -6878,7 +6774,7 @@ packages:
resolution: {integrity: sha512-Au7slXB08C6h+xbJPp7VIb6U0XX5Kc9uel/WFc6/rcTzGiaVCBRngBExSYuXSLFPULPSYU3cJ3ybS988lNFQhQ==}
engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0}
dependencies:
'@types/node': 16.4.0
'@types/node': 18.0.0
merge-stream: 2.0.0
supports-color: 8.1.1
dev: true
@ -7001,14 +6897,6 @@ packages:
minimist: 1.2.6
dev: true
/json5/2.2.0:
resolution: {integrity: sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==}
engines: {node: '>=6'}
hasBin: true
dependencies:
minimist: 1.2.5
dev: true
/json5/2.2.1:
resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==}
engines: {node: '>=6'}
@ -7209,6 +7097,12 @@ packages:
tmpl: 1.0.4
dev: true
/marked/4.0.17:
resolution: {integrity: sha512-Wfk0ATOK5iPxM4ptrORkFemqroz0ZDxp5MWfYA7H/F+wO17NRWV5Ypxi6p3g2Xmw2bKeiYOl6oVnLHKxBA0VhA==}
engines: {node: '>= 12'}
hasBin: true
dev: false
/mdn-data/2.0.14:
resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==}
dev: true
@ -7323,10 +7217,6 @@ packages:
brace-expansion: 2.0.1
dev: true
/minimist/1.2.5:
resolution: {integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==}
dev: true
/minimist/1.2.6:
resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==}
dev: true
@ -7722,11 +7612,6 @@ packages:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
dev: true
/picomatch/2.3.0:
resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==}
engines: {node: '>=8.6'}
dev: true
/picomatch/2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
@ -8742,11 +8627,6 @@ packages:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: true
/react-refresh/0.10.0:
resolution: {integrity: sha512-PgidR3wST3dDYKr6b4pJoqQFpPGNKDSCDx4cZoshjXipw3LzO7mG1My2pwEzz2JVkF+inx3xRpDeQLFQGH/hsQ==}
engines: {node: '>=0.10.0'}
dev: true
/react-refresh/0.11.0:
resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==}
engines: {node: '>=0.10.0'}
@ -9406,6 +9286,7 @@ packages:
/source-map/0.5.7:
resolution: {integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=}
engines: {node: '>=0.10.0'}
dev: false
/source-map/0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}

33
public/blog/2017/convergence-of-world-lines.md

@ -0,0 +1,33 @@
---
title: Convergence of World Lines
published: December 14, 2017
category: Thoughts
featured-img: convergence.png
snippet: In an infinite number of universes where anything is possible, I want to meet you again in all of them, and fall in love with you all over again.
---
I’ve been watching Steins;Gate over the last week and I’ve been completely blown away by the sheer amount of work gone into this series. When it ended. it left me speechless and sad because I missed being part of the universe of Okabe Rintarou (Hououin Kyouma) and his lab. I learnt that a sequel is in the making and the visual novel Steins;Gate 0 is already out… But in the mean time, I needed to let some feelings out.
> No matter how many times I have to watch [death] to save you, it’s not going to break me!
>
> – Okabe Rintarou
Okabe repeatedly travels to the past, each time to watch someone close to him die, and fervently keeps trying despite the mental trauma he experiences. This line affected me the hardest, because it reminded me of something I told someone, a long time ago.
> In an infinite number of universes where anything is possible, I want to meet you again in all of them, and fall in love with you all over again.
And again, when recollecting to someone else:
> If I knew what was going to happen, and how we would separate, I wouldn’t change a thing about it. I would go back in time and watch us fall in love, and watch her break apart, without a moment of regret.
It’s funny how world lines converge to form a consistent story-line, some times even from a fictional world. We relate to fictional characters, a piece of someone else’s imagination, and we live them with our lives… Well then, who is to say they aren’t real?
I realize it’s December 14th, 4:00 AM as I write this. today, 3 years ago, the event that will separate us will happen. After all this time, I haven’t changed. I hoped I would, and I tried. I thought I moved on, but I am emotionally and mentally bound to the past. I do not think that I want us back together. If she is happy elsewhere, then that’s the reality I want to see. I’m simply in love with a woman in the past. It’s sad, but it’s what makes me me. All the events that happened in the past 22 years are part of the threads that form the continuum that is me.
![Threads of time](/blog/assets/threads-of-time.jpg)
Okabe reached the Steins;Gate world line… Maybe this is my Steins;Gate world line, and this is the best outcome. Maybe our souls passed by and are fated to never meet again—
![kurisu-okabe-crossing](/blog/assets/kurisu-okabe-crossing.jpg)
🎶 Maybe this is for the best —

BIN
public/blog/assets/convergence.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 686 KiB

BIN
public/blog/assets/kurisu-okabe-crossing.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
public/blog/assets/threads-of-time.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

50
scripts/blog.js

@ -0,0 +1,50 @@
import { readdir, readFile, writeFile } from "node:fs/promises";
const toplevel = (await readdir("blog")).filter(x => x !== "assets");
const removeExtn = path => {
const parts = path.split(".");
parts.splice(parts.length - 1);
return parts.join(".");
};
const parseMetadata = (slug, contents) => {
return Object.fromEntries(
contents
.split("---")[1]
.trim()
.split("\n")
.map(line => {
const [name, value] = line.split(/:(.*)/).map(x => x.trim());
return [name, value];
})
.concat([["slug", slug]]),
);
};
const data = Object.fromEntries(
await Promise.all(
toplevel.map(year =>
readdir("public/blog/" + year).then(slugs =>
Promise.all(
slugs.map(async slug => {
const path = `public/blog/${year}/${slug}`;
const contents = await readFile(path, "utf-8");
return parseMetadata(removeExtn(slug), contents);
}),
)
.then(list =>
list
.sort((a, b) => new Date(a).valueOf() - new Date(b).valueOf())
.map(x => [x.slug, x]),
)
.then(list => Object.fromEntries(list))
.then(cont => [year, cont]),
),
),
),
);
writeFile("src/blog.json", JSON.stringify(data, null, "\t"), "utf-8").then(() =>
console.log("Done"),
);

3
src/assets/arrow-thin.svg

@ -0,0 +1,3 @@
<svg width="15" height="12" viewBox="0 0 15 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.46939 -3.8275e-07L7.34694 1.03125L11.9679 5.26339L3.64741e-07 5.26339L4.93534e-07 6.73661L11.9679 6.73661L7.34694 10.9821L8.46939 12L15 6L8.46939 -3.8275e-07Z" fill="#C8C8C8"/>
</svg>

After

Width:  |  Height:  |  Size: 292 B

17
src/blog.css

@ -0,0 +1,17 @@
:root {
--primary-colour: #d6a700;
font-size: max(18px, 0.8vw);
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--text-colour);
}
a:hover {
color: var(--text-colour);
}

32
src/blog.json

@ -0,0 +1,32 @@
{
"2016": {
"convergence-of-world-lines-2": {
"title": "Convergence of World Lines 2",
"published": "December 14, 2016",
"category": "Thoughts",
"featured-img": "convergence.png",
"snippet": "In an infinite number of universes where anything is possible, I want to meet you again in all of them, and fall in love with you all over again.",
"slug": "convergence-of-world-lines-2"
}
},
"2017": {
"convergence-of-world-lines": {
"title": "Convergence of World Lines",
"published": "December 14, 2017",
"category": "Thoughts",
"featured-img": "convergence.png",
"snippet": "In an infinite number of universes where anything is possible, I want to meet you again in all of them, and fall in love with you all over again.",
"slug": "convergence-of-world-lines"
}
},
"2018": {
"convergence-of-world-lines-3": {
"title": "Convergence of World Lines 3",
"published": "December 14, 2018",
"category": "Thoughts",
"featured-img": "convergence.png",
"snippet": "In an infinite number of universes where anything is possible, I want to meet you again in all of them, and fall in love with you all over again.",
"slug": "convergence-of-world-lines-3"
}
}
}

15
src/blog.tsx

@ -0,0 +1,15 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import "./index.css";
import BlogHome from "./pages/blog/Home";
createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Router>
<BlogHome />
</Router>
</React.StrictMode>,
);

4
src/components/Menu.tsx

@ -43,10 +43,6 @@ const menuList = css`
& > li {
margin-left: 1rem;
& > a {
text-decoration: none;
}
}
`;

23
src/components/Spacer.tsx

@ -0,0 +1,23 @@
import React from "react";
type Props = {
inline?: boolean;
x?: number;
y?: number;
};
const rem = (x?: number) => (x === 0 ? x : x ? `${x}rem` : "100%");
export const Spacer: React.FC<Props> = ({ inline, x, y = 1 }) => {
return (
<span
// identifier
className="spacer"
style={{
display: inline ? "inline-block" : "block",
width: rem(x),
height: rem(y),
}}
/>
);
};

34
src/data.ts

@ -0,0 +1,34 @@
import json from "./blog.json";
export type Article = {
"title": string;
"category": string;
"snippet": string;
"slug": string;
"published": string;
"featured-img": string;
};
export const blog = json as Record<string, Record<string, Article>>;
export const articles = Object.values(blog)
.flatMap(year => Object.values(year))
.sort(
(a, b) => new Date(b.published).valueOf() - new Date(a.published).valueOf(),
);
export const nextAndPrev = (
year: string,
slug: string,
): [Article | undefined, Article | undefined] => {
const idx = articles.findIndex(
article =>
String(new Date(article.published).getFullYear()) === year &&
article.slug === slug,
);
return [articles[idx - 1], articles[idx + 1]];
};
export const getBlogPath = (article: Article) =>
`/blog/${new Date(article.published).getFullYear()}/${article.slug}`;

7
src/index.css

@ -5,6 +5,7 @@
--card-tags-hover: rgb(25, 25, 25);
--primary-colour: rgb(255, 85, 85);
--text-colour: rgb(211, 207, 201);
--text-subdued: rgb(163, 163, 163);
font-weight: 500;
font-size: max(16px, 0.8vw);
}
@ -19,6 +20,11 @@ body {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu",
"Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
color: var(--text-colour);
height: 100vh;
}
#root {
height: 100%;
}
code {
@ -68,6 +74,7 @@ h4 {
a {
color: var(--text-colour);
text-decoration: none;
}
a:hover {

3
src/index.tsx

@ -12,6 +12,7 @@ import Contact from "./pages/main/Contact";
import Live from "./pages/main/Live";
import NotFound from "./pages/main/404";
import BlogHome from "./pages/blog/Home";
createRoot(document.getElementById("root")!).render(
<React.StrictMode>
@ -23,6 +24,8 @@ createRoot(document.getElementById("root")!).render(
<Route path="/contact" element={<Contact />} />
<Route path="/live" element={<Live />} />
<Route path="/blog/*" element={<BlogHome />} />
<Route path="/*" element={<NotFound />} />
</Routes>
</Router>

142
src/pages/blog/Home.tsx

@ -0,0 +1,142 @@
import { css } from "@emotion/css";
import React from "react";
import { Routes, Route, Link } from "react-router-dom";
import { Spacer } from "../../components/Spacer";
import { ArticleSubHeader } from "./components/ArticleSubHeader";
import { BlogPost } from "./components/BlogContent";
import { articles, getBlogPath } from "../../data";
const Header: React.FC = () => {
return (
<header
className={css`
padding: 1rem;
margin-bottom: 4rem;
`}>
<div>
<h1
className={css`
display: inline-block;
font-size: 3rem;
color: #fff;
position: relative;
z-index: 100;
&::before {
position: absolute;
content: "";
background: #d6a700;
width: 120%;
height: 50%;
z-index: -100;
left: -10%;
top: 25%;
}
`}>
#MKR
</h1>
<p>
Words from{" "}
<a
href="https://mkr.pw"
className={css`
font-weight: 700;
&:hover {
color: var(--primary-colour);
}
`}>
Muthu Kumar
</a>
</p>
</div>
<div>
<p>Designer / Developer / Architect</p>
</div>
</header>
);
};
const BlogHome: React.FC = () => {
return (
<div
className={css`
display: flex;
background: #1d1d1d;
height: 100%;
width: 100vw;
overflow: hidden;
`}>
<aside
className={css`
height: 100%;
width: 100%;
overflow-y: auto;
flex: 2;
display: flex;
flex-direction: column;
gap: 1rem;
padding: 8rem;
max-width: 45rem;
`}>
<Header />
{articles.map(article => {
const { title, snippet } = article;
const path = getBlogPath(article);
return (
<div key={path}>
<Link
to={path}
className={css`
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 1rem;
border-radius: 0.5rem;
transition: all 300ms;
&:hover {
background: #262626;
}
& h3 {
padding: 0;
font-size: 1.4rem;
}
& * {
line-height: 1.2em;
}
`}>
<h3>{title}</h3>
<ArticleSubHeader article={article} />
<Spacer />
<p>{snippet}</p>
</Link>
</div>
);
})}
</aside>
<article
className={css`
height: 100%;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
padding: 8rem;
background-color: #262626;
flex: 3;
display: flex;
flex-direction: column;
gap: 1.5rem;
`}>
<Routes>
<Route path="/" element={<div></div>} />
<Route path="/*" element={<BlogPost />} />
</Routes>
</article>
</div>
);
};
export default BlogHome;

21
src/pages/blog/components/ArticleSubHeader.tsx

@ -0,0 +1,21 @@
import { css } from "@emotion/css";
import React from "react";
import { Article } from "../../../data";
export const ArticleSubHeader: React.FC<{ article: Article }> = ({
article: { category, published },
}) => {
return (
<div
className={css`
font-size: 0.8rem;
display: inline-flex;
gap: 0.25rem;
color: var(--text-subdued);
`}>
<span>{category}</span>
<span>·</span>
<span>{new Date(published).toLocaleDateString()}</span>
</div>
);
};

210
src/pages/blog/components/BlogContent.tsx

@ -0,0 +1,210 @@
import React, { useEffect, useState } from "react";
import { useLocation } from "react-router-dom";
import { marked } from "marked";
import { Article, blog, getBlogPath, nextAndPrev } from "../../../data";
import "../../../blog.css";
import { ArticleSubHeader } from "./ArticleSubHeader";
import { css, cx } from "@emotion/css";
import { ReactComponent as Arrow } from "../../../assets/arrow-thin.svg";
import { ellipses, useNav } from "../../../util";
const Markdown: React.FC<{ content: string }> = ({ content }) => {
return (
<div
className={css`
& img {
max-width: 100%;
padding-block: 1.5rem;
}
& blockquote {
margin-inline: 0;
padding-inline-start: 1.5rem;
border-inline-start: 1px solid var(--text-colour);
}
`}
dangerouslySetInnerHTML={{ __html: marked(content) }}></div>
);
};
const Preview: React.FC<{ article: Article }> = ({ article }) => {
return (
<div
className={cx(
"preview",
css`
background-color: #353535;
opacity: 0;
pointer-events: none;
position: absolute;
transition: opacity 300ms;
max-width: 100%;
bottom: 4rem;
right: 0;
border-radius: 0.5rem;
overflow: hidden;
width: 13rem;
display: flex;
flex-direction: column;
& * {
line-height: 1.25em;
}
& img {
max-width: 100%;
}
& header {
padding: 0.6rem 1rem 0.8rem 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
& h3 {
font-size: 1.2rem;
}
& p {
font-size: 0.9rem;
color: var(--text-subdued);
}
`,
)}>
<img src={"/blog/assets/" + article["featured-img"]} alt="Featured" />
<header>
<h3>{article.title}</h3>
<p>{ellipses(article.snippet, 110)}</p>
</header>
</div>
);
};
const btn = css`
background-color: #535353;
border: 0;
cursor: pointer;
border-radius: 0.5rem;
padding: 0.5rem 1.4rem;
color: #c8c8c8;
font-size: 1.2rem;
display: flex;
align-items: center;
gap: 0.8rem;
font-weight: 600;
transition: background-color 150ms;
&:hover {
background-color: #414141;
color: var(--text-colour);
}
& svg {
height: 2rem;
width: 1.5rem;
}
`;
export const BlogPost: React.FC = () => {
const navigate = useNav();
const location = useLocation();
const [content, setContent] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(true);
const [year, slug] = location.pathname.split("/").slice(-2);
const article = blog[year]?.[slug];
const [next, prev] = nextAndPrev(year, slug);
useEffect(() => {
async function query() {
setLoading(true);
const path = getBlogPath(article) + ".md";
const res = await fetch(path);
console.log(res.status);
// not success and not a cached response
if (res.status > 299 && res.status !== 304) {
if (res.status > 399) {
const err = await res.text().catch(() => "Unknown error");
return setError(err);
} else return setError("Unexpected redirect");
}
const content = await res.text();
setContent(content.split("---").slice(2).join("---"));
setLoading(false);
}
query();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location]);
if (loading) return <div>Loading...</div>;
if (!article || error) return <div>{error || "Unknown error occurred"}</div>;
return (
<>
<span className="closer"></span>
<img
className={css`
max-width: 80%;
`}
src={"/blog/assets/" + article["featured-img"]}
alt="Featured"
/>
<h1
className={css`
font-size: 2.2rem;
`}>
{article.title}
</h1>
<ArticleSubHeader article={article} />
<Markdown content={content} />
<div
className={css`
display: inline-flex;
justify-content: flex-end;
gap: 1rem;
position: relative;
& .btn-holder:hover .preview {
opacity: 100;
pointer-events: all;
}
`}>
{prev && (
<span className="btn-holder">
<Preview article={prev} />
<button className={btn} onClick={navigate(getBlogPath(prev))}>
<Arrow
className={css`
transform: rotate(180deg);
`}
/>
{!next ? <span>Previous</span> : ""}
</button>
</span>
)}
{next && (
<span className="btn-holder">
<Preview article={next} />
<button className={btn} onClick={navigate(getBlogPath(next))}>
<span
className={css`
padding-bottom: 0.1em;
`}>
Next
</span>
<Arrow />
</button>
</span>
)}
</div>
</>
);
};

15
src/util/index.ts

@ -1,3 +1,6 @@
import React from "react";
import { useNavigate } from "react-router-dom";
export const getTimeout = () => {
const clearables = new Set<number>();
@ -13,3 +16,15 @@ export const getTimeout = () => {
return [timeout, clearTimers] as const;
};
export const ellipses = (text: string, length: number = 100) =>
text.length > length ? text.slice(0, length - 3) + "..." : text;
export const useNav = () => {
const navigate = useNavigate();
return (link: string) => (e: React.MouseEvent) => {
if (e.ctrlKey) return window.open(link, "_blank", "noreferrer noopener");
navigate(link);
};
};

13
vite.config.ts

@ -1,11 +1,18 @@
import { defineConfig } from "vite";
import { resolve } from "path";
import react from "@vitejs/plugin-react";
import svgr from "@svgr/rollup";
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 12000,
},
server: { port: 3000 },
plugins: [react(), Object.assign(svgr({ ref: true, svgo: false }), { enforce: "pre" } as const)],
build: {
rollupOptions: {
input: {
main: resolve(__dirname, "index.html"),
blog: resolve(__dirname, "blog.html"),
},
},
},
});

Loading…
Cancel
Save