diff --git a/web/app/(app)/dashboard/(components)/main-dashboard.tsx b/web/app/(app)/dashboard/(components)/main-dashboard.tsx index c54124e..c4486a8 100644 --- a/web/app/(app)/dashboard/(components)/main-dashboard.tsx +++ b/web/app/(app)/dashboard/(components)/main-dashboard.tsx @@ -1,23 +1,16 @@ 'use client' -import { useRouter, usePathname } from 'next/navigation' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Alert, AlertDescription } from '@/components/ui/alert' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -// import Overview from "@/components/overview"; -// import DeviceList from "@/components/device-list"; -// import ApiKeys from "@/components/api-keys"; -// import MessagingPanel from "@/components/messaging-panel"; + import { Webhook, MessageSquare } from 'lucide-react' import { useState } from 'react' import Overview from './overview' import DeviceList from './device-list' import ApiKeys from './api-keys' import Messaging from './messaging' +import WebhooksSection from './webhooks/webhooks-section' export default function DashboardOverview() { - const router = useRouter() - const pathname = usePathname() const [currentTab, setCurrentTab] = useState('overview') @@ -49,19 +42,7 @@ export default function DashboardOverview() { - - - Webhooks (Coming Soon) - - - - - Webhook support is coming soon! You'll be able to configure - endpoints to receive SMS notifications in real-time. - - - - + diff --git a/web/app/(app)/dashboard/(components)/webhooks/create-webhook-dialog.tsx b/web/app/(app)/dashboard/(components)/webhooks/create-webhook-dialog.tsx new file mode 100644 index 0000000..80e1001 --- /dev/null +++ b/web/app/(app)/dashboard/(components)/webhooks/create-webhook-dialog.tsx @@ -0,0 +1,198 @@ +'use client' + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import * as z from 'zod' +import { v4 as uuidv4 } from 'uuid' +import { WebhookData } from '@/lib/types' +import { WEBHOOK_EVENTS } from '@/lib/constants' +import httpBrowserClient from '@/lib/httpBrowserClient' +import { ApiEndpoints } from '@/config/api' +import { useToast } from '@/hooks/use-toast' +import { useMutation, useQueryClient } from '@tanstack/react-query' + +const formSchema = z.object({ + deliveryUrl: z.string().url({ message: 'Please enter a valid URL' }), + events: z.array(z.string()).min(1, { message: 'Select at least one event' }), + isActive: z.boolean().default(true), + signingSecret: z.string().min(1, { message: 'Signing secret is required' }), +}) + +interface CreateWebhookDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function CreateWebhookDialog({ + open, + onOpenChange, +}: CreateWebhookDialogProps) { + const { toast } = useToast() + const queryClient = useQueryClient() + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + deliveryUrl: '', + events: [WEBHOOK_EVENTS.MESSAGE_RECEIVED], + isActive: true, + signingSecret: uuidv4(), + }, + }) + + const createWebhookMutation = useMutation({ + mutationFn: (values: z.infer) => + httpBrowserClient.post(ApiEndpoints.gateway.createWebhook(), values), + onSuccess: () => { + toast({ + title: 'Success', + description: 'Webhook created successfully', + }) + queryClient.invalidateQueries({ queryKey: ['webhooks'] }) + onOpenChange(false) + form.reset() + }, + onError: () => { + toast({ + title: 'Error', + description: 'Failed to create webhook', + variant: 'destructive', + }) + }, + }) + + const onSubmit = (values: z.infer) => { + createWebhookMutation.mutate(values) + } + + return ( + + + + Create Webhook + + Configure your webhook endpoint to receive real-time SMS + notifications. + + +
+ + ( + + Delivery URL + + + + + The URL where webhook notifications will be sent via POST + requests + + + + )} + /> + ( + + Signing Secret + +
+ + +
+
+ + Used to verify webhook payload authenticity + + +
+ )} + /> + ( + + Events + + + Choose the events you want to receive notifications for + + + + )} + /> +
+ + +
+ + +
+
+ ) +} \ No newline at end of file diff --git a/web/app/(app)/dashboard/(components)/webhooks/delete-webhook-button.tsx b/web/app/(app)/dashboard/(components)/webhooks/delete-webhook-button.tsx new file mode 100644 index 0000000..a00cdc8 --- /dev/null +++ b/web/app/(app)/dashboard/(components)/webhooks/delete-webhook-button.tsx @@ -0,0 +1,50 @@ +'use client' + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from '@/components/ui/alert-dialog' +import { Button } from '@/components/ui/button' +import { Trash2 } from 'lucide-react' + +interface DeleteWebhookButtonProps { + onDelete: () => void +} + +export function DeleteWebhookButton({ onDelete }: DeleteWebhookButtonProps) { + return ( + + + + + + + Delete Webhook + + Are you sure you want to delete this webhook? This action cannot be + undone. + + + + Cancel + + Delete + + + + + ) +} diff --git a/web/app/(app)/dashboard/(components)/webhooks/edit-webhook-dialog.tsx b/web/app/(app)/dashboard/(components)/webhooks/edit-webhook-dialog.tsx new file mode 100644 index 0000000..b0677e2 --- /dev/null +++ b/web/app/(app)/dashboard/(components)/webhooks/edit-webhook-dialog.tsx @@ -0,0 +1,201 @@ +'use client' + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import * as z from 'zod' +import { useEffect, useState } from 'react' +import { v4 as uuidv4 } from 'uuid' +import { WebhookData } from '@/lib/types' +import { WEBHOOK_EVENTS } from '@/lib/constants' +import httpBrowserClient from '@/lib/httpBrowserClient' +import { ApiEndpoints } from '@/config/api' +import { useToast } from '@/hooks/use-toast' +import { useMutation, useQueryClient } from '@tanstack/react-query' + +const formSchema = z.object({ + deliveryUrl: z.string().url({ message: 'Please enter a valid URL' }), + events: z.array(z.string()).min(1, { message: 'Select at least one event' }), + isActive: z.boolean().default(true), + signingSecret: z.string().min(1, { message: 'Signing secret is required' }), +}) + +interface EditWebhookDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + webhook: WebhookData +} + +export function EditWebhookDialog({ + open, + onOpenChange, + webhook, +}: EditWebhookDialogProps) { + const queryClient = useQueryClient() + const { toast } = useToast() + + const form = useForm>({ + resolver: zodResolver(formSchema), + values: { + deliveryUrl: webhook.deliveryUrl, + events: webhook.events, + isActive: webhook.isActive, + signingSecret: webhook.signingSecret, + }, + }) + + const { mutate: updateWebhook, isPending } = useMutation({ + mutationFn: async (values: z.infer) => { + return httpBrowserClient.patch( + ApiEndpoints.gateway.updateWebhook(webhook._id), + values + ) + }, + onSuccess: () => { + toast({ + title: 'Success', + description: 'Webhook updated successfully', + }) + // Invalidate and refetch webhooks list + queryClient.invalidateQueries({ queryKey: ['webhooks'] }) + onOpenChange(false) + }, + onError: () => { + toast({ + title: 'Error', + description: 'Failed to update webhook', + variant: 'destructive', + }) + }, + }) + + const onSubmit = (values: z.infer) => { + updateWebhook(values) + } + + return ( + + + + Edit Webhook + + Update your webhook configuration. + + +
+ + ( + + Delivery URL + + + + + The URL where webhook notifications will be sent via POST + requests + + + + )} + /> + ( + + Signing Secret + +
+ + +
+
+ + Used to verify webhook payload authenticity + + +
+ )} + /> + ( + + Events + + + Choose the events you want to receive notifications for + + + + )} + /> +
+ + +
+ + +
+
+ ) +} \ No newline at end of file diff --git a/web/app/(app)/dashboard/(components)/webhooks/webhook-card.tsx b/web/app/(app)/dashboard/(components)/webhooks/webhook-card.tsx new file mode 100644 index 0000000..8c5a2f3 --- /dev/null +++ b/web/app/(app)/dashboard/(components)/webhooks/webhook-card.tsx @@ -0,0 +1,139 @@ +'use client' + +import { Card, CardContent, CardHeader } from '@/components/ui/card' +import { Button } from '@/components/ui/button' +import { Badge } from '@/components/ui/badge' +import { DeleteWebhookButton } from './delete-webhook-button' +import { Edit2, Eye, EyeOff } from 'lucide-react' +import { Switch } from '@/components/ui/switch' +import { useState } from 'react' +import { useToast } from '@/hooks/use-toast' +import { CopyButton } from '@/components/shared/copy-button' +import { WebhookData } from '@/lib/types' +import httpBrowserClient from '@/lib/httpBrowserClient' +import { ApiEndpoints } from '@/config/api' +import { useQueryClient } from '@tanstack/react-query' + +interface WebhookCardProps { + webhook: WebhookData + onEdit: () => void + onDelete?: () => void +} + +export function WebhookCard({ webhook, onEdit, onDelete }: WebhookCardProps) { + const { toast } = useToast() + const [isLoading, setIsLoading] = useState(false) + const queryClient = useQueryClient() + const [showSecret, setShowSecret] = useState(false) + + const handleToggle = async (checked: boolean) => { + setIsLoading(true) + try { + await httpBrowserClient.patch( + ApiEndpoints.gateway.updateWebhook(webhook._id), + { isActive: checked } + ) + + await queryClient.invalidateQueries({ + queryKey: ['webhooks'] + }) + + toast({ + title: `Webhook ${checked ? 'enabled' : 'disabled'}`, + description: `Webhook notifications are now ${ + checked ? 'enabled' : 'disabled' + }.`, + }) + } catch (error) { + toast({ + title: 'Error', + description: `Failed to ${checked ? 'enable' : 'disable'} webhook`, + variant: 'destructive', + }) + } finally { + setIsLoading(false) + } + } + + const maskSecret = (secret: string) => { + // if the secret is less than 18 characters, show all + if (secret.length <= 18) { + return secret.slice(0, 18) + } + return secret.slice(0, 18) + '*'.repeat(secret.length - 24) + } + + return ( + + +
+
+

Webhook Endpoint

+ + {webhook.isActive ? 'Active' : 'Inactive'} + +
+

+ Notifications for SMS events +

+
+
+ + + +
+
+ +
+
+ +
+ + {webhook.deliveryUrl} + + +
+
+
+ +
+ + {showSecret ? webhook.signingSecret : maskSecret(webhook.signingSecret)} + + + +
+
+
+ +
+ {webhook.events.map((event) => ( + + {event} + + ))} +
+
+
+
+
+ ) +} diff --git a/web/app/(app)/dashboard/(components)/webhooks/webhook-docs.tsx b/web/app/(app)/dashboard/(components)/webhooks/webhook-docs.tsx new file mode 100644 index 0000000..c848ea5 --- /dev/null +++ b/web/app/(app)/dashboard/(components)/webhooks/webhook-docs.tsx @@ -0,0 +1,169 @@ +'use client' + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@/components/ui/accordion' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' +import { Code } from '@/components/ui/code' +import { AlertCircle } from 'lucide-react' + +const SAMPLE_PAYLOAD = { + smsId: 'smsId', + sender: '+123456789', + message: 'message', + receivedAt: 'datetime', + deviceId: 'deviceId', + webhookSubscriptionId: 'webhookSubscriptionId', + webhookEvent: 'sms.received', +} + +const VERIFICATION_CODE = ` +// Node.js example using crypto +const crypto = require('crypto'); + +function verifyWebhookSignature(payload, signature, secret) { + const hmac = crypto.createHmac('sha256', secret); + const digest = hmac.update(JSON.stringify(payload)).digest('hex'); + const signatureHash = signature.split('=')[1]; + + return crypto.timingSafeEqual( + Buffer.from(signatureHash), + Buffer.from(digest) + ); +} + +// Express middleware example +app.post('/webhook', (req, res) => { + const signature = req.headers['x-signature']; + const payload = req.body; + + if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) { + return res.status(401).json({ error: 'Invalid signature' }); + } + + // Process the webhook + console.log('Webhook verified:', payload); + res.status(200).send('OK'); +}); +` + +const PYTHON_CODE = ` +# Python example using hmac +import hmac +import hashlib +import json +from flask import Flask, request + +app = Flask(__name__) + +def verify_signature(payload, signature, secret): + expected = hmac.new( + secret.encode('utf-8'), + json.dumps(payload).encode('utf-8'), + hashlib.sha256 + ).hexdigest() + return hmac.compare_digest(signature.split('=')[1], expected) + +@app.route('/webhook', methods=['POST']) +def webhook(): + signature = request.headers.get('X-Signature') + if not verify_signature(request.json, signature, WEBHOOK_SECRET): + return 'Invalid signature', 401 + + # Process the webhook + print('Webhook verified:', request.json) + return 'OK', 200 +` + +export function WebhookDocs() { + return ( + + + +
+ + Webhook Delivery Information +
+
+ +
+

+ When a new SMS is received, we'll send a POST request to your + webhook URL with the event data. Your endpoint should: +

+
    +
  • Accept POST requests
  • +
  • Return a 2XX status code to acknowledge receipt
  • +
  • Process the request within 10 seconds
  • +
+

+ If we don't receive a successful response, we'll retry the + delivery at increasing intervals: 3 minutes, 5 minutes, 30 minutes, + 1 hour, 6 hours, 1 day, 3 days, 7 days, 30 days. +

+
+
+
+ + + +
+ + Security & Implementation Guide +
+
+ + + + Overview + Payload + Verification + + + +
+

Each webhook request includes:

+
    +
  • Payload in JSON format
  • +
  • X-Signature header for verification
  • +
  • + Signature format: sha256=HMAC_SHA256(payload, secret) +
  • +
+
+
+ + +
+

Sample Payload

+ {JSON.stringify(SAMPLE_PAYLOAD, null, 2)} +
+
+ + +
+ + + Node.js + Python + + + + {VERIFICATION_CODE} + + + + {PYTHON_CODE} + + +
+
+
+
+
+
+ ) +} diff --git a/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx b/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx new file mode 100644 index 0000000..acbae7f --- /dev/null +++ b/web/app/(app)/dashboard/(components)/webhooks/webhooks-section.tsx @@ -0,0 +1,178 @@ +'use client' + +import { Button } from '@/components/ui/button' +import { PlusCircle, Webhook } from 'lucide-react' +import { useState } from 'react' +import { WebhookData } from '@/lib/types' +import { WebhookCard } from './webhook-card' +import { WebhookDocs } from './webhook-docs' +import { CreateWebhookDialog } from './create-webhook-dialog' +import { EditWebhookDialog } from './edit-webhook-dialog' +import { useQuery, useQueryClient } from '@tanstack/react-query' +import httpBrowserClient from '@/lib/httpBrowserClient' +import { ApiEndpoints } from '@/config/api' +import { Skeleton } from '@/components/ui/skeleton' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip" + +function WebhookCardSkeleton() { + return ( +
+
+
+ + +
+
+ + + +
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ ) +} + +export default function WebhooksSection() { + const [createDialogOpen, setCreateDialogOpen] = useState(false) + const [editDialogOpen, setEditDialogOpen] = useState(false) + const [selectedWebhook, setSelectedWebhook] = useState( + null + ) + const queryClient = useQueryClient() + + const { + data: webhooks, + isLoading, + error, + } = useQuery({ + queryKey: ['webhooks'], + queryFn: () => + httpBrowserClient + .get(ApiEndpoints.gateway.getWebhooks()) + .then((res) => res.data), + }) + + const handleCreateClick = () => { + setCreateDialogOpen(true) + } + + const handleEditClick = (webhook: WebhookData) => { + setSelectedWebhook(webhook) + setEditDialogOpen(true) + } + + return ( +
+
+
+

+ + Webhooks + + + + + BETA + + + +

This feature is in beta and may undergo changes. Use with caution in production environments.

+
+
+
+

+

+ Manage webhook notifications for your SMS events +

+
+ +
+ +
+
+ {isLoading ? ( +
+ + +
+ ) : error ? ( +
+ Error: {error.message} +
+ ) : webhooks?.data?.length > 0 ? ( +
+ {webhooks.data.map((webhook) => ( + handleEditClick(webhook)} + /> + ))} +
+ ) : ( +
+

No webhook configured

+

+ Create a webhook to receive real-time notifications for SMS events +

+ +
+ )} +
+ +
+ +
+
+ +
+ +
+ + + + {selectedWebhook && ( + + )} +
+ ) +} diff --git a/web/components/shared/copy-button.tsx b/web/components/shared/copy-button.tsx new file mode 100644 index 0000000..7d30011 --- /dev/null +++ b/web/components/shared/copy-button.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Check, Copy } from "lucide-react"; +import { useState } from "react"; +import { useToast } from "@/hooks/use-toast"; + +interface CopyButtonProps { + value: string; + label: string; +} + +export function CopyButton({ value, label }: CopyButtonProps) { + const [copied, setCopied] = useState(false); + const { toast } = useToast(); + + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(value); + setCopied(true); + toast({ + title: "Copied!", + description: `${label} copied to clipboard`, + }); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + toast({ + title: "Failed to copy", + description: "Please try again", + variant: "destructive", + }); + } + }; + + return ( + + ); +} \ No newline at end of file diff --git a/web/components/ui/alert-dialog.tsx b/web/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..57760f2 --- /dev/null +++ b/web/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +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" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/web/components/ui/code.tsx b/web/components/ui/code.tsx new file mode 100644 index 0000000..d9f7b91 --- /dev/null +++ b/web/components/ui/code.tsx @@ -0,0 +1,17 @@ +import { cn } from "@/lib/utils"; + +interface CodeProps extends React.HTMLAttributes {} + +export function Code({ className, children, ...props }: CodeProps) { + return ( +
+      {children}
+    
+ ); +} \ No newline at end of file diff --git a/web/components/ui/skeleton.tsx b/web/components/ui/skeleton.tsx new file mode 100644 index 0000000..d7e45f7 --- /dev/null +++ b/web/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/web/components/ui/tooltip.tsx b/web/components/ui/tooltip.tsx new file mode 100644 index 0000000..a66b3f2 --- /dev/null +++ b/web/components/ui/tooltip.tsx @@ -0,0 +1,32 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/web/config/api.ts b/web/config/api.ts index 472b907..d6a9c1b 100644 --- a/web/config/api.ts +++ b/web/config/api.ts @@ -23,6 +23,9 @@ export const ApiEndpoints = { sendBulkSMS: (id: string) => `/gateway/devices/${id}/send-bulk-sms`, getReceivedSMS: (id: string) => `/gateway/devices/${id}/get-received-sms`, + getWebhooks: () => '/webhooks', + createWebhook: () => '/webhooks', + updateWebhook: (id: string) => `/webhooks/${id}`, getStats: () => '/gateway/stats', }, } diff --git a/web/lib/constants.ts b/web/lib/constants.ts new file mode 100644 index 0000000..9967356 --- /dev/null +++ b/web/lib/constants.ts @@ -0,0 +1,3 @@ +export const WEBHOOK_EVENTS = { + MESSAGE_RECEIVED: 'MESSAGE_RECEIVED', +} as const diff --git a/web/lib/types.ts b/web/lib/types.ts new file mode 100644 index 0000000..8378f6e --- /dev/null +++ b/web/lib/types.ts @@ -0,0 +1,17 @@ +export interface WebhookData { + _id?: string + deliveryUrl: string + events: string[] + isActive: boolean + signingSecret: string +} + +export interface WebhookPayload { + smsId: string + sender: string + message: string + receivedAt: string + deviceId: string + webhookSubscriptionId: string + webhookEvent: string +} diff --git a/web/package.json b/web/package.json index 2cb9e1c..b920eab 100644 --- a/web/package.json +++ b/web/package.json @@ -15,6 +15,7 @@ "@hookform/resolvers": "^3.9.1", "@prisma/client": "^5.22.0", "@radix-ui/react-accordion": "^1.2.1", + "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", @@ -28,6 +29,7 @@ "@radix-ui/react-switch": "^1.1.1", "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.2.2", + "@radix-ui/react-tooltip": "^1.1.6", "@react-oauth/google": "^0.12.1", "@tanstack/react-query": "^5.61.0", "axios": "^1.6.5", @@ -48,6 +50,7 @@ "react-syntax-highlighter": "^15.5.0", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", + "uuid": "^11.0.3", "zod": "^3.23.8" }, "engines": { diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index ee66ea1..8c06c5e 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@radix-ui/react-accordion': specifier: ^1.2.1 version: 1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-alert-dialog': + specifier: ^1.1.4 + version: 1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@radix-ui/react-avatar': specifier: ^1.1.1 version: 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -56,6 +59,9 @@ importers: '@radix-ui/react-toast': specifier: ^1.2.2 version: 1.2.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-tooltip': + specifier: ^1.1.6 + version: 1.1.6(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) '@react-oauth/google': specifier: ^0.12.1 version: 0.12.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -116,6 +122,9 @@ importers: tailwindcss-animate: specifier: ^1.0.7 version: 1.0.7(tailwindcss@3.4.14) + uuid: + specifier: ^11.0.3 + version: 11.0.3 zod: specifier: ^3.23.8 version: 3.23.8 @@ -348,6 +357,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-accordion@1.2.1': resolution: {integrity: sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ==} peerDependencies: @@ -361,6 +373,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-alert-dialog@1.1.4': + resolution: {integrity: sha512-A6Kh23qZDLy3PSU4bh2UJZznOrUdHImIXqF8YtUa6CN73f8EOO9XlXSCd9IHyPvIquTaa/kwaSWzZTtUvgXVGw==} + 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-arrow@1.1.0': resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} peerDependencies: @@ -374,6 +399,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-arrow@1.1.1': + resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} + 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-avatar@1.1.1': resolution: {integrity: sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==} peerDependencies: @@ -435,6 +473,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: @@ -466,6 +513,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dialog@1.1.4': + resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==} + 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-direction@1.1.0': resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} peerDependencies: @@ -488,6 +548,19 @@ packages: '@types/react-dom': 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-dropdown-menu@2.1.2': resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==} peerDependencies: @@ -523,6 +596,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-focus-scope@1.1.1': + resolution: {integrity: sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==} + 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.0': resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: @@ -589,6 +675,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-popper@1.2.1': + resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} + 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-portal@1.1.2': resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==} peerDependencies: @@ -602,6 +701,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: @@ -615,6 +727,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: @@ -628,6 +753,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: @@ -676,6 +814,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-switch@1.1.1': resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==} peerDependencies: @@ -715,6 +862,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-tooltip@1.1.6': + resolution: {integrity: sha512-TLB5D8QLExS1uDn7+wH/bjEmRurNMTzNrtq7IjaS4kjion9NtzsTGkvR5+i7yc9q01Pi2KMM2cN3f8UG4IvvXA==} + 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: @@ -791,6 +951,19 @@ packages: '@types/react-dom': 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 + '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} @@ -2081,6 +2254,16 @@ packages: '@types/react': optional: true + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-remove-scroll@2.6.0: resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} engines: {node: '>=10'} @@ -2091,6 +2274,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.6.2: + resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-style-singleton@2.2.1: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -2101,6 +2294,16 @@ packages: '@types/react': optional: true + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react-syntax-highlighter@15.5.0: resolution: {integrity: sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==} peerDependencies: @@ -2384,6 +2587,16 @@ packages: '@types/react': optional: true + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + use-sidecar@1.1.2: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} @@ -2397,6 +2610,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@11.0.3: + resolution: {integrity: sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -2621,6 +2838,8 @@ snapshots: '@radix-ui/primitive@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-accordion@1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -2638,6 +2857,20 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-alert-dialog@1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-dialog': 1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-arrow@1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -2647,6 +2880,15 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-arrow@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-avatar@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) @@ -2709,6 +2951,12 @@ snapshots: optionalDependencies: '@types/react': 18.2.48 + '@radix-ui/react-compose-refs@1.1.1(@types/react@18.2.48)(react@18.2.0)': + dependencies: + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.48 + '@radix-ui/react-context@1.1.0(@types/react@18.2.48)(react@18.2.0)': dependencies: react: 18.2.0 @@ -2743,6 +2991,28 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-dialog@1.1.4(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.48)(react@18.2.0) + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.6.2(@types/react@18.2.48)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-direction@1.1.0(@types/react@18.2.48)(react@18.2.0)': dependencies: react: 18.2.0 @@ -2762,6 +3032,19 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -2794,6 +3077,17 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-focus-scope@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-icons@1.3.0(react@18.2.0)': dependencies: react: 18.2.0 @@ -2880,6 +3174,24 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-popper@1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@floating-ui/react-dom': 2.1.2(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-arrow': 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-rect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-size': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/rect': 1.1.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) @@ -2890,6 +3202,16 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-portal@1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-presence@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.2.48)(react@18.2.0) @@ -2900,6 +3222,16 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-presence@1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-primitive@2.0.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/react-slot': 1.1.0(@types/react@18.2.48)(react@18.2.0) @@ -2909,6 +3241,15 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-primitive@2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-roving-focus@1.1.0(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -2979,6 +3320,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.48 + '@radix-ui/react-slot@1.1.1(@types/react@18.2.48)(react@18.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + react: 18.2.0 + optionalDependencies: + '@types/react': 18.2.48 + '@radix-ui/react-switch@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -3030,6 +3378,26 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-tooltip@1.1.6(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-context': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-id': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + '@radix-ui/react-slot': 1.1.1(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.2.48)(react@18.2.0) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.2.48)(react@18.2.0)': dependencies: react: 18.2.0 @@ -3085,6 +3453,15 @@ snapshots: '@types/react': 18.2.48 '@types/react-dom': 18.2.18 + '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.2.18)(@types/react@18.2.48)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + '@types/react-dom': 18.2.18 + '@radix-ui/rect@1.1.0': {} '@react-oauth/google@0.12.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0)': @@ -4525,6 +4902,14 @@ snapshots: optionalDependencies: '@types/react': 18.2.48 + react-remove-scroll-bar@2.3.8(@types/react@18.2.48)(react@18.2.0): + dependencies: + react: 18.2.0 + react-style-singleton: 2.2.3(@types/react@18.2.48)(react@18.2.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.2.48 + react-remove-scroll@2.6.0(@types/react@18.2.48)(react@18.2.0): dependencies: react: 18.2.0 @@ -4536,6 +4921,17 @@ snapshots: optionalDependencies: '@types/react': 18.2.48 + react-remove-scroll@2.6.2(@types/react@18.2.48)(react@18.2.0): + dependencies: + react: 18.2.0 + react-remove-scroll-bar: 2.3.8(@types/react@18.2.48)(react@18.2.0) + react-style-singleton: 2.2.1(@types/react@18.2.48)(react@18.2.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.2.48)(react@18.2.0) + use-sidecar: 1.1.2(@types/react@18.2.48)(react@18.2.0) + optionalDependencies: + '@types/react': 18.2.48 + react-style-singleton@2.2.1(@types/react@18.2.48)(react@18.2.0): dependencies: get-nonce: 1.0.1 @@ -4545,6 +4941,14 @@ snapshots: optionalDependencies: '@types/react': 18.2.48 + react-style-singleton@2.2.3(@types/react@18.2.48)(react@18.2.0): + dependencies: + get-nonce: 1.0.1 + react: 18.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.2.48 + react-syntax-highlighter@15.5.0(react@18.2.0): dependencies: '@babel/runtime': 7.23.8 @@ -4878,6 +5282,13 @@ snapshots: optionalDependencies: '@types/react': 18.2.48 + use-callback-ref@1.3.3(@types/react@18.2.48)(react@18.2.0): + dependencies: + react: 18.2.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.2.48 + use-sidecar@1.1.2(@types/react@18.2.48)(react@18.2.0): dependencies: detect-node-es: 1.1.0 @@ -4888,6 +5299,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@11.0.3: {} + uuid@8.3.2: {} which-boxed-primitive@1.0.2: