feat: update schema, add ProjectCard
, readme
This commit is contained in:
parent
fbeab5a744
commit
b93eddea6b
24 changed files with 373 additions and 72 deletions
33
src/components/ProjectCard.astro
Normal file
33
src/components/ProjectCard.astro
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import { Image } from 'astro:assets'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import Link from '@components/Link.astro'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
|
||||
type Props = {
|
||||
project: CollectionEntry<'projects'>
|
||||
}
|
||||
|
||||
const { project } = Astro.props
|
||||
---
|
||||
|
||||
<div class="overflow-hidden rounded-xl border transition-colors duration-300 ease-in-out hover:bg-secondary/50">
|
||||
<Link href={project.data.link} class="block">
|
||||
<Image
|
||||
src={project.data.image}
|
||||
alt={project.data.name}
|
||||
width={400}
|
||||
height={200}
|
||||
class="w-full object-cover"
|
||||
/>
|
||||
<div class="p-4">
|
||||
<h3 class="mb-2 text-lg font-semibold">{project.data.name}</h3>
|
||||
<p class="mb-4 text-sm text-muted-foreground">{project.data.description}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{project.data.tags.map((tag) => (
|
||||
<Badge variant="secondary" showHash={false}>{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
|
@ -25,12 +25,14 @@ const badgeVariants = cva(
|
|||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
VariantProps<typeof badgeVariants> {
|
||||
showHash?: boolean
|
||||
}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
function Badge({ className, variant, showHash = true, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props}>
|
||||
<Hash className="size-3 -translate-x-0.5" />
|
||||
{showHash && <Hash className="size-3 -translate-x-0.5" />}
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ const Card = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('bg-card text-card-foreground rounded-xl border', className)}
|
||||
className={cn('bg-background rounded-xl border', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
|
|
@ -23,7 +23,16 @@ export function ModeToggle() {
|
|||
theme === 'dark' ||
|
||||
(theme === 'system' &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
|
||||
document.documentElement.classList.add('disable-transitions')
|
||||
|
||||
document.documentElement.classList[isDark ? 'add' : 'remove']('dark')
|
||||
|
||||
window.getComputedStyle(document.documentElement).getPropertyValue('opacity')
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
document.documentElement.classList.remove('disable-transitions')
|
||||
})
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
|
@ -56,4 +65,4 @@ export function ModeToggle() {
|
|||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ export const NAV_LINKS: Link[] = [
|
|||
{ href: '/blog', label: 'blog' },
|
||||
{ href: '/authors', label: 'authors' },
|
||||
{ href: '/about', label: 'about' },
|
||||
{ href: '/tags', label: 'tags' },
|
||||
]
|
||||
|
||||
export const SOCIAL_LINKS: Link[] = [
|
||||
|
|
BIN
src/content/blog/2022-post/2022.png
Normal file
BIN
src/content/blog/2022-post/2022.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 90 KiB |
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
title: '2022 Post'
|
||||
description: 'This a dummy post written in the year 2022.'
|
||||
date: '2022-01-01'
|
||||
date: 2022-01-01
|
||||
tags: ['dummy', 'placeholder']
|
||||
image: '/static/1200x630.png'
|
||||
image: './2022.png'
|
||||
---
|
||||
|
||||
This is a dummy post written in the year 2022.
|
BIN
src/content/blog/2023-post/2023.png
Normal file
BIN
src/content/blog/2023-post/2023.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
title: '2023 Post'
|
||||
description: 'This a dummy post written in the year 2023.'
|
||||
date: '2023-01-01'
|
||||
date: 2023-01-01
|
||||
tags: ['dummy', 'placeholder']
|
||||
image: '/static/1200x630.png'
|
||||
image: './2023.png'
|
||||
authors: ['enscribe']
|
||||
---
|
||||
|
BIN
src/content/blog/2024-post/2024.png
Normal file
BIN
src/content/blog/2024-post/2024.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 93 KiB |
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
title: '2024 Post'
|
||||
description: 'This a dummy post written in the year 2024 (with multiple authors).'
|
||||
date: '2024-01-01'
|
||||
date: 2024-01-01
|
||||
tags: ['dummy', 'placeholder']
|
||||
image: '/static/1200x630.png'
|
||||
image: './2024.png'
|
||||
authors: ['enscribe', 'jktrn']
|
||||
---
|
||||
|
BIN
src/content/blog/the-state-of-static-blogs/1200x630.png
Normal file
BIN
src/content/blog/the-state-of-static-blogs/1200x630.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 77 KiB |
|
@ -1,9 +1,9 @@
|
|||
---
|
||||
title: 'The State of Static Blogs in 2024'
|
||||
description: 'There should not be a single reason why you would need a command palette search bar to find a blog post on your own site.'
|
||||
date: '2024-07-25'
|
||||
date: 2024-07-25
|
||||
tags: ['webdev', 'opinion']
|
||||
image: '/static/1200x630.png'
|
||||
image: './1200x630.png'
|
||||
authors: ['enscribe']
|
||||
---
|
||||
|
|
@ -2,15 +2,31 @@ import { defineCollection, z } from 'astro:content'
|
|||
|
||||
const blog = defineCollection({
|
||||
type: 'content',
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
date: z.coerce.date(),
|
||||
draft: z.boolean().optional(),
|
||||
image: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
authors: z.array(z.string()).optional(),
|
||||
}),
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
title: z
|
||||
.string()
|
||||
.max(
|
||||
60,
|
||||
'Title should be 60 characters or less for optimal Open Graph display.',
|
||||
),
|
||||
description: z
|
||||
.string()
|
||||
.max(
|
||||
155,
|
||||
'Description should be 155 characters or less for optimal Open Graph display.',
|
||||
),
|
||||
date: z.coerce.date(),
|
||||
image: image()
|
||||
.refine((img) => img.width === 1200 && img.height === 630, {
|
||||
message:
|
||||
'The image must be exactly 1200px × 630px for Open Graph requirements.',
|
||||
})
|
||||
.optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
authors: z.array(z.string()).optional(),
|
||||
draft: z.boolean().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
const authors = defineCollection({
|
||||
|
@ -20,13 +36,28 @@ const authors = defineCollection({
|
|||
pronouns: z.string().optional(),
|
||||
avatar: z.string().url(),
|
||||
bio: z.string().optional(),
|
||||
website: z.string().url().optional(),
|
||||
twitter: z.string().optional(),
|
||||
github: z.string().optional(),
|
||||
linkedin: z.string().optional(),
|
||||
mail: z.string().email().optional(),
|
||||
discord: z.string().optional(),
|
||||
website: z.string().url().optional(),
|
||||
twitter: z.string().url().optional(),
|
||||
github: z.string().url().optional(),
|
||||
linkedin: z.string().url().optional(),
|
||||
discord: z.string().url().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const collections = { blog, authors }
|
||||
const projects = defineCollection({
|
||||
type: 'content',
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
tags: z.array(z.string()),
|
||||
image: image().refine((img) => img.width === 1200 && img.height === 630, {
|
||||
message:
|
||||
'The image must be exactly 1200px × 630px for Open Graph requirements.',
|
||||
}),
|
||||
link: z.string().url(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const collections = { blog, authors, projects }
|
||||
|
|
7
src/content/projects/project-a.md
Normal file
7
src/content/projects/project-a.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: "Project A"
|
||||
description: "This is an example project description! You should replace this with a description of your own project."
|
||||
tags: ["Framework A", "Library B", "Tool C", "Resource D"]
|
||||
image: "../../../public/static/1200x630.png"
|
||||
link: "https://example.com"
|
||||
---
|
7
src/content/projects/project-b.md
Normal file
7
src/content/projects/project-b.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: "Project B"
|
||||
description: "This is an example project description! You should replace this with a description of your own project."
|
||||
tags: ["Framework A", "Library B", "Tool C", "Resource D"]
|
||||
image: "../../../public/static/1200x630.png"
|
||||
link: "https://example.com"
|
||||
---
|
7
src/content/projects/project-c.md
Normal file
7
src/content/projects/project-c.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: "Project C"
|
||||
description: "This is an example project description! You should replace this with a description of your own project."
|
||||
tags: ["Framework A", "Library B", "Tool C", "Resource D"]
|
||||
image: "../../../public/static/1200x630.png"
|
||||
link: "https://example.com"
|
||||
---
|
|
@ -1,8 +1,12 @@
|
|||
---
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||
import Container from '@components/Container.astro'
|
||||
import ProjectCard from '@components/ProjectCard.astro'
|
||||
import { SITE } from '@consts'
|
||||
import Layout from '@layouts/Layout.astro'
|
||||
import { getCollection } from 'astro:content'
|
||||
|
||||
const projects = await getCollection('projects')
|
||||
---
|
||||
|
||||
<Layout title="About" description={SITE.DESCRIPTION}>
|
||||
|
@ -12,11 +16,18 @@ import Layout from '@layouts/Layout.astro'
|
|||
<section>
|
||||
<div class="min-w-full">
|
||||
<h1 class="mb-4 text-3xl font-bold">Some more about us</h1>
|
||||
<p class="prose prose-neutral dark:prose-invert">
|
||||
<p class="prose prose-neutral dark:prose-invert mb-8">
|
||||
{SITE.TITLE} is an opinionated, no-frills static blogging template built
|
||||
with Astro.
|
||||
</p>
|
||||
|
||||
<h2 class="mb-4 text-2xl font-semibold">Our Projects</h2>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{projects.map((project) => (
|
||||
<ProjectCard project={project} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Container>
|
||||
</Layout>
|
||||
</Layout>
|
|
@ -11,14 +11,18 @@ const authors = await getCollection('authors')
|
|||
<Layout title="Authors" description="A list of authors on this site.">
|
||||
<Container class="flex flex-col gap-y-6">
|
||||
<Breadcrumbs items={[{ label: 'Authors' }]} />
|
||||
<ul class="not-prose flex flex-col gap-4">
|
||||
{
|
||||
authors.map((author) => (
|
||||
<li>
|
||||
<AuthorCard author={author} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
{authors.length > 0 ? (
|
||||
<ul class="not-prose flex flex-col gap-4">
|
||||
{
|
||||
authors.map((author) => (
|
||||
<li>
|
||||
<AuthorCard author={author} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="text-center text-muted-foreground">No authors found.</p>
|
||||
)}
|
||||
</Container>
|
||||
</Layout>
|
||||
|
|
|
@ -57,7 +57,7 @@ const authors = await parseAuthors(post.data.authors ?? [])
|
|||
<Layout
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
image={post.data.image ?? '/static/1200x630.png'}
|
||||
image={post.data.image?.src ?? '/static/1200x630.png'}
|
||||
>
|
||||
<Container class="flex flex-col gap-y-6">
|
||||
<Breadcrumbs
|
||||
|
|
|
@ -9,13 +9,8 @@ export async function GET(context: APIContext) {
|
|||
(post) => !post.data.draft,
|
||||
)
|
||||
|
||||
// Filter posts by tag 'rss-feed'
|
||||
const filteredBlogs = blog.filter(
|
||||
(post) => post.data.tags && post.data.tags.includes('rss-feed'),
|
||||
)
|
||||
|
||||
// Sort posts by date
|
||||
const items = [...filteredBlogs].sort(
|
||||
const items = [...blog].sort(
|
||||
(a, b) =>
|
||||
new Date(b.data.date).valueOf() - new Date(a.data.date).valueOf(),
|
||||
)
|
||||
|
|
|
@ -38,12 +38,12 @@ export async function getStaticPaths() {
|
|||
>
|
||||
<Container class="flex flex-col gap-y-6">
|
||||
<Breadcrumbs items={[{ href: '/tags', label: 'Tags' }, { label: tag }]} />
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center flex-wrap gap-2">
|
||||
<h1 class="text-3xl font-semibold">Posts tagged with</h1>
|
||||
<span
|
||||
class="flex items-center gap-x-1 rounded-full bg-secondary px-4 py-2 text-2xl font-medium"
|
||||
class="flex items-center gap-x-1 rounded-full bg-secondary px-4 py-2 text-2xl font-bold"
|
||||
>
|
||||
<Hash className="size-6" />{tag}
|
||||
<Hash className="size-6 -translate-x-0.5" strokeWidth={3} />{tag}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
|
|
|
@ -6,10 +6,6 @@
|
|||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 80.1%;
|
||||
|
@ -23,22 +19,11 @@
|
|||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
|
@ -52,13 +37,7 @@
|
|||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
|
||||
*,
|
||||
|
@ -80,6 +59,11 @@
|
|||
@apply bg-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.disable-transitions,
|
||||
.disable-transitions * {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue