chore: update
26
package-lock.json
generated
|
@ -17,6 +17,7 @@
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@fontsource/geist-mono": "^5.0.3",
|
"@fontsource/geist-mono": "^5.0.3",
|
||||||
"@fontsource/geist-sans": "^5.0.3",
|
"@fontsource/geist-sans": "^5.0.3",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
@ -1823,6 +1824,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-avatar": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-context": "1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "2.0.0",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-collection": {
|
"node_modules/@radix-ui/react-collection": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"@astrojs/tailwind": "^5.1.0",
|
"@astrojs/tailwind": "^5.1.0",
|
||||||
"@fontsource/geist-mono": "^5.0.3",
|
"@fontsource/geist-mono": "^5.0.3",
|
||||||
"@fontsource/geist-sans": "^5.0.3",
|
"@fontsource/geist-sans": "^5.0.3",
|
||||||
|
"@radix-ui/react-avatar": "^1.1.0",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
"@radix-ui/react-dropdown-menu": "^2.1.1",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
|
|
BIN
public/1200x630.png
Normal file
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 241 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 31 KiB |
|
@ -1,17 +0,0 @@
|
||||||
<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>
|
|
Before Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 354 KiB |
Before Width: | Height: | Size: 218 KiB |
|
@ -1,42 +0,0 @@
|
||||||
---
|
|
||||||
import type { CollectionEntry } from 'astro:content'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
entry: CollectionEntry<'blog'>
|
|
||||||
}
|
|
||||||
|
|
||||||
const { entry } = Astro.props as {
|
|
||||||
entry: CollectionEntry<'blog'>
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<a
|
|
||||||
href={`/${entry.collection}/${entry.slug}`}
|
|
||||||
class="not-prose group relative flex flex-nowrap rounded-lg border px-4 py-3 pr-10 transition-colors duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<div class="flex flex-1 flex-col 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 right-2 top-1/2 size-5 -translate-y-1/2 fill-none stroke-current stroke-2"
|
|
||||||
>
|
|
||||||
<line
|
|
||||||
x1="5"
|
|
||||||
y1="12"
|
|
||||||
x2="19"
|
|
||||||
y2="12"
|
|
||||||
class="translate-x-3 scale-x-0 transition-transform duration-300 ease-in-out group-focus-visible:translate-x-0 group-focus-visible:scale-x-100"
|
|
||||||
></line>
|
|
||||||
<polyline
|
|
||||||
points="12 5 19 12 12 19"
|
|
||||||
class="-translate-x-1 transition-transform duration-300 ease-in-out group-focus-visible:translate-x-0"
|
|
||||||
></polyline>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
29
src/components/AuthorCard.astro
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
---
|
||||||
|
import type { CollectionEntry } from 'astro:content'
|
||||||
|
import { Image } from 'astro:assets'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
author: CollectionEntry<'authors'>
|
||||||
|
}
|
||||||
|
|
||||||
|
const { author } = Astro.props
|
||||||
|
const { name, avatar, bio } = author.data
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="rounded-lg border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
|
||||||
|
>
|
||||||
|
<a href={`/authors/${author.slug}`} class="flex flex-wrap gap-4">
|
||||||
|
<Image
|
||||||
|
src={avatar}
|
||||||
|
alt={`Avatar of ${name}`}
|
||||||
|
width={128}
|
||||||
|
height={128}
|
||||||
|
class="rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="mb-1 text-lg font-semibold">{name}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">{bio}</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
|
@ -1,33 +0,0 @@
|
||||||
---
|
|
||||||
type Props = {
|
|
||||||
href: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const { href } = Astro.props
|
|
||||||
---
|
|
||||||
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
class="not-prose group relative flex w-fit flex-nowrap rounded border py-1.5 pl-7 pr-3 transition-colors duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
class="absolute left-2 top-1/2 size-4 -translate-y-1/2 fill-none stroke-current stroke-2"
|
|
||||||
>
|
|
||||||
<line
|
|
||||||
x1="5"
|
|
||||||
y1="12"
|
|
||||||
x2="19"
|
|
||||||
y2="12"
|
|
||||||
class="translate-x-2 scale-x-0 transition-transform duration-300 ease-in-out"
|
|
||||||
></line>
|
|
||||||
<polyline
|
|
||||||
points="12 5 5 12 12 19"
|
|
||||||
class="translate-x-1 transition-transform duration-300 ease-in-out"
|
|
||||||
></polyline>
|
|
||||||
</svg>
|
|
||||||
<div class="text-sm">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
61
src/components/BlogCard.astro
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
import type { CollectionEntry } from 'astro:content'
|
||||||
|
import { formatDate, readingTime } from '@lib/utils'
|
||||||
|
import { Image } from 'astro:assets'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
entry: CollectionEntry<'blog'>
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entry } = Astro.props as {
|
||||||
|
entry: CollectionEntry<'blog'>
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDate = formatDate(entry.data.date)
|
||||||
|
const readTime = readingTime(entry.body)
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="not-prose rounded-lg border p-4 transition-colors duration-300 ease-in-out hover:bg-secondary/50"
|
||||||
|
>
|
||||||
|
<a href={`/${entry.collection}/${entry.slug}`} class="flex flex-wrap gap-4">
|
||||||
|
{
|
||||||
|
entry.data.image && (
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<Image
|
||||||
|
src={entry.data.image}
|
||||||
|
alt={entry.data.title}
|
||||||
|
width={200}
|
||||||
|
height={200}
|
||||||
|
class="rounded-lg object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="flex-grow">
|
||||||
|
<h3 class="mb-1 text-lg font-semibold">
|
||||||
|
{entry.data.title}
|
||||||
|
</h3>
|
||||||
|
<p class="mb-2 text-sm text-muted-foreground">
|
||||||
|
{entry.data.description}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
class="mb-2 flex items-center space-x-2 text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
<span>{formattedDate}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{readTime}</span>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
entry.data.tags && (
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{entry.data.tags.map((tag) => (
|
||||||
|
<Badge variant="secondary">{tag}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
|
@ -6,10 +6,16 @@ import { ModeToggle } from '@components/ui/mode-toggle'
|
||||||
|
|
||||||
<footer class="py-4">
|
<footer class="py-4">
|
||||||
<Container>
|
<Container>
|
||||||
<div class="flex justify-between items-center">
|
<div
|
||||||
<div class="flex items-center space-x-4">
|
class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:justify-between"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="mr-2 hidden sm:block">
|
||||||
<ModeToggle client:load />
|
<ModeToggle client:load />
|
||||||
<p class="text-sm text-muted-foreground">© {new Date().getFullYear()} All rights reserved.</p>
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
© {new Date().getFullYear()} All rights reserved.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<SocialIcons />
|
<SocialIcons />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,27 +7,34 @@ const items = [
|
||||||
{ href: '/blog', label: 'blog' },
|
{ href: '/blog', label: 'blog' },
|
||||||
{ href: '/authors', label: 'authors' },
|
{ href: '/authors', label: 'authors' },
|
||||||
{ href: '/about', label: 'about' },
|
{ href: '/about', label: 'about' },
|
||||||
{ href: '/tags', label: 'tags' },
|
|
||||||
]
|
]
|
||||||
---
|
---
|
||||||
|
|
||||||
<header class="sticky top-0 z-10 bg-background/50 backdrop-blur-md" transition:persist>
|
<header
|
||||||
|
class="sticky top-0 z-10 bg-background/50 backdrop-blur-md"
|
||||||
|
transition:persist
|
||||||
|
>
|
||||||
<Container>
|
<Container>
|
||||||
<div class="flex items-center justify-between py-4">
|
<div class="flex items-center justify-between py-4">
|
||||||
<Link href="/" class="text-xl font-semibold hover:text-primary transition-colors duration-300">
|
<Link
|
||||||
|
href="/"
|
||||||
|
class="text-xl font-semibold transition-colors duration-300 hover:text-primary"
|
||||||
|
>
|
||||||
{SITE.TITLE}
|
{SITE.TITLE}
|
||||||
</Link>
|
</Link>
|
||||||
<nav class="flex items-center gap-4 md:gap-6 text-sm">
|
<nav class="flex items-center gap-4 text-sm sm:gap-6">
|
||||||
{items.map((item, index) => (
|
{
|
||||||
|
items.map((item, index) => (
|
||||||
<Fragment key={item.href}>
|
<Fragment key={item.href}>
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
class="transition-colors hover:text-foreground/80 text-foreground/60 capitalize"
|
class="capitalize text-foreground/60 transition-colors hover:text-foreground/80"
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))
|
||||||
|
}
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -8,7 +8,13 @@ type Props = {
|
||||||
'data-heading'?: string
|
'data-heading'?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { href, external, class: className, 'data-heading': dataHeading, ...rest } = Astro.props
|
const {
|
||||||
|
href,
|
||||||
|
external,
|
||||||
|
class: className,
|
||||||
|
'data-heading': dataHeading,
|
||||||
|
...rest
|
||||||
|
} = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
@ -16,7 +22,7 @@ const { href, external, class: className, 'data-heading': dataHeading, ...rest }
|
||||||
target={external ? '_blank' : '_self'}
|
target={external ? '_blank' : '_self'}
|
||||||
class={cn(
|
class={cn(
|
||||||
'inline-block transition-colors duration-300 ease-in-out hover:underline underline-offset-[3px]',
|
'inline-block transition-colors duration-300 ease-in-out hover:underline underline-offset-[3px]',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
data-heading={dataHeading}
|
data-heading={dataHeading}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
---
|
|
||||||
import type { CollectionEntry } from 'astro:content'
|
|
||||||
import { Image } from 'astro:assets'
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
member: CollectionEntry<'authors'>
|
|
||||||
}
|
|
||||||
|
|
||||||
const { member } = Astro.props
|
|
||||||
const { name, avatar, bio } = member.data
|
|
||||||
---
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="not-prose flex size-full flex-col gap-4 overflow-hidden rounded-xl border p-6 hover:bg-secondary sm:flex-row sm:items-center"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={avatar}
|
|
||||||
alt={`Avatar of ${name}`}
|
|
||||||
width={256}
|
|
||||||
height={256}
|
|
||||||
class="aspect-square size-32 rounded-md object-cover"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col justify-between">
|
|
||||||
<a href={`/authors/${member.slug}`}>
|
|
||||||
<h3 class="mb-2 text-3xl text-foreground">{name}</h3>
|
|
||||||
<p class="mb-4 text-sm text-foreground">{bio}</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -10,10 +10,6 @@ export interface Props {
|
||||||
const { URL, icon, icon_size } = Astro.props
|
const { URL, icon, icon_size } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<a
|
<a href={URL} target={'_blank'} class={`inline-block ${icon_size}`}>
|
||||||
href={URL}
|
|
||||||
target={'_blank'}
|
|
||||||
class={`inline-block ${icon_size}`}
|
|
||||||
>
|
|
||||||
<i class={`bi bi-${icon}`}></i>
|
<i class={`bi bi-${icon}`}></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
---
|
---
|
||||||
import { Twitter, Github, Linkedin, Mail, GraduationCap, Rss } from 'lucide-react'
|
import {
|
||||||
|
Twitter,
|
||||||
|
Github,
|
||||||
|
Linkedin,
|
||||||
|
Mail,
|
||||||
|
GraduationCap,
|
||||||
|
Rss,
|
||||||
|
} from 'lucide-react'
|
||||||
import { SITE } from '@consts'
|
import { SITE } from '@consts'
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<ul class="not-prose flex flex-wrap gap-2">
|
<ul class="not-prose flex flex-wrap gap-2">
|
||||||
|
|
|
@ -27,16 +27,20 @@ function buildToc(headings: Heading[]) {
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<details open class="block xl:hidden rounded-lg border p-3 mb-8">
|
<details open class="mb-8 block rounded-lg border p-3 xl:hidden">
|
||||||
<summary class="text-xl font-semibold">Table of Contents</summary>
|
<summary class="cursor-pointer text-xl font-semibold"
|
||||||
|
>Table of Contents</summary
|
||||||
|
>
|
||||||
<nav>
|
<nav>
|
||||||
<ul class="py-3">
|
<ul class="py-3">
|
||||||
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</details>
|
</details>
|
||||||
<nav class="sticky top-16 hidden xl:block h-0 w-[calc(50vw-50%-4rem)] overflow-wrap-break-word text-xs leading-4 translate-x-[calc(-100%-2em)]">
|
<nav
|
||||||
<h2 class="text-xl font-semibold mb-4">Table of Contents</h2>
|
class="overflow-wrap-break-word sticky top-16 hidden h-0 w-[calc(50vw-50%-4rem)] translate-x-[calc(-100%-2em)] text-xs leading-4 xl:block"
|
||||||
|
>
|
||||||
|
<h2 class="mb-4 text-xl font-semibold">Table of Contents</h2>
|
||||||
<ul class="space-y-2">
|
<ul class="space-y-2">
|
||||||
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -5,13 +5,15 @@ import Link from './Link.astro'
|
||||||
const { heading } = Astro.props
|
const { heading } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<li class="list-inside list-disc px-6 py-1.5 xl:list-none xl:p-0 text-sm">
|
<li
|
||||||
|
class="list-inside list-disc px-6 py-1.5 text-sm text-foreground/60 xl:list-none xl:p-0"
|
||||||
|
>
|
||||||
<Link href={'#' + heading.slug} class="toc-link" data-heading={heading.slug}>
|
<Link href={'#' + heading.slug} class="toc-link" data-heading={heading.slug}>
|
||||||
{heading.text}
|
{heading.text}
|
||||||
</Link>
|
</Link>
|
||||||
{
|
{
|
||||||
heading.subheadings.length > 0 && (
|
heading.subheadings.length > 0 && (
|
||||||
<ul class="translate-x-3 xl:translate-x-0 xl:ml-4 xl:mt-2 xl:space-y-2">
|
<ul class="translate-x-3 xl:ml-4 xl:mt-2 xl:translate-x-0 xl:space-y-2">
|
||||||
{heading.subheadings.map((subheading: Heading) => (
|
{heading.subheadings.map((subheading: Heading) => (
|
||||||
<Astro.self heading={subheading} />
|
<Astro.self heading={subheading} />
|
||||||
))}
|
))}
|
||||||
|
@ -22,28 +24,30 @@ const { heading } = Astro.props
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function updateActiveHeading() {
|
function updateActiveHeading() {
|
||||||
const headings = document.querySelectorAll('h2, h3, h4, h5, h6');
|
const headings = document.querySelectorAll('h2, h3, h4, h5, h6')
|
||||||
const tocLinks = document.querySelectorAll('.toc-link');
|
const tocLinks = document.querySelectorAll('.toc-link')
|
||||||
|
|
||||||
let currentHeading = '';
|
let currentHeading = ''
|
||||||
|
|
||||||
headings.forEach(heading => {
|
headings.forEach((heading) => {
|
||||||
const top = heading.getBoundingClientRect().top;
|
const top = heading.getBoundingClientRect().top
|
||||||
if (top < 100) {
|
if (top < 200) {
|
||||||
currentHeading = heading.id;
|
currentHeading = heading.id
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
|
||||||
tocLinks.forEach(link => {
|
tocLinks.forEach((link) => {
|
||||||
const headingSlug = link.getAttribute('data-heading');
|
const headingSlug = link.getAttribute('data-heading')
|
||||||
if (headingSlug === currentHeading) {
|
if (headingSlug === currentHeading) {
|
||||||
link.classList.add('underline');
|
link.classList.add('underline')
|
||||||
|
link.classList.add('text-foreground')
|
||||||
} else {
|
} else {
|
||||||
link.classList.remove('underline');
|
link.classList.remove('underline')
|
||||||
|
link.classList.remove('text-foreground')
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('scroll', updateActiveHeading);
|
window.addEventListener('scroll', updateActiveHeading)
|
||||||
window.addEventListener('load', updateActiveHeading);
|
window.addEventListener('load', updateActiveHeading)
|
||||||
</script>
|
</script>
|
48
src/components/ui/avatar.tsx
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import * as AvatarPrimitive from '@radix-ui/react-avatar'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const Avatar = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Avatar.displayName = AvatarPrimitive.Root.displayName
|
||||||
|
|
||||||
|
const AvatarImage = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Image>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Image
|
||||||
|
ref={ref}
|
||||||
|
className={cn('aspect-square h-full w-full', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarImage.displayName = AvatarPrimitive.Image.displayName
|
||||||
|
|
||||||
|
const AvatarFallback = React.forwardRef<
|
||||||
|
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<AvatarPrimitive.Fallback
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'flex h-full w-full items-center justify-center rounded-full bg-muted',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
|
||||||
|
|
||||||
|
export { Avatar, AvatarFallback, AvatarImage }
|
36
src/components/ui/badge.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
|
||||||
|
secondary:
|
||||||
|
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||||
|
destructive:
|
||||||
|
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80',
|
||||||
|
outline: 'text-foreground',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'default',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants }
|
|
@ -1,37 +1,37 @@
|
||||||
import * as React from "react"
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
import { Slot } from "@radix-ui/react-slot"
|
import { cva, type VariantProps } from 'class-variance-authority'
|
||||||
import { cva, type VariantProps } from "class-variance-authority"
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||||
outline:
|
outline:
|
||||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: 'text-primary underline-offset-4 hover:underline',
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2",
|
default: 'h-9 px-4 py-2',
|
||||||
sm: "h-8 rounded-md px-3 text-xs",
|
sm: 'h-8 rounded-md px-3 text-xs',
|
||||||
lg: "h-10 rounded-md px-8",
|
lg: 'h-10 rounded-md px-8',
|
||||||
icon: "h-9 w-9",
|
icon: 'h-9 w-9',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: 'default',
|
||||||
size: "default",
|
size: 'default',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export interface ButtonProps
|
export interface ButtonProps
|
||||||
|
@ -42,7 +42,7 @@ export interface ButtonProps
|
||||||
|
|
||||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : 'button'
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
@ -50,8 +50,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
Button.displayName = "Button"
|
Button.displayName = 'Button'
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import * as React from "react"
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
|
||||||
import {
|
import {
|
||||||
CheckIcon,
|
CheckIcon,
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
DotFilledIcon,
|
DotFilledIcon,
|
||||||
} from "@radix-ui/react-icons"
|
} from '@radix-ui/react-icons'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
const DropdownMenu = DropdownMenuPrimitive.Root
|
const DropdownMenu = DropdownMenuPrimitive.Root
|
||||||
|
|
||||||
|
@ -29,9 +29,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.SubTrigger
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
|
||||||
inset && "pl-8",
|
inset && 'pl-8',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
@ -49,8 +49,8 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.SubContent
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -67,9 +67,9 @@ const DropdownMenuContent = React.forwardRef<
|
||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md',
|
||||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -86,9 +86,9 @@ const DropdownMenuItem = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
inset && "pl-8",
|
inset && 'pl-8',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -102,8 +102,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.CheckboxItem
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -126,8 +126,8 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.RadioItem
|
<DropdownMenuPrimitive.RadioItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
@ -150,9 +150,9 @@ const DropdownMenuLabel = React.forwardRef<
|
||||||
<DropdownMenuPrimitive.Label
|
<DropdownMenuPrimitive.Label
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-2 py-1.5 text-sm font-semibold",
|
'px-2 py-1.5 text-sm font-semibold',
|
||||||
inset && "pl-8",
|
inset && 'pl-8',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
@ -165,7 +165,7 @@ const DropdownMenuSeparator = React.forwardRef<
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<DropdownMenuPrimitive.Separator
|
<DropdownMenuPrimitive.Separator
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
className={cn('-mx-1 my-1 h-px bg-muted', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
@ -177,27 +177,27 @@ const DropdownMenuShortcut = ({
|
||||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
DropdownMenuRadioItem,
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuShortcut,
|
DropdownMenuShortcut,
|
||||||
DropdownMenuGroup,
|
|
||||||
DropdownMenuPortal,
|
|
||||||
DropdownMenuSub,
|
DropdownMenuSub,
|
||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuRadioGroup,
|
DropdownMenuTrigger,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import * as React from "react"
|
import { Moon, Sun } from 'lucide-react'
|
||||||
import { Moon, Sun } from "lucide-react"
|
import * as React from 'react'
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from '@/components/ui/button'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const [theme, setThemeState] = React.useState<
|
const [theme, setThemeState] = React.useState<
|
||||||
"theme-light" | "dark" | "system"
|
'theme-light' | 'dark' | 'system'
|
||||||
>("theme-light")
|
>('theme-light')
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const isDarkMode = document.documentElement.classList.contains("dark")
|
const isDarkMode = document.documentElement.classList.contains('dark')
|
||||||
setThemeState(isDarkMode ? "dark" : "theme-light")
|
setThemeState(isDarkMode ? 'dark' : 'theme-light')
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const isDark =
|
const isDark =
|
||||||
theme === "dark" ||
|
theme === 'dark' ||
|
||||||
(theme === "system" &&
|
(theme === 'system' &&
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
document.documentElement.classList[isDark ? "add" : "remove"]("dark")
|
document.documentElement.classList[isDark ? 'add' : 'remove']('dark')
|
||||||
}, [theme])
|
}, [theme])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -37,13 +37,13 @@ export function ModeToggle() {
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => setThemeState("theme-light")}>
|
<DropdownMenuItem onClick={() => setThemeState('theme-light')}>
|
||||||
Light
|
Light
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setThemeState("dark")}>
|
<DropdownMenuItem onClick={() => setThemeState('dark')}>
|
||||||
Dark
|
Dark
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setThemeState("system")}>
|
<DropdownMenuItem onClick={() => setThemeState('system')}>
|
||||||
System
|
System
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|
|
@ -8,7 +8,8 @@ export type Site = {
|
||||||
|
|
||||||
export const SITE: Site = {
|
export const SITE: Site = {
|
||||||
TITLE: 'astro-erudite',
|
TITLE: 'astro-erudite',
|
||||||
DESCRIPTION: 'astro-erudite is a opinionated, no-frills blogging template. Built with Astro.',
|
DESCRIPTION:
|
||||||
|
'astro-erudite is a opinionated, no-frills blogging template. Built with Astro.',
|
||||||
EMAIL: 'youremail@gmail.com',
|
EMAIL: 'youremail@gmail.com',
|
||||||
NUM_POSTS_ON_HOMEPAGE: 2,
|
NUM_POSTS_ON_HOMEPAGE: 2,
|
||||||
SITEURL: 'https://astro-erudite.vercel.app',
|
SITEURL: 'https://astro-erudite.vercel.app',
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
---
|
|
||||||
title: '[Logs] What is new in Astro Micro Academic'
|
|
||||||
description: 'Features, enhancements, and changes.'
|
|
||||||
date: '2024-07-25'
|
|
||||||
tags: ['log', 'rss-feed']
|
|
||||||
---
|
|
||||||
|
|
||||||
## h2 Heading
|
|
||||||
### h3 Heading
|
|
||||||
#### h4 Heading
|
|
||||||
##### h5 Heading
|
|
||||||
###### h6 Heading
|
|
||||||
|
|
||||||
## Emphasis
|
|
||||||
|
|
||||||
**This is bold text**
|
|
||||||
|
|
||||||
__This is bold text__
|
|
||||||
|
|
||||||
*This is italic text*
|
|
||||||
|
|
||||||
_This is italic text_
|
|
||||||
|
|
||||||
~~Strikethrough~~
|
|
||||||
|
|
||||||
|
|
||||||
## Blockquotes
|
|
||||||
|
|
||||||
|
|
||||||
> Blockquotes can also be nested...
|
|
||||||
>> ...by using additional greater-than signs right next to each other...
|
|
||||||
> > > ...or with spaces between arrows.
|
|
||||||
|
|
||||||
|
|
||||||
## Lists
|
|
||||||
|
|
||||||
Unordered
|
|
||||||
|
|
||||||
+ Create a list by starting a line with `+`, `-`, or `*`
|
|
||||||
+ Sub-lists are made by indenting 2 spaces:
|
|
||||||
- Marker character change forces new list start:
|
|
||||||
* Ac tristique libero volutpat at
|
|
||||||
+ Facilisis in pretium nisl aliquet
|
|
||||||
- Nulla volutpat aliquam velit
|
|
||||||
+ Very easy!
|
|
||||||
|
|
||||||
Ordered
|
|
||||||
|
|
||||||
1. Lorem ipsum dolor sit amet
|
|
||||||
2. Consectetur adipiscing elit
|
|
||||||
3. Integer molestie lorem at massa
|
|
||||||
|
|
||||||
|
|
||||||
1. You can use sequential numbers...
|
|
||||||
1. ...or keep all the numbers as `1.`
|
|
||||||
|
|
||||||
Start numbering with offset:
|
|
||||||
|
|
||||||
57. foo
|
|
||||||
1. bar
|
|
||||||
|
|
||||||
|
|
||||||
## Code
|
|
||||||
|
|
||||||
Inline `code`
|
|
||||||
|
|
||||||
Indented code
|
|
||||||
|
|
||||||
// Some comments
|
|
||||||
line 1 of code
|
|
||||||
line 2 of code
|
|
||||||
line 3 of code
|
|
||||||
|
|
||||||
|
|
||||||
Block code "fences"
|
|
||||||
|
|
||||||
```
|
|
||||||
Sample text here...
|
|
||||||
```
|
|
||||||
|
|
||||||
Syntax highlighting
|
|
||||||
|
|
||||||
``` js
|
|
||||||
var foo = function (bar) {
|
|
||||||
return bar++;
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(foo(5));
|
|
||||||
```
|
|
||||||
|
|
||||||
## Tables
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
| ------ | ----------- |
|
|
||||||
| data | path to data files to supply the data that will be passed into templates. |
|
|
||||||
| engine | engine to be used for processing templates. Handlebars is the default. |
|
|
||||||
| ext | extension to be used for dest files. |
|
|
||||||
|
|
||||||
Right aligned columns
|
|
||||||
|
|
||||||
| Option | Description |
|
|
||||||
| ------:| -----------:|
|
|
||||||
| data | path to data files to supply the data that will be passed into templates. |
|
|
||||||
| engine | engine to be used for processing templates. Handlebars is the default. |
|
|
||||||
| ext | extension to be used for dest files. |
|
|
||||||
|
|
||||||
|
|
||||||
## Links
|
|
||||||
|
|
||||||
[link text](http://dev.nodeca.com)
|
|
||||||
|
|
||||||
[link with title](http://nodeca.github.io/pica/demo/ "title text!")
|
|
113
src/content/blog/blog-post-1.mdx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
---
|
||||||
|
title: 'Blog Post'
|
||||||
|
description: 'Features, enhancements, and changes.'
|
||||||
|
date: '2022-07-25'
|
||||||
|
tags: ['log', 'rss-feed']
|
||||||
|
image: '/1200x630.png'
|
||||||
|
author: 'enscribe'
|
||||||
|
---
|
||||||
|
|
||||||
|
## h2 Heading
|
||||||
|
|
||||||
|
### h3 Heading
|
||||||
|
|
||||||
|
#### h4 Heading
|
||||||
|
|
||||||
|
##### h5 Heading
|
||||||
|
|
||||||
|
###### h6 Heading
|
||||||
|
|
||||||
|
## Emphasis
|
||||||
|
|
||||||
|
**This is bold text**
|
||||||
|
|
||||||
|
**This is bold text**
|
||||||
|
|
||||||
|
_This is italic text_
|
||||||
|
|
||||||
|
_This is italic text_
|
||||||
|
|
||||||
|
~~Strikethrough~~
|
||||||
|
|
||||||
|
## Blockquotes
|
||||||
|
|
||||||
|
> Blockquotes can also be nested...
|
||||||
|
>
|
||||||
|
> > ...by using additional greater-than signs right next to each other...
|
||||||
|
> >
|
||||||
|
> > > ...or with spaces between arrows.
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
Unordered
|
||||||
|
|
||||||
|
- Create a list by starting a line with `+`, `-`, or `*`
|
||||||
|
- Sub-lists are made by indenting 2 spaces:
|
||||||
|
- Marker character change forces new list start:
|
||||||
|
- Ac tristique libero volutpat at
|
||||||
|
* Facilisis in pretium nisl aliquet
|
||||||
|
- Nulla volutpat aliquam velit
|
||||||
|
- Very easy!
|
||||||
|
|
||||||
|
Ordered
|
||||||
|
|
||||||
|
1. Lorem ipsum dolor sit amet
|
||||||
|
2. Consectetur adipiscing elit
|
||||||
|
3. Integer molestie lorem at massa
|
||||||
|
|
||||||
|
4. You can use sequential numbers...
|
||||||
|
5. ...or keep all the numbers as `1.`
|
||||||
|
|
||||||
|
Start numbering with offset:
|
||||||
|
|
||||||
|
57. foo
|
||||||
|
1. bar
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
Inline `code`
|
||||||
|
|
||||||
|
Indented code
|
||||||
|
|
||||||
|
// Some comments
|
||||||
|
line 1 of code
|
||||||
|
line 2 of code
|
||||||
|
line 3 of code
|
||||||
|
|
||||||
|
Block code "fences"
|
||||||
|
|
||||||
|
```
|
||||||
|
Sample text here...
|
||||||
|
```
|
||||||
|
|
||||||
|
Syntax highlighting
|
||||||
|
|
||||||
|
```js
|
||||||
|
var foo = function (bar) {
|
||||||
|
return bar++
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(foo(5))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tables
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| ------ | ------------------------------------------------------------------------- |
|
||||||
|
| data | path to data files to supply the data that will be passed into templates. |
|
||||||
|
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||||
|
| ext | extension to be used for dest files. |
|
||||||
|
|
||||||
|
Right aligned columns
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| -----: | ------------------------------------------------------------------------: |
|
||||||
|
| data | path to data files to supply the data that will be passed into templates. |
|
||||||
|
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||||
|
| ext | extension to be used for dest files. |
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
[link text](http://dev.nodeca.com)
|
||||||
|
|
||||||
|
[link with title](http://nodeca.github.io/pica/demo/ 'title text!')
|
113
src/content/blog/blog-post-2.mdx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
---
|
||||||
|
title: 'Blog Post'
|
||||||
|
description: 'Features, enhancements, and changes.'
|
||||||
|
date: '2023-07-25'
|
||||||
|
tags: ['log', 'rss-feed']
|
||||||
|
image: '/1200x630.png'
|
||||||
|
author: 'enscribe'
|
||||||
|
---
|
||||||
|
|
||||||
|
## h2 Heading
|
||||||
|
|
||||||
|
### h3 Heading
|
||||||
|
|
||||||
|
#### h4 Heading
|
||||||
|
|
||||||
|
##### h5 Heading
|
||||||
|
|
||||||
|
###### h6 Heading
|
||||||
|
|
||||||
|
## Emphasis
|
||||||
|
|
||||||
|
**This is bold text**
|
||||||
|
|
||||||
|
**This is bold text**
|
||||||
|
|
||||||
|
_This is italic text_
|
||||||
|
|
||||||
|
_This is italic text_
|
||||||
|
|
||||||
|
~~Strikethrough~~
|
||||||
|
|
||||||
|
## Blockquotes
|
||||||
|
|
||||||
|
> Blockquotes can also be nested...
|
||||||
|
>
|
||||||
|
> > ...by using additional greater-than signs right next to each other...
|
||||||
|
> >
|
||||||
|
> > > ...or with spaces between arrows.
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
Unordered
|
||||||
|
|
||||||
|
- Create a list by starting a line with `+`, `-`, or `*`
|
||||||
|
- Sub-lists are made by indenting 2 spaces:
|
||||||
|
- Marker character change forces new list start:
|
||||||
|
- Ac tristique libero volutpat at
|
||||||
|
* Facilisis in pretium nisl aliquet
|
||||||
|
- Nulla volutpat aliquam velit
|
||||||
|
- Very easy!
|
||||||
|
|
||||||
|
Ordered
|
||||||
|
|
||||||
|
1. Lorem ipsum dolor sit amet
|
||||||
|
2. Consectetur adipiscing elit
|
||||||
|
3. Integer molestie lorem at massa
|
||||||
|
|
||||||
|
4. You can use sequential numbers...
|
||||||
|
5. ...or keep all the numbers as `1.`
|
||||||
|
|
||||||
|
Start numbering with offset:
|
||||||
|
|
||||||
|
57. foo
|
||||||
|
1. bar
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
Inline `code`
|
||||||
|
|
||||||
|
Indented code
|
||||||
|
|
||||||
|
// Some comments
|
||||||
|
line 1 of code
|
||||||
|
line 2 of code
|
||||||
|
line 3 of code
|
||||||
|
|
||||||
|
Block code "fences"
|
||||||
|
|
||||||
|
```
|
||||||
|
Sample text here...
|
||||||
|
```
|
||||||
|
|
||||||
|
Syntax highlighting
|
||||||
|
|
||||||
|
```js
|
||||||
|
var foo = function (bar) {
|
||||||
|
return bar++
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(foo(5))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tables
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| ------ | ------------------------------------------------------------------------- |
|
||||||
|
| data | path to data files to supply the data that will be passed into templates. |
|
||||||
|
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||||
|
| ext | extension to be used for dest files. |
|
||||||
|
|
||||||
|
Right aligned columns
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| -----: | ------------------------------------------------------------------------: |
|
||||||
|
| data | path to data files to supply the data that will be passed into templates. |
|
||||||
|
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||||
|
| ext | extension to be used for dest files. |
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
[link text](http://dev.nodeca.com)
|
||||||
|
|
||||||
|
[link with title](http://nodeca.github.io/pica/demo/ 'title text!')
|
113
src/content/blog/blog-post-3.mdx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
---
|
||||||
|
title: 'Blog Post'
|
||||||
|
description: 'Features, enhancements, and changes.'
|
||||||
|
date: '2024-07-25'
|
||||||
|
tags: ['log', 'rss-feed']
|
||||||
|
image: '/1200x630.png'
|
||||||
|
author: 'enscribe'
|
||||||
|
---
|
||||||
|
|
||||||
|
## h2 Heading
|
||||||
|
|
||||||
|
### h3 Heading
|
||||||
|
|
||||||
|
#### h4 Heading
|
||||||
|
|
||||||
|
##### h5 Heading
|
||||||
|
|
||||||
|
###### h6 Heading
|
||||||
|
|
||||||
|
## Emphasis
|
||||||
|
|
||||||
|
**This is bold text**
|
||||||
|
|
||||||
|
**This is bold text**
|
||||||
|
|
||||||
|
_This is italic text_
|
||||||
|
|
||||||
|
_This is italic text_
|
||||||
|
|
||||||
|
~~Strikethrough~~
|
||||||
|
|
||||||
|
## Blockquotes
|
||||||
|
|
||||||
|
> Blockquotes can also be nested...
|
||||||
|
>
|
||||||
|
> > ...by using additional greater-than signs right next to each other...
|
||||||
|
> >
|
||||||
|
> > > ...or with spaces between arrows.
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
Unordered
|
||||||
|
|
||||||
|
- Create a list by starting a line with `+`, `-`, or `*`
|
||||||
|
- Sub-lists are made by indenting 2 spaces:
|
||||||
|
- Marker character change forces new list start:
|
||||||
|
- Ac tristique libero volutpat at
|
||||||
|
* Facilisis in pretium nisl aliquet
|
||||||
|
- Nulla volutpat aliquam velit
|
||||||
|
- Very easy!
|
||||||
|
|
||||||
|
Ordered
|
||||||
|
|
||||||
|
1. Lorem ipsum dolor sit amet
|
||||||
|
2. Consectetur adipiscing elit
|
||||||
|
3. Integer molestie lorem at massa
|
||||||
|
|
||||||
|
4. You can use sequential numbers...
|
||||||
|
5. ...or keep all the numbers as `1.`
|
||||||
|
|
||||||
|
Start numbering with offset:
|
||||||
|
|
||||||
|
57. foo
|
||||||
|
1. bar
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
Inline `code`
|
||||||
|
|
||||||
|
Indented code
|
||||||
|
|
||||||
|
// Some comments
|
||||||
|
line 1 of code
|
||||||
|
line 2 of code
|
||||||
|
line 3 of code
|
||||||
|
|
||||||
|
Block code "fences"
|
||||||
|
|
||||||
|
```
|
||||||
|
Sample text here...
|
||||||
|
```
|
||||||
|
|
||||||
|
Syntax highlighting
|
||||||
|
|
||||||
|
```js
|
||||||
|
var foo = function (bar) {
|
||||||
|
return bar++
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(foo(5))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tables
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| ------ | ------------------------------------------------------------------------- |
|
||||||
|
| data | path to data files to supply the data that will be passed into templates. |
|
||||||
|
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||||
|
| ext | extension to be used for dest files. |
|
||||||
|
|
||||||
|
Right aligned columns
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| -----: | ------------------------------------------------------------------------: |
|
||||||
|
| data | path to data files to supply the data that will be passed into templates. |
|
||||||
|
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||||
|
| ext | extension to be used for dest files. |
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
[link text](http://dev.nodeca.com)
|
||||||
|
|
||||||
|
[link with title](http://nodeca.github.io/pica/demo/ 'title text!')
|
113
src/content/blog/blog-post-4.mdx
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
---
|
||||||
|
title: 'Blog Post'
|
||||||
|
description: 'Features, enhancements, and changes.'
|
||||||
|
date: '2024-07-25'
|
||||||
|
tags: ['log', 'rss-feed']
|
||||||
|
image: '/1200x630.png'
|
||||||
|
author: 'enscribe'
|
||||||
|
---
|
||||||
|
|
||||||
|
## h2 Heading
|
||||||
|
|
||||||
|
### h3 Heading
|
||||||
|
|
||||||
|
#### h4 Heading
|
||||||
|
|
||||||
|
##### h5 Heading
|
||||||
|
|
||||||
|
###### h6 Heading
|
||||||
|
|
||||||
|
## Emphasis
|
||||||
|
|
||||||
|
**This is bold text**
|
||||||
|
|
||||||
|
**This is bold text**
|
||||||
|
|
||||||
|
_This is italic text_
|
||||||
|
|
||||||
|
_This is italic text_
|
||||||
|
|
||||||
|
~~Strikethrough~~
|
||||||
|
|
||||||
|
## Blockquotes
|
||||||
|
|
||||||
|
> Blockquotes can also be nested...
|
||||||
|
>
|
||||||
|
> > ...by using additional greater-than signs right next to each other...
|
||||||
|
> >
|
||||||
|
> > > ...or with spaces between arrows.
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
Unordered
|
||||||
|
|
||||||
|
- Create a list by starting a line with `+`, `-`, or `*`
|
||||||
|
- Sub-lists are made by indenting 2 spaces:
|
||||||
|
- Marker character change forces new list start:
|
||||||
|
- Ac tristique libero volutpat at
|
||||||
|
* Facilisis in pretium nisl aliquet
|
||||||
|
- Nulla volutpat aliquam velit
|
||||||
|
- Very easy!
|
||||||
|
|
||||||
|
Ordered
|
||||||
|
|
||||||
|
1. Lorem ipsum dolor sit amet
|
||||||
|
2. Consectetur adipiscing elit
|
||||||
|
3. Integer molestie lorem at massa
|
||||||
|
|
||||||
|
4. You can use sequential numbers...
|
||||||
|
5. ...or keep all the numbers as `1.`
|
||||||
|
|
||||||
|
Start numbering with offset:
|
||||||
|
|
||||||
|
57. foo
|
||||||
|
1. bar
|
||||||
|
|
||||||
|
## Code
|
||||||
|
|
||||||
|
Inline `code`
|
||||||
|
|
||||||
|
Indented code
|
||||||
|
|
||||||
|
// Some comments
|
||||||
|
line 1 of code
|
||||||
|
line 2 of code
|
||||||
|
line 3 of code
|
||||||
|
|
||||||
|
Block code "fences"
|
||||||
|
|
||||||
|
```
|
||||||
|
Sample text here...
|
||||||
|
```
|
||||||
|
|
||||||
|
Syntax highlighting
|
||||||
|
|
||||||
|
```js
|
||||||
|
var foo = function (bar) {
|
||||||
|
return bar++
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(foo(5))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tables
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| ------ | ------------------------------------------------------------------------- |
|
||||||
|
| data | path to data files to supply the data that will be passed into templates. |
|
||||||
|
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||||
|
| ext | extension to be used for dest files. |
|
||||||
|
|
||||||
|
Right aligned columns
|
||||||
|
|
||||||
|
| Option | Description |
|
||||||
|
| -----: | ------------------------------------------------------------------------: |
|
||||||
|
| data | path to data files to supply the data that will be passed into templates. |
|
||||||
|
| engine | engine to be used for processing templates. Handlebars is the default. |
|
||||||
|
| ext | extension to be used for dest files. |
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
[link text](http://dev.nodeca.com)
|
||||||
|
|
||||||
|
[link with title](http://nodeca.github.io/pica/demo/ 'title text!')
|
|
@ -7,6 +7,7 @@ const blog = defineCollection({
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
date: z.coerce.date(),
|
date: z.coerce.date(),
|
||||||
draft: z.boolean().optional(),
|
draft: z.boolean().optional(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
author: z.union([reference('authors'), z.string()]).optional(),
|
author: z.union([reference('authors'), z.string()]).optional(),
|
||||||
|
|
|
@ -18,7 +18,7 @@ const { title, description } = Astro.props
|
||||||
<Head title={`${title} | ${SITE.TITLE}`} description={description} />
|
<Head title={`${title} | ${SITE.TITLE}`} description={description} />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class="box-border flex h-fit min-h-screen flex-col px-4 font-sans bg-background text-foreground antialiased gap-y-4"
|
class="box-border flex h-fit min-h-screen flex-col gap-y-4 bg-background px-4 font-sans text-foreground antialiased"
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<main class="flex-grow">
|
<main class="flex-grow">
|
||||||
|
|
|
@ -2,18 +2,18 @@
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import Link from '@components/Link.astro'
|
import Link from '@components/Link.astro'
|
||||||
import BackToPrevious from '@components/BackToPrevious.astro'
|
|
||||||
import { SITE } from '@consts'
|
import { SITE } from '@consts'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="404" description={SITE.DESCRIPTION}>
|
<Layout title="404" description={SITE.DESCRIPTION}>
|
||||||
<Container>
|
<Container>
|
||||||
<div class="mt-16 grid place-items-center gap-3">
|
<div class="mt-16 grid place-items-center gap-3">
|
||||||
<h4 class="text-2xl font-semibold">
|
<h4 class="text-2xl font-semibold">404: Page not found</h4>
|
||||||
404: Page not found
|
|
||||||
</h4>
|
|
||||||
<span>
|
<span>
|
||||||
<BackToPrevious href="/">Go to home page</BackToPrevious>
|
<a href="/">
|
||||||
|
<Button variant="outline"> Go to home page </Button>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
|
|
|
@ -1,62 +1,35 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content'
|
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import { Image } from 'astro:assets'
|
import Link from '@components/Link.astro'
|
||||||
|
import { SITE } from '@consts'
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="About" description="About">
|
<Layout title="About" description={SITE.DESCRIPTION}>
|
||||||
<Container>
|
<Container>
|
||||||
<div class="space-y-10">
|
<article class="prose max-w-none dark:prose-invert">
|
||||||
<div class="font-semibold">About</div>
|
<h1 class="mb-8 text-3xl font-bold">About Us</h1>
|
||||||
<section class="not-prose flex flex-col gap-4 text-justify">
|
<p class="mb-4">
|
||||||
<p class="text-justify">
|
Welcome to {SITE.TITLE}, a platform dedicated to sharing insights,
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolores porro
|
knowledge, and experiences in the world of technology and beyond. Our
|
||||||
hic minima incidunt explicabo obcaecati consectetur consequuntur at
|
mission is to provide valuable content that informs, inspires, and
|
||||||
quisquam commodi.
|
engages our readers.
|
||||||
|
</p>
|
||||||
|
<p class="mb-8">
|
||||||
|
Founded in 2023, we've grown into a community of passionate writers and
|
||||||
|
tech enthusiasts. Our team brings diverse expertise to the table,
|
||||||
|
covering topics ranging from software development and artificial
|
||||||
|
intelligence to digital culture and tech ethics.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="text-justify">
|
<h2 class="mb-4 text-2xl font-semibold">Get in Touch</h2>
|
||||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolores porro
|
<p class="mb-4">
|
||||||
hic minima incidunt explicabo obcaecati consectetur consequuntur at
|
We love hearing from our readers! Whether you have a question,
|
||||||
quisquam commodi.
|
suggestion, or just want to say hello, don't hesitate to reach out.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
<p>
|
||||||
|
<Link href="/contact" class="font-semibold">Contact Us</Link>
|
||||||
<div class="flex flex-col justify-center md:flex-row">
|
|
||||||
<div class="my-10 text-center">
|
|
||||||
<div
|
|
||||||
class="h-[250px] w-[350px] -rotate-6 overflow-hidden rounded-xl object-cover"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={'/astro-nano.png'}
|
|
||||||
alt={'life2'}
|
|
||||||
width={350}
|
|
||||||
height={250}
|
|
||||||
class="h-[250px] w-[350px] overflow-hidden rounded-xl object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-4 text-sm">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</article>
|
||||||
<div class="mx-10 my-10 text-center">
|
|
||||||
<div
|
|
||||||
class="mx-auto h-[250px] w-[150px] rotate-6 rounded-xl object-cover sm:ml-auto"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
src={'/astro-micro.jpg'}
|
|
||||||
alt={'life2'}
|
|
||||||
width={150}
|
|
||||||
height={250}
|
|
||||||
class="h-[250px] w-[150px] rounded-xl object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p class="mt-4 text-sm">
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,29 +1,32 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry, getCollection, getEntry } from 'astro:content'
|
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import MemberCard from '@components/MemberCard.astro'
|
import Container from '@components/Container.astro'
|
||||||
|
import AuthorCard from '@components/AuthorCard.astro'
|
||||||
|
import BlogCard from '@components/BlogCard.astro'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const authors = await getCollection('authors')
|
const authors = await getCollection('authors')
|
||||||
return authors.map((member) => ({
|
return authors.map((author) => ({
|
||||||
params: { slug: member.slug },
|
params: { slug: author.slug },
|
||||||
props: { member },
|
props: { author },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
member: CollectionEntry<'authors'>
|
author: CollectionEntry<'authors'>
|
||||||
}
|
}
|
||||||
|
|
||||||
const { member } = Astro.props
|
const { author } = Astro.props
|
||||||
|
|
||||||
const allPosts = await getCollection('blog')
|
const allPosts = await getCollection('blog')
|
||||||
const memberPosts = allPosts
|
const authorPosts = allPosts
|
||||||
.filter((post) => {
|
.filter((post) => {
|
||||||
if (typeof post.data.author === 'string') {
|
if (typeof post.data.author === 'string') {
|
||||||
return post.data.author === member.data.name && !post.data.draft
|
return post.data.author === author.data.name && !post.data.draft
|
||||||
} else if (post.data.author && 'slug' in post.data.author) {
|
} else if (post.data.author && 'slug' in post.data.author) {
|
||||||
return post.data.author.slug === member.slug && !post.data.draft
|
return post.data.author.slug === author.slug && !post.data.draft
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
@ -31,10 +34,35 @@ const memberPosts = allPosts
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
title={`${member.data.name} - Team Member`}
|
title={`${author.data.name} - Author`}
|
||||||
description={member.data.bio || `Profile of ${member.data.name}`}
|
description={author.data.bio || `Profile of ${author.data.name}`}
|
||||||
>
|
>
|
||||||
<section class="mx-auto flex max-w-screen-xl flex-col gap-4">
|
<Container>
|
||||||
<MemberCard member={member} />
|
<a href="/authors">
|
||||||
|
<Button variant="ghost" className="mb-8">← Back to authors</Button>
|
||||||
|
</a>
|
||||||
|
<div class="space-y-10">
|
||||||
|
<section>
|
||||||
|
<AuthorCard author={author} />
|
||||||
</section>
|
</section>
|
||||||
|
<section class="space-y-4">
|
||||||
|
<h2 class="text-2xl font-semibold">Posts by {author.data.name}</h2>
|
||||||
|
{
|
||||||
|
authorPosts.length > 0 ? (
|
||||||
|
<ul class="not-prose flex flex-col gap-4">
|
||||||
|
{authorPosts.map((post) => (
|
||||||
|
<li>
|
||||||
|
<BlogCard entry={post} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p class="text-muted-foreground">
|
||||||
|
No posts available from this author.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content'
|
import { getCollection } from 'astro:content'
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import MemberCard from '@components/MemberCard.astro'
|
import Container from '@components/Container.astro'
|
||||||
|
import AuthorCard from '@components/AuthorCard.astro'
|
||||||
|
|
||||||
const authors = await getCollection('authors')
|
const authors = await getCollection('authors')
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="authors" description="authors">
|
<Layout title="Authors" description="Authors">
|
||||||
<section>
|
<Container>
|
||||||
<ul class="not-prose grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
<div class="space-y-10">
|
||||||
|
<h1 class="text-3xl font-semibold">Authors</h1>
|
||||||
|
<ul class="not-prose flex flex-col gap-4">
|
||||||
{
|
{
|
||||||
authors.map((member) => (
|
authors.map((author) => (
|
||||||
<li>
|
<li>
|
||||||
<MemberCard member={member} />
|
<AuthorCard author={author} />
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</div>
|
||||||
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
---
|
---
|
||||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
import { type CollectionEntry, getCollection, getEntry } from 'astro:content'
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import FormattedDate from '@components/FormattedDate.astro'
|
import FormattedDate from '@components/FormattedDate.astro'
|
||||||
import { readingTime } from '@lib/utils'
|
import { readingTime } from '@lib/utils'
|
||||||
import BackToPrevious from '@components/BackToPrevious.astro'
|
|
||||||
import PostNavigation from '@components/PostNavigation.astro'
|
import PostNavigation from '@components/PostNavigation.astro'
|
||||||
import TableOfContents from '@components/TableOfContents.astro'
|
import TableOfContents from '@components/TableOfContents.astro'
|
||||||
|
import { Image } from 'astro:assets'
|
||||||
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = (await getCollection('blog'))
|
const posts = (await getCollection('blog'))
|
||||||
|
@ -45,47 +47,112 @@ const prevPost = getPrevPost(currentPostSlug)
|
||||||
|
|
||||||
const post = Astro.props
|
const post = Astro.props
|
||||||
const { Content, headings } = await post.render()
|
const { Content, headings } = await post.render()
|
||||||
|
|
||||||
|
let author = null
|
||||||
|
if (
|
||||||
|
post.data.author &&
|
||||||
|
typeof post.data.author === 'object' &&
|
||||||
|
'collection' in post.data.author
|
||||||
|
) {
|
||||||
|
author = await getEntry(post.data.author)
|
||||||
|
} else if (typeof post.data.author === 'string') {
|
||||||
|
author = {
|
||||||
|
data: {
|
||||||
|
name: post.data.author,
|
||||||
|
avatar: '/favicons/android-chrome-512x512.png',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={post.data.title} description={post.data.description}>
|
<Layout title={post.data.title} description={post.data.description}>
|
||||||
<Container>
|
<Container>
|
||||||
<div class="animate">
|
<a href="/blog">
|
||||||
<BackToPrevious href="/blog">Back to blog</BackToPrevious>
|
<Button variant="ghost" className="mb-8">← Back to blog</Button>
|
||||||
|
</a>
|
||||||
|
{
|
||||||
|
post.data.image && (
|
||||||
|
<div class="mb-8 flex justify-center">
|
||||||
|
<Image
|
||||||
|
src={post.data.image}
|
||||||
|
alt={post.data.title}
|
||||||
|
width={1200}
|
||||||
|
height={630}
|
||||||
|
class="rounded-lg object-cover shadow-lg"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-10 space-y-4">
|
)
|
||||||
<div class="flex items-center gap-1.5">
|
}
|
||||||
<div class="font-base text-sm">
|
<div class="mb-8 flex flex-col items-center gap-y-4">
|
||||||
|
<div class="flex items-center gap-3 text-sm text-muted-foreground">
|
||||||
<FormattedDate date={post.data.date} />
|
<FormattedDate date={post.data.date} />
|
||||||
|
<span>•</span>
|
||||||
|
<span>{readingTime(post.body)}</span>
|
||||||
</div>
|
</div>
|
||||||
•
|
<h1 class="text-4xl font-bold leading-tight sm:text-5xl">
|
||||||
<div class="font-base text-sm">
|
|
||||||
{readingTime(post.body)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<h1 class="text-4xl font-semibold">
|
|
||||||
{post.data.title}
|
{post.data.title}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="font-base text-sm">
|
<div class="flex flex-wrap gap-2">
|
||||||
{
|
{
|
||||||
post.data.tags && post.data.tags.length > 0 ? (
|
post.data.tags && post.data.tags.length > 0 ? (
|
||||||
post.data.tags.map((tag) => (
|
post.data.tags.map((tag) => (
|
||||||
<div class="my-1 inline-block">
|
<Button variant="outline" asChild>
|
||||||
<a
|
<a href={`/tags/${tag}`}>
|
||||||
href={`/tags/${tag}`}
|
<Badge variant="outline">{tag}</Badge>
|
||||||
class="mx-1 rounded-full px-2 py-1 transition-colors duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</Button>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<span>No tags available</span>
|
<span class="text-sm text-muted-foreground">No tags available</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
author && (
|
||||||
|
<div>
|
||||||
|
{typeof post.data.author === 'object' &&
|
||||||
|
'collection' in post.data.author ? (
|
||||||
|
<Button variant="ghost" asChild>
|
||||||
|
<a
|
||||||
|
class="group flex items-center gap-3 rounded-xl p-2 transition-colors duration-300 ease-in-out hover:bg-secondary"
|
||||||
|
href={`/authors/${post.data.author.slug}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={author.data.avatar}
|
||||||
|
alt={`Avatar of ${author.data.name}`}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
class="rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium group-hover:text-primary">
|
||||||
|
{author.data.name}
|
||||||
|
</span>
|
||||||
|
<p class="text-sm text-muted-foreground">Author</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div class="flex items-center gap-3 rounded-xl p-2">
|
||||||
|
<Image
|
||||||
|
src={author.data.avatar || '/default-avatar.png'}
|
||||||
|
alt={`Avatar of ${author.data.name}`}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
class="rounded-full"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">{author.data.name}</span>
|
||||||
|
<p class="text-sm text-muted-foreground">Author</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{headings.length > 0 && <TableOfContents headings={headings} />}
|
{headings.length > 0 && <TableOfContents headings={headings} />}
|
||||||
<article class="prose prose-neutral max-w-full dark:prose-invert prose-img:mx-auto prose-img:my-auto">
|
<article class="max-w-full">
|
||||||
<Content />
|
<Content />
|
||||||
<div class="mt-24">
|
<div class="mt-24">
|
||||||
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
|
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import ArrowCard from '@components/ArrowCard.astro'
|
import BlogCard from '@components/BlogCard.astro'
|
||||||
|
|
||||||
const data = (await getCollection('blog'))
|
const data = (await getCollection('blog'))
|
||||||
.filter((post) => !post.data.draft)
|
.filter((post) => !post.data.draft)
|
||||||
|
@ -36,7 +36,7 @@ const years = Object.keys(posts).sort((a, b) => parseInt(b) - parseInt(a))
|
||||||
<ul class="not-prose flex flex-col gap-4">
|
<ul class="not-prose flex flex-col gap-4">
|
||||||
{posts[year].map((post) => (
|
{posts[year].map((post) => (
|
||||||
<li>
|
<li>
|
||||||
<ArrowCard entry={post} />
|
<BlogCard entry={post} />
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import { SITE } from '@consts'
|
import { SITE } from '@consts'
|
||||||
import ArrowCard from '@components/ArrowCard.astro'
|
import BlogCard from '@components/BlogCard.astro'
|
||||||
import Link from '@components/Link.astro'
|
import Link from '@components/Link.astro'
|
||||||
import { getCollection } from 'astro:content'
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ const blog = (await getCollection('blog'))
|
||||||
|
|
||||||
<Layout title="Home" description="Home">
|
<Layout title="Home" description="Home">
|
||||||
<Container>
|
<Container>
|
||||||
<section class="space-y-6">
|
<section class="space-y-4">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||||
<h2 class="font-semibold">Latest posts</h2>
|
<h2 class="font-semibold">Latest posts</h2>
|
||||||
<Link href="/blog"> See all posts </Link>
|
<Link href="/blog"> See all posts </Link>
|
||||||
|
@ -23,7 +23,7 @@ const blog = (await getCollection('blog'))
|
||||||
{
|
{
|
||||||
blog.map((post) => (
|
blog.map((post) => (
|
||||||
<li>
|
<li>
|
||||||
<ArrowCard entry={post} />
|
<BlogCard entry={post} />
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { type CollectionEntry, getCollection } from 'astro:content'
|
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import ArrowCard from '@components/ArrowCard.astro'
|
import BlogCard from '@components/BlogCard.astro'
|
||||||
|
|
||||||
type BlogPost = CollectionEntry<'blog'>
|
type BlogPost = CollectionEntry<'blog'>
|
||||||
|
|
||||||
|
@ -36,11 +36,11 @@ export async function getStaticPaths() {
|
||||||
>
|
>
|
||||||
<Container>
|
<Container>
|
||||||
<div class="space-y-10">
|
<div class="space-y-10">
|
||||||
<div class="font-semibold">
|
<div class="flex items-center gap-2">
|
||||||
Tag: <span
|
<h1 class="text-3xl font-semibold">Posts tagged with</h1>
|
||||||
class="mx-2 rounded-full px-3 py-2 transition-colors duration-300 ease-in-out"
|
<span class="rounded-full bg-secondary px-4 py-2 text-2xl font-medium">
|
||||||
>#{tag}</span
|
{tag}
|
||||||
>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
{
|
{
|
||||||
|
@ -49,7 +49,7 @@ export async function getStaticPaths() {
|
||||||
<div>
|
<div>
|
||||||
<ul class="not-prose flex flex-col gap-4">
|
<ul class="not-prose flex flex-col gap-4">
|
||||||
<li>
|
<li>
|
||||||
<ArrowCard entry={post} />
|
<BlogCard entry={post} />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { getCollection } from 'astro:content'
|
import { getCollection } from 'astro:content'
|
||||||
import Layout from '@layouts/Layout.astro'
|
import Layout from '@layouts/Layout.astro'
|
||||||
import Container from '@components/Container.astro'
|
import Container from '@components/Container.astro'
|
||||||
import Image from 'astro/components/Image.astro'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
||||||
const blog = (await getCollection('blog')).filter((post) => !post.data.draft)
|
const blog = (await getCollection('blog')).filter((post) => !post.data.draft)
|
||||||
|
|
||||||
|
@ -14,21 +14,18 @@ const tags = blog
|
||||||
<Layout title="Tags" description="Tags">
|
<Layout title="Tags" description="Tags">
|
||||||
<Container>
|
<Container>
|
||||||
<div class="space-y-10">
|
<div class="space-y-10">
|
||||||
<div class="font-semibold">Tags</div>
|
<h1 class="text-3xl font-semibold">Tags</h1>
|
||||||
<ul class="flex flex-wrap">
|
<div class="flex flex-wrap gap-2">
|
||||||
{
|
{
|
||||||
tags.map((tag) => (
|
tags.map((tag) => (
|
||||||
<li class="my-3">
|
<a href={`/tags/${tag}`}>
|
||||||
<a
|
<Badge variant="secondary" className="hover:bg-secondary/80">
|
||||||
href={`/tags/${tag}`}
|
|
||||||
class="mx-2 rounded-full px-3 py-2 transition-colors duration-300 ease-in-out"
|
|
||||||
>
|
|
||||||
#{tag}
|
#{tag}
|
||||||
|
</Badge>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</ul>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -93,7 +93,7 @@
|
||||||
|
|
||||||
html {
|
html {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable both-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
html.dark {
|
html.dark {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
article {
|
article {
|
||||||
|
@apply prose prose-neutral dark:prose-invert;
|
||||||
@apply prose-h1:scroll-m-20 prose-h1:text-4xl prose-h1:font-extrabold prose-h1:tracking-tight prose-h1:lg:text-5xl;
|
@apply prose-h1:scroll-m-20 prose-h1:text-4xl prose-h1:font-extrabold prose-h1:tracking-tight prose-h1:lg:text-5xl;
|
||||||
@apply prose-h2:mt-10 prose-h2:scroll-m-20 prose-h2:border-b prose-h2:pb-2 prose-h2:text-3xl prose-h2:font-semibold prose-h2:tracking-tight prose-h2:transition-colors prose-h2:first:mt-0;
|
@apply prose-h2:mt-10 prose-h2:scroll-m-20 prose-h2:border-b prose-h2:pb-2 prose-h2:text-3xl prose-h2:font-semibold prose-h2:tracking-tight prose-h2:transition-colors prose-h2:first:mt-0;
|
||||||
@apply prose-h3:mt-8 prose-h3:scroll-m-20 prose-h3:text-2xl prose-h3:font-semibold prose-h3:tracking-tight;
|
@apply prose-h3:mt-8 prose-h3:scroll-m-20 prose-h3:text-2xl prose-h3:font-semibold prose-h3:tracking-tight;
|
||||||
|
@ -12,5 +13,6 @@
|
||||||
@apply prose-blockquote:mt-6 prose-blockquote:border-l-2 prose-blockquote:pl-6 prose-blockquote:italic;
|
@apply prose-blockquote:mt-6 prose-blockquote:border-l-2 prose-blockquote:pl-6 prose-blockquote:italic;
|
||||||
@apply prose-ul:my-6 prose-ul:ml-6 prose-ul:list-disc prose-ul:[&>li]:mt-2;
|
@apply prose-ul:my-6 prose-ul:ml-6 prose-ul:list-disc prose-ul:[&>li]:mt-2;
|
||||||
@apply prose-pre:border prose-pre:border-border;
|
@apply prose-pre:border prose-pre:border-border;
|
||||||
|
@apply prose-img:mx-auto prose-img:my-auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|