chore: update
26
package-lock.json
generated
|
@ -17,6 +17,7 @@
|
|||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@fontsource/geist-mono": "^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-icons": "^1.3.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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"@astrojs/tailwind": "^5.1.0",
|
||||
"@fontsource/geist-mono": "^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-icons": "^1.3.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">
|
||||
<Container>
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex items-center space-x-4">
|
||||
<ModeToggle client:load />
|
||||
<p class="text-sm text-muted-foreground">© {new Date().getFullYear()} All rights reserved.</p>
|
||||
<div
|
||||
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 />
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
<SocialIcons />
|
||||
</div>
|
||||
|
|
|
@ -7,27 +7,34 @@ const items = [
|
|||
{ href: '/blog', label: 'blog' },
|
||||
{ href: '/authors', label: 'authors' },
|
||||
{ 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>
|
||||
<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}
|
||||
</Link>
|
||||
<nav class="flex items-center gap-4 md:gap-6 text-sm">
|
||||
{items.map((item, index) => (
|
||||
<Fragment key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
class="transition-colors hover:text-foreground/80 text-foreground/60 capitalize"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
<nav class="flex items-center gap-4 text-sm sm:gap-6">
|
||||
{
|
||||
items.map((item, index) => (
|
||||
<Fragment key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
class="capitalize text-foreground/60 transition-colors hover:text-foreground/80"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</Fragment>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
</Container>
|
||||
|
|
|
@ -8,7 +8,13 @@ type Props = {
|
|||
'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
|
||||
|
@ -16,7 +22,7 @@ const { href, external, class: className, 'data-heading': dataHeading, ...rest }
|
|||
target={external ? '_blank' : '_self'}
|
||||
class={cn(
|
||||
'inline-block transition-colors duration-300 ease-in-out hover:underline underline-offset-[3px]',
|
||||
className
|
||||
className,
|
||||
)}
|
||||
data-heading={dataHeading}
|
||||
{...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
|
||||
---
|
||||
|
||||
<a
|
||||
href={URL}
|
||||
target={'_blank'}
|
||||
class={`inline-block ${icon_size}`}
|
||||
>
|
||||
<a href={URL} target={'_blank'} class={`inline-block ${icon_size}`}>
|
||||
<i class={`bi bi-${icon}`}></i>
|
||||
</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'
|
||||
|
||||
---
|
||||
|
||||
<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">
|
||||
<summary class="text-xl font-semibold">Table of Contents</summary>
|
||||
<details open class="mb-8 block rounded-lg border p-3 xl:hidden">
|
||||
<summary class="cursor-pointer text-xl font-semibold"
|
||||
>Table of Contents</summary
|
||||
>
|
||||
<nav>
|
||||
<ul class="py-3">
|
||||
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
||||
</ul>
|
||||
</nav>
|
||||
</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)]">
|
||||
<h2 class="text-xl font-semibold mb-4">Table of Contents</h2>
|
||||
<nav
|
||||
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">
|
||||
{toc.map((heading) => <TableOfContentsHeading heading={heading} />)}
|
||||
</ul>
|
||||
|
|
|
@ -5,13 +5,15 @@ import Link from './Link.astro'
|
|||
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}>
|
||||
{heading.text}
|
||||
</Link>
|
||||
{
|
||||
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) => (
|
||||
<Astro.self heading={subheading} />
|
||||
))}
|
||||
|
@ -22,28 +24,30 @@ const { heading } = Astro.props
|
|||
|
||||
<script>
|
||||
function updateActiveHeading() {
|
||||
const headings = document.querySelectorAll('h2, h3, h4, h5, h6');
|
||||
const tocLinks = document.querySelectorAll('.toc-link');
|
||||
const headings = document.querySelectorAll('h2, h3, h4, h5, h6')
|
||||
const tocLinks = document.querySelectorAll('.toc-link')
|
||||
|
||||
let currentHeading = '';
|
||||
let currentHeading = ''
|
||||
|
||||
headings.forEach(heading => {
|
||||
const top = heading.getBoundingClientRect().top;
|
||||
if (top < 100) {
|
||||
currentHeading = heading.id;
|
||||
headings.forEach((heading) => {
|
||||
const top = heading.getBoundingClientRect().top
|
||||
if (top < 200) {
|
||||
currentHeading = heading.id
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
tocLinks.forEach(link => {
|
||||
const headingSlug = link.getAttribute('data-heading');
|
||||
tocLinks.forEach((link) => {
|
||||
const headingSlug = link.getAttribute('data-heading')
|
||||
if (headingSlug === currentHeading) {
|
||||
link.classList.add('underline');
|
||||
link.classList.add('underline')
|
||||
link.classList.add('text-foreground')
|
||||
} else {
|
||||
link.classList.remove('underline');
|
||||
link.classList.remove('underline')
|
||||
link.classList.remove('text-foreground')
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', updateActiveHeading);
|
||||
window.addEventListener('load', updateActiveHeading);
|
||||
window.addEventListener('scroll', updateActiveHeading)
|
||||
window.addEventListener('load', updateActiveHeading)
|
||||
</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 { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Slot } from '@radix-ui/react-slot'
|
||||
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(
|
||||
"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: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
'bg-primary text-primary-foreground shadow hover:bg-primary/90',
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',
|
||||
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:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
default: 'h-9 px-4 py-2',
|
||||
sm: 'h-8 rounded-md px-3 text-xs',
|
||||
lg: 'h-10 rounded-md px-8',
|
||||
icon: 'h-9 w-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
|
@ -42,7 +42,7 @@ export interface ButtonProps
|
|||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
const Comp = asChild ? Slot : 'button'
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
|
@ -50,8 +50,8 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
Button.displayName = 'Button'
|
||||
|
||||
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 {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
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
|
||||
|
||||
|
@ -29,9 +29,9 @@ const DropdownMenuSubTrigger = React.forwardRef<
|
|||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
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",
|
||||
inset && "pl-8",
|
||||
className
|
||||
'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',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
@ -49,8 +49,8 @@ const DropdownMenuSubContent = React.forwardRef<
|
|||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
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",
|
||||
className
|
||||
'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,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -67,9 +67,9 @@ const DropdownMenuContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"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",
|
||||
className
|
||||
'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',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -86,9 +86,9 @@ const DropdownMenuItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
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",
|
||||
inset && "pl-8",
|
||||
className
|
||||
'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',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -102,8 +102,8 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
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",
|
||||
className
|
||||
'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,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
|
@ -126,8 +126,8 @@ const DropdownMenuRadioItem = React.forwardRef<
|
|||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
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",
|
||||
className
|
||||
'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,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
@ -150,9 +150,9 @@ const DropdownMenuLabel = React.forwardRef<
|
|||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
'px-2 py-1.5 text-sm font-semibold',
|
||||
inset && 'pl-8',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
@ -165,7 +165,7 @@ const DropdownMenuSeparator = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
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}
|
||||
/>
|
||||
))
|
||||
|
@ -177,27 +177,27 @@ const DropdownMenuShortcut = ({
|
|||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
|
||||
export function ModeToggle() {
|
||||
const [theme, setThemeState] = React.useState<
|
||||
"theme-light" | "dark" | "system"
|
||||
>("theme-light")
|
||||
'theme-light' | 'dark' | 'system'
|
||||
>('theme-light')
|
||||
|
||||
React.useEffect(() => {
|
||||
const isDarkMode = document.documentElement.classList.contains("dark")
|
||||
setThemeState(isDarkMode ? "dark" : "theme-light")
|
||||
const isDarkMode = document.documentElement.classList.contains('dark')
|
||||
setThemeState(isDarkMode ? 'dark' : 'theme-light')
|
||||
}, [])
|
||||
|
||||
React.useEffect(() => {
|
||||
const isDark =
|
||||
theme === "dark" ||
|
||||
(theme === "system" &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches)
|
||||
document.documentElement.classList[isDark ? "add" : "remove"]("dark")
|
||||
theme === 'dark' ||
|
||||
(theme === 'system' &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
document.documentElement.classList[isDark ? 'add' : 'remove']('dark')
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
|
@ -37,13 +37,13 @@ export function ModeToggle() {
|
|||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setThemeState("theme-light")}>
|
||||
<DropdownMenuItem onClick={() => setThemeState('theme-light')}>
|
||||
Light
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setThemeState("dark")}>
|
||||
<DropdownMenuItem onClick={() => setThemeState('dark')}>
|
||||
Dark
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setThemeState("system")}>
|
||||
<DropdownMenuItem onClick={() => setThemeState('system')}>
|
||||
System
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
|
@ -8,7 +8,8 @@ export type Site = {
|
|||
|
||||
export const SITE: Site = {
|
||||
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',
|
||||
NUM_POSTS_ON_HOMEPAGE: 2,
|
||||
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(),
|
||||
date: z.coerce.date(),
|
||||
draft: z.boolean().optional(),
|
||||
image: z.string().optional(),
|
||||
|
||||
tags: z.array(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>
|
||||
<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 />
|
||||
<main class="flex-grow">
|
||||
|
|
|
@ -2,18 +2,18 @@
|
|||
import Layout from '@layouts/Layout.astro'
|
||||
import Container from '@components/Container.astro'
|
||||
import Link from '@components/Link.astro'
|
||||
import BackToPrevious from '@components/BackToPrevious.astro'
|
||||
import { SITE } from '@consts'
|
||||
import { Button } from '@/components/ui/button'
|
||||
---
|
||||
|
||||
<Layout title="404" description={SITE.DESCRIPTION}>
|
||||
<Container>
|
||||
<div class="mt-16 grid place-items-center gap-3">
|
||||
<h4 class="text-2xl font-semibold">
|
||||
404: Page not found
|
||||
</h4>
|
||||
<h4 class="text-2xl font-semibold">404: Page not found</h4>
|
||||
<span>
|
||||
<BackToPrevious href="/">Go to home page</BackToPrevious>
|
||||
<a href="/">
|
||||
<Button variant="outline"> Go to home page </Button>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</Container>
|
||||
|
|
|
@ -1,62 +1,35 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content'
|
||||
import Layout from '@layouts/Layout.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>
|
||||
<div class="space-y-10">
|
||||
<div class="font-semibold">About</div>
|
||||
<section class="not-prose flex flex-col gap-4 text-justify">
|
||||
<p class="text-justify">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolores porro
|
||||
hic minima incidunt explicabo obcaecati consectetur consequuntur at
|
||||
quisquam commodi.
|
||||
</p>
|
||||
<article class="prose max-w-none dark:prose-invert">
|
||||
<h1 class="mb-8 text-3xl font-bold">About Us</h1>
|
||||
<p class="mb-4">
|
||||
Welcome to {SITE.TITLE}, a platform dedicated to sharing insights,
|
||||
knowledge, and experiences in the world of technology and beyond. Our
|
||||
mission is to provide valuable content that informs, inspires, and
|
||||
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 class="text-justify">
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Dolores porro
|
||||
hic minima incidunt explicabo obcaecati consectetur consequuntur at
|
||||
quisquam commodi.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
<h2 class="mb-4 text-2xl font-semibold">Get in Touch</h2>
|
||||
<p class="mb-4">
|
||||
We love hearing from our readers! Whether you have a question,
|
||||
suggestion, or just want to say hello, don't hesitate to reach out.
|
||||
</p>
|
||||
<p>
|
||||
<Link href="/contact" class="font-semibold">Contact Us</Link>
|
||||
</p>
|
||||
</article>
|
||||
</Container>
|
||||
</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 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() {
|
||||
const authors = await getCollection('authors')
|
||||
return authors.map((member) => ({
|
||||
params: { slug: member.slug },
|
||||
props: { member },
|
||||
return authors.map((author) => ({
|
||||
params: { slug: author.slug },
|
||||
props: { author },
|
||||
}))
|
||||
}
|
||||
|
||||
type Props = {
|
||||
member: CollectionEntry<'authors'>
|
||||
author: CollectionEntry<'authors'>
|
||||
}
|
||||
|
||||
const { member } = Astro.props
|
||||
const { author } = Astro.props
|
||||
|
||||
const allPosts = await getCollection('blog')
|
||||
const memberPosts = allPosts
|
||||
const authorPosts = allPosts
|
||||
.filter((post) => {
|
||||
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) {
|
||||
return post.data.author.slug === member.slug && !post.data.draft
|
||||
return post.data.author.slug === author.slug && !post.data.draft
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
@ -31,10 +34,35 @@ const memberPosts = allPosts
|
|||
---
|
||||
|
||||
<Layout
|
||||
title={`${member.data.name} - Team Member`}
|
||||
description={member.data.bio || `Profile of ${member.data.name}`}
|
||||
title={`${author.data.name} - Author`}
|
||||
description={author.data.bio || `Profile of ${author.data.name}`}
|
||||
>
|
||||
<section class="mx-auto flex max-w-screen-xl flex-col gap-4">
|
||||
<MemberCard member={member} />
|
||||
</section>
|
||||
<Container>
|
||||
<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 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>
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content'
|
||||
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')
|
||||
---
|
||||
|
||||
<Layout title="authors" description="authors">
|
||||
<section>
|
||||
<ul class="not-prose grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3">
|
||||
{
|
||||
authors.map((member) => (
|
||||
<li>
|
||||
<MemberCard member={member} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
<Layout title="Authors" description="Authors">
|
||||
<Container>
|
||||
<div class="space-y-10">
|
||||
<h1 class="text-3xl font-semibold">Authors</h1>
|
||||
<ul class="not-prose flex flex-col gap-4">
|
||||
{
|
||||
authors.map((author) => (
|
||||
<li>
|
||||
<AuthorCard author={author} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</Container>
|
||||
</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 Container from '@components/Container.astro'
|
||||
import FormattedDate from '@components/FormattedDate.astro'
|
||||
import { readingTime } from '@lib/utils'
|
||||
import BackToPrevious from '@components/BackToPrevious.astro'
|
||||
import PostNavigation from '@components/PostNavigation.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() {
|
||||
const posts = (await getCollection('blog'))
|
||||
|
@ -45,47 +47,112 @@ const prevPost = getPrevPost(currentPostSlug)
|
|||
|
||||
const post = Astro.props
|
||||
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}>
|
||||
<Container>
|
||||
<div class="animate">
|
||||
<BackToPrevious href="/blog">Back to blog</BackToPrevious>
|
||||
</div>
|
||||
<div class="my-10 space-y-4">
|
||||
<div class="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)}
|
||||
<a href="/blog">
|
||||
<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 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} />
|
||||
<span>•</span>
|
||||
<span>{readingTime(post.body)}</span>
|
||||
</div>
|
||||
<h1 class="text-4xl font-semibold">
|
||||
<h1 class="text-4xl font-bold leading-tight sm:text-5xl">
|
||||
{post.data.title}
|
||||
</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.map((tag) => (
|
||||
<div class="my-1 inline-block">
|
||||
<a
|
||||
href={`/tags/${tag}`}
|
||||
class="mx-1 rounded-full px-2 py-1 transition-colors duration-300 ease-in-out"
|
||||
>
|
||||
#{tag}
|
||||
<Button variant="outline" asChild>
|
||||
<a href={`/tags/${tag}`}>
|
||||
<Badge variant="outline">{tag}</Badge>
|
||||
</a>
|
||||
</div>
|
||||
</Button>
|
||||
))
|
||||
) : (
|
||||
<span>No tags available</span>
|
||||
<span class="text-sm text-muted-foreground">No tags available</span>
|
||||
)
|
||||
}
|
||||
</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>
|
||||
{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 />
|
||||
<div class="mt-24">
|
||||
<PostNavigation prevPost={prevPost} nextPost={nextPost} />
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||
import Layout from '@layouts/Layout.astro'
|
||||
import Container from '@components/Container.astro'
|
||||
import ArrowCard from '@components/ArrowCard.astro'
|
||||
import BlogCard from '@components/BlogCard.astro'
|
||||
|
||||
const data = (await getCollection('blog'))
|
||||
.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">
|
||||
{posts[year].map((post) => (
|
||||
<li>
|
||||
<ArrowCard entry={post} />
|
||||
<BlogCard entry={post} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import Layout from '@layouts/Layout.astro'
|
||||
import Container from '@components/Container.astro'
|
||||
import { SITE } from '@consts'
|
||||
import ArrowCard from '@components/ArrowCard.astro'
|
||||
import BlogCard from '@components/BlogCard.astro'
|
||||
import Link from '@components/Link.astro'
|
||||
import { getCollection } from 'astro:content'
|
||||
|
||||
|
@ -14,7 +14,7 @@ const blog = (await getCollection('blog'))
|
|||
|
||||
<Layout title="Home" description="Home">
|
||||
<Container>
|
||||
<section class="space-y-6">
|
||||
<section class="space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2">
|
||||
<h2 class="font-semibold">Latest posts</h2>
|
||||
<Link href="/blog"> See all posts </Link>
|
||||
|
@ -23,7 +23,7 @@ const blog = (await getCollection('blog'))
|
|||
{
|
||||
blog.map((post) => (
|
||||
<li>
|
||||
<ArrowCard entry={post} />
|
||||
<BlogCard entry={post} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { type CollectionEntry, getCollection } from 'astro:content'
|
||||
import Layout from '@layouts/Layout.astro'
|
||||
import Container from '@components/Container.astro'
|
||||
import ArrowCard from '@components/ArrowCard.astro'
|
||||
import BlogCard from '@components/BlogCard.astro'
|
||||
|
||||
type BlogPost = CollectionEntry<'blog'>
|
||||
|
||||
|
@ -36,11 +36,11 @@ export async function getStaticPaths() {
|
|||
>
|
||||
<Container>
|
||||
<div class="space-y-10">
|
||||
<div class="font-semibold">
|
||||
Tag: <span
|
||||
class="mx-2 rounded-full px-3 py-2 transition-colors duration-300 ease-in-out"
|
||||
>#{tag}</span
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-3xl font-semibold">Posts tagged with</h1>
|
||||
<span class="rounded-full bg-secondary px-4 py-2 text-2xl font-medium">
|
||||
{tag}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
{
|
||||
|
@ -49,7 +49,7 @@ export async function getStaticPaths() {
|
|||
<div>
|
||||
<ul class="not-prose flex flex-col gap-4">
|
||||
<li>
|
||||
<ArrowCard entry={post} />
|
||||
<BlogCard entry={post} />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { getCollection } from 'astro:content'
|
||||
import Layout from '@layouts/Layout.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)
|
||||
|
||||
|
@ -14,21 +14,18 @@ const tags = blog
|
|||
<Layout title="Tags" description="Tags">
|
||||
<Container>
|
||||
<div class="space-y-10">
|
||||
<div class="font-semibold">Tags</div>
|
||||
<ul class="flex flex-wrap">
|
||||
<h1 class="text-3xl font-semibold">Tags</h1>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{
|
||||
tags.map((tag) => (
|
||||
<li class="my-3">
|
||||
<a
|
||||
href={`/tags/${tag}`}
|
||||
class="mx-2 rounded-full px-3 py-2 transition-colors duration-300 ease-in-out"
|
||||
>
|
||||
<a href={`/tags/${tag}`}>
|
||||
<Badge variant="secondary" className="hover:bg-secondary/80">
|
||||
#{tag}
|
||||
</a>
|
||||
</li>
|
||||
</Badge>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</Layout>
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
|
||||
html {
|
||||
color-scheme: light;
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
@layer base {
|
||||
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-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;
|
||||
|
@ -12,5 +13,6 @@
|
|||
@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-pre:border prose-pre:border-border;
|
||||
@apply prose-img:mx-auto prose-img:my-auto;
|
||||
}
|
||||
}
|
||||
|
|