feat: update schema, add ProjectCard, readme

This commit is contained in:
enscribe 2024-09-13 15:18:47 -07:00
parent fbeab5a744
commit b93eddea6b
No known key found for this signature in database
GPG key ID: 9BBD5C4114E25322
24 changed files with 373 additions and 72 deletions

View 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>

View file

@ -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>
)

View file

@ -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}
/>
))

View file

@ -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>
)
}
}

View file

@ -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[] = [

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View file

@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View file

@ -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']
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View file

@ -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']
---

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View file

@ -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']
---

View file

@ -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 }

View 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"
---

View 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"
---

View 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"
---

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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(),
)

View file

@ -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">

View file

@ -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 {