init
This commit is contained in:
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024-2025 Mark Horn
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
64
README.md
Normal file
64
README.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https://github.com/markhorn-dev/astro-nano) [](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
|
||||||
|

|
||||||
|
|
||||||
|
## 🕊️ 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
|
||||||
9
astro.config.mjs
Normal file
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://damhw.ee",
|
||||||
|
integrations: [mdx(), sitemap(), tailwind()],
|
||||||
|
});
|
||||||
36
package.json
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.9.4",
|
||||||
|
"@astrojs/mdx": "^4.0.2",
|
||||||
|
"@astrojs/rss": "^4.0.10",
|
||||||
|
"@astrojs/sitemap": "^3.2.1",
|
||||||
|
"@astrojs/tailwind": "^5.1.3",
|
||||||
|
"@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": "^5.0.5",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6353
pnpm-lock.yaml
generated
Normal file
6353
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
BIN
public/astro-nano.png
Normal file
BIN
public/astro-nano.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 241 KiB |
BIN
public/astro-sphere.jpg
Normal file
BIN
public/astro-sphere.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 68 KiB |
11
public/favicon-dark.svg
Normal file
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
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-Light.woff2
Normal file
Binary file not shown.
BIN
public/fonts/MonaSans-Regular.woff2
Normal file
BIN
public/fonts/MonaSans-Regular.woff2
Normal file
Binary file not shown.
BIN
public/fonts/MonaSans-SemiBold.woff2
Normal file
BIN
public/fonts/MonaSans-SemiBold.woff2
Normal file
Binary file not shown.
BIN
public/fonts/atkinson-bold.woff
Normal file
BIN
public/fonts/atkinson-bold.woff
Normal file
Binary file not shown.
BIN
public/fonts/atkinson-regular.woff
Normal file
BIN
public/fonts/atkinson-regular.woff
Normal file
Binary file not shown.
BIN
public/patrick.webp
Normal file
BIN
public/patrick.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
27
src/components/ArrowCard.astro
Normal file
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
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
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
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
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
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>
|
||||||
186
src/components/Head.astro
Normal file
186
src/components/Head.astro
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
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 { ClientRouter } from "astro:transitions";
|
||||||
|
import { SITE } from "@consts";
|
||||||
|
|
||||||
|
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)} />
|
||||||
|
|
||||||
|
<ClientRouter />
|
||||||
|
|
||||||
|
<!-- RSS Link -->
|
||||||
|
<link rel="alternate" type="application/rss+xml" title={SITE.NAME} href={new URL("rss.xml", Astro.site)} />
|
||||||
|
|
||||||
|
<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" || !localStorage.theme) {
|
||||||
|
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
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
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>
|
||||||
40
src/consts.ts
Normal file
40
src/consts.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Site, Metadata, Socials } from "@types";
|
||||||
|
|
||||||
|
export const SITE: Site = {
|
||||||
|
NAME: "Damhwee Ahn",
|
||||||
|
EMAIL: "me@damhw.ee",
|
||||||
|
NUM_POSTS_ON_HOMEPAGE: 5,
|
||||||
|
NUM_WORKS_ON_HOMEPAGE: 0,
|
||||||
|
NUM_PROJECTS_ON_HOMEPAGE: 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HOME: Metadata = {
|
||||||
|
TITLE: "Home",
|
||||||
|
DESCRIPTION: "도메인을 방치하기 싫어서 대충 astro-nano 기반으로 만든 웹사이트",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BLOG: Metadata = {
|
||||||
|
TITLE: "Blog",
|
||||||
|
DESCRIPTION: "똥글 모음집",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const WORK: Metadata = {
|
||||||
|
TITLE: "Career",
|
||||||
|
DESCRIPTION: "커리어",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROJECTS: Metadata = {
|
||||||
|
TITLE: "Projects",
|
||||||
|
DESCRIPTION: "프로젝트 레포, 데모 목록",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SOCIALS: Socials = [
|
||||||
|
{
|
||||||
|
NAME: "instagram",
|
||||||
|
HREF: "https://instagram.com/damhwee",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NAME: "github",
|
||||||
|
HREF: "https://github.com/yeorinhieut"
|
||||||
|
}
|
||||||
|
];
|
||||||
7
src/content/blog/01-whpre-writeup/index.md
Normal file
7
src/content/blog/01-whpre-writeup/index.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: "화이트햇 콘테스트 2025 예선 writeup (forensics/ai)"
|
||||||
|
description: "나머지는 시간이 된다면 작성할지도"
|
||||||
|
date: "2025년 10월 23일"
|
||||||
|
---
|
||||||
|
|
||||||
|
시간나면작성
|
||||||
35
src/content/config.ts
Normal file
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 };
|
||||||
9
src/content/projects/percept/index.md
Normal file
9
src/content/projects/percept/index.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: "Percept"
|
||||||
|
description: "Percept 팀의 리드 개발자"
|
||||||
|
date: "2025년~"
|
||||||
|
demoURL: "https://percept.cloud"
|
||||||
|
repoURL: "https://github.com/percept-cloud"
|
||||||
|
---
|
||||||
|
|
||||||
|
지금은 방치된 Percept팀의 리드 개발자입니다. 수험기간이 끝난 뒤 부활할 예정입니다.
|
||||||
6
src/content/work/apple.md
Normal file
6
src/content/work/apple.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
company: "서울 양정고등학교"
|
||||||
|
role: "3학년 재학중"
|
||||||
|
dateStart: "2023"
|
||||||
|
dateEnd: "재학중"
|
||||||
|
---
|
||||||
2
src/env.d.ts
vendored
Normal file
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
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
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
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
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>
|
||||||
140
src/pages/index.astro
Normal file
140
src/pages/index.astro
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
---
|
||||||
|
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">
|
||||||
|
Hi, I'm Damhwee Ahn <span class="text-xl">👋🏻</span>
|
||||||
|
</h4>
|
||||||
|
<div class="space-y-16">
|
||||||
|
<section>
|
||||||
|
<article class="space-y-4">
|
||||||
|
<p class="animate">
|
||||||
|
안녕하세요 저는 서울특별시 소재 양정고등학교에 재학중인 안담휘 입니다. 프로그래밍과 정보보안에 관심이 있는 초보입니다.
|
||||||
|
</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">
|
||||||
|
Career
|
||||||
|
</h5>
|
||||||
|
<Link href="/work">
|
||||||
|
See my career
|
||||||
|
</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
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
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
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
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
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
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-stone-900;
|
||||||
|
@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-stone-900/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
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
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
10
tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user