aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authornolawz <amananudhiu@gmail.com>2026-03-03 04:04:23 +0530
committernolawz <amananudhiu@gmail.com>2026-03-03 04:04:23 +0530
commit45026eee4c93bbbe6141dd23a664abe6e9ff3ca2 (patch)
tree3d5762cb539d88cf1fc464ab07f9ecd49182ad08 /src
parentd41eff9eccf66644c4c60f53cb516bf079937a28 (diff)
Added events section and components required for that
Diffstat (limited to 'src')
-rw-r--r--src/components/EventCard.astro63
-rw-r--r--src/components/EventNavigation.astro54
-rw-r--r--src/components/Footer.astro4
-rw-r--r--src/consts.ts6
-rw-r--r--src/content.config.ts16
-rw-r--r--src/content/events/beginning/index.mdx9
-rw-r--r--src/content/events/second/index.mdx8
-rw-r--r--src/lib/data-utils.ts54
-rw-r--r--src/lib/utils.ts10
-rw-r--r--src/pages/events/[...id].astro168
-rw-r--r--src/pages/events/[...page].astro60
-rw-r--r--src/pages/index.astro90
-rw-r--r--src/pages/types/[...id].astro53
-rw-r--r--src/pages/types/index.astro35
-rw-r--r--src/types.ts2
15 files changed, 577 insertions, 55 deletions
diff --git a/src/components/EventCard.astro b/src/components/EventCard.astro
new file mode 100644
index 0000000..b615c2d
--- /dev/null
+++ b/src/components/EventCard.astro
@@ -0,0 +1,63 @@
+---
+import { Badge } from '@/components/ui/badge'
+import { formatDate, iconMappings } from '@/lib/utils'
+import { Icon } from 'astro-icon/components'
+import { Image } from 'astro:assets'
+import type { CollectionEntry } from 'astro:content'
+import Link from './Link.astro'
+
+interface Props {
+ entry: CollectionEntry<'events'>
+}
+
+const { entry } = Astro.props
+const formattedDate = formatDate(entry.data.date)
+---
+
+<div
+ class="hover:bg-muted/50 rounded-xl border p-4 transition-colors duration-300 ease-in-out"
+>
+ <Link
+ href={`/${entry.collection}/${entry.id}`}
+ class="flex flex-col gap-4 sm:flex-row"
+ >
+ {
+ entry.data.image && (
+ <div class="w-full sm:max-w-3xs sm:shrink-0">
+ <Image
+ src={entry.data.image}
+ alt={entry.data.title}
+ width={1200}
+ height={630}
+ class="object-cover"
+ />
+ </div>
+ )
+ }
+
+ <div class="grow">
+ <h3 class="mb-1 text-lg font-medium">{entry.data.title}</h3>
+ <p class="text-muted-foreground mb-2 text-sm">{entry.data.description}</p>
+
+ <div
+ class="text-muted-foreground mb-2 flex flex-wrap items-center gap-x-2 text-xs"
+ >
+ <span>{formattedDate}</span>
+ </div>
+
+ {
+ entry.data.type && (
+ <div class="flex flex-wrap gap-2">
+ <Badge variant="muted" className="flex items-center gap-x-1">
+ <Icon
+ name={iconMappings[entry.data.type] || 'lucide:hash'}
+ class="size-3"
+ />
+ {entry.data.type}
+ </Badge>
+ </div>
+ )
+ }
+ </div>
+ </Link>
+</div>
diff --git a/src/components/EventNavigation.astro b/src/components/EventNavigation.astro
new file mode 100644
index 0000000..f1b76ce
--- /dev/null
+++ b/src/components/EventNavigation.astro
@@ -0,0 +1,54 @@
+---
+import Link from '@/components/Link.astro'
+import { buttonVariants } from '@/components/ui/button'
+import { cn } from '@/lib/utils'
+import { Icon } from 'astro-icon/components'
+
+const { newerEvent, olderEvent } = Astro.props
+---
+
+<nav class={cn('col-start-2 grid grid-cols-1 gap-4', 'sm:grid-cols-2')}>
+ <Link
+ href={olderEvent ? `/events/${olderEvent.id}#event-title` : '#'}
+ class={cn(
+ buttonVariants({ variant: 'outline' }),
+ 'rounded-lg group flex items-center justify-start size-full',
+ !olderEvent && 'pointer-events-none opacity-50 cursor-not-allowed',
+ )}
+ aria-disabled={!olderEvent}
+ >
+ <Icon
+ name="lucide:arrow-left"
+ class="mr-2 size-4 transition-transform group-hover:-translate-x-1"
+ />
+ <div class="flex flex-col items-start overflow-hidden text-wrap">
+ <span class="text-muted-foreground text-left text-xs">
+ Previous Event
+ </span>
+ <span class="w-full text-left text-sm text-balance text-ellipsis">
+ {olderEvent?.data.title || "You're at the oldest event!"}
+ </span>
+ </div>
+ </Link>
+
+ <Link
+ href={newerEvent ? `/events/${newerEvent.id}#event-title` : '#'}
+ class={cn(
+ buttonVariants({ variant: 'outline' }),
+ 'rounded-lg group flex items-center justify-end size-full',
+ !newerEvent && 'pointer-events-none opacity-50 cursor-not-allowed',
+ )}
+ aria-disabled={!newerEvent}
+ >
+ <div class="flex flex-col items-end overflow-hidden text-wrap">
+ <span class="text-muted-foreground text-right text-xs"> Next Event </span>
+ <span class="w-full text-right text-sm text-balance text-ellipsis">
+ {newerEvent?.data.title || "You're at the newest event!"}
+ </span>
+ </div>
+ <Icon
+ name="lucide:arrow-right"
+ class="ml-2 size-4 transition-transform group-hover:translate-x-1"
+ />
+ </Link>
+</nav>
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
index 7ff1e50..6b37226 100644
--- a/src/components/Footer.astro
+++ b/src/components/Footer.astro
@@ -15,10 +15,10 @@ import SocialIcons from './SocialIcons.astro'
<Separator orientation="vertical" className="hidden h-4! sm:block" />
<p class="text-muted-foreground text-sm">
Made with 🤍 by <Link
- href="https://github.com/jktrn"
+ href="https://codeberg.org/fosscet/"
class="text-foreground"
external
- underline>enscribe</Link
+ underline>fosscet</Link
>!
</p>
</div>
diff --git a/src/consts.ts b/src/consts.ts
index 53d9f3a..c66ef3e 100644
--- a/src/consts.ts
+++ b/src/consts.ts
@@ -8,7 +8,9 @@ export const SITE: Site = {
author: 'fosscet',
locale: 'en-US',
featuredPostCount: 2,
+ featuredEventsCount: 2,
postsPerPage: 3,
+ eventsPerPage: 3,
}
export const NAV_LINKS: SocialLink[] = [
@@ -24,6 +26,10 @@ export const NAV_LINKS: SocialLink[] = [
href: '/about',
label: 'about',
},
+ {
+ href: '/events',
+ label: 'events',
+ },
]
export const SOCIAL_LINKS: SocialLink[] = [
diff --git a/src/content.config.ts b/src/content.config.ts
index ed8186c..1c01708 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -1,6 +1,20 @@
import { glob } from 'astro/loaders'
import { defineCollection, z } from 'astro:content'
+const events = defineCollection({
+ loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/events' }),
+ schema: ({ image }) =>
+ z.object({
+ title: z.string(),
+ description: z.string(),
+ date: z.coerce.date(),
+ order: z.number().optional(),
+ image: image().optional(),
+ type: z.string().optional(),
+ draft: z.boolean().optional(),
+ }),
+})
+
const blog = defineCollection({
loader: glob({ pattern: '**/*.{md,mdx}', base: './src/content/blog' }),
schema: ({ image }) =>
@@ -46,4 +60,4 @@ const projects = defineCollection({
}),
})
-export const collections = { blog, authors, projects }
+export const collections = { events, blog, authors, projects }
diff --git a/src/content/events/beginning/index.mdx b/src/content/events/beginning/index.mdx
new file mode 100644
index 0000000..7427578
--- /dev/null
+++ b/src/content/events/beginning/index.mdx
@@ -0,0 +1,9 @@
+---
+title: '2024 Event'
+description: 'This is a dummy event conducted in the year 2024.'
+date: 2024-06-01
+type: 'workshop'
+---
+
+
+Just for testing purposes only
diff --git a/src/content/events/second/index.mdx b/src/content/events/second/index.mdx
new file mode 100644
index 0000000..79661e6
--- /dev/null
+++ b/src/content/events/second/index.mdx
@@ -0,0 +1,8 @@
+---
+title: '2025 Competition'
+description: 'This is a competition event conducted in 2025.'
+date: 2025-12-01
+type: 'competition'
+---
+
+I will prbbly create a component for adding the result of competition in the near future
diff --git a/src/lib/data-utils.ts b/src/lib/data-utils.ts
index f6e566a..ab0176f 100644
--- a/src/lib/data-utils.ts
+++ b/src/lib/data-utils.ts
@@ -21,6 +21,25 @@ export async function getAllPostsAndSubposts(): Promise<
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
}
+export async function getAllEvents(): Promise<CollectionEntry<'events'>[]> {
+ const events = await getCollection('events')
+ return events
+ .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
+}
+
+export function groupEventsByYear(
+ events: CollectionEntry<'events'>[],
+): Record<string, CollectionEntry<'events'>[]> {
+ return events.reduce(
+ (acc: Record<string, CollectionEntry<'events'>[]>, post) => {
+ const year = post.data.date.getFullYear().toString()
+ ;(acc[year] ??= []).push(post)
+ return acc
+ },
+ {},
+ )
+}
+
export async function getAllProjects(): Promise<CollectionEntry<'projects'>[]> {
const projects = await getCollection('projects')
return projects.sort((a, b) => {
@@ -40,6 +59,14 @@ export async function getAllTags(): Promise<Map<string, number>> {
}, new Map<string, number>())
}
+export async function getAllTypes(): Promise<Map<string, number>> {
+ const events = await getAllEvents()
+ return events.reduce((acc, event) => {
+ acc.set(event.data.type, (acc.get(event.data.type) || 0) + 1)
+ return acc
+ }, new Map<string, number>())
+}
+
export async function getAdjacentPosts(currentId: string): Promise<{
newer: CollectionEntry<'blog'> | null
older: CollectionEntry<'blog'> | null
@@ -99,6 +126,19 @@ export async function getAdjacentPosts(currentId: string): Promise<{
}
}
+export async function getAdjacentEvents(currentId: string): Promise<{
+ newer: CollectionEntry<'events'> | null
+ older: CollectionEntry<'events'> | null
+ parent: CollectionEntry<'events'> | null
+}> {
+ const allEvents = await getAllEvents()
+ const currentIndex = allEvents.findIndex((event) => event.id === currentId)
+ const newer = currentIndex > 0 ? allEvents[currentIndex - 1] : null
+ const older =
+ currentIndex < allEvents.length - 1 ? allEvents[currentIndex + 1] : null
+ return { newer, older, parent: null }
+}
+
export async function getPostsByAuthor(
authorId: string,
): Promise<CollectionEntry<'blog'>[]> {
@@ -113,6 +153,13 @@ export async function getPostsByTag(
return posts.filter((post) => post.data.tags?.includes(tag))
}
+export async function getEventsByType(
+ type: string,
+): Promise<CollectionEntry<'events'>[]> {
+ const events = await getAllEvents()
+ return events.filter((event) => event.data.type === type)
+}
+
export async function getRecentPosts(
count: number,
): Promise<CollectionEntry<'blog'>[]> {
@@ -120,6 +167,13 @@ export async function getRecentPosts(
return posts.slice(0, count)
}
+export async function getRecentEvents(
+ count: number,
+): Promise<CollectionEntry<'events'>[]> {
+ const events = await getAllEvents()
+ return events.slice(0, count)
+}
+
export async function getSortedTags(): Promise<
{ tag: string; count: number }[]
> {
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index d4d8119..e2e005b 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -35,3 +35,13 @@ export function getHeadingMargin(depth: number): string {
}
return margins[depth] || ''
}
+export function capitalizeFirstLetter(val: string): string {
+ return String(val).charAt(0).toUpperCase() + String(val).slice(1);
+}
+
+export const iconMappings = {
+ "workshop": "lucide:hammer",
+ "competition": "lucide:trophy",
+ "game": "lucide:gamepad-2",
+ "quiz": "lucide:brain"
+}
diff --git a/src/pages/events/[...id].astro b/src/pages/events/[...id].astro
new file mode 100644
index 0000000..44d8ec4
--- /dev/null
+++ b/src/pages/events/[...id].astro
@@ -0,0 +1,168 @@
+---
+import Breadcrumbs from '@/components/Breadcrumbs.astro'
+import EventNavigation from '@/components/EventNavigation.astro'
+import TOCHeader from '@/components/TOCHeader.astro'
+import TOCSidebar from '@/components/TOCSidebar.astro'
+import { badgeVariants } from '@/components/ui/badge'
+import { Button } from '@/components/ui/button'
+import Layout from '@/layouts/Layout.astro'
+import {
+ getAdjacentEvents,
+ getAllEvents,
+ getTOCSections,
+} from '@/lib/data-utils'
+import { formatDate, iconMappings } from '@/lib/utils'
+import { Icon } from 'astro-icon/components'
+import { Image } from 'astro:assets'
+import { render } from 'astro:content'
+
+export async function getStaticPaths() {
+ const events = await getAllEvents()
+ return events.map((event) => ({
+ params: { id: event.id },
+ props: event,
+ }))
+}
+
+const event = Astro.props
+const currentPostId = Astro.params.id
+const navigation = await getAdjacentEvents(currentPostId)
+const { Content, headings } = await render(event)
+
+const tocSections = await getTOCSections(currentPostId)
+---
+
+<Layout>
+ {
+ headings?.length > 0 &&
+ !(headings.length === 1 && headings[0].text === event.data.title) && (
+ <TOCHeader slot="table-of-contents" headings={headings} />
+ )
+ }
+
+ <section
+ class="grid grid-cols-[minmax(0px,1fr)_min(calc(var(--breakpoint-md)-2rem),100%)_minmax(0px,1fr)] gap-y-6"
+ >
+ <div class="col-start-2">
+ <Breadcrumbs
+ items={[{ href: '/events', label: 'Events', icon: 'lucide:calendar' }]}
+ />
+ </div>
+
+ {
+ event.data.image && (
+ <Image
+ src={event.data.image}
+ alt={event.data.title}
+ width={1200}
+ height={630}
+ class="col-span-full mx-auto w-full max-w-5xl object-cover"
+ />
+ )
+ }
+
+ <section class="col-start-2 flex flex-col gap-y-6 text-center">
+ <div class="flex flex-col">
+ <h1
+ class="mb-2 scroll-mt-31 text-3xl leading-tight font-medium sm:text-4xl"
+ id="event-title"
+ >
+ {event.data.title}
+ </h1>
+
+ <div
+ class="text-muted-foreground divide-border mb-4 flex flex-col items-center justify-center divide-y text-xs sm:flex-row sm:flex-wrap sm:divide-x sm:divide-y-0 sm:text-sm"
+ >
+ <div
+ class="flex w-full items-center justify-center gap-2 py-2 sm:w-fit sm:px-2 sm:py-0 first:sm:pl-0 last:sm:pr-0"
+ >
+ <span>{formatDate(event.data.date)}</span>
+ </div>
+ </div>
+ <div class="flex flex-wrap justify-center gap-2">
+ {
+ event.data.type && (
+ <a
+ href={`/types/${event.data.type}`}
+ class={badgeVariants({ variant: 'muted' })}
+ >
+ <Icon
+ name={iconMappings[event.data.type] || 'lucide:hash'}
+ class="size-3"
+ />
+ {event.data.type}
+ </a>
+ )
+ }
+ </div>
+ </div>
+ </section>
+
+ {
+ tocSections.length > 0 && (
+ <TOCSidebar sections={tocSections} currentPostId={currentPostId} />
+ )
+ }
+
+ <article class="prose col-start-2 max-w-none">
+ <Content />
+ </article>
+
+ <EventNavigation
+ newerEvent={navigation.newer}
+ olderEvent={navigation.older}
+ />
+ </section>
+
+ <Button
+ variant="outline"
+ size="icon"
+ className="group fixed right-8 bottom-8 z-50 hidden"
+ id="scroll-to-top"
+ title="Scroll to top"
+ aria-label="Scroll to top"
+ >
+ <Icon
+ name="lucide:arrow-up"
+ class="mx-auto size-4 transition-all group-hover:-translate-y-0.5"
+ />
+ </Button>
+
+ <script>
+ document.addEventListener('astro:page-load', () => {
+ const scrollToTopButton = document.getElementById('scroll-to-top')
+ const footer = document.querySelector('footer')
+
+ if (scrollToTopButton && footer) {
+ scrollToTopButton.addEventListener('click', () => {
+ window.scrollTo({ top: 0, behavior: 'smooth' })
+ })
+
+ window.addEventListener('scroll', () => {
+ const footerRect = footer.getBoundingClientRect()
+ const isFooterVisible = footerRect.top <= window.innerHeight
+
+ scrollToTopButton.classList.toggle(
+ 'hidden',
+ window.scrollY <= 300 || isFooterVisible,
+ )
+ })
+ }
+ })
+ </script>
+</Layout>
+
+<script>
+ const ensureKatexStyles = () => {
+ if (!document.querySelector('.katex')) return
+ if (document.querySelector('link[href*="katex.min.css"]')) return
+
+ const link = document.createElement('link')
+ link.rel = 'stylesheet'
+ link.href = 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css'
+ document.head.appendChild(link)
+ }
+
+ document.addEventListener('astro:page-load', ensureKatexStyles)
+ document.addEventListener('astro:after-swap', ensureKatexStyles)
+</script>
diff --git a/src/pages/events/[...page].astro b/src/pages/events/[...page].astro
new file mode 100644
index 0000000..2625ab0
--- /dev/null
+++ b/src/pages/events/[...page].astro
@@ -0,0 +1,60 @@
+---
+import Breadcrumbs from '@/components/Breadcrumbs.astro'
+import EventCard from '@/components/EventCard.astro'
+import PageHead from '@/components/PageHead.astro'
+import PaginationComponent from '@/components/ui/pagination'
+import { SITE } from '@/consts'
+import Layout from '@/layouts/Layout.astro'
+import { getAllEvents, groupEventsByYear } from '@/lib/data-utils'
+import type { PaginateFunction } from 'astro'
+
+export async function getStaticPaths({
+ paginate,
+}: {
+ paginate: PaginateFunction
+}) {
+ const allEvents = await getAllEvents()
+ return paginate(allEvents, { pageSize: SITE.eventsPerPage })
+}
+
+const { page } = Astro.props
+
+const eventsByYear = groupEventsByYear(page.data)
+const years = Object.keys(eventsByYear).sort(
+ (a, b) => parseInt(b) - parseInt(a),
+)
+---
+
+<Layout class="max-w-3xl">
+ <PageHead slot="head" title="Events" />
+ <Breadcrumbs
+ items={[
+ { label: 'Events', href: '/events', icon: 'lucide:calendar-range' },
+ { label: `Page ${page.currentPage}`, icon: 'lucide:calendar' },
+ ]}
+ />
+
+ <div class="flex min-h-[calc(100vh-18rem)] flex-col gap-y-8">
+ {
+ years.map((year) => (
+ <section class="flex flex-col gap-y-4">
+ <div class="font-medium">{year}</div>
+ <ul class="flex flex-col gap-4">
+ {eventsByYear[year].map((event) => (
+ <li>
+ <EventCard entry={event} />
+ </li>
+ ))}
+ </ul>
+ </section>
+ ))
+ }
+ </div>
+
+ <PaginationComponent
+ currentPage={page.currentPage}
+ totalPages={page.lastPage}
+ baseUrl="/events/"
+ client:load
+ />
+</Layout>
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 019283d..9549a7c 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,13 +1,15 @@
---
import BlogCard from '@/components/BlogCard.astro'
+import EventCard from '@/components/EventCard.astro'
import Link from '@/components/Link.astro'
import PageHead from '@/components/PageHead.astro'
import { buttonVariants } from '@/components/ui/button'
import { SITE } from '@/consts'
import Layout from '@/layouts/Layout.astro'
-import { getRecentPosts } from '@/lib/data-utils'
+import { getRecentPosts, getRecentEvents } from '@/lib/data-utils'
const blog = await getRecentPosts(SITE.featuredPostCount)
+const events = await getRecentEvents(SITE.featuredEventsCount)
---
<Layout class="max-w-3xl">
@@ -16,69 +18,53 @@ const blog = await getRecentPosts(SITE.featuredPostCount)
<div class="flex flex-col space-y-1.5 p-6">
<h3 class="text-3xl leading-none font-medium">er·u·dite</h3>
<p class="text-muted-foreground text-sm">
- /ˈer(y)əˌdīt/ &bull; <span class="font-medium">adjective</span>
+ /ˈer(y)əˌdīt/ &bull; <span class="font-medium">adjective</span>
</p>
</div>
- <div class="p-6 pt-0">
- <p class="text-muted-foreground mb-2 text-sm">
- astro-erudite is an opinionated, unstyled static blogging template built
- with <Link
- href="https://astro.build"
- class="text-foreground"
- external
- underline>Astro</Link
- >, <Link
- href="https://tailwindcss.com"
- class="text-foreground"
- external
- underline>Tailwind</Link
- >, and <Link
- href="https://ui.shadcn.com"
- class="text-foreground"
- external
- underline>shadcn/ui</Link
- >. Extraordinarily loosely based on the <Link
- href="https://astro-micro.vercel.app/"
- class="text-foreground"
- external
- underline>Astro Micro</Link
- > theme.
- </p>
- <p class="text-muted-foreground text-sm">
- To use this template, check out the <Link
- href="https://github.com/jktrn/astro-erudite"
- class="text-foreground"
- underline
- external>GitHub</Link
- > repository. To learn more about why this template exists, read this blog
- post: <Link
- href="/blog/the-state-of-static-blogs"
- class="text-foreground"
- underline>The State of Static Blogs in 2024</Link
- >.
- </p>
+ </section>
+ <section class="flex flex-col gap-y-4">
+ <h2 class="text-2xl font-medium">Latest events</h2>
+ <ul class="flex flex-col gap-y-4">
+ {
+ events.map((event) => (
+ <li>
+ <EventCard entry={event} />
+ </li>
+ ))
+ }
+ </ul>
+ <div class="flex justify-center">
+ <Link
+ href="/events"
+ class={buttonVariants({ variant: 'ghost' }) + ' group'}
+ >
+ See all posts <span
+ class="ml-1.5 transition-transform group-hover:translate-x-1"
+ >&rarr;</span
+ >
+ </Link>
</div>
</section>
<section class="flex flex-col gap-y-4">
<h2 class="text-2xl font-medium">Latest posts</h2>
<ul class="flex flex-col gap-y-4">
{
- blog.map((post) => (
- <li>
- <BlogCard entry={post} />
- </li>
- ))
+ blog.map((post) => (
+ <li>
+ <BlogCard entry={post} />
+ </li>
+ ))
}
</ul>
<div class="flex justify-center">
<Link
- href="/blog"
- class={buttonVariants({ variant: 'ghost' }) + ' group'}
- >
- See all posts <span
- class="ml-1.5 transition-transform group-hover:translate-x-1"
- >&rarr;</span
- >
+ href="/blog"
+ class={buttonVariants({ variant: 'ghost' }) + ' group'}
+ >
+ See all posts <span
+ class="ml-1.5 transition-transform group-hover:translate-x-1"
+ >&rarr;</span
+ >
</Link>
</div>
</section>
diff --git a/src/pages/types/[...id].astro b/src/pages/types/[...id].astro
new file mode 100644
index 0000000..26640a9
--- /dev/null
+++ b/src/pages/types/[...id].astro
@@ -0,0 +1,53 @@
+---
+import Breadcrumbs from '@/components/Breadcrumbs.astro'
+import PageHead from '@/components/PageHead.astro'
+import Layout from '@/layouts/Layout.astro'
+import { getAllTypes, getEventsByType } from '@/lib/data-utils'
+import { capitalizeFirstLetter, iconMappings } from '@/lib/utils'
+import EventCard from '../../components/EventCard.astro'
+
+export async function getStaticPaths() {
+ const typeMap = await getAllTypes()
+ const uniqueTags = Array.from(typeMap.keys())
+
+ return Promise.all(
+ uniqueTags.map(async (type) => {
+ const events = await getEventsByType(type)
+ return {
+ params: { id: type },
+ props: {
+ type,
+ events,
+ },
+ }
+ }),
+ )
+}
+
+const { type, events } = Astro.props
+---
+
+<Layout class="max-w-3xl">
+ <PageHead
+ slot="head"
+ title={`${capitalizeFirstLetter(type)} Events`}
+ description={`A collection of ${type} events.`}
+ noindex
+ />
+ <Breadcrumbs
+ items={[
+ { href: '/types', label: 'Event types', icon: 'lucide:calendar' },
+ { label: type, icon: iconMappings[type] || 'lucide:ticket' },
+ ]}
+ />
+
+ <ul class="flex flex-col gap-y-4">
+ {
+ events.map((event) => (
+ <li>
+ <EventCard entry={event} />
+ </li>
+ ))
+ }
+ </ul>
+</Layout>
diff --git a/src/pages/types/index.astro b/src/pages/types/index.astro
new file mode 100644
index 0000000..c269c22
--- /dev/null
+++ b/src/pages/types/index.astro
@@ -0,0 +1,35 @@
+---
+import Breadcrumbs from '@/components/Breadcrumbs.astro'
+import Link from '@/components/Link.astro'
+import PageHead from '@/components/PageHead.astro'
+import { badgeVariants } from '@/components/ui/badge'
+import Layout from '@/layouts/Layout.astro'
+import { getAllTypes } from '@/lib/data-utils'
+import { iconMappings } from '@/lib/utils'
+import { Icon } from 'astro-icon/components'
+
+const types = await getAllTypes()
+---
+
+<Layout class="max-w-3xl">
+ <PageHead slot="head" title="Tags" />
+ <Breadcrumbs items={[{ label: 'Tags', icon: 'lucide:tags' }]} />
+
+ <div class="flex flex-col gap-4">
+ <div class="flex flex-wrap gap-2">
+ {console.log([...types.entries()].map((a) => console.log(a)))}
+ {
+ [...types.entries()].map(([type, count]) => (
+ <Link
+ href={`/types/${type}`}
+ class={badgeVariants({ variant: 'muted' })}
+ >
+ <Icon name={iconMappings[type] || 'lucide:hash'} class="size-3" />
+ {type}
+ <span class="text-muted-foreground ml-1.5">({count})</span>
+ </Link>
+ ))
+ }
+ </div>
+ </div>
+</Layout>
diff --git a/src/types.ts b/src/types.ts
index b03a955..4fedcdd 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -5,7 +5,9 @@ export type Site = {
author: string
locale: string
featuredPostCount: number
+ featuredEventsCount: number
postsPerPage: number
+ eventsPerPage: number
}
export type SocialLink = {