+
setShareDialogOpen(true)}
>
@@ -60,7 +60,7 @@ export function SidebarActions({
setDeleteDialogOpen(true)}
>
diff --git a/components/sidebar-mobile.tsx b/components/sidebar-mobile.tsx
index 507f83e..82f0aa5 100644
--- a/components/sidebar-mobile.tsx
+++ b/components/sidebar-mobile.tsx
@@ -20,7 +20,10 @@ export function SidebarMobile({ children }: SidebarMobileProps) {
Toggle Sidebar
-
+
{children}
diff --git a/components/signup-form.tsx b/components/signup-form.tsx
new file mode 100644
index 0000000..a2e9aef
--- /dev/null
+++ b/components/signup-form.tsx
@@ -0,0 +1,94 @@
+'use client'
+
+import { useFormState, useFormStatus } from 'react-dom'
+import { signup } from '@/app/signup/actions'
+import Link from 'next/link'
+import { useEffect } from 'react'
+import { toast } from 'sonner'
+import { IconSpinner } from './ui/icons'
+import { useRouter } from 'next/navigation'
+
+export default function SignupForm() {
+ const router = useRouter()
+ const [result, dispatch] = useFormState(signup, undefined)
+
+ useEffect(() => {
+ if (result) {
+ if (result.type === 'error') {
+ toast.error(result.message)
+ } else {
+ router.refresh()
+ toast.success(result.message)
+ }
+ }
+ }, [result, router])
+
+ return (
+
+ )
+}
+
+function LoginButton() {
+ const { pending } = useFormStatus()
+
+ return (
+
+ {pending ? : 'Create account'}
+
+ )
+}
diff --git a/components/stocks/event.tsx b/components/stocks/event.tsx
new file mode 100644
index 0000000..2b7d7ab
--- /dev/null
+++ b/components/stocks/event.tsx
@@ -0,0 +1,30 @@
+import { format, parseISO } from 'date-fns'
+
+interface Event {
+ date: string
+ headline: string
+ description: string
+}
+
+export function Events({ props: events }: { props: Event[] }) {
+ return (
+
+ {events.map(event => (
+
+
+ {format(parseISO(event.date), 'dd LLL, yyyy')}
+
+
+ {event.headline}
+
+
+ {event.description.slice(0, 70)}...
+
+
+ ))}
+
+ )
+}
diff --git a/components/stocks/events-skeleton.tsx b/components/stocks/events-skeleton.tsx
new file mode 100644
index 0000000..8b109b2
--- /dev/null
+++ b/components/stocks/events-skeleton.tsx
@@ -0,0 +1,31 @@
+const placeholderEvents = [
+ {
+ date: '2022-10-01',
+ headline: 'NVIDIA releases new AI-powered graphics card',
+ description:
+ 'NVIDIA unveils the latest graphics card infused with AI capabilities, revolutionizing gaming and rendering experiences.'
+ }
+]
+
+export const EventsSkeleton = ({ events = placeholderEvents }) => {
+ return (
+
+ {placeholderEvents.map(event => (
+
+
+ {event.date}
+
+
+ {event.headline}
+
+
+ {event.description.slice(0, 70)}...
+
+
+ ))}
+
+ )
+}
diff --git a/components/stocks/index.tsx b/components/stocks/index.tsx
new file mode 100644
index 0000000..15d167c
--- /dev/null
+++ b/components/stocks/index.tsx
@@ -0,0 +1,38 @@
+'use client'
+
+import dynamic from 'next/dynamic'
+import { StockSkeleton } from './stock-skeleton'
+import { StocksSkeleton } from './stocks-skeleton'
+import { EventsSkeleton } from './events-skeleton'
+
+export { spinner } from './spinner'
+export { BotCard, BotMessage, SystemMessage } from './message'
+
+const Stock = dynamic(() => import('./stock').then(mod => mod.Stock), {
+ ssr: false,
+ loading: () =>
+})
+
+const Purchase = dynamic(
+ () => import('./stock-purchase').then(mod => mod.Purchase),
+ {
+ ssr: false,
+ loading: () => (
+
+ Loading stock info...
+
+ )
+ }
+)
+
+const Stocks = dynamic(() => import('./stocks').then(mod => mod.Stocks), {
+ ssr: false,
+ loading: () =>
+})
+
+const Events = dynamic(() => import('./event').then(mod => mod.Events), {
+ ssr: false,
+ loading: () =>
+})
+
+export { Stock, Purchase, Stocks, Events }
diff --git a/components/stocks/message.tsx b/components/stocks/message.tsx
new file mode 100644
index 0000000..bba6724
--- /dev/null
+++ b/components/stocks/message.tsx
@@ -0,0 +1,134 @@
+'use client'
+
+import { IconOpenAI, IconUser } from '@/components/ui/icons'
+import { cn } from '@/lib/utils'
+import { spinner } from './spinner'
+import { CodeBlock } from '../ui/codeblock'
+import { MemoizedReactMarkdown } from '../markdown'
+import remarkGfm from 'remark-gfm'
+import remarkMath from 'remark-math'
+import { StreamableValue } from 'ai/rsc'
+import { useStreamableText } from '@/lib/hooks/use-streamable-text'
+
+// Different types of message bubbles.
+
+export function UserMessage({ children }: { children: React.ReactNode }) {
+ return (
+
+ )
+}
+
+export function BotMessage({
+ content,
+ className
+}: {
+ content: string | StreamableValue
+ className?: string
+}) {
+ const text = useStreamableText(content)
+
+ return (
+
+
+
+
+
+ {children}
+ },
+ code({ node, inline, className, children, ...props }) {
+ if (children.length) {
+ if (children[0] == '▍') {
+ return (
+ ▍
+ )
+ }
+
+ children[0] = (children[0] as string).replace('`▍`', '▍')
+ }
+
+ const match = /language-(\w+)/.exec(className || '')
+
+ if (inline) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ return (
+
+ )
+ }
+ }}
+ >
+ {text}
+
+
+
+ )
+}
+
+export function BotCard({
+ children,
+ showAvatar = true
+}: {
+ children: React.ReactNode
+ showAvatar?: boolean
+}) {
+ return (
+
+ )
+}
+
+export function SystemMessage({ children }: { children: React.ReactNode }) {
+ return (
+
+ )
+}
+
+export function SpinnerMessage() {
+ return (
+
+ )
+}
diff --git a/components/stocks/spinner.tsx b/components/stocks/spinner.tsx
new file mode 100644
index 0000000..8eab1d3
--- /dev/null
+++ b/components/stocks/spinner.tsx
@@ -0,0 +1,16 @@
+'use client'
+
+export const spinner = (
+
+
+
+)
diff --git a/components/stocks/stock-purchase.tsx b/components/stocks/stock-purchase.tsx
new file mode 100644
index 0000000..5a6d364
--- /dev/null
+++ b/components/stocks/stock-purchase.tsx
@@ -0,0 +1,146 @@
+'use client'
+
+import { useId, useState } from 'react'
+import { useActions, useAIState, useUIState } from 'ai/rsc'
+import { formatNumber } from '@/lib/utils'
+
+import type { AI } from '@/lib/chat/actions'
+
+interface Purchase {
+ defaultAmount?: number
+ name: string
+ price: number
+ status: 'requires_action' | 'completed' | 'expired'
+}
+
+export function Purchase({
+ props: { defaultAmount, name, price, status = 'expired' }
+}: {
+ props: Purchase
+}) {
+ const [value, setValue] = useState(defaultAmount || 100)
+ const [purchasingUI, setPurchasingUI] = useState(null)
+ const [aiState, setAIState] = useAIState()
+ const [, setMessages] = useUIState()
+ const { confirmPurchase } = useActions()
+
+ // Unique identifier for this UI component.
+ const id = useId()
+
+ // Whenever the slider changes, we need to update the local value state and the history
+ // so LLM also knows what's going on.
+ function onSliderChange(e: React.ChangeEvent) {
+ const newValue = Number(e.target.value)
+ setValue(newValue)
+
+ // Insert a hidden history info to the list.
+ const message = {
+ role: 'system' as const,
+ content: `[User has changed to purchase ${newValue} shares of ${name}. Total cost: $${(
+ newValue * price
+ ).toFixed(2)}]`,
+
+ // Identifier of this UI component, so we don't insert it many times.
+ id
+ }
+
+ // If last history state is already this info, update it. This is to avoid
+ // adding every slider change to the history.
+ if (aiState.messages[aiState.messages.length - 1]?.id === id) {
+ setAIState({
+ ...aiState,
+ messages: [...aiState.messages.slice(0, -1), message]
+ })
+
+ return
+ }
+
+ // If it doesn't exist, append it to history.
+ setAIState({ ...aiState, messages: [...aiState.messages, message] })
+ }
+
+ return (
+
+
+ +1.23% ↑
+
+
{name}
+
${price}
+ {purchasingUI ? (
+
{purchasingUI}
+ ) : status === 'requires_action' ? (
+ <>
+
+
Shares to purchase
+
+
+ 10
+
+
+ 100
+
+
+ 500
+
+
+ 1000
+
+
+
+
+
Total cost
+
+
+ {value}
+
+ shares
+
+
+
×
+
+ ${price}
+
+ per share
+
+
+
+ = {formatNumber(value * price)}
+
+
+
+
+
{
+ const response = await confirmPurchase(name, price, value)
+ setPurchasingUI(response.purchasingUI)
+
+ // Insert a new system message to the UI.
+ setMessages((currentMessages: any) => [
+ ...currentMessages,
+ response.newMessage
+ ])
+ }}
+ >
+ Purchase
+
+ >
+ ) : status === 'completed' ? (
+
+ You have successfully purchased {value} ${name}. Total cost:{' '}
+ {formatNumber(value * price)}
+
+ ) : status === 'expired' ? (
+
Your checkout session has expired!
+ ) : null}
+
+ )
+}
diff --git a/components/stocks/stock-skeleton.tsx b/components/stocks/stock-skeleton.tsx
new file mode 100644
index 0000000..e7b7a93
--- /dev/null
+++ b/components/stocks/stock-skeleton.tsx
@@ -0,0 +1,22 @@
+export const StockSkeleton = () => {
+ return (
+
+
+ xxxxxxx
+
+
+ xxxx
+
+
+ xxxx
+
+
+ xxxxxx xxx xx xxxx xx xxx
+
+
+
+
+ )
+}
diff --git a/components/stocks/stock.tsx b/components/stocks/stock.tsx
new file mode 100644
index 0000000..aef249b
--- /dev/null
+++ b/components/stocks/stock.tsx
@@ -0,0 +1,210 @@
+'use client'
+
+import { useState, useRef, useEffect, useId } from 'react'
+import { scaleLinear } from 'd3-scale'
+import { subMonths, format } from 'date-fns'
+import { useResizeObserver } from 'usehooks-ts'
+import { useAIState } from 'ai/rsc'
+
+interface Stock {
+ symbol: string
+ price: number
+ delta: number
+}
+
+export function Stock({ props: { symbol, price, delta } }: { props: Stock }) {
+ const [aiState, setAIState] = useAIState()
+ const id = useId()
+
+ const [priceAtTime, setPriceAtTime] = useState({
+ time: '00:00',
+ value: price.toFixed(2),
+ x: 0
+ })
+
+ const [startHighlight, setStartHighlight] = useState(0)
+ const [endHighlight, setEndHighlight] = useState(0)
+
+ const chartRef = useRef(null)
+ const { width = 0 } = useResizeObserver({
+ ref: chartRef,
+ box: 'border-box'
+ })
+
+ const xToDate = scaleLinear(
+ [0, width],
+ [subMonths(new Date(), 6), new Date()]
+ )
+ const xToValue = scaleLinear(
+ [0, width],
+ [price - price / 2, price + price / 2]
+ )
+
+ useEffect(() => {
+ if (startHighlight && endHighlight) {
+ const message = {
+ id,
+ role: 'system' as const,
+ content: `[User has highlighted dates between between ${format(
+ xToDate(startHighlight),
+ 'd LLL'
+ )} and ${format(xToDate(endHighlight), 'd LLL, yyyy')}`
+ }
+
+ if (aiState.messages[aiState.messages.length - 1]?.id === id) {
+ setAIState({
+ ...aiState,
+ messages: [...aiState.messages.slice(0, -1), message]
+ })
+ } else {
+ setAIState({
+ ...aiState,
+ messages: [...aiState.messages, message]
+ })
+ }
+ }
+ }, [startHighlight, endHighlight])
+
+ return (
+
+
+ {`${delta > 0 ? '+' : ''}${((delta / price) * 100).toFixed(2)}% ${
+ delta > 0 ? '↑' : '↓'
+ }`}
+
+
{symbol}
+
${price}
+
+ Closed: Feb 27, 4:59 PM EST
+
+
+
{
+ if (chartRef.current) {
+ const { clientX } = event
+ const { left } = chartRef.current.getBoundingClientRect()
+
+ setStartHighlight(clientX - left)
+ setEndHighlight(0)
+
+ setPriceAtTime({
+ time: format(xToDate(clientX), 'dd LLL yy'),
+ value: xToValue(clientX).toFixed(2),
+ x: clientX - left
+ })
+ }
+ }}
+ onPointerUp={event => {
+ if (chartRef.current) {
+ const { clientX } = event
+ const { left } = chartRef.current.getBoundingClientRect()
+
+ setEndHighlight(clientX - left)
+ }
+ }}
+ onPointerMove={event => {
+ if (chartRef.current) {
+ const { clientX } = event
+ const { left } = chartRef.current.getBoundingClientRect()
+
+ setPriceAtTime({
+ time: format(xToDate(clientX), 'dd LLL yy'),
+ value: xToValue(clientX).toFixed(2),
+ x: clientX - left
+ })
+ }
+ }}
+ onPointerLeave={() => {
+ setPriceAtTime({
+ time: '00:00',
+ value: price.toFixed(2),
+ x: 0
+ })
+ }}
+ ref={chartRef}
+ >
+ {priceAtTime.x > 0 ? (
+
+
${priceAtTime.value}
+
+ {priceAtTime.time}
+
+
+ ) : null}
+
+ {startHighlight ? (
+
+ ) : null}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/stocks/stocks-skeleton.tsx b/components/stocks/stocks-skeleton.tsx
new file mode 100644
index 0000000..efe5628
--- /dev/null
+++ b/components/stocks/stocks-skeleton.tsx
@@ -0,0 +1,9 @@
+export const StocksSkeleton = () => {
+ return (
+
+ )
+}
diff --git a/components/stocks/stocks.tsx b/components/stocks/stocks.tsx
new file mode 100644
index 0000000..004ac87
--- /dev/null
+++ b/components/stocks/stocks.tsx
@@ -0,0 +1,59 @@
+'use client'
+
+import { useActions, useUIState } from 'ai/rsc'
+
+import type { AI } from '@/lib/chat/actions'
+
+interface Stock {
+ symbol: string
+ price: number
+ delta: number
+}
+
+export function Stocks({ props: stocks }: { props: Stock[] }) {
+ const [, setMessages] = useUIState()
+ const { submitUserMessage } = useActions()
+
+ return (
+
+ {stocks.map(stock => (
+
{
+ const response = await submitUserMessage(`View ${stock.symbol}`)
+ setMessages(currentMessages => [...currentMessages, response])
+ }}
+ >
+ 0 ? 'text-green-600' : 'text-red-600'
+ } flex w-11 flex-row justify-center rounded-md bg-white/10 p-2`}
+ >
+ {stock.delta > 0 ? '↑' : '↓'}
+
+
+
{stock.symbol}
+
${stock.price}
+
+
+
0 ? 'text-green-600' : 'text-red-600'
+ } bold text-right uppercase`}
+ >
+ {` ${((stock.delta / stock.price) * 100).toFixed(2)}%`}
+
+
0 ? 'text-green-700' : 'text-red-700'
+ } text-right text-base`}
+ >
+ {stock.delta}
+
+
+
+ ))}
+
+ )
+}
diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx
index 2302ff2..57760f2 100644
--- a/components/ui/alert-dialog.tsx
+++ b/components/ui/alert-dialog.tsx
@@ -1,10 +1,10 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
+import * as React from "react"
+import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
-import { cn } from '@/lib/utils'
-import { buttonVariants } from '@/components/ui/button'
+import { cn } from "@/lib/utils"
+import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
@@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, ...props }, ref) => (
) => (
)
-AlertDialogHeader.displayName = 'AlertDialogHeader'
+AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
@@ -65,13 +65,13 @@ const AlertDialogFooter = ({
}: React.HTMLAttributes) => (
)
-AlertDialogFooter.displayName = 'AlertDialogFooter'
+AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef,
@@ -79,7 +79,7 @@ const AlertDialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
))
@@ -91,7 +91,7 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
))
@@ -117,8 +117,8 @@ const AlertDialogCancel = React.forwardRef<
= memo(({ language, value }) => {
padding: '1.5rem 1rem'
}}
lineNumberStyle={{
- userSelect: "none",
+ userSelect: 'none'
}}
codeTagProps={{
style: {
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
index 7649a10..5ad11c8 100644
--- a/components/ui/dialog.tsx
+++ b/components/ui/dialog.tsx
@@ -1,10 +1,10 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as DialogPrimitive from '@radix-ui/react-dialog'
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { Cross2Icon } from "@radix-ui/react-icons"
-import { cn } from '@/lib/utils'
-import { IconClose } from '@/components/ui/icons'
+import { cn } from "@/lib/utils"
const Dialog = DialogPrimitive.Root
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
{children}
-
+
Close
@@ -59,13 +59,13 @@ const DialogHeader = ({
}: React.HTMLAttributes) => (
)
-DialogHeader.displayName = 'DialogHeader'
+DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
@@ -73,13 +73,13 @@ const DialogFooter = ({
}: React.HTMLAttributes) => (
)
-DialogFooter.displayName = 'DialogFooter'
+DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef,
@@ -88,7 +88,7 @@ const DialogTitle = React.forwardRef<
(({ className, ...props }, ref) => (
))
@@ -112,11 +112,11 @@ export {
Dialog,
DialogPortal,
DialogOverlay,
- DialogClose,
DialogTrigger,
+ DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
- DialogDescription
+ DialogDescription,
}
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
index 184d4e6..1ff35de 100644
--- a/components/ui/dropdown-menu.tsx
+++ b/components/ui/dropdown-menu.tsx
@@ -1,9 +1,14 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import {
+ CheckIcon,
+ ChevronRightIcon,
+ DotFilledIcon,
+} from "@radix-ui/react-icons"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
@@ -17,6 +22,28 @@ const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
+const DropdownMenuSubTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+))
+DropdownMenuSubTrigger.displayName =
+ DropdownMenuPrimitive.SubTrigger.displayName
+
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
@@ -24,7 +51,7 @@ const DropdownMenuSubContent = React.forwardRef<
,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuCheckboxItem.displayName =
+ DropdownMenuPrimitive.CheckboxItem.displayName
+
+const DropdownMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+))
+DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
+
const DropdownMenuLabel = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef & {
@@ -78,8 +152,8 @@ const DropdownMenuLabel = React.forwardRef<
(({ className, ...props }, ref) => (
))
@@ -105,18 +179,20 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes) => {
return (
)
}
-DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'
+DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
@@ -124,5 +200,6 @@ export {
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
- DropdownMenuRadioGroup
+ DropdownMenuSubTrigger,
+ DropdownMenuRadioGroup,
}
diff --git a/components/ui/input.tsx b/components/ui/input.tsx
index 684a857..a92b8e0 100644
--- a/components/ui/input.tsx
+++ b/components/ui/input.tsx
@@ -1,6 +1,6 @@
-import * as React from 'react'
+import * as React from "react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes {}
@@ -11,7 +11,7 @@ const Input = React.forwardRef(
(
)
}
)
-Input.displayName = 'Input'
+Input.displayName = "Input"
export { Input }
diff --git a/components/ui/select.tsx b/components/ui/select.tsx
index c9986a6..f489242 100644
--- a/components/ui/select.tsx
+++ b/components/ui/select.tsx
@@ -1,14 +1,15 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as SelectPrimitive from '@radix-ui/react-select'
-
-import { cn } from '@/lib/utils'
+import * as React from "react"
import {
- IconArrowDown,
- IconCheck,
- IconChevronUpDown
-} from '@/components/ui/icons'
+ CaretSortIcon,
+ CheckIcon,
+ ChevronDownIcon,
+ ChevronUpIcon,
+} from "@radix-ui/react-icons"
+import * as SelectPrimitive from "@radix-ui/react-select"
+
+import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
@@ -23,43 +24,81 @@ const SelectTrigger = React.forwardRef<
span]:line-clamp-1",
className
)}
{...props}
>
{children}
-
+
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName
+
const SelectContent = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
->(({ className, children, position = 'popper', ...props }, ref) => (
+>(({ className, children, position = "popper", ...props }, ref) => (
+
{children}
+
))
@@ -71,7 +110,7 @@ const SelectLabel = React.forwardRef<
>(({ className, ...props }, ref) => (
))
@@ -84,14 +123,14 @@ const SelectItem = React.forwardRef<
-
+
-
+
{children}
@@ -105,7 +144,7 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
))
@@ -119,5 +158,7 @@ export {
SelectContent,
SelectLabel,
SelectItem,
- SelectSeparator
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
}
diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx
index 6c55e0b..12d81c4 100644
--- a/components/ui/separator.tsx
+++ b/components/ui/separator.tsx
@@ -1,16 +1,16 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as SeparatorPrimitive from '@radix-ui/react-separator'
+import * as React from "react"
+import * as SeparatorPrimitive from "@radix-ui/react-separator"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef,
React.ComponentPropsWithoutRef
>(
(
- { className, orientation = 'horizontal', decorative = true, ...props },
+ { className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
(({ className, ...props }, ref) => (
,
SheetContentProps
->(({ side = 'left', className, children, ...props }, ref) => (
+>(({ side = "right", className, children, ...props }, ref) => (
{children}
-
+
Close
@@ -80,13 +80,13 @@ const SheetHeader = ({
}: React.HTMLAttributes) => (
)
-SheetHeader.displayName = 'SheetHeader'
+SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
@@ -94,13 +94,13 @@ const SheetFooter = ({
}: React.HTMLAttributes) => (
)
-SheetFooter.displayName = 'SheetFooter'
+SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef,
@@ -108,7 +108,7 @@ const SheetTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
))
@@ -120,7 +120,7 @@ const SheetDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
))
@@ -136,5 +136,5 @@ export {
SheetHeader,
SheetFooter,
SheetTitle,
- SheetDescription
+ SheetDescription,
}
diff --git a/components/ui/sonner.tsx b/components/ui/sonner.tsx
new file mode 100644
index 0000000..452f4d9
--- /dev/null
+++ b/components/ui/sonner.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import { useTheme } from "next-themes"
+import { Toaster as Sonner } from "sonner"
+
+type ToasterProps = React.ComponentProps
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+
+ )
+}
+
+export { Toaster }
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
index 29b8c29..37d61e4 100644
--- a/components/ui/switch.tsx
+++ b/components/ui/switch.tsx
@@ -1,9 +1,9 @@
-'use client'
+"use client"
-import * as React from 'react'
-import * as SwitchPrimitives from '@radix-ui/react-switch'
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef,
@@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
diff --git a/components/ui/textarea.tsx b/components/ui/textarea.tsx
index e25af72..d1258e4 100644
--- a/components/ui/textarea.tsx
+++ b/components/ui/textarea.tsx
@@ -1,6 +1,6 @@
-import * as React from 'react'
+import * as React from "react"
-import { cn } from '@/lib/utils'
+import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes {}
@@ -10,7 +10,7 @@ const Textarea = React.forwardRef(
return (
diff --git a/lib/chat/actions.tsx b/lib/chat/actions.tsx
new file mode 100644
index 0000000..94130d9
--- /dev/null
+++ b/lib/chat/actions.tsx
@@ -0,0 +1,509 @@
+import 'server-only'
+
+import {
+ createAI,
+ createStreamableUI,
+ getMutableAIState,
+ getAIState,
+ render,
+ createStreamableValue
+} from 'ai/rsc'
+import OpenAI from 'openai'
+
+import {
+ spinner,
+ BotCard,
+ BotMessage,
+ SystemMessage,
+ Stock,
+ Purchase
+} from '@/components/stocks'
+
+import { z } from 'zod'
+import { EventsSkeleton } from '@/components/stocks/events-skeleton'
+import { Events } from '@/components/stocks/event'
+import { StocksSkeleton } from '@/components/stocks/stocks-skeleton'
+import { Stocks } from '@/components/stocks/stocks'
+import { StockSkeleton } from '@/components/stocks/stock-skeleton'
+import {
+ formatNumber,
+ runAsyncFnWithoutBlocking,
+ sleep,
+ nanoid
+} from '@/lib/utils'
+import { saveChat } from '@/app/actions'
+import { SpinnerMessage, UserMessage } from '@/components/stocks/message'
+import { Chat } from '@/lib/types'
+import { auth } from '@/auth'
+
+const openai = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY || ''
+})
+
+async function confirmPurchase(symbol: string, price: number, amount: number) {
+ 'use server'
+
+ const aiState = getMutableAIState
()
+
+ const purchasing = createStreamableUI(
+
+ {spinner}
+
+ Purchasing {amount} ${symbol}...
+
+
+ )
+
+ const systemMessage = createStreamableUI(null)
+
+ runAsyncFnWithoutBlocking(async () => {
+ await sleep(1000)
+
+ purchasing.update(
+
+ {spinner}
+
+ Purchasing {amount} ${symbol}... working on it...
+
+
+ )
+
+ await sleep(1000)
+
+ purchasing.done(
+
+
+ You have successfully purchased {amount} ${symbol}. Total cost:{' '}
+ {formatNumber(amount * price)}
+
+
+ )
+
+ systemMessage.done(
+
+ You have purchased {amount} shares of {symbol} at ${price}. Total cost ={' '}
+ {formatNumber(amount * price)}.
+
+ )
+
+ aiState.done({
+ ...aiState.get(),
+ messages: [
+ ...aiState.get().messages.slice(0, -1),
+ {
+ id: nanoid(),
+ role: 'function',
+ name: 'showStockPurchase',
+ content: JSON.stringify({
+ name: symbol,
+ price,
+ defaultAmount: amount,
+ status: 'completed'
+ })
+ },
+ {
+ id: nanoid(),
+ role: 'system',
+ content: `[User has purchased ${amount} shares of ${symbol} at ${price}. Total cost = ${
+ amount * price
+ }]`
+ }
+ ]
+ })
+ })
+
+ return {
+ purchasingUI: purchasing.value,
+ newMessage: {
+ id: nanoid(),
+ display: systemMessage.value
+ }
+ }
+}
+
+async function submitUserMessage(content: string) {
+ 'use server'
+
+ const aiState = getMutableAIState()
+
+ aiState.update({
+ ...aiState.get(),
+ messages: [
+ ...aiState.get().messages,
+ {
+ id: nanoid(),
+ role: 'user',
+ content
+ }
+ ]
+ })
+
+ let textStream: undefined | ReturnType>
+ let textNode: undefined | React.ReactNode
+
+ const ui = render({
+ model: 'gpt-3.5-turbo',
+ provider: openai,
+ initial: ,
+ messages: [
+ {
+ role: 'system',
+ content: `\
+You are a stock trading conversation bot and you can help users buy stocks, step by step.
+You and the user can discuss stock prices and the user can adjust the amount of stocks they want to buy, or place an order, in the UI.
+
+Messages inside [] means that it's a UI element or a user event. For example:
+- "[Price of AAPL = 100]" means that an interface of the stock price of AAPL is shown to the user.
+- "[User has changed the amount of AAPL to 10]" means that the user has changed the amount of AAPL to 10 in the UI.
+
+If the user requests purchasing a stock, call \`show_stock_purchase_ui\` to show the purchase UI.
+If the user just wants the price, call \`show_stock_price\` to show the price.
+If you want to show trending stocks, call \`list_stocks\`.
+If you want to show events, call \`get_events\`.
+If the user wants to sell stock, or complete another impossible task, respond that you are a demo and cannot do that.
+
+Besides that, you can also chat with users and do some calculations if needed.`
+ },
+ ...aiState.get().messages.map((message: any) => ({
+ role: message.role,
+ content: message.content,
+ name: message.name
+ }))
+ ],
+ text: ({ content, done, delta }) => {
+ if (!textStream) {
+ textStream = createStreamableValue('')
+ textNode =
+ }
+
+ if (done) {
+ textStream.done()
+ aiState.done({
+ ...aiState.get(),
+ messages: [
+ ...aiState.get().messages,
+ {
+ id: nanoid(),
+ role: 'assistant',
+ content
+ }
+ ]
+ })
+ } else {
+ textStream.update(delta)
+ }
+
+ return textNode
+ },
+ functions: {
+ listStocks: {
+ description: 'List three imaginary stocks that are trending.',
+ parameters: z.object({
+ stocks: z.array(
+ z.object({
+ symbol: z.string().describe('The symbol of the stock'),
+ price: z.number().describe('The price of the stock'),
+ delta: z.number().describe('The change in price of the stock')
+ })
+ )
+ }),
+ render: async function* ({ stocks }) {
+ yield (
+
+
+
+ )
+
+ await sleep(1000)
+
+ aiState.done({
+ ...aiState.get(),
+ messages: [
+ ...aiState.get().messages,
+ {
+ id: nanoid(),
+ role: 'function',
+ name: 'listStocks',
+ content: JSON.stringify(stocks)
+ }
+ ]
+ })
+
+ return (
+
+
+
+ )
+ }
+ },
+ showStockPrice: {
+ description:
+ 'Get the current stock price of a given stock or currency. Use this to show the price to the user.',
+ parameters: z.object({
+ symbol: z
+ .string()
+ .describe(
+ 'The name or symbol of the stock or currency. e.g. DOGE/AAPL/USD.'
+ ),
+ price: z.number().describe('The price of the stock.'),
+ delta: z.number().describe('The change in price of the stock')
+ }),
+ render: async function* ({ symbol, price, delta }) {
+ yield (
+
+
+
+ )
+
+ await sleep(1000)
+
+ aiState.done({
+ ...aiState.get(),
+ messages: [
+ ...aiState.get().messages,
+ {
+ id: nanoid(),
+ role: 'function',
+ name: 'showStockPrice',
+ content: JSON.stringify({ symbol, price, delta })
+ }
+ ]
+ })
+
+ return (
+
+
+
+ )
+ }
+ },
+ showStockPurchase: {
+ description:
+ 'Show price and the UI to purchase a stock or currency. Use this if the user wants to purchase a stock or currency.',
+ parameters: z.object({
+ symbol: z
+ .string()
+ .describe(
+ 'The name or symbol of the stock or currency. e.g. DOGE/AAPL/USD.'
+ ),
+ price: z.number().describe('The price of the stock.'),
+ numberOfShares: z
+ .number()
+ .describe(
+ 'The **number of shares** for a stock or currency to purchase. Can be optional if the user did not specify it.'
+ )
+ }),
+ render: async function* ({ symbol, price, numberOfShares = 100 }) {
+ if (numberOfShares <= 0 || numberOfShares > 1000) {
+ aiState.done({
+ ...aiState.get(),
+ messages: [
+ ...aiState.get().messages,
+ {
+ id: nanoid(),
+ role: 'system',
+ content: `[User has selected an invalid amount]`
+ }
+ ]
+ })
+
+ return
+ }
+
+ aiState.done({
+ ...aiState.get(),
+ messages: [
+ ...aiState.get().messages,
+ {
+ id: nanoid(),
+ role: 'function',
+ name: 'showStockPurchase',
+ content: JSON.stringify({
+ symbol,
+ price,
+ numberOfShares
+ })
+ }
+ ]
+ })
+
+ return (
+ <>
+
+
+
+ >
+ )
+ }
+ },
+ getEvents: {
+ description:
+ 'List funny imaginary events between user highlighted dates that describe stock activity.',
+ parameters: z.object({
+ events: z.array(
+ z.object({
+ date: z
+ .string()
+ .describe('The date of the event, in ISO-8601 format'),
+ headline: z.string().describe('The headline of the event'),
+ description: z.string().describe('The description of the event')
+ })
+ )
+ }),
+ render: async function* ({ events }) {
+ yield (
+
+
+
+ )
+
+ await sleep(1000)
+
+ aiState.done({
+ ...aiState.get(),
+ messages: [
+ ...aiState.get().messages,
+ {
+ id: nanoid(),
+ role: 'function',
+ name: 'getEvents',
+ content: JSON.stringify(events)
+ }
+ ]
+ })
+
+ return (
+
+
+
+ )
+ }
+ }
+ }
+ })
+
+ return {
+ id: nanoid(),
+ display: ui
+ }
+}
+
+export type Message = {
+ role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool'
+ content: string
+ id?: string
+ name?: string
+}
+
+export type AIState = {
+ chatId: string
+ messages: {
+ role: 'user' | 'assistant' | 'system' | 'function' | 'data' | 'tool'
+ content: string
+ id: string
+ name?: string
+ }[]
+}
+
+export type UIState = {
+ id: string
+ display: React.ReactNode
+}[]
+
+export const AI = createAI({
+ actions: {
+ submitUserMessage,
+ confirmPurchase
+ },
+ initialUIState: [],
+ initialAIState: { chatId: nanoid(), messages: [] },
+ unstable_onGetUIState: async () => {
+ 'use server'
+
+ const session = await auth()
+
+ if (session && session.user) {
+ const aiState = getAIState()
+
+ if (aiState) {
+ const uiState = getUIStateFromAIState(aiState)
+ return uiState
+ }
+ } else {
+ return
+ }
+ },
+ unstable_onSetAIState: async ({ state }) => {
+ 'use server'
+
+ const session = await auth()
+
+ if (session && session.user) {
+ const { chatId, messages } = state
+
+ const createdAt = new Date()
+ const userId = session.user.id as string
+ const path = `/chat/${chatId}`
+ const title = messages[0].content.substring(0, 100)
+
+ const chat: Chat = {
+ id: chatId,
+ title,
+ userId,
+ createdAt,
+ messages,
+ path
+ }
+
+ await saveChat(chat)
+ } else {
+ return
+ }
+ }
+})
+
+export const getUIStateFromAIState = (aiState: Chat) => {
+ return aiState.messages
+ .filter(message => message.role !== 'system')
+ .map((message, index) => ({
+ id: `${aiState.chatId}-${index}`,
+ display:
+ message.role === 'function' ? (
+ message.name === 'listStocks' ? (
+
+
+
+ ) : message.name === 'showStockPrice' ? (
+
+
+
+ ) : message.name === 'showStockPurchase' ? (
+
+
+
+ ) : message.name === 'getEvents' ? (
+
+
+
+ ) : null
+ ) : message.role === 'user' ? (
+ {message.content}
+ ) : (
+
+ )
+ }))
+}
diff --git a/lib/hooks/use-streamable-text.ts b/lib/hooks/use-streamable-text.ts
new file mode 100644
index 0000000..f951823
--- /dev/null
+++ b/lib/hooks/use-streamable-text.ts
@@ -0,0 +1,26 @@
+import { StreamableValue, readStreamableValue } from 'ai/rsc'
+import { useEffect, useState } from 'react'
+
+export const useStreamableText = (
+ content: string | StreamableValue
+) => {
+ const [rawContent, setRawContent] = useState(
+ typeof content === 'string' ? content : ''
+ )
+
+ useEffect(() => {
+ ;(async () => {
+ if (typeof content === 'object') {
+ let value = ''
+ for await (const delta of readStreamableValue(content)) {
+ console.log(delta)
+ if (typeof delta === 'string') {
+ setRawContent((value = value + delta))
+ }
+ }
+ }
+ })()
+ }, [content])
+
+ return rawContent
+}
diff --git a/lib/types.ts b/lib/types.ts
index 6f86a9c..e419468 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -1,4 +1,4 @@
-import { type Message } from 'ai'
+import { Message } from 'ai'
export interface Chat extends Record {
id: string
@@ -16,3 +16,15 @@ export type ServerActionResult = Promise<
error: string
}
>
+
+export interface Session {
+ user: {
+ id: string
+ email: string
+ }
+}
+
+export interface AuthResult {
+ type: string
+ message: string
+}
diff --git a/lib/utils.ts b/lib/utils.ts
index cea3d17..ce37adc 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -41,3 +41,23 @@ export function formatDate(input: string | number | Date): string {
year: 'numeric'
})
}
+
+export const formatNumber = (value: number) =>
+ new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD'
+ }).format(value)
+
+export const runAsyncFnWithoutBlocking = (
+ fn: (...args: any) => Promise
+) => {
+ fn()
+}
+
+export const sleep = (ms: number) =>
+ new Promise(resolve => setTimeout(resolve, ms))
+
+export const getStringFromBuffer = (buffer: ArrayBuffer) =>
+ Array.from(new Uint8Array(buffer))
+ .map(b => b.toString(16).padStart(2, '0'))
+ .join('')
diff --git a/middleware.ts b/middleware.ts
index 83a5d08..e663a31 100644
--- a/middleware.ts
+++ b/middleware.ts
@@ -1,5 +1,8 @@
-export { auth as middleware } from './auth'
+import NextAuth from 'next-auth'
+import { authConfig } from './auth.config'
+
+export default NextAuth(authConfig).auth
export const config = {
- matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)']
+ matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)']
}
diff --git a/package.json b/package.json
index 85b32f4..74c3a55 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"lint": "next lint",
"lint:fix": "next lint --fix",
"preview": "next build && next start",
+ "seed": "node -r dotenv/config ./scripts/seed.mjs",
"type-check": "tsc --noEmit",
"format:write": "prettier --write \"{app,lib,components}/**/*.{ts,tsx,mdx}\" --cache",
"format:check": "prettier --check \"{app,lib,components}**/*.{ts,tsx,mdx}\" --cache"
@@ -15,6 +16,7 @@
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
+ "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
@@ -24,29 +26,35 @@
"@vercel/analytics": "^1.1.2",
"@vercel/kv": "^1.0.1",
"@vercel/og": "^0.6.2",
- "ai": "^2.2.31",
+ "@vercel/postgres": "^0.7.2",
+ "ai": "^3.0.12",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
+ "d3-scale": "^4.0.2",
+ "date-fns": "^3.3.1",
"focus-trap-react": "^10.2.3",
"framer-motion": "^10.18.0",
"geist": "^1.2.1",
"nanoid": "^5.0.4",
- "next": "14.1.0",
+ "next": "14.2.0-canary.6",
"next-auth": "5.0.0-beta.4",
"next-themes": "^0.2.1",
"openai": "^4.24.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
- "react-hot-toast": "^2.4.1",
"react-intersection-observer": "^9.5.3",
"react-markdown": "^8.0.7",
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.5.3",
"remark-gfm": "^3.0.1",
- "remark-math": "^5.1.1"
+ "remark-math": "^5.1.1",
+ "sonner": "^1.4.3",
+ "usehooks-ts": "^2.16.0",
+ "zod": "^3.22.4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",
+ "@types/d3-scale": "^4.0.8",
"@types/node": "^20.11.5",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dd83cb5..7b3e1e1 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,9 @@ dependencies:
'@radix-ui/react-dropdown-menu':
specifier: ^2.0.6
version: 2.0.6(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
+ '@radix-ui/react-icons':
+ specifier: ^1.3.0
+ version: 1.3.0(react@18.2.0)
'@radix-ui/react-label':
specifier: ^2.0.2
version: 2.0.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0)(react@18.2.0)
@@ -41,15 +44,24 @@ dependencies:
'@vercel/og':
specifier: ^0.6.2
version: 0.6.2
+ '@vercel/postgres':
+ specifier: ^0.7.2
+ version: 0.7.2
ai:
- specifier: ^2.2.31
- version: 2.2.31(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15)
+ specifier: ^3.0.12
+ version: 3.0.12(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15)(zod@3.22.4)
class-variance-authority:
specifier: ^0.7.0
version: 0.7.0
clsx:
specifier: ^2.1.0
version: 2.1.0
+ d3-scale:
+ specifier: ^4.0.2
+ version: 4.0.2
+ date-fns:
+ specifier: ^3.3.1
+ version: 3.3.1
focus-trap-react:
specifier: ^10.2.3
version: 10.2.3(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)
@@ -58,19 +70,19 @@ dependencies:
version: 10.18.0(react-dom@18.2.0)(react@18.2.0)
geist:
specifier: ^1.2.1
- version: 1.2.1(next@14.1.0)
+ version: 1.2.1(next@14.2.0-canary.6)
nanoid:
specifier: ^5.0.4
version: 5.0.4
next:
- specifier: 14.1.0
- version: 14.1.0(react-dom@18.2.0)(react@18.2.0)
+ specifier: 14.2.0-canary.6
+ version: 14.2.0-canary.6(react-dom@18.2.0)(react@18.2.0)
next-auth:
specifier: 5.0.0-beta.4
- version: 5.0.0-beta.4(next@14.1.0)(react@18.2.0)
+ version: 5.0.0-beta.4(next@14.2.0-canary.6)(react@18.2.0)
next-themes:
specifier: ^0.2.1
- version: 0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0)
+ version: 0.2.1(next@14.2.0-canary.6)(react-dom@18.2.0)(react@18.2.0)
openai:
specifier: ^4.24.7
version: 4.24.7
@@ -80,9 +92,6 @@ dependencies:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
- react-hot-toast:
- specifier: ^2.4.1
- version: 2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0)
react-intersection-observer:
specifier: ^9.5.3
version: 9.5.3(react@18.2.0)
@@ -101,11 +110,23 @@ dependencies:
remark-math:
specifier: ^5.1.1
version: 5.1.1
+ sonner:
+ specifier: ^1.4.3
+ version: 1.4.3(react-dom@18.2.0)(react@18.2.0)
+ usehooks-ts:
+ specifier: ^2.16.0
+ version: 2.16.0(react@18.2.0)
+ zod:
+ specifier: ^3.22.4
+ version: 3.22.4
devDependencies:
'@tailwindcss/typography':
specifier: ^0.5.10
version: 0.5.10(tailwindcss@3.4.1)
+ '@types/d3-scale':
+ specifier: ^4.0.8
+ version: 4.0.8
'@types/node':
specifier: ^20.11.5
version: 20.11.5
@@ -360,8 +381,14 @@ packages:
'@jridgewell/resolve-uri': 3.1.1
'@jridgewell/sourcemap-codec': 1.4.15
- /@next/env@14.1.0:
- resolution: {integrity: sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==}
+ /@neondatabase/serverless@0.7.2:
+ resolution: {integrity: sha512-wU3WA2uTyNO7wjPs3Mg0G01jztAxUxzd9/mskMmtPwPTjf7JKWi9AW5/puOGXLxmZ9PVgRFeBVRVYq5nBPhsCg==}
+ dependencies:
+ '@types/pg': 8.6.6
+ dev: false
+
+ /@next/env@14.2.0-canary.6:
+ resolution: {integrity: sha512-GVguN9O24Rh1aFTm9CIKnNgRZjjdB6E4V3KmQTq/hjqL+IzFimBSrAPHJY7Fe9uWMxa5VD78Id8iwHVrIoch3g==}
dev: false
/@next/eslint-plugin-next@14.1.0:
@@ -370,8 +397,8 @@ packages:
glob: 10.3.10
dev: true
- /@next/swc-darwin-arm64@14.1.0:
- resolution: {integrity: sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==}
+ /@next/swc-darwin-arm64@14.2.0-canary.6:
+ resolution: {integrity: sha512-9stpz9B29+/6NF6qdmAGLJR1bG/0WLzaMHTuPy482JHBcuJMVcYKCWzfTkw1v68/Zdkoja404BeaJTV1MRTB3g==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -379,8 +406,8 @@ packages:
dev: false
optional: true
- /@next/swc-darwin-x64@14.1.0:
- resolution: {integrity: sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==}
+ /@next/swc-darwin-x64@14.2.0-canary.6:
+ resolution: {integrity: sha512-NE1paZ1+eIi2hyWZrgigDKyjUoy7YeN+8VCqtcxkHsOoZRir0Ugy9BIidlVxOjlYF72sHuOy082/kl5EpMs7YA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -388,8 +415,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-arm64-gnu@14.1.0:
- resolution: {integrity: sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==}
+ /@next/swc-linux-arm64-gnu@14.2.0-canary.6:
+ resolution: {integrity: sha512-1ab2OoRyev9rA8KfkLSz7uc4Qvk/BiM5zXRI3rcrXtdrOBCafZdcSxsqRgcmHYJ5DO1QLf+i5b0q/VAYhzKKQQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -397,8 +424,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-arm64-musl@14.1.0:
- resolution: {integrity: sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==}
+ /@next/swc-linux-arm64-musl@14.2.0-canary.6:
+ resolution: {integrity: sha512-QAbbtAlBx2Jbl59md7hAkLF3V5eAa22OnHwxxyHOHjpJSVQrrcvQb5u86vfdttxcypSiG0M0xiIE7PYLwPvIXw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -406,8 +433,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-x64-gnu@14.1.0:
- resolution: {integrity: sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==}
+ /@next/swc-linux-x64-gnu@14.2.0-canary.6:
+ resolution: {integrity: sha512-O7zWKvvvjEbgzTtBTnQz4iH7ZA7RYxu0v3cUwQd3SnplInBCluaSk0lq5M9bRfHhriVW7/09cyMErY0nq+ZWlw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -415,8 +442,8 @@ packages:
dev: false
optional: true
- /@next/swc-linux-x64-musl@14.1.0:
- resolution: {integrity: sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==}
+ /@next/swc-linux-x64-musl@14.2.0-canary.6:
+ resolution: {integrity: sha512-TywXiQUsQVcQKouCJSYfi7CgziH3n+PFC7NgakwKwvvl1VDVmK7TxqkUjNOzfcgF3g/Dg0IIqzLOPqKa1Xp6uw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -424,8 +451,8 @@ packages:
dev: false
optional: true
- /@next/swc-win32-arm64-msvc@14.1.0:
- resolution: {integrity: sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==}
+ /@next/swc-win32-arm64-msvc@14.2.0-canary.6:
+ resolution: {integrity: sha512-r1m4Ugc0u00bIiYeYAC2U/TrwYOuZKWFA1I3I5qdnagoe1Bul4OijYnMkPYuhhjB6bHUUQjcUtHV5lkcnTr5uA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -433,8 +460,8 @@ packages:
dev: false
optional: true
- /@next/swc-win32-ia32-msvc@14.1.0:
- resolution: {integrity: sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==}
+ /@next/swc-win32-ia32-msvc@14.2.0-canary.6:
+ resolution: {integrity: sha512-l2+CnXMqgh6umnptBkptWk2bz+3bKJe/T5ojJlUoLweIs4eKJ3V87+CH8z5zkznqKVHZ5hQLC0JieV0OVun72Q==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -442,8 +469,8 @@ packages:
dev: false
optional: true
- /@next/swc-win32-x64-msvc@14.1.0:
- resolution: {integrity: sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==}
+ /@next/swc-win32-x64-msvc@14.2.0-canary.6:
+ resolution: {integrity: sha512-1dRrinBhP4R85NVmrxzShqNl9STmLe2ePI1LZ8HkG8RY08o570JiNEwTcbsoqCYzKi7db61xox7todeLjIleIg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -731,6 +758,14 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
+ /@radix-ui/react-icons@1.3.0(react@18.2.0):
+ resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==}
+ peerDependencies:
+ react: ^16.x || ^17.x || ^18.x
+ dependencies:
+ react: 18.2.0
+ dev: false
+
/@radix-ui/react-id@1.0.1(@types/react@18.2.48)(react@18.2.0):
resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
peerDependencies:
@@ -1211,9 +1246,14 @@ packages:
string.prototype.codepointat: 0.2.1
dev: false
- /@swc/helpers@0.5.2:
- resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
+ /@swc/counter@0.1.3:
+ resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+ dev: false
+
+ /@swc/helpers@0.5.5:
+ resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==}
dependencies:
+ '@swc/counter': 0.1.3
tslib: 2.6.2
dev: false
@@ -1229,12 +1269,26 @@ packages:
tailwindcss: 3.4.1
dev: true
+ /@types/d3-scale@4.0.8:
+ resolution: {integrity: sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==}
+ dependencies:
+ '@types/d3-time': 3.0.3
+ dev: true
+
+ /@types/d3-time@3.0.3:
+ resolution: {integrity: sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==}
+ dev: true
+
/@types/debug@4.1.12:
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
dependencies:
'@types/ms': 0.7.34
dev: false
+ /@types/diff-match-patch@1.0.36:
+ resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
+ dev: false
+
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
dev: false
@@ -1281,6 +1335,14 @@ packages:
dependencies:
undici-types: 5.26.5
+ /@types/pg@8.6.6:
+ resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==}
+ dependencies:
+ '@types/node': 20.11.5
+ pg-protocol: 1.6.0
+ pg-types: 2.2.0
+ dev: false
+
/@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
@@ -1405,6 +1467,16 @@ packages:
yoga-wasm-web: 0.3.3
dev: false
+ /@vercel/postgres@0.7.2:
+ resolution: {integrity: sha512-IqR/ZAvoPGcPaXl9eWWB5KaA+w/81RzZa/18P4izQRHpNBkTGt9HwGfYi9+wut5UgxNq4QSX9A7HIQR6QDvX2Q==}
+ engines: {node: '>=14.6'}
+ dependencies:
+ '@neondatabase/serverless': 0.7.2
+ bufferutil: 4.0.8
+ utf-8-validate: 6.0.3
+ ws: 8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3)
+ dev: false
+
/@vue/compiler-core@3.4.15:
resolution: {integrity: sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==}
dependencies:
@@ -1505,14 +1577,15 @@ packages:
humanize-ms: 1.2.1
dev: false
- /ai@2.2.31(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15):
- resolution: {integrity: sha512-WQH13RxP+RYo9IE/FX8foNQh9gcKO/dhl9OGy5JL2bHJVBlnugPmH2CYJWaRt+mvjXHaU8txB+jzGo/fbtH2HA==}
+ /ai@3.0.12(react@18.2.0)(solid-js@1.8.11)(svelte@4.2.9)(vue@3.4.15)(zod@3.22.4):
+ resolution: {integrity: sha512-cP/Moag7PcDOE3kA7WU00YS+mQiuPpAxY+uf57lkWwnqSB1K3/RzwnRF+LD1FqgJfCubI4WEbajMPbnnCr8lAg==}
engines: {node: '>=14.6'}
peerDependencies:
react: ^18.2.0
solid-js: ^1.7.7
svelte: ^3.0.0 || ^4.0.0
vue: ^3.3.4
+ zod: ^3.0.0
peerDependenciesMeta:
react:
optional: true
@@ -1522,8 +1595,11 @@ packages:
optional: true
vue:
optional: true
+ zod:
+ optional: true
dependencies:
eventsource-parser: 1.0.0
+ jsondiffpatch: 0.6.0
nanoid: 3.3.6
react: 18.2.0
solid-js: 1.8.11
@@ -1534,6 +1610,8 @@ packages:
swr-store: 0.10.6
swrv: 1.0.4(vue@3.4.15)
vue: 3.4.15(typescript@5.3.3)
+ zod: 3.22.4
+ zod-to-json-schema: 3.22.4(zod@3.22.4)
dev: false
/ajv@6.12.6:
@@ -1781,6 +1859,14 @@ packages:
update-browserslist-db: 1.0.13(browserslist@4.22.2)
dev: true
+ /bufferutil@4.0.8:
+ resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
+ engines: {node: '>=6.14.2'}
+ requiresBuild: true
+ dependencies:
+ node-gyp-build: 4.8.0
+ dev: false
+
/busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
@@ -1825,6 +1911,11 @@ packages:
supports-color: 7.2.0
dev: true
+ /chalk@5.3.0:
+ resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ dev: false
+
/character-entities-legacy@1.1.4:
resolution: {integrity: sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==}
dev: false
@@ -1989,10 +2080,63 @@ packages:
/csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+ /d3-array@3.2.4:
+ resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+ engines: {node: '>=12'}
+ dependencies:
+ internmap: 2.0.3
+ dev: false
+
+ /d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-format@3.1.0:
+ resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-color: 3.1.0
+ dev: false
+
+ /d3-scale@4.0.2:
+ resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-array: 3.2.4
+ d3-format: 3.1.0
+ d3-interpolate: 3.0.1
+ d3-time: 3.1.0
+ d3-time-format: 4.1.0
+ dev: false
+
+ /d3-time-format@4.1.0:
+ resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-time: 3.1.0
+ dev: false
+
+ /d3-time@3.1.0:
+ resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-array: 3.2.4
+ dev: false
+
/damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
+ /date-fns@3.3.1:
+ resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==}
+ dev: false
+
/debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -2060,6 +2204,10 @@ packages:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dev: true
+ /diff-match-patch@1.0.5:
+ resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
+ dev: false
+
/diff@5.1.0:
resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
engines: {node: '>=0.3.1'}
@@ -2742,12 +2890,12 @@ packages:
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
dev: true
- /geist@1.2.1(next@14.1.0):
+ /geist@1.2.1(next@14.2.0-canary.6):
resolution: {integrity: sha512-xCl7zWfnWqc+TbCG5qqyeT5tnVlOO4pSJsT3Ei59DN1SR4N2VlauF8Fv0D1pPFXGUJgu6RMoeZX+wsR4T9bMhg==}
peerDependencies:
next: ^13.2 || ^14
dependencies:
- next: 14.1.0(react-dom@18.2.0)(react@18.2.0)
+ next: 14.2.0-canary.6(react-dom@18.2.0)(react@18.2.0)
dev: false
/get-intrinsic@1.2.2:
@@ -2841,14 +2989,6 @@ packages:
slash: 3.0.0
dev: true
- /goober@2.1.14(csstype@3.1.3):
- resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==}
- peerDependencies:
- csstype: ^3.0.10
- dependencies:
- csstype: 3.1.3
- dev: false
-
/gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
dependencies:
@@ -2976,6 +3116,11 @@ packages:
side-channel: 1.0.4
dev: true
+ /internmap@2.0.3:
+ resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+ engines: {node: '>=12'}
+ dev: false
+
/invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
dependencies:
@@ -3252,6 +3397,16 @@ packages:
minimist: 1.2.8
dev: true
+ /jsondiffpatch@0.6.0:
+ resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ dependencies:
+ '@types/diff-match-patch': 1.0.36
+ chalk: 5.3.0
+ diff-match-patch: 1.0.5
+ dev: false
+
/jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
@@ -3335,6 +3490,10 @@ packages:
resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==}
dev: true
+ /lodash.debounce@4.0.8:
+ resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
+ dev: false
+
/lodash.isplainobject@4.0.6:
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
dev: true
@@ -3880,7 +4039,7 @@ packages:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true
- /next-auth@5.0.0-beta.4(next@14.1.0)(react@18.2.0):
+ /next-auth@5.0.0-beta.4(next@14.2.0-canary.6)(react@18.2.0):
resolution: {integrity: sha512-vgocjvwPA8gxd/zrIP/vr9lJ/HeNe+C56lPP1D3sdyenHt8KncQV6ro7q0xCsDp1fcOKx7WAWVZH5o8aMxDzgw==}
peerDependencies:
next: ^14
@@ -3891,24 +4050,24 @@ packages:
optional: true
dependencies:
'@auth/core': 0.18.4
- next: 14.1.0(react-dom@18.2.0)(react@18.2.0)
+ next: 14.2.0-canary.6(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
dev: false
- /next-themes@0.2.1(next@14.1.0)(react-dom@18.2.0)(react@18.2.0):
+ /next-themes@0.2.1(next@14.2.0-canary.6)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==}
peerDependencies:
next: '*'
react: '*'
react-dom: '*'
dependencies:
- next: 14.1.0(react-dom@18.2.0)(react@18.2.0)
+ next: 14.2.0-canary.6(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
- /next@14.1.0(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==}
+ /next@14.2.0-canary.6(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-t7wmy7fdAp0aH00u9MEzmF+GPMS8TGZrP/9QTpzpy7C8i5NT45KY4ZtcCQUXEeAyf13azuGiP2swET6eTqlOhA==}
engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
@@ -3922,8 +4081,8 @@ packages:
sass:
optional: true
dependencies:
- '@next/env': 14.1.0
- '@swc/helpers': 0.5.2
+ '@next/env': 14.2.0-canary.6
+ '@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001579
graceful-fs: 4.2.11
@@ -3932,15 +4091,15 @@ packages:
react-dom: 18.2.0(react@18.2.0)
styled-jsx: 5.1.1(react@18.2.0)
optionalDependencies:
- '@next/swc-darwin-arm64': 14.1.0
- '@next/swc-darwin-x64': 14.1.0
- '@next/swc-linux-arm64-gnu': 14.1.0
- '@next/swc-linux-arm64-musl': 14.1.0
- '@next/swc-linux-x64-gnu': 14.1.0
- '@next/swc-linux-x64-musl': 14.1.0
- '@next/swc-win32-arm64-msvc': 14.1.0
- '@next/swc-win32-ia32-msvc': 14.1.0
- '@next/swc-win32-x64-msvc': 14.1.0
+ '@next/swc-darwin-arm64': 14.2.0-canary.6
+ '@next/swc-darwin-x64': 14.2.0-canary.6
+ '@next/swc-linux-arm64-gnu': 14.2.0-canary.6
+ '@next/swc-linux-arm64-musl': 14.2.0-canary.6
+ '@next/swc-linux-x64-gnu': 14.2.0-canary.6
+ '@next/swc-linux-x64-musl': 14.2.0-canary.6
+ '@next/swc-win32-arm64-msvc': 14.2.0-canary.6
+ '@next/swc-win32-ia32-msvc': 14.2.0-canary.6
+ '@next/swc-win32-x64-msvc': 14.2.0-canary.6
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@@ -3963,6 +4122,11 @@ packages:
whatwg-url: 5.0.0
dev: false
+ /node-gyp-build@4.8.0:
+ resolution: {integrity: sha512-u6fs2AEUljNho3EYTJNBfImO5QTo/J/1Etd+NVdCj7qWKUSN/bSLkZwhDv7I+w/MSC6qJ4cknepkAYykDdK8og==}
+ hasBin: true
+ dev: false
+
/node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
dev: true
@@ -4170,6 +4334,26 @@ packages:
is-reference: 3.0.2
dev: false
+ /pg-int8@1.0.1:
+ resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
+ engines: {node: '>=4.0.0'}
+ dev: false
+
+ /pg-protocol@1.6.0:
+ resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==}
+ dev: false
+
+ /pg-types@2.2.0:
+ resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
+ engines: {node: '>=4'}
+ dependencies:
+ pg-int8: 1.0.1
+ postgres-array: 2.0.0
+ postgres-bytea: 1.0.0
+ postgres-date: 1.0.7
+ postgres-interval: 1.2.0
+ dev: false
+
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@@ -4273,6 +4457,28 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
+ /postgres-array@2.0.0:
+ resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
+ engines: {node: '>=4'}
+ dev: false
+
+ /postgres-bytea@1.0.0:
+ resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /postgres-date@1.0.7:
+ resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
+ engines: {node: '>=0.10.0'}
+ dev: false
+
+ /postgres-interval@1.2.0:
+ resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ xtend: 4.0.2
+ dev: false
+
/preact-render-to-string@5.2.3(preact@10.11.3):
resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==}
peerDependencies:
@@ -4347,20 +4553,6 @@ packages:
scheduler: 0.23.0
dev: false
- /react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==}
- engines: {node: '>=10'}
- peerDependencies:
- react: '>=16'
- react-dom: '>=16'
- dependencies:
- goober: 2.1.14(csstype@3.1.3)
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
- transitivePeerDependencies:
- - csstype
- dev: false
-
/react-intersection-observer@9.5.3(react@18.2.0):
resolution: {integrity: sha512-NJzagSdUPS5rPhaLsHXYeJbsvdpbJwL6yCHtMk91hc0ufQ2BnXis+0QQ9NBh6n9n+Q3OyjR6OQLShYbaNBkThQ==}
peerDependencies:
@@ -4766,6 +4958,16 @@ packages:
swr-store: 0.10.6
dev: false
+ /sonner@1.4.3(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-SArYlHbkjqRuLiR0iGY2ZSr09oOrxw081ZZkQPfXrs8aZQLIBOLOdzTYxGJB5yIZ7qL56UEPmrX1YqbODwG0Lw==}
+ peerDependencies:
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ dev: false
+
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
@@ -5313,6 +5515,24 @@ packages:
react: 18.2.0
dev: false
+ /usehooks-ts@2.16.0(react@18.2.0):
+ resolution: {integrity: sha512-bez95WqYujxp6hFdM/CpRDiVPirZPxlMzOH2QB8yopoKQMXpscyZoxOjpEdaxvV+CAWUDSM62cWnqHE0E/MZ7w==}
+ engines: {node: '>=16.15.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18
+ dependencies:
+ lodash.debounce: 4.0.8
+ react: 18.2.0
+ dev: false
+
+ /utf-8-validate@6.0.3:
+ resolution: {integrity: sha512-uIuGf9TWQ/y+0Lp+KGZCMuJWc3N9BHA+l/UmHd/oUHwJJDeysyTRxNQVkbzsIWfGFbRe3OcgML/i0mvVRPOyDA==}
+ engines: {node: '>=6.14.2'}
+ requiresBuild: true
+ dependencies:
+ node-gyp-build: 4.8.0
+ dev: false
+
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: true
@@ -5459,6 +5679,22 @@ packages:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
dev: true
+ /ws@8.14.2(bufferutil@4.0.8)(utf-8-validate@6.0.3):
+ resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==}
+ engines: {node: '>=10.0.0'}
+ peerDependencies:
+ bufferutil: ^4.0.1
+ utf-8-validate: '>=5.0.2'
+ peerDependenciesMeta:
+ bufferutil:
+ optional: true
+ utf-8-validate:
+ optional: true
+ dependencies:
+ bufferutil: 4.0.8
+ utf-8-validate: 6.0.3
+ dev: false
+
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
@@ -5482,6 +5718,18 @@ packages:
resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==}
dev: false
+ /zod-to-json-schema@3.22.4(zod@3.22.4):
+ resolution: {integrity: sha512-2Ed5dJ+n/O3cU383xSY28cuVi0BCQhF8nYqWU5paEpl7fVdqdAmiLdqLyfblbNdfOFwFfi/mqU4O1pwc60iBhQ==}
+ peerDependencies:
+ zod: ^3.22.4
+ dependencies:
+ zod: 3.22.4
+ dev: false
+
+ /zod@3.22.4:
+ resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
+ dev: false
+
/zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
dev: false
diff --git a/scripts/seed.mjs b/scripts/seed.mjs
new file mode 100644
index 0000000..fdff088
--- /dev/null
+++ b/scripts/seed.mjs
@@ -0,0 +1,34 @@
+import { db } from '@vercel/postgres'
+
+async function seedUsers(client) {
+ try {
+ await client.sql`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`
+ const createTable = await client.sql`
+ CREATE TABLE IF NOT EXISTS users (
+ id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
+ email TEXT NOT NULL UNIQUE,
+ password TEXT NOT NULL
+ salt TEXT NOT NULL
+ );
+ `
+
+ console.log(`Created "users" table`)
+
+ return {
+ createTable,
+ }
+ } catch (error) {
+ console.error('Error seeding users:', error)
+ throw error
+ }
+}
+
+async function main() {
+ const client = await db.connect()
+ await seedUsers(client)
+ await client.end()
+}
+
+main().catch(err => {
+ console.error('An error occurred while attempting to seed the database:', err)
+})
diff --git a/tailwind.config.js b/tailwind.config.ts
similarity index 72%
rename from tailwind.config.js
rename to tailwind.config.ts
index f734baf..14261e0 100644
--- a/tailwind.config.js
+++ b/tailwind.config.ts
@@ -1,7 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ['class'],
- content: ['app/**/*.{ts,tsx}', 'components/**/*.{ts,tsx}'],
+ content: [
+ './pages/**/*.{ts,tsx}',
+ './components/**/*.{ts,tsx}',
+ './app/**/*.{ts,tsx}',
+ './src/**/*.{ts,tsx}'
+ ],
+ prefix: '',
theme: {
container: {
center: true,
@@ -51,41 +57,21 @@ module.exports = {
}
},
borderRadius: {
- lg: `var(--radius)`,
- md: `calc(var(--radius) - 2px)`,
+ lg: 'var(--radius)',
+ md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
keyframes: {
'accordion-down': {
- from: { height: 0 },
+ from: { height: '0' },
to: { height: 'var(--radix-accordion-content-height)' }
},
'accordion-up': {
from: { height: 'var(--radix-accordion-content-height)' },
- to: { height: 0 }
- },
- 'slide-from-left': {
- '0%': {
- transform: 'translateX(-100%)'
- },
- '100%': {
- transform: 'translateX(0)'
- }
- },
- 'slide-to-left': {
- '0%': {
- transform: 'translateX(0)'
- },
- '100%': {
- transform: 'translateX(-100%)'
- }
+ to: { height: '0' }
}
},
animation: {
- 'slide-from-left':
- 'slide-from-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
- 'slide-to-left':
- 'slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)',
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
}