initial
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.vscode/
|
||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
public/
|
32
.eslintrc.cjs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
browser: true,
|
||||||
|
es2024: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:astro/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
semi: ["error", "always"],
|
||||||
|
quotes: ["error", "double", { "allowTemplateLiterals": true }],
|
||||||
|
"@typescript-eslint/triple-slash-reference": "off",
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ["*.astro"],
|
||||||
|
parser: "astro-eslint-parser",
|
||||||
|
parserOptions: {
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
extraFileExtensions: [".astro"],
|
||||||
|
},
|
||||||
|
rules: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
22
.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# 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
|
4
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["astro-build.astro-vscode", "unifiedjs.vscode-mdx"],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
64
README.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
![Astro Nano](_astro_nano.png)
|
||||||
|
|
||||||
|
Astro Nano is a static, minimalist, lightweight, lightning fast portfolio and blog theme.
|
||||||
|
|
||||||
|
Built with Astro, Tailwind and Typescript, an no frameworks.
|
||||||
|
|
||||||
|
It was designed as an even more minimal theme than my popular theme [Astro Sphere](https://github.com/markhorn-dev/astro-sphere)
|
||||||
|
|
||||||
|
## 🚀 Deploy your own
|
||||||
|
|
||||||
|
[![Deploy with Vercel](_deploy_vercel.svg)](https://vercel.com/new/clone?repository-url=https://github.com/markhorn-dev/astro-nano) [![Deploy with Netlify](_deploy_netlify.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/markhorn-dev/astro-nano)
|
||||||
|
|
||||||
|
## 📋 Features
|
||||||
|
|
||||||
|
- ✅ 100/100 Lighthouse performance
|
||||||
|
- ✅ Responsive
|
||||||
|
- ✅ Accessible
|
||||||
|
- ✅ SEO-friendly
|
||||||
|
- ✅ Typesafe
|
||||||
|
- ✅ Minimal style
|
||||||
|
- ✅ Light/Dark Theme
|
||||||
|
- ✅ Animated UI
|
||||||
|
- ✅ Tailwind styling
|
||||||
|
- ✅ Auto generated sitemap
|
||||||
|
- ✅ Auto generated RSS Feed
|
||||||
|
- ✅ Markdown support
|
||||||
|
- ✅ MDX Support (components in your markdown)
|
||||||
|
|
||||||
|
## 💯 Lighthouse score
|
||||||
|
![Astro Nano Lighthouse Score](_lighthouse.png)
|
||||||
|
|
||||||
|
## 🕊️ Lightweight
|
||||||
|
No frameworks or added bulk
|
||||||
|
|
||||||
|
## ⚡︎ Fast
|
||||||
|
Rendered in ~40ms on localhost
|
||||||
|
|
||||||
|
## 📄 Configuration
|
||||||
|
|
||||||
|
The blog posts on the demo serve as the documentation and configuration.
|
||||||
|
|
||||||
|
## 💻 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
Replace npm with your package manager of choice. `npm`, `pnpm`, `yarn`, `bun`, etc
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run dev:network` | Starts local dev server on local network |
|
||||||
|
| `npm run sync` | Generates TypeScript types for all Astro modules.|
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run preview:network` | Preview build on local network |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
| `npm run lint` | Run ESLint |
|
||||||
|
| `npm run lint:fix` | Auto-fix ESLint issues |
|
||||||
|
|
||||||
|
## 🏛️ License
|
||||||
|
|
||||||
|
MIT
|
BIN
_astro_nano.png
Normal file
After Width: | Height: | Size: 241 KiB |
17
_deploy_netlify.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg width="179" height="32" viewBox="0 0 179 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_8_30)">
|
||||||
|
<path d="M173 0H6C2.68629 0 0 2.68629 0 6V26C0 29.3137 2.68629 32 6 32H173C176.314 32 179 29.3137 179 26V6C179 2.68629 176.314 0 173 0Z" fill="#2E51ED"/>
|
||||||
|
<path d="M15.027 23.227H14.781L13.556 22.049V21.813L15.429 20.011H16.727L16.9 20.178V21.426L15.027 23.227ZM13.556 9.89999V9.66399L14.781 8.48499H15.027L16.9 10.287V11.535L16.727 11.701H15.429L13.556 9.89999ZM24.343 19.429H22.561L22.411 19.286V15.273C22.411 14.559 22.12 14.005 21.224 13.986C20.764 13.975 20.236 13.986 19.673 14.007L19.588 14.091V19.284L19.439 19.427H17.657L17.507 19.284V12.429L17.657 12.285H21.669C23.229 12.285 24.492 13.5 24.492 15V19.286L24.343 19.429ZM15.28 16.86H8.15L8 16.716V14.998L8.149 14.855H15.28L15.43 14.998V16.716L15.28 16.859V16.86ZM33.853 16.86H26.722L26.572 16.716V14.998L26.722 14.855H33.853L34.002 14.998V16.716L33.853 16.859V16.86ZM19.973 10.143V4.99999L20.122 4.85699H21.909L22.057 4.99999V10.143L21.909 10.287H20.122L19.973 10.143ZM19.973 26.714V21.571L20.122 21.428H21.909L22.057 21.571V26.714L21.909 26.857H20.122L19.973 26.714ZM155.15 10.64C154.72 11.06 154.51 11.64 154.51 12.39V13.43H153.28V15.1H154.51V21.19H156.47V15.1H158.11V13.43H156.47V12.38C156.47 11.85 156.75 11.58 157.3 11.58H158.34V9.99999H156.94C156.18 9.99999 155.59 10.21 155.16 10.64H155.15ZM150.93 10.13C150.57 10.13 150.27 10.25 150.05 10.48C149.84 10.7 149.73 10.98 149.73 11.32C149.73 11.66 149.84 11.95 150.05 12.19C150.27 12.42 150.57 12.54 150.93 12.54C151.29 12.54 151.57 12.42 151.78 12.19C152 11.96 152.12 11.67 152.12 11.32C152.12 10.97 152.01 10.7 151.78 10.48C151.56 10.25 151.28 10.13 150.93 10.13ZM73.23 10.14H75.19V21.19H73.23V10.14Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.32 10.63C48.49 10.63 49.49 10.84 50.33 11.27C51.17 11.69 51.81 12.3 52.25 13.09C52.7 13.88 52.92 14.83 52.92 15.92C52.92 17.01 52.7 17.96 52.25 18.75C51.81 19.54 51.17 20.14 50.33 20.56C49.49 20.98 48.49 21.19 47.32 21.19H44V10.63H47.32ZM47.23 19.22C48.31 19.22 49.16 18.93 49.78 18.35V18.36C50.41 17.77 50.72 16.97 50.72 15.92C50.72 14.87 50.41 14.05 49.78 13.47C49.16 12.89 48.31 12.6 47.23 12.6H46.11V19.22H47.23ZM58.16 13.29C58.96 13.29 59.65 13.45 60.25 13.79L60.24 13.78C60.85 14.11 61.31 14.57 61.64 15.17C61.97 15.77 62.13 16.47 62.13 17.26V17.92H56.28C56.31 18.18 56.36 18.44 56.47 18.66C56.63 18.96 56.85 19.19 57.14 19.36C57.43 19.53 57.78 19.61 58.19 19.61C58.6 19.61 58.95 19.54 59.23 19.41C59.51 19.28 59.72 19.09 59.86 18.86H61.96C61.8 19.33 61.54 19.76 61.19 20.13C60.85 20.51 60.41 20.79 59.89 21C59.38 21.21 58.8 21.31 58.17 21.31C57.38 21.31 56.68 21.15 56.07 20.82C55.47 20.49 55.01 20.02 54.67 19.41C54.34 18.81 54.18 18.1 54.18 17.3C54.18 16.5 54.34 15.8 54.67 15.2C55 14.6 55.46 14.13 56.06 13.79C56.67 13.46 57.36 13.29 58.16 13.29ZM58.16 14.99C57.8 14.99 57.47 15.08 57.18 15.26C56.89 15.42 56.66 15.66 56.49 15.97C56.41 16.14 56.35 16.31 56.31 16.49H60.05C60.0095 16.2241 59.9069 15.9714 59.7505 15.7525C59.5941 15.5336 59.3884 15.3546 59.15 15.23C58.86 15.07 58.52 14.99 58.16 14.99ZM70.16 13.74C69.64 13.42 69.04 13.26 68.35 13.26C67.66 13.26 67.06 13.42 66.57 13.74C66.28 13.93 66.05 14.19 65.86 14.47V13.43H63.9V23.85H65.86V20.16C66.06 20.45 66.29 20.7 66.57 20.9C67.06 21.23 67.65 21.39 68.35 21.39C69.05 21.39 69.63 21.23 70.16 20.91C70.68 20.58 71.09 20.12 71.38 19.51C71.67 18.89 71.81 18.17 71.81 17.33C71.81 16.49 71.67 15.75 71.38 15.15C71.09 14.53 70.68 14.07 70.16 13.75V13.74ZM69.5 18.6C69.33 18.95 69.1 19.22 68.8 19.41C68.51 19.6 68.18 19.69 67.81 19.69C67.22 19.69 66.75 19.48 66.4 19.07C66.05 18.65 65.87 18.08 65.87 17.35C65.87 16.62 66.05 16.07 66.4 15.66C66.75 15.25 67.23 15.04 67.81 15.04C68.18 15.04 68.51 15.14 68.8 15.33C69.1 15.52 69.33 15.79 69.5 16.14C69.67 16.49 69.75 16.89 69.75 17.34C69.75 17.79 69.67 18.23 69.5 18.59V18.6ZM82.85 13.79C82.23 13.44 81.51 13.26 80.68 13.26C79.85 13.26 79.13 13.44 78.51 13.79C77.9 14.14 77.44 14.62 77.11 15.23C76.78 15.85 76.62 16.54 76.62 17.32C76.62 18.1 76.78 18.79 77.11 19.41C77.44 20.02 77.9 20.5 78.51 20.85C79.13 21.2 79.85 21.38 80.68 21.38C81.51 21.38 82.23 21.2 82.85 20.85C83.47 20.5 83.93 20.01 84.25 19.39C84.58 18.77 84.74 18.08 84.74 17.32C84.74 16.56 84.58 15.85 84.25 15.23C83.93 14.61 83.47 14.13 82.85 13.79ZM82.43 18.49C82.27 18.83 82.04 19.09 81.73 19.27C81.43 19.46 81.08 19.55 80.68 19.55C80.28 19.55 79.91 19.46 79.62 19.27C79.32 19.08 79.09 18.82 78.92 18.49C78.76 18.15 78.68 17.76 78.68 17.31C78.68 16.86 78.76 16.46 78.92 16.12C79.09 15.78 79.32 15.53 79.62 15.35C79.92 15.16 80.27 15.07 80.68 15.07C81.09 15.07 81.43 15.16 81.73 15.35C82.04 15.53 82.27 15.79 82.43 16.13C82.6 16.47 82.68 16.86 82.68 17.31C82.68 17.76 82.6 18.15 82.43 18.49Z" fill="white"/>
|
||||||
|
<path d="M87.11 13.43L89.15 18.5L91.26 13.43H93.15L88.84 23.75H86.95L88.14 20.9L85.13 13.43H87.11ZM102.71 10.98H100.75V13.43H99.26V15.1H100.75V21.19H102.71V15.1H104.39V13.43H102.71V10.98Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M109.18 13.26C110.01 13.26 110.73 13.44 111.35 13.79C111.97 14.13 112.43 14.61 112.75 15.23C113.08 15.85 113.24 16.56 113.24 17.32C113.24 18.08 113.08 18.77 112.75 19.39C112.43 20.01 111.97 20.5 111.35 20.85C110.73 21.2 110.01 21.38 109.18 21.38C108.35 21.38 107.63 21.2 107.01 20.85C106.4 20.5 105.94 20.02 105.61 19.41C105.28 18.79 105.12 18.1 105.12 17.32C105.12 16.54 105.28 15.85 105.61 15.23C105.94 14.62 106.4 14.14 107.01 13.79C107.63 13.44 108.35 13.26 109.18 13.26ZM110.23 19.27C110.54 19.09 110.77 18.83 110.93 18.49C111.1 18.15 111.18 17.76 111.18 17.31C111.18 16.86 111.1 16.47 110.93 16.13C110.77 15.79 110.54 15.53 110.23 15.35C109.93 15.16 109.59 15.07 109.18 15.07C108.77 15.07 108.42 15.16 108.12 15.35C107.82 15.53 107.59 15.78 107.42 16.12C107.26 16.46 107.18 16.86 107.18 17.31C107.18 17.76 107.26 18.15 107.42 18.49C107.59 18.82 107.82 19.08 108.12 19.27C108.41 19.46 108.78 19.55 109.18 19.55C109.58 19.55 109.93 19.46 110.23 19.27Z" fill="white"/>
|
||||||
|
<path d="M126.91 16.02L122.22 10.63H120.41V21.19H122.52V14.26L126.91 19.31V21.19H129.02V10.63H126.91V16.02Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M134.5 13.29C135.3 13.29 135.99 13.45 136.59 13.79L136.58 13.78C137.19 14.11 137.65 14.57 137.98 15.17C138.31 15.77 138.47 16.47 138.47 17.26V17.92H132.62C132.65 18.18 132.7 18.44 132.81 18.66C132.97 18.96 133.19 19.19 133.48 19.36C133.77 19.53 134.12 19.61 134.53 19.61C134.94 19.61 135.29 19.54 135.57 19.41C135.85 19.28 136.06 19.09 136.2 18.86H138.3C138.14 19.33 137.88 19.76 137.53 20.13C137.19 20.51 136.75 20.79 136.23 21C135.72 21.21 135.14 21.31 134.51 21.31C133.72 21.31 133.02 21.15 132.41 20.82C131.81 20.49 131.35 20.02 131.01 19.41C130.68 18.81 130.52 18.1 130.52 17.3C130.52 16.5 130.68 15.8 131.01 15.2C131.34 14.6 131.8 14.13 132.4 13.79C133.01 13.46 133.7 13.29 134.5 13.29ZM134.5 14.99C134.14 14.99 133.81 15.08 133.52 15.26C133.23 15.42 133 15.66 132.83 15.97C132.75 16.14 132.69 16.31 132.65 16.49H136.39C136.349 16.224 136.247 15.9714 136.09 15.7525C135.934 15.5336 135.728 15.3546 135.49 15.23C135.2 15.07 134.86 14.99 134.5 14.99Z" fill="white"/>
|
||||||
|
<path d="M142.58 10.98H140.62V13.43H139.14V15.1H140.62V21.19H142.58V15.1H144.26V13.43H142.58V10.98ZM145.92 10.14H147.88V21.19H145.92V10.14ZM149.95 21.18V13.26C150.21 13.45 150.53 13.55 150.93 13.55C151.33 13.55 151.66 13.46 151.91 13.26V21.18H149.95ZM163.14 18.5L165.26 13.43H167.15L162.84 23.75H160.95L162.14 20.9L159.13 13.43H161.1L163.14 18.5Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_8_30">
|
||||||
|
<rect width="179" height="32" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 7.5 KiB |
5
_deploy_vercel.svg
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
_lighthouse.png
Normal file
After Width: | Height: | Size: 50 KiB |
9
astro.config.mjs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig } from "astro/config";
|
||||||
|
import mdx from "@astrojs/mdx";
|
||||||
|
import sitemap from "@astrojs/sitemap";
|
||||||
|
import tailwind from "@astrojs/tailwind";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
site: "https://astro-nano-demo.vercel.app",
|
||||||
|
integrations: [mdx(), sitemap(), tailwind()],
|
||||||
|
});
|
11371
package-lock.json
generated
Normal file
36
package.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"name": "astro-nano",
|
||||||
|
"type": "module",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"dev:network": "astro dev --host",
|
||||||
|
"build": "astro check && astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"preview:network": "astro preview --host",
|
||||||
|
"astro": "astro",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.5.9",
|
||||||
|
"@astrojs/mdx": "^2.2.0",
|
||||||
|
"@astrojs/rss": "^4.0.5",
|
||||||
|
"@astrojs/sitemap": "^3.1.1",
|
||||||
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
|
"@fontsource/inter": "^5.0.17",
|
||||||
|
"@fontsource/lora": "^5.0.16",
|
||||||
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||||
|
"@typescript-eslint/parser": "^7.3.1",
|
||||||
|
"astro": "^4.5.6",
|
||||||
|
"clsx": "^2.1.0",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-astro": "^0.32.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
|
"sharp": "^0.33.3",
|
||||||
|
"tailwind-merge": "^2.2.2",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
|
"typescript": "^5.4.2"
|
||||||
|
}
|
||||||
|
}
|
6448
pnpm-lock.yaml
Normal file
BIN
public/astro-nano.png
Normal file
After Width: | Height: | Size: 241 KiB |
BIN
public/astro-sphere.jpg
Normal file
After Width: | Height: | Size: 68 KiB |
17
public/deploy_netlify.svg
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<svg width="179" height="32" viewBox="0 0 179 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_8_30)">
|
||||||
|
<path d="M173 0H6C2.68629 0 0 2.68629 0 6V26C0 29.3137 2.68629 32 6 32H173C176.314 32 179 29.3137 179 26V6C179 2.68629 176.314 0 173 0Z" fill="#2E51ED"/>
|
||||||
|
<path d="M15.027 23.227H14.781L13.556 22.049V21.813L15.429 20.011H16.727L16.9 20.178V21.426L15.027 23.227ZM13.556 9.89999V9.66399L14.781 8.48499H15.027L16.9 10.287V11.535L16.727 11.701H15.429L13.556 9.89999ZM24.343 19.429H22.561L22.411 19.286V15.273C22.411 14.559 22.12 14.005 21.224 13.986C20.764 13.975 20.236 13.986 19.673 14.007L19.588 14.091V19.284L19.439 19.427H17.657L17.507 19.284V12.429L17.657 12.285H21.669C23.229 12.285 24.492 13.5 24.492 15V19.286L24.343 19.429ZM15.28 16.86H8.15L8 16.716V14.998L8.149 14.855H15.28L15.43 14.998V16.716L15.28 16.859V16.86ZM33.853 16.86H26.722L26.572 16.716V14.998L26.722 14.855H33.853L34.002 14.998V16.716L33.853 16.859V16.86ZM19.973 10.143V4.99999L20.122 4.85699H21.909L22.057 4.99999V10.143L21.909 10.287H20.122L19.973 10.143ZM19.973 26.714V21.571L20.122 21.428H21.909L22.057 21.571V26.714L21.909 26.857H20.122L19.973 26.714ZM155.15 10.64C154.72 11.06 154.51 11.64 154.51 12.39V13.43H153.28V15.1H154.51V21.19H156.47V15.1H158.11V13.43H156.47V12.38C156.47 11.85 156.75 11.58 157.3 11.58H158.34V9.99999H156.94C156.18 9.99999 155.59 10.21 155.16 10.64H155.15ZM150.93 10.13C150.57 10.13 150.27 10.25 150.05 10.48C149.84 10.7 149.73 10.98 149.73 11.32C149.73 11.66 149.84 11.95 150.05 12.19C150.27 12.42 150.57 12.54 150.93 12.54C151.29 12.54 151.57 12.42 151.78 12.19C152 11.96 152.12 11.67 152.12 11.32C152.12 10.97 152.01 10.7 151.78 10.48C151.56 10.25 151.28 10.13 150.93 10.13ZM73.23 10.14H75.19V21.19H73.23V10.14Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.32 10.63C48.49 10.63 49.49 10.84 50.33 11.27C51.17 11.69 51.81 12.3 52.25 13.09C52.7 13.88 52.92 14.83 52.92 15.92C52.92 17.01 52.7 17.96 52.25 18.75C51.81 19.54 51.17 20.14 50.33 20.56C49.49 20.98 48.49 21.19 47.32 21.19H44V10.63H47.32ZM47.23 19.22C48.31 19.22 49.16 18.93 49.78 18.35V18.36C50.41 17.77 50.72 16.97 50.72 15.92C50.72 14.87 50.41 14.05 49.78 13.47C49.16 12.89 48.31 12.6 47.23 12.6H46.11V19.22H47.23ZM58.16 13.29C58.96 13.29 59.65 13.45 60.25 13.79L60.24 13.78C60.85 14.11 61.31 14.57 61.64 15.17C61.97 15.77 62.13 16.47 62.13 17.26V17.92H56.28C56.31 18.18 56.36 18.44 56.47 18.66C56.63 18.96 56.85 19.19 57.14 19.36C57.43 19.53 57.78 19.61 58.19 19.61C58.6 19.61 58.95 19.54 59.23 19.41C59.51 19.28 59.72 19.09 59.86 18.86H61.96C61.8 19.33 61.54 19.76 61.19 20.13C60.85 20.51 60.41 20.79 59.89 21C59.38 21.21 58.8 21.31 58.17 21.31C57.38 21.31 56.68 21.15 56.07 20.82C55.47 20.49 55.01 20.02 54.67 19.41C54.34 18.81 54.18 18.1 54.18 17.3C54.18 16.5 54.34 15.8 54.67 15.2C55 14.6 55.46 14.13 56.06 13.79C56.67 13.46 57.36 13.29 58.16 13.29ZM58.16 14.99C57.8 14.99 57.47 15.08 57.18 15.26C56.89 15.42 56.66 15.66 56.49 15.97C56.41 16.14 56.35 16.31 56.31 16.49H60.05C60.0095 16.2241 59.9069 15.9714 59.7505 15.7525C59.5941 15.5336 59.3884 15.3546 59.15 15.23C58.86 15.07 58.52 14.99 58.16 14.99ZM70.16 13.74C69.64 13.42 69.04 13.26 68.35 13.26C67.66 13.26 67.06 13.42 66.57 13.74C66.28 13.93 66.05 14.19 65.86 14.47V13.43H63.9V23.85H65.86V20.16C66.06 20.45 66.29 20.7 66.57 20.9C67.06 21.23 67.65 21.39 68.35 21.39C69.05 21.39 69.63 21.23 70.16 20.91C70.68 20.58 71.09 20.12 71.38 19.51C71.67 18.89 71.81 18.17 71.81 17.33C71.81 16.49 71.67 15.75 71.38 15.15C71.09 14.53 70.68 14.07 70.16 13.75V13.74ZM69.5 18.6C69.33 18.95 69.1 19.22 68.8 19.41C68.51 19.6 68.18 19.69 67.81 19.69C67.22 19.69 66.75 19.48 66.4 19.07C66.05 18.65 65.87 18.08 65.87 17.35C65.87 16.62 66.05 16.07 66.4 15.66C66.75 15.25 67.23 15.04 67.81 15.04C68.18 15.04 68.51 15.14 68.8 15.33C69.1 15.52 69.33 15.79 69.5 16.14C69.67 16.49 69.75 16.89 69.75 17.34C69.75 17.79 69.67 18.23 69.5 18.59V18.6ZM82.85 13.79C82.23 13.44 81.51 13.26 80.68 13.26C79.85 13.26 79.13 13.44 78.51 13.79C77.9 14.14 77.44 14.62 77.11 15.23C76.78 15.85 76.62 16.54 76.62 17.32C76.62 18.1 76.78 18.79 77.11 19.41C77.44 20.02 77.9 20.5 78.51 20.85C79.13 21.2 79.85 21.38 80.68 21.38C81.51 21.38 82.23 21.2 82.85 20.85C83.47 20.5 83.93 20.01 84.25 19.39C84.58 18.77 84.74 18.08 84.74 17.32C84.74 16.56 84.58 15.85 84.25 15.23C83.93 14.61 83.47 14.13 82.85 13.79ZM82.43 18.49C82.27 18.83 82.04 19.09 81.73 19.27C81.43 19.46 81.08 19.55 80.68 19.55C80.28 19.55 79.91 19.46 79.62 19.27C79.32 19.08 79.09 18.82 78.92 18.49C78.76 18.15 78.68 17.76 78.68 17.31C78.68 16.86 78.76 16.46 78.92 16.12C79.09 15.78 79.32 15.53 79.62 15.35C79.92 15.16 80.27 15.07 80.68 15.07C81.09 15.07 81.43 15.16 81.73 15.35C82.04 15.53 82.27 15.79 82.43 16.13C82.6 16.47 82.68 16.86 82.68 17.31C82.68 17.76 82.6 18.15 82.43 18.49Z" fill="white"/>
|
||||||
|
<path d="M87.11 13.43L89.15 18.5L91.26 13.43H93.15L88.84 23.75H86.95L88.14 20.9L85.13 13.43H87.11ZM102.71 10.98H100.75V13.43H99.26V15.1H100.75V21.19H102.71V15.1H104.39V13.43H102.71V10.98Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M109.18 13.26C110.01 13.26 110.73 13.44 111.35 13.79C111.97 14.13 112.43 14.61 112.75 15.23C113.08 15.85 113.24 16.56 113.24 17.32C113.24 18.08 113.08 18.77 112.75 19.39C112.43 20.01 111.97 20.5 111.35 20.85C110.73 21.2 110.01 21.38 109.18 21.38C108.35 21.38 107.63 21.2 107.01 20.85C106.4 20.5 105.94 20.02 105.61 19.41C105.28 18.79 105.12 18.1 105.12 17.32C105.12 16.54 105.28 15.85 105.61 15.23C105.94 14.62 106.4 14.14 107.01 13.79C107.63 13.44 108.35 13.26 109.18 13.26ZM110.23 19.27C110.54 19.09 110.77 18.83 110.93 18.49C111.1 18.15 111.18 17.76 111.18 17.31C111.18 16.86 111.1 16.47 110.93 16.13C110.77 15.79 110.54 15.53 110.23 15.35C109.93 15.16 109.59 15.07 109.18 15.07C108.77 15.07 108.42 15.16 108.12 15.35C107.82 15.53 107.59 15.78 107.42 16.12C107.26 16.46 107.18 16.86 107.18 17.31C107.18 17.76 107.26 18.15 107.42 18.49C107.59 18.82 107.82 19.08 108.12 19.27C108.41 19.46 108.78 19.55 109.18 19.55C109.58 19.55 109.93 19.46 110.23 19.27Z" fill="white"/>
|
||||||
|
<path d="M126.91 16.02L122.22 10.63H120.41V21.19H122.52V14.26L126.91 19.31V21.19H129.02V10.63H126.91V16.02Z" fill="white"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M134.5 13.29C135.3 13.29 135.99 13.45 136.59 13.79L136.58 13.78C137.19 14.11 137.65 14.57 137.98 15.17C138.31 15.77 138.47 16.47 138.47 17.26V17.92H132.62C132.65 18.18 132.7 18.44 132.81 18.66C132.97 18.96 133.19 19.19 133.48 19.36C133.77 19.53 134.12 19.61 134.53 19.61C134.94 19.61 135.29 19.54 135.57 19.41C135.85 19.28 136.06 19.09 136.2 18.86H138.3C138.14 19.33 137.88 19.76 137.53 20.13C137.19 20.51 136.75 20.79 136.23 21C135.72 21.21 135.14 21.31 134.51 21.31C133.72 21.31 133.02 21.15 132.41 20.82C131.81 20.49 131.35 20.02 131.01 19.41C130.68 18.81 130.52 18.1 130.52 17.3C130.52 16.5 130.68 15.8 131.01 15.2C131.34 14.6 131.8 14.13 132.4 13.79C133.01 13.46 133.7 13.29 134.5 13.29ZM134.5 14.99C134.14 14.99 133.81 15.08 133.52 15.26C133.23 15.42 133 15.66 132.83 15.97C132.75 16.14 132.69 16.31 132.65 16.49H136.39C136.349 16.224 136.247 15.9714 136.09 15.7525C135.934 15.5336 135.728 15.3546 135.49 15.23C135.2 15.07 134.86 14.99 134.5 14.99Z" fill="white"/>
|
||||||
|
<path d="M142.58 10.98H140.62V13.43H139.14V15.1H140.62V21.19H142.58V15.1H144.26V13.43H142.58V10.98ZM145.92 10.14H147.88V21.19H145.92V10.14ZM149.95 21.18V13.26C150.21 13.45 150.53 13.55 150.93 13.55C151.33 13.55 151.66 13.46 151.91 13.26V21.18H149.95ZM163.14 18.5L165.26 13.43H167.15L162.84 23.75H160.95L162.14 20.9L159.13 13.43H161.1L163.14 18.5Z" fill="white"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_8_30">
|
||||||
|
<rect width="179" height="32" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 7.5 KiB |
5
public/deploy_vercel.svg
Normal file
After Width: | Height: | Size: 10 KiB |
11
public/favicon-dark.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="85" height="107" viewBox="0 0 85 107" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="white" d="M27.5894 91.1365C22.7555 86.7178 21.3444 77.4335 23.3583 70.7072C26.8503 74.948 31.6888 76.2914 36.7005 77.0497C44.4375 78.2199 52.0359 77.7822 59.2232 74.2459C60.0454 73.841 60.8052 73.3027 61.7036 72.7574C62.378 74.714 62.5535 76.6892 62.318 78.6996C61.7452 83.5957 59.3086 87.3778 55.4332 90.2448C53.8835 91.3916 52.2437 92.4167 50.6432 93.4979C45.7262 96.8213 44.3959 100.718 46.2435 106.386C46.2874 106.525 46.3267 106.663 46.426 107C43.9155 105.876 42.0817 104.24 40.6845 102.089C39.2087 99.8193 38.5066 97.3081 38.4696 94.5909C38.4511 93.2686 38.4511 91.9345 38.2733 90.6309C37.8391 87.4527 36.3471 86.0297 33.5364 85.9478C30.6518 85.8636 28.37 87.6469 27.7649 90.4554C27.7187 90.6707 27.6517 90.8837 27.5847 91.1341L27.5894 91.1365Z" />
|
||||||
|
<path fill="url(#paint0_linear_1_59)" d="M27.5894 91.1365C22.7555 86.7178 21.3444 77.4335 23.3583 70.7072C26.8503 74.948 31.6888 76.2914 36.7005 77.0497C44.4375 78.2199 52.0359 77.7822 59.2232 74.2459C60.0454 73.841 60.8052 73.3027 61.7036 72.7574C62.378 74.714 62.5535 76.6892 62.318 78.6996C61.7452 83.5957 59.3086 87.3778 55.4332 90.2448C53.8835 91.3916 52.2437 92.4167 50.6432 93.4979C45.7262 96.8213 44.3959 100.718 46.2435 106.386C46.2874 106.525 46.3267 106.663 46.426 107C43.9155 105.876 42.0817 104.24 40.6845 102.089C39.2087 99.8193 38.5066 97.3081 38.4696 94.5909C38.4511 93.2686 38.4511 91.9345 38.2733 90.6309C37.8391 87.4527 36.3471 86.0297 33.5364 85.9478C30.6518 85.8636 28.37 87.6469 27.7649 90.4554C27.7187 90.6707 27.6517 90.8837 27.5847 91.1341L27.5894 91.1365Z" />
|
||||||
|
<path fill="white" d="M0 69.5866C0 69.5866 14.3139 62.6137 28.6678 62.6137L39.4901 29.1204C39.8953 27.5007 41.0783 26.3999 42.4139 26.3999C43.7495 26.3999 44.9325 27.5007 45.3377 29.1204L56.1601 62.6137C73.1601 62.6137 84.8278 69.5866 84.8278 69.5866C84.8278 69.5866 60.5145 3.35233 60.467 3.21944C59.7692 1.2612 58.5911 0 57.0029 0H27.8274C26.2392 0 25.1087 1.2612 24.3634 3.21944C24.3108 3.34983 0 69.5866 0 69.5866Z" />
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_1_59" x1="22.4702" y1="107" x2="69.1451" y2="84.9468" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#D83333"/>
|
||||||
|
<stop offset="1" stop-color="#F041FF"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
11
public/favicon-light.svg
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<svg width="85" height="107" viewBox="0 0 85 107" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="black" d="M27.5894 91.1365C22.7555 86.7178 21.3444 77.4335 23.3583 70.7072C26.8503 74.948 31.6888 76.2914 36.7005 77.0497C44.4375 78.2199 52.0359 77.7822 59.2232 74.2459C60.0454 73.841 60.8052 73.3027 61.7036 72.7574C62.378 74.714 62.5535 76.6892 62.318 78.6996C61.7452 83.5957 59.3086 87.3778 55.4332 90.2448C53.8835 91.3916 52.2437 92.4167 50.6432 93.4979C45.7262 96.8213 44.3959 100.718 46.2435 106.386C46.2874 106.525 46.3267 106.663 46.426 107C43.9155 105.876 42.0817 104.24 40.6845 102.089C39.2087 99.8193 38.5066 97.3081 38.4696 94.5909C38.4511 93.2686 38.4511 91.9345 38.2733 90.6309C37.8391 87.4527 36.3471 86.0297 33.5364 85.9478C30.6518 85.8636 28.37 87.6469 27.7649 90.4554C27.7187 90.6707 27.6517 90.8837 27.5847 91.1341L27.5894 91.1365Z" />
|
||||||
|
<path fill="url(#paint0_linear_1_59)" d="M27.5894 91.1365C22.7555 86.7178 21.3444 77.4335 23.3583 70.7072C26.8503 74.948 31.6888 76.2914 36.7005 77.0497C44.4375 78.2199 52.0359 77.7822 59.2232 74.2459C60.0454 73.841 60.8052 73.3027 61.7036 72.7574C62.378 74.714 62.5535 76.6892 62.318 78.6996C61.7452 83.5957 59.3086 87.3778 55.4332 90.2448C53.8835 91.3916 52.2437 92.4167 50.6432 93.4979C45.7262 96.8213 44.3959 100.718 46.2435 106.386C46.2874 106.525 46.3267 106.663 46.426 107C43.9155 105.876 42.0817 104.24 40.6845 102.089C39.2087 99.8193 38.5066 97.3081 38.4696 94.5909C38.4511 93.2686 38.4511 91.9345 38.2733 90.6309C37.8391 87.4527 36.3471 86.0297 33.5364 85.9478C30.6518 85.8636 28.37 87.6469 27.7649 90.4554C27.7187 90.6707 27.6517 90.8837 27.5847 91.1341L27.5894 91.1365Z" />
|
||||||
|
<path fill="black" d="M0 69.5866C0 69.5866 14.3139 62.6137 28.6678 62.6137L39.4901 29.1204C39.8953 27.5007 41.0783 26.3999 42.4139 26.3999C43.7495 26.3999 44.9325 27.5007 45.3377 29.1204L56.1601 62.6137C73.1601 62.6137 84.8278 69.5866 84.8278 69.5866C84.8278 69.5866 60.5145 3.35233 60.467 3.21944C59.7692 1.2612 58.5911 0 57.0029 0H27.8274C26.2392 0 25.1087 1.2612 24.3634 3.21944C24.3108 3.34983 0 69.5866 0 69.5866Z" />
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="paint0_linear_1_59" x1="22.4702" y1="107" x2="69.1451" y2="84.9468" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop stop-color="#D83333"/>
|
||||||
|
<stop offset="1" stop-color="#F041FF"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/fonts/MonaSans-Light.woff2
Normal file
BIN
public/fonts/MonaSans-Regular.woff2
Normal file
BIN
public/fonts/MonaSans-SemiBold.woff2
Normal file
BIN
public/fonts/atkinson-bold.woff
Normal file
BIN
public/fonts/atkinson-regular.woff
Normal file
BIN
public/lighthouse.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
public/patrick.webp
Normal file
After Width: | Height: | Size: 20 KiB |
27
src/components/ArrowCard.astro
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
import type { CollectionEntry } from "astro:content";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entry: CollectionEntry<"blog"> | CollectionEntry<"projects">;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a href={`/${entry.collection}/${entry.slug}`} class="relative group flex flex-nowrap py-3 px-4 pr-10 rounded-lg border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
|
||||||
|
<div class="flex flex-col flex-1 truncate">
|
||||||
|
<div class="font-semibold">
|
||||||
|
{entry.data.title}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm">
|
||||||
|
{entry.data.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="absolute top-1/2 right-2 -translate-y-1/2 size-5 stroke-2 fill-none stroke-current">
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" class="translate-x-3 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out" />
|
||||||
|
<polyline points="12 5 19 12 12 19" class="-translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
20
src/components/BackToPrev.astro
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
type Props = {
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { href } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a href={href} class="relative group w-fit flex pl-7 pr-3 py-1.5 flex-nowrap rounded border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="absolute top-1/2 left-2 -translate-y-1/2 size-4 stroke-2 fill-none stroke-current">
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" class="translate-x-2 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out" />
|
||||||
|
<polyline points="12 5 5 12 12 19" class="translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out" />
|
||||||
|
</svg>
|
||||||
|
<div class="text-sm">
|
||||||
|
<slot/>
|
||||||
|
</div>
|
||||||
|
</a>
|
12
src/components/BackToTop.astro
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<button id="back-to-top" class="relative group w-fit flex pl-8 pr-3 py-1.5 flex-nowrap rounded border border-black/15 dark:border-white/20 hover:bg-black/5 dark:hover:bg-white/5 hover:text-black dark:hover:text-white transition-colors duration-300 ease-in-out">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
class="absolute top-1/2 left-2 -translate-y-1/2 size-4 stroke-2 fill-none stroke-current rotate-90">
|
||||||
|
<line x1="5" y1="12" x2="19" y2="12" class="translate-x-2 group-hover:translate-x-0 scale-x-0 group-hover:scale-x-100 transition-transform duration-300 ease-in-out" />
|
||||||
|
<polyline points="12 5 5 12 12 19" class="translate-x-1 group-hover:translate-x-0 transition-transform duration-300 ease-in-out" />
|
||||||
|
</svg>
|
||||||
|
<div class="text-sm">
|
||||||
|
Back to top
|
||||||
|
</div>
|
||||||
|
</button>
|
7
src/components/Container.astro
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="mx-auto max-w-screen-sm px-5">
|
||||||
|
<slot />
|
||||||
|
</div>
|
92
src/components/Footer.astro
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
---
|
||||||
|
import Container from "@components/Container.astro";
|
||||||
|
import { SITE } from "@consts";
|
||||||
|
import BackToTop from "@components/BackToTop.astro";
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="animate">
|
||||||
|
<Container>
|
||||||
|
<div class="relative">
|
||||||
|
<div class="absolute right-0 -top-20">
|
||||||
|
<BackToTop />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div>
|
||||||
|
© 2024 {`|`} {SITE.NAME}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-1 items-center">
|
||||||
|
<button
|
||||||
|
id="light-theme-button"
|
||||||
|
aria-label="Light theme"
|
||||||
|
class="group size-8 flex items-center justify-center rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="group-hover:stroke-black group-hover:dark:stroke-white transition-colors duration-300 ease-in-out"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="5"></circle>
|
||||||
|
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||||
|
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||||
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||||
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||||
|
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||||
|
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||||
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||||
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="dark-theme-button"
|
||||||
|
aria-label="Dark theme"
|
||||||
|
class="group size-8 flex items-center justify-center rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="group-hover:stroke-black group-hover:dark:stroke-white transition-colors duration-300 ease-in-out"
|
||||||
|
>
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="system-theme-button"
|
||||||
|
aria-label="System theme"
|
||||||
|
class="group size-8 flex items-center justify-center rounded-full"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="group-hover:stroke-black group-hover:dark:stroke-white transition-colors duration-300 ease-in-out"
|
||||||
|
>
|
||||||
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
|
||||||
|
<line x1="8" y1="21" x2="16" y2="21"></line>
|
||||||
|
<line x1="12" y1="17" x2="12" y2="21"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</footer>
|
17
src/components/FormattedDate.astro
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { date } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<time datetime={date.toISOString()}>
|
||||||
|
{
|
||||||
|
date.toLocaleDateString("en-us", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</time>
|
181
src/components/Head.astro
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
---
|
||||||
|
import "../styles/global.css";
|
||||||
|
import "@fontsource/inter/latin-400.css";
|
||||||
|
import "@fontsource/inter/latin-600.css";
|
||||||
|
import "@fontsource/lora/400.css";
|
||||||
|
import "@fontsource/lora/600.css";
|
||||||
|
import inter400 from "@fontsource/inter/files/inter-latin-400-normal.woff2";
|
||||||
|
import inter600 from "@fontsource/inter/files/inter-latin-600-normal.woff2";
|
||||||
|
import lora400 from "@fontsource/lora/files/lora-latin-400-normal.woff2";
|
||||||
|
import lora600 from "@fontsource/lora/files/lora-latin-600-normal.woff2";
|
||||||
|
import { ViewTransitions } from "astro:transitions";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
image?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
|
||||||
|
|
||||||
|
const { title, description, image = "/nano.png" } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Global Metadata -->
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon-dark.svg" media="(prefers-color-scheme: dark)">
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon-light.svg" media="(prefers-color-scheme: light)">
|
||||||
|
<link rel="icon" type="image/x-icon" href="/favicon-light.svg">
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
|
||||||
|
<!-- Font preloads -->
|
||||||
|
<link rel="preload" href={inter400} as="font" type="font/woff2" crossorigin/>
|
||||||
|
<link rel="preload" href={inter600} as="font" type="font/woff2" crossorigin/>
|
||||||
|
<link rel="preload" href={lora400} as="font" type="font/woff2" crossorigin/>
|
||||||
|
<link rel="preload" href={lora600} as="font" type="font/woff2" crossorigin/>
|
||||||
|
|
||||||
|
<!-- Canonical URL -->
|
||||||
|
<link rel="canonical" href={canonicalURL} />
|
||||||
|
|
||||||
|
<!-- Primary Meta Tags -->
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="title" content={title} />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content={Astro.url} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:image" content={new URL(image, Astro.url)} />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:url" content={Astro.url} />
|
||||||
|
<meta property="twitter:title" content={title} />
|
||||||
|
<meta property="twitter:description" content={description} />
|
||||||
|
<meta property="twitter:image" content={new URL(image, Astro.url)} />
|
||||||
|
|
||||||
|
<ViewTransitions />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import type { TransitionBeforeSwapEvent } from "astro:transitions/client";
|
||||||
|
document.addEventListener("astro:before-swap", (e) =>
|
||||||
|
[
|
||||||
|
...(e as TransitionBeforeSwapEvent).newDocument.head.querySelectorAll(
|
||||||
|
"link[as=\"font\"]"
|
||||||
|
),
|
||||||
|
].forEach((link) => link.remove())
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
function init() {
|
||||||
|
preloadTheme();
|
||||||
|
onScroll();
|
||||||
|
animate();
|
||||||
|
|
||||||
|
const backToTop = document.getElementById("back-to-top");
|
||||||
|
backToTop?.addEventListener("click", (event) => scrollToTop(event));
|
||||||
|
|
||||||
|
const backToPrev = document.getElementById("back-to-prev");
|
||||||
|
backToPrev?.addEventListener("click", () => window.history.back());
|
||||||
|
|
||||||
|
const lightThemeButton = document.getElementById("light-theme-button");
|
||||||
|
lightThemeButton?.addEventListener("click", () => {
|
||||||
|
localStorage.setItem("theme", "light");
|
||||||
|
toggleTheme(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const darkThemeButton = document.getElementById("dark-theme-button");
|
||||||
|
darkThemeButton?.addEventListener("click", () => {
|
||||||
|
localStorage.setItem("theme", "dark");
|
||||||
|
toggleTheme(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const systemThemeButton = document.getElementById("system-theme-button");
|
||||||
|
systemThemeButton?.addEventListener("click", () => {
|
||||||
|
localStorage.setItem("theme", "system");
|
||||||
|
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.matchMedia("(prefers-color-scheme: dark)")
|
||||||
|
.addEventListener("change", event => {
|
||||||
|
if (localStorage.theme === "system") {
|
||||||
|
toggleTheme(event.matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
document.addEventListener("scroll", onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
const animateElements = document.querySelectorAll(".animate");
|
||||||
|
|
||||||
|
animateElements.forEach((element, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
element.classList.add("show");
|
||||||
|
}, index * 150);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onScroll() {
|
||||||
|
if (window.scrollY > 0) {
|
||||||
|
document.documentElement.classList.add("scrolled");
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("scrolled");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToTop(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
window.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: "smooth"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme(dark) {
|
||||||
|
const css = document.createElement("style");
|
||||||
|
|
||||||
|
css.appendChild(
|
||||||
|
document.createTextNode(
|
||||||
|
`* {
|
||||||
|
-webkit-transition: none !important;
|
||||||
|
-moz-transition: none !important;
|
||||||
|
-o-transition: none !important;
|
||||||
|
-ms-transition: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
document.head.appendChild(css);
|
||||||
|
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.classList.add("dark");
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.getComputedStyle(css).opacity;
|
||||||
|
document.head.removeChild(css);
|
||||||
|
}
|
||||||
|
|
||||||
|
function preloadTheme() {
|
||||||
|
const userTheme = localStorage.theme;
|
||||||
|
|
||||||
|
if (userTheme === "light" || userTheme === "dark") {
|
||||||
|
toggleTheme(userTheme === "dark");
|
||||||
|
} else {
|
||||||
|
toggleTheme(window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => init());
|
||||||
|
document.addEventListener("astro:after-swap", () => init());
|
||||||
|
preloadTheme();
|
||||||
|
</script>
|
34
src/components/Header.astro
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
import Container from "@components/Container.astro";
|
||||||
|
import Link from "@components/Link.astro";
|
||||||
|
import { SITE } from "@consts";
|
||||||
|
---
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<Container>
|
||||||
|
<div class="flex flex-wrap gap-y-2 justify-between">
|
||||||
|
<Link href="/" underline={false}>
|
||||||
|
<div class="font-semibold">
|
||||||
|
{SITE.NAME}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
<nav class="flex gap-1">
|
||||||
|
<Link href="/blog">
|
||||||
|
blog
|
||||||
|
</Link>
|
||||||
|
<span>
|
||||||
|
{`/`}
|
||||||
|
</span>
|
||||||
|
<Link href="/work">
|
||||||
|
work
|
||||||
|
</Link>
|
||||||
|
<span>
|
||||||
|
{`/`}
|
||||||
|
</span>
|
||||||
|
<Link href="/projects">
|
||||||
|
projects
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</header>
|
19
src/components/Link.astro
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
import { cn } from "@lib/utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
href: string;
|
||||||
|
external?: boolean;
|
||||||
|
underline?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { href, external, underline = true, ...rest } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target={ external ? "_blank" : "_self" }
|
||||||
|
class={cn("inline-block decoration-black/15 dark:decoration-white/30 hover:decoration-black/25 hover:dark:decoration-white/50 text-current hover:text-black hover:dark:text-white transition-colors duration-300 ease-in-out", underline && "underline underline-offset-2")}
|
||||||
|
{...rest}>
|
||||||
|
<slot/>
|
||||||
|
</a>
|
44
src/consts.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import type { Site, Metadata, Socials } from "@types";
|
||||||
|
|
||||||
|
export const SITE: Site = {
|
||||||
|
NAME: "Michael Rausch",
|
||||||
|
EMAIL: "michael@rausch.nz",
|
||||||
|
NUM_POSTS_ON_HOMEPAGE: 3,
|
||||||
|
NUM_WORKS_ON_HOMEPAGE: 2,
|
||||||
|
NUM_PROJECTS_ON_HOMEPAGE: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HOME: Metadata = {
|
||||||
|
TITLE: "Home",
|
||||||
|
DESCRIPTION: "Astro Nano is a minimal and lightweight blog and portfolio.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BLOG: Metadata = {
|
||||||
|
TITLE: "Blog",
|
||||||
|
DESCRIPTION: "A collection of articles on topics I am passionate about.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WORK: Metadata = {
|
||||||
|
TITLE: "Work",
|
||||||
|
DESCRIPTION: "Where I have worked and what I have done.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROJECTS: Metadata = {
|
||||||
|
TITLE: "Projects",
|
||||||
|
DESCRIPTION: "A collection of my projects, with links to repositories and demos.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SOCIALS: Socials = [
|
||||||
|
{
|
||||||
|
NAME: "twitter-x",
|
||||||
|
HREF: "https://twitter.com/markhorn_dev",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NAME: "github",
|
||||||
|
HREF: "https://github.com/markhorn-dev"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NAME: "linkedin",
|
||||||
|
HREF: "https://www.linkedin.com/in/markhorn-dev",
|
||||||
|
}
|
||||||
|
];
|
5
src/content/blog/01-getting-started/index.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: "Coming Soon"
|
||||||
|
description: ""
|
||||||
|
date: "Mar 22 2024"
|
||||||
|
---
|
35
src/content/config.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { defineCollection, z } from "astro:content";
|
||||||
|
|
||||||
|
const blog = defineCollection({
|
||||||
|
type: "content",
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
date: z.coerce.date(),
|
||||||
|
draft: z.boolean().optional()
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const work = defineCollection({
|
||||||
|
type: "content",
|
||||||
|
schema: z.object({
|
||||||
|
company: z.string(),
|
||||||
|
role: z.string(),
|
||||||
|
dateStart: z.coerce.date(),
|
||||||
|
dateEnd: z.union([z.coerce.date(), z.string()]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const projects = defineCollection({
|
||||||
|
type: "content",
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
date: z.coerce.date(),
|
||||||
|
draft: z.boolean().optional(),
|
||||||
|
demoURL: z.string().optional(),
|
||||||
|
repoURL: z.string().optional()
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { blog, work, projects };
|
76
src/content/projects/project-1/index.md
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
title: "Astro Sphere"
|
||||||
|
description: "Portfolio and blog build with astro."
|
||||||
|
date: "Mar 18 2024"
|
||||||
|
demoURL: "https://astro-sphere-demo.vercel.app"
|
||||||
|
repoURL: "https://github.com/markhorn-dev/astro-sphere"
|
||||||
|
---
|
||||||
|
|
||||||
|
![Astro Sphere Lighthouse Score](/astro-sphere.jpg)
|
||||||
|
|
||||||
|
Astro Sphere is a static, minimalist, lightweight, lightning fast portfolio and blog theme based on my personal website.
|
||||||
|
|
||||||
|
It is primarily Astro, Tailwind and Typescript, with a very small amount of SolidJS for stateful components.
|
||||||
|
|
||||||
|
## 🚀 Deploy your own
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a target="_blank" aria-label="Deploy with Vercel" href="https://vercel.com/new/clone?repository-url=https://github.com/markhorn-dev/astro-sphere">
|
||||||
|
<img src="/deploy_vercel.svg" />
|
||||||
|
</a>
|
||||||
|
<a target="_blank" aria-label="Deploy with Netlify" href="https://app.netlify.com/start/deploy?repository=https://github.com/markhorn-dev/astro-sphere">
|
||||||
|
<img src="/deploy_netlify.svg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 📋 Features
|
||||||
|
|
||||||
|
- ✅ 100/100 Lighthouse performance
|
||||||
|
- ✅ Responsive
|
||||||
|
- ✅ Accessible
|
||||||
|
- ✅ SEO-friendly
|
||||||
|
- ✅ Typesafe
|
||||||
|
- ✅ Minimal style
|
||||||
|
- ✅ Light/Dark Theme
|
||||||
|
- ✅ Animated UI
|
||||||
|
- ✅ Tailwind styling
|
||||||
|
- ✅ Auto generated sitemap
|
||||||
|
- ✅ Auto generated RSS Feed
|
||||||
|
- ✅ Markdown support
|
||||||
|
- ✅ MDX Support (components in your markdown)
|
||||||
|
- ✅ Searchable content (posts and projects)
|
||||||
|
|
||||||
|
## 💯 Lighthouse score
|
||||||
|
![Astro Sphere Lighthouse Score](/lighthouse.png)
|
||||||
|
|
||||||
|
## 🕊️ Lightweight
|
||||||
|
All pages under 100kb (including fonts)
|
||||||
|
|
||||||
|
## ⚡︎ Fast
|
||||||
|
Rendered in ~40ms on localhost
|
||||||
|
|
||||||
|
## 📄 Configuration
|
||||||
|
|
||||||
|
The blog posts on the demo serve as the documentation and configuration.
|
||||||
|
|
||||||
|
## 💻 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
Replace npm with your package manager of choice. `npm`, `pnpm`, `yarn`, `bun`, etc
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run sync` | Generates TypeScript types for all Astro modules.|
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
| `npm run lint` | Run ESLint |
|
||||||
|
| `npm run lint:fix` | Auto-fix ESLint issues |
|
||||||
|
|
||||||
|
## 🏛️ License
|
||||||
|
|
||||||
|
MIT
|
79
src/content/projects/project-2/index.md
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: "Astro Nano"
|
||||||
|
description: "Minimal portfolio and blog build with astro and no frameworks."
|
||||||
|
date: "Mar 26 2024"
|
||||||
|
demoURL: "https://astro-nano-demo.vercel.app"
|
||||||
|
repoURL: "https://github.com/markhorn-dev/astro-nano"
|
||||||
|
---
|
||||||
|
|
||||||
|
![Astro Nano](/astro-nano.png)
|
||||||
|
|
||||||
|
Astro Nano is a static, minimalist, lightweight, lightning fast portfolio and blog theme.
|
||||||
|
|
||||||
|
Built with Astro, Tailwind and Typescript, an no frameworks.
|
||||||
|
|
||||||
|
It was designed as an even more minimal theme than my popular theme [Astro Sphere](https://github.com/markhorn-dev/astro-sphere)
|
||||||
|
|
||||||
|
## 🚀 Deploy your own
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<a target="_blank" aria-label="Deploy with Vercel" href="https://vercel.com/new/clone?repository-url=https://github.com/markhorn-dev/astro-nano">
|
||||||
|
<img src="/deploy_vercel.svg" />
|
||||||
|
</a>
|
||||||
|
<a target="_blank" aria-label="Deploy with Netlify" href="https://app.netlify.com/start/deploy?repository=https://github.com/markhorn-dev/astro-nano">
|
||||||
|
<img src="/deploy_netlify.svg" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## 📋 Features
|
||||||
|
|
||||||
|
- ✅ 100/100 Lighthouse performance
|
||||||
|
- ✅ Responsive
|
||||||
|
- ✅ Accessible
|
||||||
|
- ✅ SEO-friendly
|
||||||
|
- ✅ Typesafe
|
||||||
|
- ✅ Minimal style
|
||||||
|
- ✅ Light/Dark Theme
|
||||||
|
- ✅ Animated UI
|
||||||
|
- ✅ Tailwind styling
|
||||||
|
- ✅ Auto generated sitemap
|
||||||
|
- ✅ Auto generated RSS Feed
|
||||||
|
- ✅ Markdown support
|
||||||
|
- ✅ MDX Support (components in your markdown)
|
||||||
|
|
||||||
|
## 💯 Lighthouse score
|
||||||
|
![Astro Nano Lighthouse Score](/lighthouse.png)
|
||||||
|
|
||||||
|
## 🕊️ Lightweight
|
||||||
|
No frameworks or added bulk
|
||||||
|
|
||||||
|
## ⚡︎ Fast
|
||||||
|
Rendered in ~40ms on localhost
|
||||||
|
|
||||||
|
## 📄 Configuration
|
||||||
|
|
||||||
|
The blog posts on the demo serve as the documentation and configuration.
|
||||||
|
|
||||||
|
## 💻 Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
Replace npm with your package manager of choice. `npm`, `pnpm`, `yarn`, `bun`, etc
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :------------------------ | :----------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:4321` |
|
||||||
|
| `npm run dev:network` | Starts local dev server on local network |
|
||||||
|
| `npm run sync` | Generates TypeScript types for all Astro modules.|
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run preview:network` | Preview build on local network |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||||
|
| `npm run astro -- --help` | Get help using the Astro CLI |
|
||||||
|
| `npm run lint` | Run ESLint |
|
||||||
|
| `npm run lint:fix` | Auto-fix ESLint issues |
|
||||||
|
|
||||||
|
## 🏛️ License
|
||||||
|
|
||||||
|
MIT
|
8
src/content/work/UniversityOfCanterbury.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
company: "University of Canterbury"
|
||||||
|
role: "Software Engineer"
|
||||||
|
dateStart: "08/01/2023"
|
||||||
|
dateEnd: "present"
|
||||||
|
---
|
||||||
|
|
||||||
|
Voluptatem est quaerat voluptas praesentium ipsa dolorem dignissimos nulla ratione distinctio quae maiores eligendi nostrum? Quibusdam, debitis voluptatum, lorem ipsum dolor. Sit amet consectetur adipisicing elit. Iure illo neque tempora.
|
8
src/content/work/actuality.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
company: "Actuality"
|
||||||
|
role: "Software Engineer"
|
||||||
|
dateStart: "02/11/2022"
|
||||||
|
dateEnd: "05/05/2023"
|
||||||
|
---
|
||||||
|
|
||||||
|
Created a suite of innovative Augmented Reality product visualisation tools.
|
8
src/content/work/standard.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
company: "Standard"
|
||||||
|
role: "Software Engineer | Director"
|
||||||
|
dateStart: "03/16/2018"
|
||||||
|
dateEnd: "07/01/2019"
|
||||||
|
---
|
||||||
|
|
||||||
|
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Iure illo neque tempora, voluptatem est quaerat voluptas praesentium ipsa dolorem dignissimos nulla ratione distinctio quae maiores eligendi nostrum? Quibusdam, debitis voluptatum.
|
2
src/env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
27
src/layouts/PageLayout.astro
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
import Head from "@components/Head.astro";
|
||||||
|
import Header from "@components/Header.astro";
|
||||||
|
import Footer from "@components/Footer.astro";
|
||||||
|
import { SITE } from "@consts";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { title, description } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<Head title={`${title} | ${SITE.NAME}`} description={description} />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</body>
|
||||||
|
</html>
|
40
src/lib/utils.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDate(date: Date) {
|
||||||
|
return Intl.DateTimeFormat("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric"
|
||||||
|
}).format(date);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readingTime(html: string) {
|
||||||
|
const textOnly = html.replace(/<[^>]+>/g, "");
|
||||||
|
const wordCount = textOnly.split(/\s+/).length;
|
||||||
|
const readingTimeMinutes = ((wordCount / 200) + 1).toFixed();
|
||||||
|
return `${readingTimeMinutes} min read`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateRange(startDate: Date, endDate?: Date | string): string {
|
||||||
|
const startMonth = startDate.toLocaleString("default", { month: "short" });
|
||||||
|
const startYear = startDate.getFullYear().toString();
|
||||||
|
let endMonth;
|
||||||
|
let endYear;
|
||||||
|
|
||||||
|
if (endDate) {
|
||||||
|
if (typeof endDate === "string") {
|
||||||
|
endMonth = "";
|
||||||
|
endYear = endDate;
|
||||||
|
} else {
|
||||||
|
endMonth = endDate.toLocaleString("default", { month: "short" });
|
||||||
|
endYear = endDate.getFullYear().toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${startMonth} ${startYear} - ${endMonth} ${endYear}`;
|
||||||
|
}
|
49
src/pages/blog/[...slug].astro
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
import PageLayout from "@layouts/PageLayout.astro";
|
||||||
|
import Container from "@components/Container.astro";
|
||||||
|
import FormattedDate from "@components/FormattedDate.astro";
|
||||||
|
import { readingTime } from "@lib/utils";
|
||||||
|
import BackToPrev from "@components/BackToPrev.astro";
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const posts = (await getCollection("blog"))
|
||||||
|
.filter(post => !post.data.draft)
|
||||||
|
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||||
|
return posts.map((post) => ({
|
||||||
|
params: { slug: post.slug },
|
||||||
|
props: post,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
type Props = CollectionEntry<"blog">;
|
||||||
|
|
||||||
|
const post = Astro.props;
|
||||||
|
const { Content } = await post.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title={post.data.title} description={post.data.description}>
|
||||||
|
<Container>
|
||||||
|
<div class="animate">
|
||||||
|
<BackToPrev href="/blog">
|
||||||
|
Back to blog
|
||||||
|
</BackToPrev>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 my-10">
|
||||||
|
<div class="animate flex items-center gap-1.5">
|
||||||
|
<div class="font-base text-sm">
|
||||||
|
<FormattedDate date={post.data.date} />
|
||||||
|
</div>
|
||||||
|
•
|
||||||
|
<div class="font-base text-sm">
|
||||||
|
{readingTime(post.body)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="animate text-2xl font-semibold text-black dark:text-white">
|
||||||
|
{post.data.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<article class="animate">
|
||||||
|
<Content />
|
||||||
|
</article>
|
||||||
|
</Container>
|
||||||
|
</PageLayout>
|
56
src/pages/blog/index.astro
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
import PageLayout from "@layouts/PageLayout.astro";
|
||||||
|
import Container from "@components/Container.astro";
|
||||||
|
import ArrowCard from "@components/ArrowCard.astro";
|
||||||
|
import { BLOG } from "@consts";
|
||||||
|
|
||||||
|
const data = (await getCollection("blog"))
|
||||||
|
.filter(post => !post.data.draft)
|
||||||
|
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||||
|
|
||||||
|
type Acc = {
|
||||||
|
[year: string]: CollectionEntry<"blog">[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = data.reduce((acc: Acc, post) => {
|
||||||
|
const year = post.data.date.getFullYear().toString();
|
||||||
|
if (!acc[year]) {
|
||||||
|
acc[year] = [];
|
||||||
|
}
|
||||||
|
acc[year].push(post);
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const years = Object.keys(posts).sort((a, b) => parseInt(b) - parseInt(a));
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title={BLOG.TITLE} description={BLOG.DESCRIPTION}>
|
||||||
|
<Container>
|
||||||
|
<div class="space-y-10">
|
||||||
|
<div class="animate font-semibold text-black dark:text-white">
|
||||||
|
Blog
|
||||||
|
</div>
|
||||||
|
<div class="space-y-4">
|
||||||
|
{years.map(year => (
|
||||||
|
<section class="animate space-y-4">
|
||||||
|
<div class="font-semibold text-black dark:text-white">
|
||||||
|
{year}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul class="flex flex-col gap-4">
|
||||||
|
{
|
||||||
|
posts[year].map((post) => (
|
||||||
|
<li>
|
||||||
|
<ArrowCard entry={post}/>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</PageLayout>
|
144
src/pages/index.astro
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import Container from "@components/Container.astro";
|
||||||
|
import PageLayout from "@layouts/PageLayout.astro";
|
||||||
|
import ArrowCard from "@components/ArrowCard.astro";
|
||||||
|
import Link from "@components/Link.astro";
|
||||||
|
import { dateRange } from "@lib/utils";
|
||||||
|
import { SITE, HOME, SOCIALS } from "@consts";
|
||||||
|
|
||||||
|
const blog = (await getCollection("blog"))
|
||||||
|
.filter(post => !post.data.draft)
|
||||||
|
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||||
|
.slice(0,SITE.NUM_POSTS_ON_HOMEPAGE);
|
||||||
|
|
||||||
|
const projects = (await getCollection("projects"))
|
||||||
|
.filter(project => !project.data.draft)
|
||||||
|
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||||
|
.slice(0,SITE.NUM_PROJECTS_ON_HOMEPAGE);
|
||||||
|
|
||||||
|
const allwork = (await getCollection("work"))
|
||||||
|
.sort((a, b) => new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf())
|
||||||
|
.slice(0,SITE.NUM_WORKS_ON_HOMEPAGE);
|
||||||
|
|
||||||
|
const work = await Promise.all(
|
||||||
|
allwork.map(async (item) => {
|
||||||
|
const { Content } = await item.render();
|
||||||
|
return { ...item, Content };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title={HOME.TITLE} description={HOME.DESCRIPTION}>
|
||||||
|
<Container>
|
||||||
|
<h4 class="animate font-semibold text-black dark:text-white text-4xl font-serif">
|
||||||
|
Hi, I'm Michael <span class="text-4xl">👋🏻</span>
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-16">
|
||||||
|
<section>
|
||||||
|
<article class="space-y-4">
|
||||||
|
<p class="animate">
|
||||||
|
I'm a lead software engineer at the University of Canterbury, working on <Link href="https://uconline.ac.nz" external>Tuihono UC | UC Online</Link>.
|
||||||
|
|
||||||
|
I specialize in full-stack development. While my primary expertise is in software engineering, I also have a strong interest in cloud infrastructure and DevOps. Based in Christchurch, New Zealand
|
||||||
|
<br/><br/>
|
||||||
|
Feel free to connect with me through any of the social media links at the bottom of this page!
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="animate space-y-6">
|
||||||
|
<div class="flex flex-wrap gap-y-2 items-center justify-between">
|
||||||
|
<h5 class="font-semibold text-black dark:text-white">
|
||||||
|
Latest posts
|
||||||
|
</h5>
|
||||||
|
<Link href="/blog">
|
||||||
|
See all posts
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ul class="flex flex-col gap-4">
|
||||||
|
{blog.map(post => (
|
||||||
|
<li>
|
||||||
|
<ArrowCard entry={post} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="animate space-y-6">
|
||||||
|
<div class="flex flex-wrap gap-y-2 items-center justify-between">
|
||||||
|
<h5 class="font-semibold text-black dark:text-white">
|
||||||
|
Work Experience
|
||||||
|
</h5>
|
||||||
|
<Link href="/work">
|
||||||
|
See all work
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ul class="flex flex-col space-y-4">
|
||||||
|
{work.map(entry => (
|
||||||
|
<li>
|
||||||
|
<div class="text-sm opacity-75">
|
||||||
|
{dateRange(entry.data.dateStart, entry.data.dateEnd)}
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold text-black dark:text-white">
|
||||||
|
{entry.data.company}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm opacity-75">
|
||||||
|
{entry.data.role}
|
||||||
|
</div>
|
||||||
|
<article>
|
||||||
|
<entry.Content />
|
||||||
|
</article>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="animate space-y-6">
|
||||||
|
<div class="flex flex-wrap gap-y-2 items-center justify-between">
|
||||||
|
<h5 class="font-semibold text-black dark:text-white">
|
||||||
|
Recent projects
|
||||||
|
</h5>
|
||||||
|
<Link href="/projects">
|
||||||
|
See all projects
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ul class="flex flex-col gap-4">
|
||||||
|
{projects.map(project => (
|
||||||
|
<li>
|
||||||
|
<ArrowCard entry={project} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="animate space-y-4">
|
||||||
|
<h5 class="font-semibold text-black dark:text-white">
|
||||||
|
Let's Connect
|
||||||
|
</h5>
|
||||||
|
<article>
|
||||||
|
<p>
|
||||||
|
If you want to get in touch with me about something or just to say hi,
|
||||||
|
reach out on social media or send me an email.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<ul class="flex flex-wrap gap-2">
|
||||||
|
{SOCIALS.map(SOCIAL => (
|
||||||
|
<li class="flex gap-x-2 text-nowrap">
|
||||||
|
<Link href={SOCIAL.HREF} external aria-label={`${SITE.NAME} on ${SOCIAL.NAME}`}>
|
||||||
|
{SOCIAL.NAME}
|
||||||
|
</Link>
|
||||||
|
{"/"}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
<li class="line-clamp-1">
|
||||||
|
<Link href={`mailto:${SITE.EMAIL}`} aria-label={`Email ${SITE.NAME}`}>
|
||||||
|
{SITE.EMAIL}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</PageLayout>
|
67
src/pages/projects/[...slug].astro
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
import { type CollectionEntry, getCollection } from "astro:content";
|
||||||
|
import PageLayout from "@layouts/PageLayout.astro";
|
||||||
|
import Container from "@components/Container.astro";
|
||||||
|
import FormattedDate from "@components/FormattedDate.astro";
|
||||||
|
import { readingTime } from "@lib/utils";
|
||||||
|
import BackToPrev from "@components/BackToPrev.astro";
|
||||||
|
import Link from "@components/Link.astro";
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const projects = (await getCollection("projects"))
|
||||||
|
.filter(post => !post.data.draft)
|
||||||
|
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||||
|
return projects.map((project) => ({
|
||||||
|
params: { slug: project.slug },
|
||||||
|
props: project,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
type Props = CollectionEntry<"projects">;
|
||||||
|
|
||||||
|
const project = Astro.props;
|
||||||
|
const { Content } = await project.render();
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title={project.data.title} description={project.data.description}>
|
||||||
|
<Container>
|
||||||
|
<div class="animate">
|
||||||
|
<BackToPrev href="/projects">
|
||||||
|
Back to projects
|
||||||
|
</BackToPrev>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 my-10">
|
||||||
|
<div class="animate flex items-center gap-1.5">
|
||||||
|
<div class="font-base text-sm">
|
||||||
|
<FormattedDate date={project.data.date} />
|
||||||
|
</div>
|
||||||
|
•
|
||||||
|
<div class="font-base text-sm">
|
||||||
|
{readingTime(project.body)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="animate text-2xl font-semibold text-black dark:text-white">
|
||||||
|
{project.data.title}
|
||||||
|
</div>
|
||||||
|
{(project.data.demoURL || project.data.repoURL) && (
|
||||||
|
<nav class="animate flex gap-1">
|
||||||
|
{project.data.demoURL && (
|
||||||
|
<Link href={project.data.demoURL} external>
|
||||||
|
demo
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
{project.data.demoURL && project.data.repoURL && (
|
||||||
|
<span>/</span>
|
||||||
|
)}
|
||||||
|
{project.data.repoURL && (
|
||||||
|
<Link href={project.data.repoURL} external>
|
||||||
|
repo
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<article class="animate">
|
||||||
|
<Content />
|
||||||
|
</article>
|
||||||
|
</Container>
|
||||||
|
</PageLayout>
|
30
src/pages/projects/index.astro
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import PageLayout from "@layouts/PageLayout.astro";
|
||||||
|
import Container from "@components/Container.astro";
|
||||||
|
import ArrowCard from "@components/ArrowCard.astro";
|
||||||
|
import { PROJECTS } from "@consts";
|
||||||
|
|
||||||
|
const projects = (await getCollection("projects"))
|
||||||
|
.filter(project => !project.data.draft)
|
||||||
|
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title={PROJECTS.TITLE} description={PROJECTS.DESCRIPTION}>
|
||||||
|
<Container>
|
||||||
|
<div class="space-y-10">
|
||||||
|
<div class="animate font-semibold text-black dark:text-white">
|
||||||
|
Projects
|
||||||
|
</div>
|
||||||
|
<ul class="animate flex flex-col gap-4">
|
||||||
|
{
|
||||||
|
projects.map((project) => (
|
||||||
|
<li>
|
||||||
|
<ArrowCard entry={project}/>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</PageLayout>
|
16
src/pages/robots.txt.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import type { APIRoute } from "astro";
|
||||||
|
|
||||||
|
const robotsTxt = `
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: ${new URL("sitemap-index.xml", import.meta.env.SITE).href}
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
export const GET: APIRoute = () => {
|
||||||
|
return new Response(robotsTxt, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "text/plain; charset=utf-8",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
30
src/pages/rss.xml.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import rss from "@astrojs/rss";
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import { HOME } from "@consts";
|
||||||
|
|
||||||
|
type Context = {
|
||||||
|
site: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(context: Context) {
|
||||||
|
const blog = (await getCollection("blog"))
|
||||||
|
.filter(post => !post.data.draft);
|
||||||
|
|
||||||
|
const projects = (await getCollection("projects"))
|
||||||
|
.filter(project => !project.data.draft);
|
||||||
|
|
||||||
|
const items = [...blog, ...projects]
|
||||||
|
.sort((a, b) => new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf());
|
||||||
|
|
||||||
|
return rss({
|
||||||
|
title: HOME.TITLE,
|
||||||
|
description: HOME.DESCRIPTION,
|
||||||
|
site: context.site,
|
||||||
|
items: items.map((item) => ({
|
||||||
|
title: item.data.title,
|
||||||
|
description: item.data.description,
|
||||||
|
pubDate: item.data.date,
|
||||||
|
link: `/${item.collection}/${item.slug}/`,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
51
src/pages/work/index.astro
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from "astro:content";
|
||||||
|
import PageLayout from "@layouts/PageLayout.astro";
|
||||||
|
import Container from "@components/Container.astro";
|
||||||
|
import { dateRange } from "@lib/utils";
|
||||||
|
import { WORK } from "@consts";
|
||||||
|
|
||||||
|
const collection = (await getCollection("work"))
|
||||||
|
.sort((a, b) => new Date(b.data.dateStart).valueOf() - new Date(a.data.dateStart).valueOf());
|
||||||
|
|
||||||
|
const work = await Promise.all(
|
||||||
|
collection.map(async (item) => {
|
||||||
|
const { Content } = await item.render();
|
||||||
|
return { ...item, Content };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout title={WORK.TITLE} description={WORK.DESCRIPTION}>
|
||||||
|
<Container>
|
||||||
|
<div class="space-y-10">
|
||||||
|
<div class="animate font-semibold text-black dark:text-white">
|
||||||
|
Work
|
||||||
|
</div>
|
||||||
|
<ul class="flex flex-col space-y-4">
|
||||||
|
{
|
||||||
|
work.map(entry => (
|
||||||
|
<li class="animate">
|
||||||
|
<div class="text-sm opacity-75">
|
||||||
|
{dateRange(entry.data.dateStart, entry.data.dateEnd)}
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold text-black dark:text-white">
|
||||||
|
{entry.data.company}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm opacity-75">
|
||||||
|
{entry.data.role}
|
||||||
|
</div>
|
||||||
|
<article>
|
||||||
|
<entry.Content />
|
||||||
|
</article>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<!--
|
||||||
|
<ul class="animate flex flex-col gap-4">
|
||||||
|
|
||||||
|
</ul> -->
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</PageLayout>
|
73
src/styles/global.css
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
html {
|
||||||
|
overflow-y: scroll;
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
@apply size-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
@apply font-sans antialiased;
|
||||||
|
@apply flex flex-col;
|
||||||
|
@apply bg-stone-100 dark:bg-black;
|
||||||
|
@apply text-black/50 dark:text-white/75;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
@apply fixed top-0 left-0 right-0 z-50 py-5;
|
||||||
|
@apply bg-stone-100/75 dark:bg-black/25;
|
||||||
|
@apply backdrop-blur-sm saturate-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
@apply flex-1 py-32;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
@apply py-5 text-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
@apply max-w-full prose dark:prose-invert prose-img:mx-auto prose-img:my-auto;
|
||||||
|
@apply prose-headings:font-semibold prose-p:font-serif;
|
||||||
|
@apply prose-headings:text-black prose-headings:dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
article a {
|
||||||
|
@apply font-sans text-current underline underline-offset-2;
|
||||||
|
@apply decoration-black/15 dark:decoration-white/30;
|
||||||
|
@apply transition-colors duration-300 ease-in-out;
|
||||||
|
}
|
||||||
|
article a:hover {
|
||||||
|
@apply text-black dark:text-white;
|
||||||
|
@apply decoration-black/25 dark:decoration-white/50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate {
|
||||||
|
@apply opacity-0 translate-y-3;
|
||||||
|
@apply transition-all duration-700 ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate.show {
|
||||||
|
@apply opacity-100 translate-y-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html #back-to-top {
|
||||||
|
@apply opacity-0 pointer-events-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.scrolled #back-to-top {
|
||||||
|
@apply opacity-100 pointer-events-auto;
|
||||||
|
}
|
17
src/types.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export type Site = {
|
||||||
|
NAME: string;
|
||||||
|
EMAIL: string;
|
||||||
|
NUM_POSTS_ON_HOMEPAGE: number;
|
||||||
|
NUM_WORKS_ON_HOMEPAGE: number;
|
||||||
|
NUM_PROJECTS_ON_HOMEPAGE: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Metadata = {
|
||||||
|
TITLE: string;
|
||||||
|
DESCRIPTION: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Socials = {
|
||||||
|
NAME: string;
|
||||||
|
HREF: string;
|
||||||
|
}[];
|
18
tailwind.config.mjs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import defaultTheme from "tailwindcss/defaultTheme";
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["Inter", ...defaultTheme.fontFamily.sans],
|
||||||
|
serif: ["Lora", ...defaultTheme.fontFamily.serif],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require("@tailwindcss/typography")],
|
||||||
|
};
|
10
tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|