Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 3 additions & 137 deletions app/[lang]/devcon-7/sections/Devcon7Section.tsx
Original file line number Diff line number Diff line change
@@ -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')
Expand Down
134 changes: 134 additions & 0 deletions app/[lang]/events/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
'use client'

import { useEffect, 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 {
data: { events, page } = { events: [], page: {} },
isLoading,
refresh,
} = useGetNotionEvents()

useEffect(() => {
refresh()
}, [])

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>
) : (
page?.title && <Label.PageTitle label={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>
)
}
64 changes: 64 additions & 0 deletions app/api/events/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Client } from '@notionhq/client'
import { NextResponse } from 'next/server'

export async function GET(req: Request) {
const notion = new Client({
auth: process.env.NOTION_API_KEY,
})

try {
const { searchParams } = new URL(req.url)
const forceRefresh = searchParams.get('forceRefresh')

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 || '',
attachments: page.properties?.Attachments?.files || [],
}))

const responseJson = NextResponse.json({ events, page })

responseJson.headers.set(
'Cache-Control',
'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0, s-maxage=0, stale-while-revalidate=0'
)
responseJson.headers.set('Vercel-CDN-Cache-Control', 'no-store')
responseJson.headers.set('Pragma', 'no-cache')
responseJson.headers.set('Expires', '0')

return responseJson
} catch (error: any) {
console.error('Error fetching events:', error)
return NextResponse.json({ error: error.message }, { status: 500 })
}
}
3 changes: 2 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
Expand Down Expand Up @@ -42,5 +43,5 @@ interface RootLayoutProps {
}

export default function RootLayout({ children }: RootLayoutProps) {
return <>{children}</>
return <ProviderWrapper>{children}</ProviderWrapper>
}
15 changes: 15 additions & 0 deletions app/provider-wrapper.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}
Loading