mirror of
https://github.com/privacy-scaling-explorations/pse.dev.git
synced 2026-04-23 03:01:03 -04:00
feat: generate events page from Notion
This commit is contained in:
@@ -1,147 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { events } from '@/data/events/devcon-7'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTranslation } from '@/app/i18n/client'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Icons } from '@/components/icons'
|
||||
|
||||
const tableSection = cva('lg:grid lg:grid-cols-[200px_1fr_160px_20px] lg:gap-8')
|
||||
const tableSectionTitle = cva(
|
||||
'text-anakiwa-500 text-base lg:text-xs font-sans leading-5 tracking-[2.5px] uppercase font-bold lg:pb-3'
|
||||
)
|
||||
|
||||
const EventCard = ({ event = {}, speakers = [], location = '' }: any) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const getYouTubeEmbedURL = (url: string) => {
|
||||
const match = url.match(
|
||||
/(?:youtube\.com\/(?:watch\?v=|live\/)|youtu\.be\/)([^?&]+)/
|
||||
)
|
||||
return match ? `https://www.youtube.com/embed/${match[1]}` : null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-3',
|
||||
tableSection(),
|
||||
'py-4 px-6 lg:p-0 lg:pt-[30px] lg:pb-5 border-b border-b-tuatara-200'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 order-3 lg:order-1">
|
||||
<span className="text-sm text-tuatara-950 font-bold font-sans leading-5">
|
||||
{event?.date}
|
||||
</span>
|
||||
<div className="grid grid-cols-[1fr_16px] lg:grid-cols-1">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<Icons.time className="text-tuatara-500" />
|
||||
<span className="font-sans text-tuatara-500 text-xs lg:text-sm leading-5 font-normal">
|
||||
{event?.time}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-[6px] items-center">
|
||||
<Icons.eventLocation className="text-tuatara-500" />
|
||||
<span className="font-sans text-tuatara-500 text-xs lg:text-sm leading-5 font-normal">
|
||||
{location}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-wrap lg:flex-col gap-1 pt-1">
|
||||
{speakers?.map((speaker: any, index: number) => {
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
className="text-sm text-anakiwa-500 underline break-all"
|
||||
href={speaker.url ?? '#'}
|
||||
>
|
||||
{speaker.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start gap-[10px] lg:order-2 order-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href={event?.url ?? '#'}
|
||||
target="_blank"
|
||||
className="text-[22px] inline-flex leading-[24px] text-tuatara-950 underline font-display hover:text-anakiwa-500 font-bold duration-200"
|
||||
>
|
||||
{event?.title}
|
||||
</Link>
|
||||
<button
|
||||
className="lg:hidden flex"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen)
|
||||
}}
|
||||
>
|
||||
{isOpen ? (
|
||||
<Icons.minus
|
||||
className={cn(
|
||||
'size-5 ml-auto',
|
||||
'transition-transform duration-200'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Icons.plus
|
||||
className={cn(
|
||||
'size-5 ml-auto',
|
||||
'transition-transform duration-200'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'lg:max-h-none lg:opacity-100 lg:block',
|
||||
'transition-all duration-300 overflow-hidden',
|
||||
isOpen ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
|
||||
'lg:transition-none lg:overflow-visible'
|
||||
)}
|
||||
>
|
||||
<span className="text-base leading-6 text-tuatara-950 font-sans font-normal">
|
||||
{event?.description}
|
||||
</span>
|
||||
<div className="pt-2 flex lg:!hidden">
|
||||
{event?.youtubeLink && (
|
||||
<iframe
|
||||
width="100%"
|
||||
height="230"
|
||||
src={getYouTubeEmbedURL(event?.youtubeLink) ?? ''}
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '10px', overflow: 'hidden' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative lg:order-3 grid gap-5 pb-3 lg:pb-0 grid-cols-[1fr_32px] lg:grid-cols-1">
|
||||
<div className="hidden lg:flex flex-wrap lg:flex-col gap-1">
|
||||
{event?.youtubeLink && (
|
||||
<iframe
|
||||
width="240"
|
||||
height="140"
|
||||
src={getYouTubeEmbedURL(event?.youtubeLink) ?? ''}
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '10px', overflow: 'hidden' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="order-4 lg:flex hidden"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { EventCard } from '@/components/cards/event-card'
|
||||
import { tableSection } from '@/components/cards/event-card'
|
||||
import { tableSectionTitle } from '@/components/cards/event-card'
|
||||
|
||||
export const Devcon7Section = ({ lang }: any) => {
|
||||
const { t } = useTranslation(lang, 'devcon-7')
|
||||
|
||||
128
app/[lang]/events/page.tsx
Normal file
128
app/[lang]/events/page.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from '@/app/i18n/client'
|
||||
import { Icons } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useGetNotionEvents } from '@/hooks/useNotion'
|
||||
import { EventCard } from '@/components/cards/event-card'
|
||||
import { EventGridCard } from '@/components/cards/event-grid-card'
|
||||
import { AppContent } from '@/components/ui/app-content'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { format } from 'date-fns'
|
||||
type ViewMode = 'list' | 'grid'
|
||||
|
||||
export interface EventProps {
|
||||
event: {
|
||||
title: string
|
||||
description: string
|
||||
startDate: string | null
|
||||
endDate: string | null
|
||||
location: string
|
||||
link: string
|
||||
video?: string
|
||||
}
|
||||
speakers?: any[]
|
||||
location?: string
|
||||
}
|
||||
|
||||
export default function EventsPage({
|
||||
params: { lang },
|
||||
}: {
|
||||
params: { lang: string }
|
||||
}) {
|
||||
const [viewMode, setViewMode] = useState<ViewMode>('list')
|
||||
const { t } = useTranslation(lang, 'events')
|
||||
const { data: { events, page } = { events: [], page: {} }, isLoading } =
|
||||
useGetNotionEvents()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<div className="w-full bg-cover-gradient border-b border-tuatara-300">
|
||||
<AppContent className="flex flex-col gap-4 py-10 w-full">
|
||||
{isLoading ? (
|
||||
<div className="h-14 bg-gray-400 w-2/3 animate-pulse"></div>
|
||||
) : (
|
||||
<Label.PageTitle label={t(page.title)} />
|
||||
)}
|
||||
</AppContent>
|
||||
</div>
|
||||
<div className="mx-auto max-w-[950px] py-10 w-full">
|
||||
{!isLoading && (
|
||||
<div className="flex items-center justify-between mb-10">
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<Button
|
||||
variant={viewMode === 'list' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('list')}
|
||||
>
|
||||
<Icons.list className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={viewMode === 'grid' ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => setViewMode('grid')}
|
||||
>
|
||||
<Icons.grid className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div
|
||||
className={cn(
|
||||
'gap-6',
|
||||
viewMode === 'grid' ? 'grid grid-cols-1 ' : 'flex flex-col'
|
||||
)}
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
) : viewMode === 'list' ? (
|
||||
<div className="flex flex-col gap-10 relative">
|
||||
<div className="flex flex-col">
|
||||
{events?.map((event, index) => {
|
||||
const date =
|
||||
event?.endDate && event?.startDate
|
||||
? `${format(new Date(event?.startDate), 'MMM d')} - ${format(new Date(event?.endDate), 'MMM d')}`
|
||||
: event?.startDate
|
||||
? format(new Date(event?.startDate), 'MMM d')
|
||||
: ''
|
||||
|
||||
const time =
|
||||
event?.endDate && event?.startDate
|
||||
? `${format(new Date(event?.startDate), 'HH:mm')} - ${format(new Date(event?.endDate), 'HH:mm')}`
|
||||
: event?.startDate
|
||||
? format(new Date(event?.startDate), 'HH:mm')
|
||||
: ''
|
||||
|
||||
return (
|
||||
<EventCard
|
||||
key={index}
|
||||
event={{
|
||||
title: event.title,
|
||||
description: event.description,
|
||||
url: event.link,
|
||||
date,
|
||||
time,
|
||||
youtubeLink: event?.video,
|
||||
}}
|
||||
speakers={[]}
|
||||
location={event.location}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
{events?.map((event, index) => (
|
||||
<EventGridCard key={index} event={event} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
50
app/api/events/route.ts
Normal file
50
app/api/events/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Client } from '@notionhq/client'
|
||||
import { NextResponse } from 'next/server'
|
||||
|
||||
const notion = new Client({
|
||||
auth: process.env.NOTION_API_KEY,
|
||||
})
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const databaseInfo: any = await notion.databases.retrieve({
|
||||
database_id: process.env.NOTION_EVENTS_DATABASE_ID as string,
|
||||
})
|
||||
|
||||
const response = await notion.databases.query({
|
||||
database_id: process.env.NOTION_EVENTS_DATABASE_ID as string,
|
||||
sorts: [
|
||||
{
|
||||
property: 'Date',
|
||||
direction: 'ascending',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const page = {
|
||||
id: databaseInfo.id,
|
||||
title: databaseInfo?.title?.[0]?.plain_text || 'Untitled Document',
|
||||
description: databaseInfo?.description?.[0]?.plain_text || '',
|
||||
createdTime: databaseInfo?.created_time,
|
||||
lastEditedTime: databaseInfo?.last_edited_time,
|
||||
}
|
||||
|
||||
const events = response.results.map((page: any) => ({
|
||||
id: page.id,
|
||||
title: page.properties?.Title?.title[0]?.plain_text || '',
|
||||
startDate: page.properties?.Date?.date?.start || null,
|
||||
endDate: page.properties?.Date?.date?.end || null,
|
||||
description: page.properties?.Description?.rich_text[0]?.plain_text || '',
|
||||
location: page.properties?.Location?.rich_text[0]?.plain_text || '',
|
||||
speakers: page.properties?.Speakers?.rich_text[0]?.plain_text || '',
|
||||
status: page.properties?.Status?.select?.name || '',
|
||||
link: page.properties?.Link?.url || '',
|
||||
video: page.properties?.VideoURL?.url || '',
|
||||
}))
|
||||
|
||||
return NextResponse.json({ events, page })
|
||||
} catch (error: any) {
|
||||
console.error('Error fetching events:', error)
|
||||
return NextResponse.json({ error: error.message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Metadata } from 'next'
|
||||
import { siteConfig } from '@/config/site'
|
||||
|
||||
import { languages } from './i18n/settings'
|
||||
import ProviderWrapper from './provider-wrapper'
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return languages.map((language) => ({ language }))
|
||||
@@ -42,5 +43,5 @@ interface RootLayoutProps {
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: RootLayoutProps) {
|
||||
return <>{children}</>
|
||||
return <ProviderWrapper>{children}</ProviderWrapper>
|
||||
}
|
||||
|
||||
15
app/provider-wrapper.tsx
Normal file
15
app/provider-wrapper.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client'
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
export default function ProviderWrapper({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
144
components/cards/event-card.tsx
Normal file
144
components/cards/event-card.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cva } from 'class-variance-authority'
|
||||
import { useState } from 'react'
|
||||
import { Icons } from '@/components/icons'
|
||||
import Link from 'next/link'
|
||||
import { EventProps } from '@/app/[lang]/events/page'
|
||||
export const tableSection = cva(
|
||||
'lg:grid lg:grid-cols-[200px_1fr_160px_20px] lg:gap-8'
|
||||
)
|
||||
export const tableSectionTitle = cva(
|
||||
'text-anakiwa-500 text-base lg:text-xs font-sans leading-5 tracking-[2.5px] uppercase font-bold lg:pb-3'
|
||||
)
|
||||
|
||||
export const EventCard = ({ event, speakers = [], location = '' }: any) => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
const getYouTubeEmbedURL = (url: string) => {
|
||||
const match = url.match(
|
||||
/(?:youtube\.com\/(?:watch\?v=|live\/)|youtu\.be\/)([^?&]+)/
|
||||
)
|
||||
return match ? `https://www.youtube.com/embed/${match[1]}` : null
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col gap-3',
|
||||
tableSection(),
|
||||
'py-4 px-6 lg:p-0 lg:pt-[30px] lg:pb-5 border-b border-b-tuatara-200'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-1 order-3 lg:order-1">
|
||||
<span className="text-sm text-tuatara-950 font-bold font-sans leading-5">
|
||||
{event?.date ?? 'N.A.'}
|
||||
</span>
|
||||
<div className="grid grid-cols-[1fr_16px] lg:grid-cols-1">
|
||||
<div className="flex flex-col">
|
||||
{event?.time?.length > 0 && (
|
||||
<div className="flex items-center gap-[6px]">
|
||||
<Icons.time className="text-tuatara-500" />
|
||||
<span className="font-sans text-tuatara-500 text-xs lg:text-sm leading-5 font-normal">
|
||||
{event?.time ?? 'N.A.'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{location?.length > 0 && (
|
||||
<div className="flex gap-[6px] items-center">
|
||||
<Icons.eventLocation className="text-tuatara-500" />
|
||||
<span className="font-sans text-tuatara-500 text-xs lg:text-sm leading-5 font-normal">
|
||||
{location}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap lg:flex-col gap-1 pt-1">
|
||||
{speakers?.map((speaker: any, index: number) => {
|
||||
return (
|
||||
<Link
|
||||
key={index}
|
||||
className="text-sm text-anakiwa-500 underline break-all"
|
||||
href={speaker.url ?? '#'}
|
||||
>
|
||||
{speaker.label}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col justify-start gap-[10px] lg:order-2 order-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link
|
||||
href={event?.url ?? '#'}
|
||||
target="_blank"
|
||||
className="text-[22px] inline-flex leading-[24px] text-tuatara-950 underline font-display hover:text-anakiwa-500 font-bold duration-200"
|
||||
>
|
||||
{event?.title}
|
||||
</Link>
|
||||
<button
|
||||
className="lg:hidden flex"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen)
|
||||
}}
|
||||
>
|
||||
{isOpen ? (
|
||||
<Icons.minus
|
||||
className={cn(
|
||||
'size-5 ml-auto',
|
||||
'transition-transform duration-200'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Icons.plus
|
||||
className={cn(
|
||||
'size-5 ml-auto',
|
||||
'transition-transform duration-200'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'lg:max-h-none lg:opacity-100 lg:block',
|
||||
'transition-all duration-300 overflow-hidden',
|
||||
isOpen ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0',
|
||||
'lg:transition-none lg:overflow-visible'
|
||||
)}
|
||||
>
|
||||
<span className="text-base leading-6 text-tuatara-950 font-sans font-normal">
|
||||
{event?.description}
|
||||
</span>
|
||||
<div className="pt-2 flex lg:!hidden">
|
||||
{event?.youtubeLink && (
|
||||
<iframe
|
||||
width="100%"
|
||||
height="230"
|
||||
src={getYouTubeEmbedURL(event?.youtubeLink) ?? ''}
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '10px', overflow: 'hidden' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative lg:order-3 grid gap-5 pb-3 lg:pb-0 grid-cols-[1fr_32px] lg:grid-cols-1">
|
||||
<div className="hidden lg:flex flex-wrap lg:flex-col gap-1">
|
||||
{event?.youtubeLink && (
|
||||
<iframe
|
||||
width="240"
|
||||
height="140"
|
||||
src={getYouTubeEmbedURL(event?.youtubeLink) ?? ''}
|
||||
allowFullScreen
|
||||
style={{ borderRadius: '10px', overflow: 'hidden' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="order-4 lg:flex hidden"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
77
components/cards/event-grid-card.tsx
Normal file
77
components/cards/event-grid-card.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { format } from 'date-fns'
|
||||
import Link from 'next/link'
|
||||
import { Icons } from '@/components/icons'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { EventProps } from '@/app/[lang]/events/page'
|
||||
|
||||
export const EventGridCard = ({ event }: EventProps) => {
|
||||
const date =
|
||||
event.endDate && event.startDate
|
||||
? `${format(new Date(event?.startDate), 'MMM d')} - ${format(new Date(event?.endDate), 'MMM d')}`
|
||||
: event?.startDate
|
||||
? format(new Date(event?.startDate), 'MMM d')
|
||||
: ''
|
||||
|
||||
const time =
|
||||
event.endDate && event.startDate
|
||||
? `${format(new Date(event?.startDate), 'HH:mm')} - ${format(new Date(event?.endDate), 'HH:mm')}`
|
||||
: event?.startDate
|
||||
? format(new Date(event?.startDate), 'HH:mm')
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-white rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
|
||||
<div className="p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<h3 className="text-xl font-bold mb-2 text-tuatara-950">
|
||||
{event.title}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-2 mb-5">
|
||||
{(date || time) && (
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Icons.calendar className="w-4 h-4 mr-2" />
|
||||
{date && (
|
||||
<>
|
||||
<span>{date}</span>
|
||||
<span className="mx-2">•</span>
|
||||
</>
|
||||
)}
|
||||
{time && <span>{time}</span>}
|
||||
</div>
|
||||
)}
|
||||
{event.location && (
|
||||
<div className="flex items-center text-sm text-gray-600">
|
||||
<Icons.eventLocation className="w-4 h-4 mr-2" />
|
||||
<span>{event.location}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 line-clamp-4 mb-4">
|
||||
{event.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{event?.video && (
|
||||
<div className="flex px-6 pb-6 mt-auto">
|
||||
<Link
|
||||
href={event?.video}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="ml-auto"
|
||||
>
|
||||
<Button variant="outline">
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Watch Video</span>
|
||||
<Icons.externalUrl className="h-4 w-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,42 @@ import {
|
||||
export type Icon = LucideIcon
|
||||
|
||||
export const Icons = {
|
||||
grid: (props: any) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<rect width="7" height="7" x="3" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="3" rx="1" />
|
||||
<rect width="7" height="7" x="14" y="14" rx="1" />
|
||||
<rect width="7" height="7" x="3" y="14" rx="1" />
|
||||
</svg>
|
||||
),
|
||||
list: (props: any) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
{...props}
|
||||
>
|
||||
<line x1="8" y1="6" x2="21" y2="6" />
|
||||
<line x1="8" y1="12" x2="21" y2="12" />
|
||||
<line x1="8" y1="18" x2="21" y2="18" />
|
||||
<line x1="3" y1="6" x2="3.01" y2="6" />
|
||||
<line x1="3" y1="12" x2="3.01" y2="12" />
|
||||
<line x1="3" y1="18" x2="3.01" y2="18" />
|
||||
</svg>
|
||||
),
|
||||
sun: SunMedium,
|
||||
time: (props: LucideProps) => (
|
||||
<svg
|
||||
|
||||
16
hooks/useNotion.ts
Normal file
16
hooks/useNotion.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { EventProps } from '@/app/[lang]/events/page'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
export function useGetNotionEvents() {
|
||||
return useQuery<{ events: EventProps['event'][]; page: any }>({
|
||||
queryKey: ['events'],
|
||||
queryFn: async () => {
|
||||
const response = await fetch('/api/events')
|
||||
const data = await response.json()
|
||||
return {
|
||||
events: data.events,
|
||||
page: data.page,
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
6
locales/en/events.json
Normal file
6
locales/en/events.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"title": "Upcoming Events",
|
||||
"subtitle": "Join us at our upcoming events and connect with the community",
|
||||
"errorLoading": "Error loading events. Please try again later.",
|
||||
"noEvents": "No upcoming events at the moment."
|
||||
}
|
||||
@@ -21,14 +21,17 @@
|
||||
"dependencies": {
|
||||
"@discordjs/rest": "2.0.0",
|
||||
"@next/mdx": "^13.5.0",
|
||||
"@notionhq/client": "^2.2.17",
|
||||
"@radix-ui/react-accordion": "^1.1.2",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@tanstack/react-query": "^5.67.3",
|
||||
"accept-language": "^3.0.18",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"discord.js": "14.4.0",
|
||||
"dotenv": "^16.4.4",
|
||||
"framer-motion": "^10.12.17",
|
||||
|
||||
97
yarn.lock
97
yarn.lock
@@ -624,6 +624,14 @@
|
||||
"@nodelib/fs.scandir" "2.1.5"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@notionhq/client@^2.2.17":
|
||||
version "2.2.17"
|
||||
resolved "https://registry.yarnpkg.com/@notionhq/client/-/client-2.2.17.tgz#2086dd4fca370736df8fd5e69c2615a2d496ccb9"
|
||||
integrity sha512-whkUc2RFAk7Vo93todfwsK6bxEHrBg4JSUHN+8cvopZGKsnU8aVL4JtJ6W2cexRz0Bp0AfznHsY7eD8/vNgMCw==
|
||||
dependencies:
|
||||
"@types/node-fetch" "^2.5.10"
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
"@pkgjs/parseargs@^0.11.0":
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||
@@ -948,6 +956,18 @@
|
||||
"@swc/counter" "^0.1.3"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@tanstack/query-core@5.67.3":
|
||||
version "5.67.3"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.67.3.tgz#222795762c584d572f6f41bd4eb97f86f46f2eea"
|
||||
integrity sha512-pq76ObpjcaspAW4OmCbpXLF6BCZP2Zr/J5ztnyizXhSlNe7fIUp0QKZsd0JMkw9aDa+vxDX/OY7N+hjNY/dCGg==
|
||||
|
||||
"@tanstack/react-query@^5.67.3":
|
||||
version "5.67.3"
|
||||
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.67.3.tgz#4a1a1a54828465ab6bc421e66dc7ebd88191e851"
|
||||
integrity sha512-u/n2HsQeH1vpZIOzB/w2lqKlXUDUKo6BxTdGXSMvNzIq5MHYFckRMVuFABp+QB7RN8LFXWV6X1/oSkuDq+MPIA==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.67.3"
|
||||
|
||||
"@tokenizer/token@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276"
|
||||
@@ -1017,6 +1037,14 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78"
|
||||
integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
|
||||
|
||||
"@types/node-fetch@^2.5.10":
|
||||
version "2.6.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.12.tgz#8ab5c3ef8330f13100a7479e2cd56d3386830a03"
|
||||
integrity sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^4.0.0"
|
||||
|
||||
"@types/node@*":
|
||||
version "22.13.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.2.tgz#6f401c5ccadac75354f5652128e9fcc3b0cf23b7"
|
||||
@@ -1409,6 +1437,11 @@ async-function@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b"
|
||||
integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
autoprefixer@^10.4.14:
|
||||
version "10.4.20"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b"
|
||||
@@ -1657,6 +1690,13 @@ colorette@^2.0.20:
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
comma-separated-tokens@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
|
||||
@@ -1760,6 +1800,11 @@ data-view-byte-offset@^1.0.1:
|
||||
es-errors "^1.3.0"
|
||||
is-data-view "^1.0.1"
|
||||
|
||||
date-fns@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-4.1.0.tgz#64b3d83fff5aa80438f5b1a633c2e83b8a1c2d14"
|
||||
integrity sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==
|
||||
|
||||
debug@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
@@ -1804,6 +1849,11 @@ define-properties@^1.1.3, define-properties@^1.2.1:
|
||||
has-property-descriptors "^1.0.0"
|
||||
object-keys "^1.1.1"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
dequal@^2.0.0:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||
@@ -2456,6 +2506,16 @@ foreground-child@^3.1.0:
|
||||
cross-spawn "^7.0.0"
|
||||
signal-exit "^4.0.1"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.2.tgz#35cabbdd30c3ce73deb2c42d3c8d3ed9ca51794c"
|
||||
integrity sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fraction.js@^4.3.7:
|
||||
version "4.3.7"
|
||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
||||
@@ -3778,6 +3838,18 @@ micromatch@^4.0.8:
|
||||
braces "^3.0.3"
|
||||
picomatch "^2.3.1"
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
mimic-fn@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"
|
||||
@@ -3883,6 +3955,13 @@ next@14:
|
||||
"@next/swc-win32-ia32-msvc" "14.2.24"
|
||||
"@next/swc-win32-x64-msvc" "14.2.24"
|
||||
|
||||
node-fetch@^2.6.1:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
|
||||
integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
|
||||
dependencies:
|
||||
whatwg-url "^5.0.0"
|
||||
|
||||
node-releases@^2.0.19:
|
||||
version "2.0.19"
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
|
||||
@@ -5103,6 +5182,11 @@ touch@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.1.tgz#097a23d7b161476435e5c1344a95c0f75b4a5694"
|
||||
integrity sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==
|
||||
|
||||
tr46@~0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
trim-lines@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338"
|
||||
@@ -5390,6 +5474,19 @@ void-elements@3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
|
||||
integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==
|
||||
|
||||
webidl-conversions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
|
||||
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
|
||||
|
||||
whatwg-url@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
|
||||
integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
|
||||
dependencies:
|
||||
tr46 "~0.0.3"
|
||||
webidl-conversions "^3.0.0"
|
||||
|
||||
which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e"
|
||||
|
||||
Reference in New Issue
Block a user