feat: generate events page from Notion

This commit is contained in:
Kalidou Diagne
2025-03-13 15:11:21 +00:00
parent aa79059661
commit ae5f331e01
12 changed files with 577 additions and 138 deletions

View File

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

View File

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

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

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

View File

@@ -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
View 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
View 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."
}

View File

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

View File

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