diff --git a/www/app/Chat.tsx b/www/app/Chat.tsx index d2a6e9bc..3da49e29 100644 --- a/www/app/Chat.tsx +++ b/www/app/Chat.tsx @@ -5,6 +5,7 @@ import dynamic from 'next/dynamic'; import { FaLightbulb, FaPaperPlane } from 'react-icons/fa'; import Swal from 'sweetalert2'; +import { useToast } from '@/hooks/use-toast'; import { useRef, useEffect, useState, ElementRef, useMemo } from 'react'; // import { useRouter } from 'next/navigation'; @@ -35,56 +36,6 @@ const Sidebar = dynamic(() => import('@/components/sidebar'), { ssr: false, }); -async function fetchStream( - type: 'thought' | 'response' | 'honcho', - message: string, - conversationId: string, - thought = '', - honchoThought = '' -) { - try { - const response = await fetch(`/api/chat/${type}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - message, - conversationId, - thought, - honchoThought, - }), - }); - - if (!response.ok) { - if (response.status === 402) { - Swal.fire({ - title: 'Subscription Required', - text: 'You have no active subscription. Subscribe to continue using Bloom!', - icon: 'warning', - confirmButtonColor: '#3085d6', - confirmButtonText: 'Subscribe', - showCancelButton: false, - }); - throw new Error(`Subscription is required to chat: ${response.status}`); - } - const errorText = await response.text(); - console.error(`Stream error for ${type}:`, { - status: response.status, - statusText: response.statusText, - error: errorText, - }); - console.error(response); - throw new Error(`Failed to fetch ${type} stream: ${response.status}`); - } - - return response.body; - } catch (error) { - console.error(`Error in fetchStream (${type}):`, error); - throw error; - } -} - interface ChatProps { initialUserId: string; initialEmail: string | undefined; @@ -149,6 +100,59 @@ export default function Chat({ initialChatAccess.freeMessages, ]); + const { toast } = useToast(); + + async function fetchStream( + type: 'thought' | 'response' | 'honcho', + message: string, + conversationId: string, + thought = '', + honchoThought = '' + ) { + try { + const response = await fetch(`/api/chat/${type}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message, + conversationId, + thought, + honchoThought, + }), + }); + + if (!response.ok) { + if (response.status === 402) { + Swal.fire({ + title: 'Subscription Required', + text: 'You have no active subscription. Subscribe to continue using Bloom!', + icon: 'warning', + confirmButtonColor: '#3085d6', + confirmButtonText: 'Subscribe', + showCancelButton: false, + }); + throw new Error( + `Subscription is required to chat: ${response.status}` + ); + } + const errorText = await response.text(); + toast({ + variant: 'destructive', + title: `Stream error for ${type}`, + description: `Status ${response.status}: ${errorText}`, + }); + throw new Error(`Failed to fetch ${type} stream: ${response.status}`); + } + + return response.body; + } catch (error) { + console.error(`Error in fetchStream (${type}):`, error); + throw error; + } + } + // Since this message is just rendered in the UI, this naive check may result in edge cases where the incorrect message is shown. // (Ex. will show on all chats after creating a new session or messaging Bloom, even the first chat). // Also, clearing chats will revert the message to the initial description. @@ -278,6 +282,11 @@ What\'s on your mind? Let\'s dive in. 🌱`, { revalidate: false } ); } catch (error) { + toast({ + title: 'Error', + description: 'Failed to update reaction', + // variant: 'destructive', + }); console.error('Failed to update reaction:', error); } }; @@ -325,6 +334,11 @@ What\'s on your mind? Let\'s dive in. 🌱`, ); if (!thoughtStream) { + toast({ + title: 'Error', + description: 'Failed to get thought stream', + variant: 'destructive', + }); throw new Error('Failed to get thought stream'); } @@ -426,6 +440,11 @@ What\'s on your mind? Let\'s dive in. 🌱`, try { thoughtReader.releaseLock(); } catch (e) { + toast({ + variant: 'destructive', + title: 'Error', + description: `Failed to release thought reader: ${e}`, + }); console.error('Error releasing thought reader:', e); } } @@ -433,6 +452,11 @@ What\'s on your mind? Let\'s dive in. 🌱`, try { responseReader.releaseLock(); } catch (e) { + toast({ + variant: 'destructive', + title: 'Error', + description: `Failed to release response reader: ${e}`, + }); console.error('Error releasing response reader:', e); } } diff --git a/www/app/api/chat/honcho/route.ts b/www/app/api/chat/honcho/route.ts index 6964a6cd..11c1b153 100644 --- a/www/app/api/chat/honcho/route.ts +++ b/www/app/api/chat/honcho/route.ts @@ -8,9 +8,9 @@ export const dynamic = 'force-dynamic'; // always run dynamically function parseHonchoContent(str: string) { try { - const match = str.match(/(.*?)<\/honcho>/s); + const match = str.match(/(.*?)<\/honcho>/); return match ? match[1].trim() : str; - } catch (error) { + } catch { return str; } } diff --git a/www/app/api/chat/response/route.ts b/www/app/api/chat/response/route.ts index 550a384c..4df5b5b2 100644 --- a/www/app/api/chat/response/route.ts +++ b/www/app/api/chat/response/route.ts @@ -1,10 +1,4 @@ -import { - assistant, - createStream, - getUserData, - Message, - user, -} from '@/utils/ai'; +import { assistant, createStream, getUserData, user } from '@/utils/ai'; import { honcho } from '@/utils/honcho'; import { responsePrompt } from '@/utils/prompts/response'; import { NextRequest, NextResponse } from 'next/server'; @@ -52,7 +46,7 @@ export async function POST(req: NextRequest) { honchoHistory.find((m) => m.message_id === id)?.content || 'No Honcho Message'; - const history = responseHistory.map((message, i) => { + const history = responseHistory.map((message) => { if (message.is_user) { return user`${getHonchoMessage(message.id)} ${message.content}`; diff --git a/www/app/globals.css b/www/app/globals.css index 6061cea6..23b96c8d 100644 --- a/www/app/globals.css +++ b/www/app/globals.css @@ -35,17 +35,58 @@ html { @layer base { :root { - --background: 40, 20%, 94%; /* #f3f1ed */ - --foreground: 0, 0%, 10%; /* #191919 */ - --secondary: 0, 0%, 98%; /* #FAFAFA */ - --accent: 0, 0%, 89%; /* #E2E2E2 */ + --background: 0, 0%, 10%; /* #191919 */ + --foreground: 40, 20%, 94%; /* #f3f1ed */ + --secondary: 0, 0%, 0%; /* #000000 */ + --accent: 0, 0%, 22%; /* #383838 */ font-size: 87.5%; + --card: 0 0% 100%; + --card-foreground: 0 0% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 0 0% 3.9%; + --primary: 0 0% 9%; + --primary-foreground: 0 0% 98%; + --secondary-foreground: 0 0% 9%; + --muted: 0 0% 96.1%; + --muted-foreground: 0 0% 45.1%; + --accent-foreground: 0 0% 9%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 89.8%; + --input: 0 0% 89.8%; + --ring: 0 0% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; } .dark { --background: 0, 0%, 10%; /* #191919 */ --foreground: 40, 20%, 94%; /* #f3f1ed */ --secondary: 0, 0%, 0%; /* #000000 */ --accent: 0, 0%, 22%; /* #383838 */ + --card: 0 0% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 0 0% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 0 0% 9%; + --secondary-foreground: 0 0% 98%; + --muted: 0 0% 14.9%; + --muted-foreground: 0 0% 63.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 0 0% 14.9%; + --input: 0 0% 14.9%; + --ring: 0 0% 83.1%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; } } diff --git a/www/app/layout.tsx b/www/app/layout.tsx index 62e00472..c65554fe 100644 --- a/www/app/layout.tsx +++ b/www/app/layout.tsx @@ -7,7 +7,7 @@ import { Suspense } from 'react'; import { Header } from '@/components/header'; import { ThemeProvider } from 'next-themes'; import { SpeedInsights } from '@vercel/speed-insights/next'; -import { departureMono } from '@/utils/fonts'; +import { Toaster } from '@/components/ui/toaster'; const roboto = Roboto_Mono({ weight: '400', subsets: ['latin'] }); @@ -69,6 +69,7 @@ export default function RootLayout({
{children}
+
diff --git a/www/components.json b/www/components.json index 5626bf50..dea737b8 100644 --- a/www/components.json +++ b/www/components.json @@ -12,10 +12,10 @@ }, "aliases": { "components": "@/components", - "utils": "@/utils/helpers", + "utils": "@/lib/utils", "ui": "@/components/ui", "lib": "@/lib", "hooks": "@/hooks" - } -} - + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/www/components/ui/toast.tsx b/www/components/ui/toast.tsx new file mode 100644 index 00000000..f44bb041 --- /dev/null +++ b/www/components/ui/toast.tsx @@ -0,0 +1,128 @@ +"use client" + +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/utils/helpers" +import { Cross2Icon } from "@radix-ui/react-icons" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/www/components/ui/toaster.tsx b/www/components/ui/toaster.tsx new file mode 100644 index 00000000..171beb46 --- /dev/null +++ b/www/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useToast } from "@/hooks/use-toast" +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/www/hooks/use-toast.ts b/www/hooks/use-toast.ts new file mode 100644 index 00000000..02e111d8 --- /dev/null +++ b/www/hooks/use-toast.ts @@ -0,0 +1,194 @@ +"use client" + +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/www/lib/utils.ts b/www/lib/utils.ts new file mode 100644 index 00000000..bd0c391d --- /dev/null +++ b/www/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/www/package.json b/www/package.json index 0bef4868..1fd14411 100644 --- a/www/package.json +++ b/www/package.json @@ -15,9 +15,11 @@ "@opentelemetry/api-logs": "^0.56.0", "@opentelemetry/instrumentation": "^0.56.0", "@opentelemetry/sdk-logs": "^0.56.0", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.4", "@sentry/nextjs": "^8.38.0", "@stripe/stripe-js": "^4.9.0", "@supabase/ssr": "^0.5.2", @@ -31,6 +33,7 @@ "honcho-ai": "^0.0.11", "katex": "^0.16.11", "langfuse-vercel": "^3.32.0", + "lucide-react": "^0.469.0", "next": "^14.2.16", "next-themes": "^0.3.0", "openai": "^4.72.0", diff --git a/www/pnpm-lock.yaml b/www/pnpm-lock.yaml index ba732595..50eb24a0 100644 --- a/www/pnpm-lock.yaml +++ b/www/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@opentelemetry/sdk-logs': specifier: ^0.56.0 version: 0.56.0(@opentelemetry/api@1.9.0) + '@radix-ui/react-icons': + specifier: ^1.3.2 + version: 1.3.2(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.0 version: 2.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) @@ -29,6 +32,9 @@ importers: '@radix-ui/react-tabs': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-toast': + specifier: ^1.2.4 + version: 1.2.4(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) '@sentry/nextjs': specifier: ^8.38.0 version: 8.38.0(@opentelemetry/core@1.28.0(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.28.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)(next@14.2.16(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1))(react@18.3.1)(webpack@5.94.0) @@ -68,6 +74,9 @@ importers: langfuse-vercel: specifier: ^3.32.0 version: 3.32.0(ai@4.0.1(react@18.3.1)(zod@3.23.8)) + lucide-react: + specifier: ^0.469.0 + version: 0.469.0(react@18.3.1) next: specifier: ^14.2.16 version: 14.2.16(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) @@ -1442,6 +1451,9 @@ packages: '@radix-ui/primitive@1.1.0': resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} + '@radix-ui/primitive@1.1.1': + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-collection@1.1.0': resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: @@ -1455,6 +1467,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collection@1.1.1': + resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-compose-refs@1.1.0': resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} peerDependencies: @@ -1464,6 +1489,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.1': + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.1.0': resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} peerDependencies: @@ -1491,6 +1525,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-icons@1.3.2': + resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc + '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: @@ -1513,6 +1565,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.3': + resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.1': resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==} peerDependencies: @@ -1526,6 +1591,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.2': + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.0.0': resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} peerDependencies: @@ -1539,6 +1617,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.0.1': + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.0': resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: @@ -1561,6 +1652,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.1.1': + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tabs@1.1.1': resolution: {integrity: sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==} peerDependencies: @@ -1574,6 +1674,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-toast@1.2.4': + resolution: {integrity: sha512-Sch9idFJHJTMH9YNpxxESqABcAFweJG4tKv+0zo0m5XBvUSL8FM5xKcJLFLXononpePs8IclyX1KieL5SDUNgA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: @@ -1592,6 +1705,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-escape-keydown@1.1.0': + resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-layout-effect@1.1.0': resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} peerDependencies: @@ -1601,6 +1723,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-visually-hidden@1.1.1': + resolution: {integrity: sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@react-native-community/cli-clean@12.3.6': resolution: {integrity: sha512-gUU29ep8xM0BbnZjwz9MyID74KKwutq9x5iv4BCr2im6nly4UMf1B1D+V225wR7VcDGzbgWjaezsJShLLhC5ig==} @@ -4085,6 +4220,11 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lucide-react@0.469.0: + resolution: {integrity: sha512-28vvUnnKQ/dBwiCQtwJw7QauYnE7yd2Cyp4tTTJpvglX4EMpbflcdBgrgToX2j71B3YvugK/NH3BGUk+E/p/Fw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} @@ -7556,6 +7696,8 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-collection@1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.21)(react@18.3.1) @@ -7568,12 +7710,30 @@ snapshots: '@types/react': 18.2.21 '@types/react-dom': 18.2.7 + '@radix-ui/react-collection@1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.21)(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + '@radix-ui/react-compose-refs@1.1.0(@types/react@18.2.21)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.2.21 + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.2.21)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.21 + '@radix-ui/react-context@1.1.0(@types/react@18.2.21)(react@18.3.1)': dependencies: react: 18.3.1 @@ -7592,6 +7752,23 @@ snapshots: optionalDependencies: '@types/react': 18.2.21 + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.21)(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + + '@radix-ui/react-icons@1.3.2(react@18.3.1)': + dependencies: + react: 18.3.1 + '@radix-ui/react-id@1.1.0(@types/react@18.2.21)(react@18.3.1)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.21)(react@18.3.1) @@ -7608,6 +7785,16 @@ snapshots: '@types/react': 18.2.21 '@types/react-dom': 18.2.7 + '@radix-ui/react-portal@1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.21)(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.21)(react@18.3.1) @@ -7618,6 +7805,16 @@ snapshots: '@types/react': 18.2.21 '@types/react-dom': 18.2.7 + '@radix-ui/react-presence@1.1.2(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.21)(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.2.21)(react@18.3.1) @@ -7627,6 +7824,15 @@ snapshots: '@types/react': 18.2.21 '@types/react-dom': 18.2.7 + '@radix-ui/react-primitive@2.0.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.21)(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -7651,6 +7857,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.21 + '@radix-ui/react-slot@1.1.1(@types/react@18.2.21)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.21)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.21 + '@radix-ui/react-tabs@1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -7667,6 +7880,26 @@ snapshots: '@types/react': 18.2.21 '@types/react-dom': 18.2.7 + '@radix-ui/react-toast@1.2.4(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.21)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.21)(react@18.3.1)': dependencies: react: 18.3.1 @@ -7680,12 +7913,28 @@ snapshots: optionalDependencies: '@types/react': 18.2.21 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@18.2.21)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.21)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.2.21 + '@radix-ui/react-use-layout-effect@1.1.0(@types/react@18.2.21)(react@18.3.1)': dependencies: react: 18.3.1 optionalDependencies: '@types/react': 18.2.21 + '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.7)(@types/react@18.2.21)(react-dom@18.2.0(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.2.21 + '@types/react-dom': 18.2.7 + '@react-native-community/cli-clean@12.3.6(encoding@0.1.13)': dependencies: '@react-native-community/cli-tools': 12.3.6(encoding@0.1.13) @@ -10746,6 +10995,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.469.0(react@18.3.1): + dependencies: + react: 18.3.1 + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 diff --git a/www/tailwind.config.ts b/www/tailwind.config.ts index 7c81b841..aca01404 100644 --- a/www/tailwind.config.ts +++ b/www/tailwind.config.ts @@ -9,55 +9,59 @@ const config: Config = { './app/**/*.{js,ts,jsx,tsx,mdx}', ], theme: { - extend: { - colors: { - 'neon-green': '#D1EF53', - 'dark-green': '#0E281C', - background: 'hsl(var(--background))', - foreground: 'hsl(var(--foreground))', - popover: { - DEFAULT: 'hsl(var(--popover))', - foreground: 'hsl(var(--popover-foreground))', - }, - primary: { - DEFAULT: 'hsl(var(--primary))', - foreground: 'hsl(var(--primary-foreground))', - }, - secondary: { - DEFAULT: 'hsl(var(--secondary))', - foreground: 'hsl(var(--secondary-foreground))', - }, - muted: { - DEFAULT: 'hsl(var(--muted))', - foreground: 'hsl(var(--muted-foreground))', - }, - accent: { - DEFAULT: 'hsl(var(--accent))', - foreground: 'hsl(var(--accent-foreground))', - }, - destructive: { - DEFAULT: 'hsl(var(--destructive))', - foreground: 'hsl(var(--destructive-foreground))', - }, - border: 'hsl(var(--border))', - input: 'hsl(var(--input))', - ring: 'hsl(var(--ring))', - chart: { - '1': 'hsl(var(--chart-1))', - '2': 'hsl(var(--chart-2))', - '3': 'hsl(var(--chart-3))', - '4': 'hsl(var(--chart-4))', - '5': 'hsl(var(--chart-5))', - }, - }, - borderRadius: { - lg: 'var(--radius)', - md: 'calc(var(--radius) - 2px)', - sm: 'calc(var(--radius) - 4px)', - }, - }, + extend: { + colors: { + 'neon-green': '#D1EF53', + 'dark-green': '#0E281C', + background: 'hsl(var(--background))', + foreground: 'hsl(var(--foreground))', + popover: { + DEFAULT: 'hsl(var(--popover))', + foreground: 'hsl(var(--popover-foreground))' + }, + primary: { + DEFAULT: 'hsl(var(--primary))', + foreground: 'hsl(var(--primary-foreground))' + }, + secondary: { + DEFAULT: 'hsl(var(--secondary))', + foreground: 'hsl(var(--secondary-foreground))' + }, + muted: { + DEFAULT: 'hsl(var(--muted))', + foreground: 'hsl(var(--muted-foreground))' + }, + accent: { + DEFAULT: 'hsl(var(--accent))', + foreground: 'hsl(var(--accent-foreground))' + }, + destructive: { + DEFAULT: 'hsl(var(--destructive))', + foreground: 'hsl(var(--destructive-foreground))' + }, + border: 'hsl(var(--border))', + input: 'hsl(var(--input))', + ring: 'hsl(var(--ring))', + chart: { + '1': 'hsl(var(--chart-1))', + '2': 'hsl(var(--chart-2))', + '3': 'hsl(var(--chart-3))', + '4': 'hsl(var(--chart-4))', + '5': 'hsl(var(--chart-5))' + }, + card: { + DEFAULT: 'hsl(var(--card))', + foreground: 'hsl(var(--card-foreground))' + } + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + } + } }, - plugins: [tailwindcssAnimate], + plugins: [tailwindcssAnimate, require("tailwindcss-animate")], }; export default config;