summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIwanIDev <iwan@iwani.dev>2026-03-21 13:47:35 +0000
committerIwanIDev <iwan@iwani.dev>2026-03-21 13:47:35 +0000
commitc2ad94b04f290b5197a1cc80c98015be9e7b25da (patch)
treecae81947194a92ec9dfe50f079f6816e5fba6033
parent928fbed509e1dd872de601b75712bcfd13fc00bd (diff)
feat: add blog post listing and UI components for the blog
-rw-r--r--src/components/BlogPosts.astro34
-rw-r--r--src/components/post-list.tsx0
-rw-r--r--src/components/ui/badge.tsx49
-rw-r--r--src/components/ui/card.tsx103
-rw-r--r--src/components/ui/separator.tsx26
-rw-r--r--src/content/blog/getting-started-astro.md9
-rw-r--r--src/content/config.ts14
-rw-r--r--src/pages/index.astro23
8 files changed, 249 insertions, 9 deletions
diff --git a/src/components/BlogPosts.astro b/src/components/BlogPosts.astro
new file mode 100644
index 0000000..54b7529
--- /dev/null
+++ b/src/components/BlogPosts.astro
@@ -0,0 +1,34 @@
+---
+import { getCollection } from 'astro:content';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+
+// Fetch all blog posts from the content collection
+const posts = await getCollection('blog');
+
+// Sort posts by date (newest first)
+const sortedPosts = posts.sort((a, b) =>
+ new Date(b.data.publishDate).getTime() - new Date(a.data.publishDate).getTime()
+);
+---
+
+<div class="w-full max-w-2xl mx-auto p-4">
+ <h1 class="text-3xl font-bold mb-8">Blog Posts</h1>
+ <div class="space-y-4 max-h-96 overflow-y-auto">
+ {sortedPosts.map((post) => (
+ <a href={`/posts/${post.id}`} class="block hover:opacity-80 transition-opacity">
+ <Card>
+ <CardHeader>
+ <CardTitle>{post.data.title}</CardTitle>
+ <CardDescription>{post.data.description}</CardDescription>
+ </CardHeader>
+ <CardContent>
+ <div class="flex justify-between text-sm text-gray-500">
+ <span>{post.data.publishDate.toLocaleDateString()}</span>
+ <span>by {post.data.author}</span>
+ </div>
+ </CardContent>
+ </Card>
+ </a>
+ ))}
+ </div>
+</div> \ No newline at end of file
diff --git a/src/components/post-list.tsx b/src/components/post-list.tsx
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/components/post-list.tsx
diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx
new file mode 100644
index 0000000..fe670e9
--- /dev/null
+++ b/src/components/ui/badge.tsx
@@ -0,0 +1,49 @@
+import * as React from "react"
+import { cva, type VariantProps } from "class-variance-authority"
+import { Slot } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-none border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
+ {
+ variants: {
+ variant: {
+ default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
+ secondary:
+ "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
+ destructive:
+ "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
+ outline:
+ "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
+ ghost:
+ "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
+ link: "text-primary underline-offset-4 hover:underline",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Badge({
+ className,
+ variant = "default",
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
+ const Comp = asChild ? Slot.Root : "span"
+
+ return (
+ <Comp
+ data-slot="badge"
+ data-variant={variant}
+ className={cn(badgeVariants({ variant }), className)}
+ {...props}
+ />
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx
new file mode 100644
index 0000000..45ddfa0
--- /dev/null
+++ b/src/components/ui/card.tsx
@@ -0,0 +1,103 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+function Card({
+ className,
+ size = "default",
+ ...props
+}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
+ return (
+ <div
+ data-slot="card"
+ data-size={size}
+ className={cn(
+ "group/card flex flex-col gap-4 overflow-hidden rounded-none bg-card py-4 text-xs/relaxed text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-header"
+ className={cn(
+ "group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-title"
+ className={cn(
+ "font-heading text-sm font-medium group-data-[size=sm]/card:text-sm",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-description"
+ className={cn("text-xs/relaxed text-muted-foreground", className)}
+ {...props}
+ />
+ )
+}
+
+function CardAction({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-action"
+ className={cn(
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+function CardContent({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-content"
+ className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
+ {...props}
+ />
+ )
+}
+
+function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+ <div
+ data-slot="card-footer"
+ className={cn(
+ "flex items-center rounded-none border-t p-4 group-data-[size=sm]/card:p-3",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardAction,
+ CardDescription,
+ CardContent,
+}
diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx
new file mode 100644
index 0000000..ca11501
--- /dev/null
+++ b/src/components/ui/separator.tsx
@@ -0,0 +1,26 @@
+import * as React from "react"
+import { Separator as SeparatorPrimitive } from "radix-ui"
+
+import { cn } from "@/lib/utils"
+
+function Separator({
+ className,
+ orientation = "horizontal",
+ decorative = true,
+ ...props
+}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
+ return (
+ <SeparatorPrimitive.Root
+ data-slot="separator"
+ decorative={decorative}
+ orientation={orientation}
+ className={cn(
+ "shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
+ className
+ )}
+ {...props}
+ />
+ )
+}
+
+export { Separator }
diff --git a/src/content/blog/getting-started-astro.md b/src/content/blog/getting-started-astro.md
new file mode 100644
index 0000000..fcddbcc
--- /dev/null
+++ b/src/content/blog/getting-started-astro.md
@@ -0,0 +1,9 @@
+---
+title: "Getting Started with Astro"
+description: "Basic Astro Blog Post"
+publishDate: 2026-03-21
+author: "Iwan R. Ingman"
+---
+
+# Test Blog Post
+This is a test Astro blog post. \ No newline at end of file
diff --git a/src/content/config.ts b/src/content/config.ts
new file mode 100644
index 0000000..3886d49
--- /dev/null
+++ b/src/content/config.ts
@@ -0,0 +1,14 @@
+import { defineCollection, z } from 'astro:content';
+
+const blog = defineCollection({
+ schema: z.object({
+ title: z.string(),
+ description: z.string(),
+ publishDate: z.coerce.date(),
+ author: z.string(),
+ }),
+});
+
+export const collections = {
+ blog,
+};
diff --git a/src/pages/index.astro b/src/pages/index.astro
index ad15f25..9d04eae 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,17 +1,22 @@
---
import Layout from "@/layouts/main.astro"
import { Button } from "@/components/ui/button"
+import BlogPosts from "@/components/BlogPosts.astro"
---
<Layout>
- <div class="flex min-h-svh p-6">
- <div class="flex max-w-md min-w-0 flex-col gap-4 text-sm leading-loose">
- <div>
- <h1 class="font-medium">Project ready!</h1>
- <p>You may now add components and start building.</p>
- <p>We've already added the button component for you.</p>
- <Button client:load className="mt-2">Button</Button>
- </div>
+ <main class="flex flex-col min-h-svh p-6 items-center">
+ <!-- Title -->
+ <div class="flex flex-col items-center">
+ <h1 class="text-4xl font-bold text-center mt-10">Iwan Ingman's Blog</h1>
+ <p class="text-lg text-center text-muted-foreground mt-4">
+ This is my blog.
+ </p>
</div>
- </div>
+
+ <!-- Content -->
+ <!-- List Astro posts -->
+ <BlogPosts />
+ </main>
</Layout>
+