From 370ec98234a48d3aef41025bb02684c1fac0bb16 Mon Sep 17 00:00:00 2001 From: Alaister Young Date: Sat, 26 Oct 2024 18:21:33 +0800 Subject: [PATCH] progress --- app/api/domains/route.ts | 83 +- components/WhoisInfo.tsx | 159 ++++ components/add-domain.tsx | 5 +- components/configured-section-placeholder.js | 12 - components/configured-section.tsx | 211 ----- components/domain-card-placeholder.js | 27 - components/domain-card-skeleton.tsx | 33 + components/domain-card.tsx | 282 ++++--- components/domain-for-sale.tsx | 68 ++ components/domain-overview.tsx | 53 ++ components/domain-settings.tsx | 89 +++ components/ui/blur-overlay.tsx | 59 ++ components/ui/button.tsx | 1 + components/ui/sheet.tsx | 55 +- components/ui/tabs.tsx | 15 +- lib/data/domain-name-add-mutation.ts | 11 +- lib/data/domain-name-delete-mutation.ts | 30 + lib/data/domain-name-update-mutation.ts | 39 + lib/data/domain-names-query.ts | 12 +- lib/data/update-whois-mutation.ts | 29 + lib/database.types.ts | 48 +- lib/fetcher.tsx | 15 - lib/query-client.ts | 1 + next.config.js | 1 + package-lock.json | 730 ++++++++++++++++-- package.json | 13 +- pages/api/check-domain.ts | 6 +- pages/api/remove-domain.ts | 84 -- pages/api/verify-domain.js | 17 - pages/app/index.tsx | 46 +- supabase/functions/_lib/cors.ts | 5 + supabase/functions/update-whois/index.ts | 41 +- .../20240917093911_add_domain_info.sql | 26 +- .../20240917123202_update_whois_function.sql | 2 +- 34 files changed, 1634 insertions(+), 674 deletions(-) create mode 100644 components/WhoisInfo.tsx delete mode 100644 components/configured-section-placeholder.js delete mode 100644 components/configured-section.tsx delete mode 100644 components/domain-card-placeholder.js create mode 100644 components/domain-card-skeleton.tsx create mode 100644 components/domain-for-sale.tsx create mode 100644 components/domain-overview.tsx create mode 100644 components/domain-settings.tsx create mode 100644 components/ui/blur-overlay.tsx create mode 100644 lib/data/domain-name-delete-mutation.ts create mode 100644 lib/data/domain-name-update-mutation.ts create mode 100644 lib/data/update-whois-mutation.ts delete mode 100644 pages/api/remove-domain.ts delete mode 100644 pages/api/verify-domain.js create mode 100644 supabase/functions/_lib/cors.ts diff --git a/app/api/domains/route.ts b/app/api/domains/route.ts index 2778048..c16938b 100644 --- a/app/api/domains/route.ts +++ b/app/api/domains/route.ts @@ -1,7 +1,7 @@ import { NextRequest } from 'next/server' +import { ParseResultType, parseDomain } from 'parse-domain' import * as z from 'zod' import supabaseAdmin, { getUserFromRequest } from '~/lib/supabase-admin' -import { ParseResultType, parseDomain } from 'parse-domain' const schema = z.object({ domainName: z.string().min(1, 'Domain must not be empty'), @@ -48,7 +48,7 @@ export async function POST(request: NextRequest) { const parseResult = parseDomain(rawDomainName) if (parseResult.type === ParseResultType.Listed) { const { domain, topLevelDomains } = parseResult - domainName = `${domain}.${topLevelDomains.join('.')}` + domainName = `${domain}.${topLevelDomains.join('.')}`.toLowerCase() } if (domainName === undefined) { return new Response( @@ -64,60 +64,33 @@ export async function POST(request: NextRequest) { ) } - await supabaseAdmin.from('domain_names').insert({ - domain_name: domainName, - user_id: user.id, - }) - - // We have to add the domains in order because of the redirect on the second domain - const apexResponse = await fetch( - `https://api.vercel.com/v10/projects/${process.env.VERCEL_PROJECT_ID}/domains?teamId=${process.env.VERCEL_TEAM_ID}`, - { - body: JSON.stringify({ - name: domainName, - }), - headers: { - Authorization: `Bearer ${process.env.VERCEL_AUTH_BEARER_TOKEN}`, - 'Content-Type': 'application/json', - }, - method: 'POST', - } - ) + const { data, error } = await supabaseAdmin + .from('domain_names') + .insert({ + domain_name: domainName, + user_id: user.id, + }) + .select('id') + .single() - const wwwResponse = await fetch( - `https://api.vercel.com/v10/projects/${process.env.VERCEL_PROJECT_ID}/domains?teamId=${process.env.VERCEL_TEAM_ID}`, - { - body: JSON.stringify({ - name: `www.${domainName}`, - redirect: domainName, - redirectStatusCode: 308, + if (error) { + return new Response( + JSON.stringify({ + code: 'INSERT_ERROR', + message: error.message, }), - headers: { - Authorization: `Bearer ${process.env.VERCEL_AUTH_BEARER_TOKEN}`, - 'Content-Type': 'application/json', - }, - method: 'POST', - } - ) - - const apexData = await apexResponse.json() - const wwwData = await wwwResponse.json() + { + status: 500, + headers: { + 'Content-Type': 'application/json', + }, + } + ) + } - return new Response( - JSON.stringify({ - apex: { - domainName: apexData.name, - verificationRecords: apexData.verification, - }, - www: { - domainName: wwwData.name, - verificationRecords: wwwData.verification, - }, - }), - { - headers: { - 'Content-Type': 'application/json;', - }, - } - ) + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json;', + }, + }) } diff --git a/components/WhoisInfo.tsx b/components/WhoisInfo.tsx new file mode 100644 index 0000000..3275be7 --- /dev/null +++ b/components/WhoisInfo.tsx @@ -0,0 +1,159 @@ +import { Building, Globe, Phone, Shield, User } from 'lucide-react' +import { useCallback, useMemo } from 'react' +import { Badge } from './ui/badge' + +type ContactInfo = { + id?: string + fax?: string + city?: string + name?: string + email?: string + phone?: string + street?: string + country?: string + province?: string + postal_code?: string + organization?: string +} + +type DomainInfo = { + id?: string + name?: string + domain?: string + status?: string[] + punycode?: string + extension?: string + created_date?: string + name_servers?: string[] + updated_date?: string + whois_server?: string + expiration_date?: string + created_date_in_time?: string + updated_date_in_time?: string + expiration_date_in_time?: string +} + +type RegistrarInfo = { + id?: string + name?: string + email?: string + phone?: string + referral_url?: string +} + +type WhoisData = { + domain?: DomainInfo + registrar?: RegistrarInfo + technical?: ContactInfo + registrant?: ContactInfo + administrative?: ContactInfo +} + +interface WhoisInfoProps { + data: WhoisData +} + +const WhoisInfo = ({ data }: WhoisInfoProps) => { + const renderSection = useCallback( + (data: Record | undefined, excludeFields: string[] = []) => { + if (!data) return null + return Object.entries(data) + .filter(([key]) => !excludeFields.includes(key)) + .map(([key, value]) => ( +
+
+ {key.replace(/_/g, ' ')} +
+
+ {Array.isArray(value) ? ( +
+ {value.map((item, index) => ( + + {item} + + ))} +
+ ) : ( + value || 'N/A' + )} +
+
+ )) + }, + [] + ) + + const primarySection = useMemo(() => { + if (!data?.domain) return null + + return ( +
+
+ +

Domain Information

+
+
+ {renderSection(data.domain, [ + 'created_date_in_time', + 'updated_date_in_time', + 'expiration_date_in_time', + ])} +
+
+ ) + }, [data.domain, renderSection]) + + const contactSections = useMemo( + () => [ + { + title: 'Registrar', + icon: Building, + data: data?.registrar, + }, + { + title: 'Technical Contact', + icon: Shield, + data: data?.technical, + }, + { + title: 'Registrant', + icon: User, + data: data?.registrant, + }, + { + title: 'Administrative Contact', + icon: Phone, + data: data?.administrative, + }, + ], + [data] + ) + + return ( +
+ {primarySection} + +
+ {contactSections.map( + ({ title, icon: Icon, data: sectionData }) => + sectionData && ( +
+
+ +

{title}

+
+
+ {renderSection(sectionData)} +
+
+ ) + )} +
+
+ ) +} + +export default WhoisInfo diff --git a/components/add-domain.tsx b/components/add-domain.tsx index 3f6f9dc..d6f4b05 100644 --- a/components/add-domain.tsx +++ b/components/add-domain.tsx @@ -1,4 +1,5 @@ import { zodResolver } from '@hookform/resolvers/zod' +import { Plus } from 'lucide-react' import { useEffect } from 'react' import { useForm } from 'react-hook-form' import * as z from 'zod' @@ -68,7 +69,7 @@ const AddDomain = () => {
{ type="submit" isLoading={form.formState.isSubmitting} disabled={form.formState.isSubmitting} + className="self-end" > + Add Domain diff --git a/components/configured-section-placeholder.js b/components/configured-section-placeholder.js deleted file mode 100644 index 33dc9a8..0000000 --- a/components/configured-section-placeholder.js +++ /dev/null @@ -1,12 +0,0 @@ -const ConfiguredSectionPlaceholder = () => { - return ( -
-
-

- Loading Configuration -

-
- ) -} - -export default ConfiguredSectionPlaceholder diff --git a/components/configured-section.tsx b/components/configured-section.tsx deleted file mode 100644 index 2a14167..0000000 --- a/components/configured-section.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import ConfiguredSectionPlaceholder from './configured-section-placeholder' - -function getVerificationError(verificationResponse) { - try { - const error = verificationResponse.error - if (error.code === 'missing_txt_record') { - return null - } - return error.message - } catch { - return null - } -} - -export type ConfiguredSectionProps = { - domainInfo?: any - wwwDomainInfo?: any -} - -const ConfiguredSection = ({ - domainInfo, - wwwDomainInfo, -}: ConfiguredSectionProps) => { - if (!domainInfo) { - return - } - - if (!domainInfo.verified) { - const txtVerification = domainInfo.verification.find( - (x) => x.type === 'TXT' - ) - - return ( - <> -
- - - - - -

- Domain is pending verification -

-
- -
- -
-
-
- Verify Domain Ownership -
-
-
-

- Please set the following TXT record on {domainInfo.apexName} to - prove ownership of {domainInfo.name}: -

-
-
-

Type

-

{txtVerification.type}

-
-
-

Name

-

- {txtVerification.domain.slice( - 0, - txtVerification.domain.length - - domainInfo.apexName.length - - 1 - )} -

-
-
-

Value

-

- {txtVerification.value} -

-
-
- {getVerificationError(domainInfo.verificationResponse) && ( -

- {getVerificationError(domainInfo.verificationResponse)} -

- )} -
-
- - ) - } - - return ( - <> -
- - - {domainInfo?.configured && wwwDomainInfo?.configured ? ( - <> - - - ) : ( - <> - - - - )} - -

- {domainInfo?.configured && wwwDomainInfo?.configured - ? 'Valid' - : 'Invalid'}{' '} - Configuration -

-
- - {(!domainInfo?.configured || !wwwDomainInfo?.configured) && ( - <> -
- -
-
-
- Records -
-
-
-

- Set the following record on your DNS provider to continue: -

-
-
-

Type

- {!domainInfo?.configured && ( -

A

- )} - {!wwwDomainInfo?.configured && ( -

CNAME

- )} -
-
-

Name

-

- {!domainInfo?.configured && ( -

@

- )} - {!wwwDomainInfo?.configured && ( -

www

- )} -

-
-
-

Value

-

- {!domainInfo?.configured && ( -

76.76.21.21

- )} - {!wwwDomainInfo?.configured && ( -

- cname.side.domains -

- )} -

-
-
-
-
- - )} - - ) -} - -export default ConfiguredSection diff --git a/components/domain-card-placeholder.js b/components/domain-card-placeholder.js deleted file mode 100644 index 01106d2..0000000 --- a/components/domain-card-placeholder.js +++ /dev/null @@ -1,27 +0,0 @@ -import LoadingDots from '../components/loading-dots' -import ConfiguredSectionPlaceholder from './configured-section-placeholder' - -const DomainCardPlaceholder = () => { - return ( -
-
-
-
- - -
-
- -
- ) -} - -export default DomainCardPlaceholder diff --git a/components/domain-card-skeleton.tsx b/components/domain-card-skeleton.tsx new file mode 100644 index 0000000..9368b38 --- /dev/null +++ b/components/domain-card-skeleton.tsx @@ -0,0 +1,33 @@ +import { Card, CardContent, CardHeader } from './ui/card' +import { Skeleton } from './ui/skeleton' + +const DomainOverviewSkeleton = () => { + return ( + + +
+ {/* Globe icon */} + {/* Domain name */} +
+
+ +
+
+ {/* Status icon */} + {/* "Status:" text */} +
+ {/* Status badge */} +
+
+
+ {/* Calendar icon */} + {/* "Expires:" text */} +
+ {/* Expires date */} +
+
+
+ ) +} + +export default DomainOverviewSkeleton diff --git a/components/domain-card.tsx b/components/domain-card.tsx index 9e0f196..cb2907e 100644 --- a/components/domain-card.tsx +++ b/components/domain-card.tsx @@ -1,101 +1,209 @@ -import { useQueryClient } from '@tanstack/react-query' -import { useState } from 'react' -import useSWR, { mutate } from 'swr' -import supabase from '~/lib/supabase' -import fetcher from '../lib/fetcher' -import ConfiguredSection from './configured-section' +import { format, formatDistance, isPast } from 'date-fns' +import { + BadgeDollarSign, + Calendar, + Check, + CircleDot, + CircleHelp, + Globe, + Lock, + RefreshCw, + Settings2, +} from 'lucide-react' +import { Domain } from '~/lib/data/domain-names-query' +import { useUpdateWhoisMutation } from '~/lib/data/update-whois-mutation' +import { cn } from '~/lib/utils' +import DomainForSale from './domain-for-sale' +import DomainSettings from './domain-settings' +import { Badge } from './ui/badge' +import { BlurOverlay } from './ui/blur-overlay' import { Button } from './ui/button' +import { Card, CardContent, CardHeader, CardTitle } from './ui/card' +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from './ui/sheet' +import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs' +import DomainInfo from './WhoisInfo' +import { useDomainUpdateMutation } from '~/lib/data/domain-name-update-mutation' -const DomainCard = ({ domain }) => { - const queryClient = useQueryClient() +interface DomainCardProps { + domain: Domain +} - const { data: domainInfo, isValidating } = useSWR( - `/api/check-domain?domain=${domain}`, - fetcher, - { revalidateOnMount: true, refreshInterval: 5000 } - ) - const { data: wwwDomainInfo, isValidating: isValidatingWWW } = useSWR( - `/api/check-domain?domain=www.${domain}`, - fetcher, - { revalidateOnMount: true, refreshInterval: 5000 } - ) +const getStatusDetails = (status: Domain['status']) => { + switch (status) { + case 'registered': + return { + color: 'bg-blue-500', + icon: Lock, + } + case 'available': + return { + color: 'bg-green-500', + icon: Check, + } + case 'unknown': + default: + return { + color: 'bg-gray-500', + icon: CircleHelp, + } + } +} + +const DomainCard = ({ domain }: DomainCardProps) => { + const { mutate: updateDomain, isLoading: isUpdatingDomain } = + useDomainUpdateMutation() + const { mutate: updateWhois, isLoading: isUpdatingWhois } = + useUpdateWhoisMutation() - const [removing, setRemoving] = useState(false) + const expiryDate = domain.expires_at !== null && new Date(domain.expires_at) + const isExpired = expiryDate && isPast(expiryDate) + const statusDetails = getStatusDetails(domain.status) + const StatusIcon = statusDetails.icon return ( -
-
- - {domain} + + + + +
+ + {domain.domain_name} +
+
+ +
+
+ + Status: +
+ + + {domain.status} + +
+ {expiryDate && ( +
+ + + Expires: {format(expiryDate, 'MMM d, yyyy')} + {isExpired && ( + + (Expired) + + )} + +
+ )} +
+
+
- - + + {domain.domain_name} + + + + + + + Whois + + - - - - - -
-
- - +
+ + {domain.whois_data ? ( + + ) : ( +
+
+

Whois Information

+
+

No whois information available

+
+ )} + - await fetch(`/api/remove-domain?domain=${domain}`, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) + +
+
+ +

For Sale Page

+
- await queryClient.invalidateQueries(['domain-names']) - } catch (error) { - alert(`Error removing domain`) - } finally { - setRemoving(false) - } - }} - isLoading={removing} - disabled={removing} - variant="destructive" - > - Remove - -
-
+ { + updateDomain({ + id: domain.id, + isOwned: true, + }) + }} + isLoading={isUpdatingDomain} + > + + +
+ - -
+ + + + + + ) } diff --git a/components/domain-for-sale.tsx b/components/domain-for-sale.tsx new file mode 100644 index 0000000..30c3151 --- /dev/null +++ b/components/domain-for-sale.tsx @@ -0,0 +1,68 @@ +import { Domain } from '~/lib/data/domain-names-query' + +interface DomainForSaleProps { + domain: Domain +} + +const DomainForSale = ({ domain }: DomainForSaleProps) => { + return ( +
+

+ Set the following DNS records for your domain: +

+ +
+ {/* Headers */} +
+
+

Type

+
+
+

Name

+
+
+

Value

+
+
+ + {/* DNS Records */} +
+ {/* Apex domain record */} +
+
+

A

+
+
+

{domain.domain_name}

+
+
+

209.38.6.142

+
+
+ +
+ + {/* WWW record */} +
+
+

A

+
+
+

www.{domain.domain_name}

+
+
+

209.38.6.142

+
+
+
+
+ +

+ If you're using Cloudflare, make sure you set your SSL setting to + Full (strict). Otherwise you may experience redirect loops. +

+
+ ) +} + +export default DomainForSale diff --git a/components/domain-overview.tsx b/components/domain-overview.tsx new file mode 100644 index 0000000..e2cdbfd --- /dev/null +++ b/components/domain-overview.tsx @@ -0,0 +1,53 @@ +import { format, isPast } from 'date-fns' +import { Calendar, Globe } from 'lucide-react' +import { Domain } from '~/lib/data/domain-names-query' +import { Badge } from './ui/badge' +import { cn } from '~/lib/utils' + +interface DomainOverviewProps { + domain: Domain +} + +const getStatusColor = (status: Domain['status']) => { + switch (status) { + case 'registered': + return 'bg-green-500' + case 'available': + return 'bg-blue-500' + case 'unknown': + default: + return 'bg-gray-500' + } +} + +const DomainOverview = ({ domain }: DomainOverviewProps) => { + const expiryDate = domain.expires_at !== null && new Date(domain.expires_at) + const isExpired = expiryDate && isPast(expiryDate) + + return ( +
+
+
+ + {domain.domain_name} +
+ + {domain.status} + +
+ {expiryDate && ( +
+ + + Expires: {format(expiryDate, 'MMM d, yyyy')} + {isExpired && ( + (Expired) + )} + +
+ )} +
+ ) +} + +export default DomainOverview diff --git a/components/domain-settings.tsx b/components/domain-settings.tsx new file mode 100644 index 0000000..5277024 --- /dev/null +++ b/components/domain-settings.tsx @@ -0,0 +1,89 @@ +import { Settings2, Trash2 } from 'lucide-react' +import { Button } from '~/components/ui/button' +import { Label } from '~/components/ui/label' +import { Separator } from '~/components/ui/separator' +import { Switch } from '~/components/ui/switch' +import { useDomainDeleteMutation } from '~/lib/data/domain-name-delete-mutation' +import { useDomainUpdateMutation } from '~/lib/data/domain-name-update-mutation' +import { Domain } from '~/lib/data/domain-names-query' + +interface DomainSettingsProps { + domain: Domain +} + +const DomainSettings = ({ domain }: DomainSettingsProps) => { + const { mutate: updateDomain, isLoading: isUpdatingDomain } = + useDomainUpdateMutation() + const { mutate: deleteDomain, isLoading: isDeletingDomain } = + useDomainDeleteMutation() + + return ( +
+
+ +

Settings

+
+ +
+
+
+ +

+ Mark this domain as owned and optionally enable for sale page for + this domain +

+
+ { + updateDomain({ + id: domain.id, + isOwned: !domain.is_owned, + }) + }} + disabled={isUpdatingDomain} + /> +
+ +
+
+ +

+ Receive alerts when domain status changes +

+
+ { + updateDomain({ + id: domain.id, + statusChangeNotificationsEnabled: + !domain.status_change_notifications_enabled, + }) + }} + disabled={isUpdatingDomain} + /> +
+ + + +
+ +
+
+
+ ) +} + +export default DomainSettings diff --git a/components/ui/blur-overlay.tsx b/components/ui/blur-overlay.tsx new file mode 100644 index 0000000..bdd51fc --- /dev/null +++ b/components/ui/blur-overlay.tsx @@ -0,0 +1,59 @@ +import { PropsWithChildren } from 'react' +import { Button } from '~/components/ui/button' +import { cn } from '~/lib/utils' + +interface BlurOverlayProps { + isBlurred: boolean + title: string + buttonText: string + className?: string + onClick: () => void + isLoading?: boolean +} + +export const BlurOverlay = ({ + isBlurred, + title, + buttonText, + onClick, + isLoading, + children, +}: PropsWithChildren) => { + return ( +
+ {/* Content Container */} +
+ {children} +
+ + {/* Blur Overlay Container */} +
+ {/* Scaled blur background */} +
+ + {/* Content */} +

{title}

+ +
+
+ ) +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx index b2d2c4c..fe1b7b3 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -30,6 +30,7 @@ const buttonVariants = cva( }, size: { default: 'h-10 px-4 py-2', + xs: 'h-6 px-2 text-xs', sm: 'h-9 px-3', lg: 'h-11 px-8', icon: 'h-10 w-10', diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx index 6d8887a..16f156f 100644 --- a/components/ui/sheet.tsx +++ b/components/ui/sheet.tsx @@ -1,9 +1,10 @@ -import * as React from "react" -import * as SheetPrimitive from "@radix-ui/react-dialog" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" +'use client' -import { cn } from "~/lib/utils" +import * as SheetPrimitive from '@radix-ui/react-dialog' +import { cva, type VariantProps } from 'class-variance-authority' +import { X } from 'lucide-react' +import * as React from 'react' +import { cn } from '~/lib/utils' const Sheet = SheetPrimitive.Root @@ -11,13 +12,7 @@ const SheetTrigger = SheetPrimitive.Trigger const SheetClose = SheetPrimitive.Close -const SheetPortal = ({ - className, - ...props -}: SheetPrimitive.DialogPortalProps) => ( - -) -SheetPortal.displayName = SheetPrimitive.Portal.displayName +const SheetPortal = SheetPrimitive.Portal const SheetOverlay = React.forwardRef< React.ElementRef, @@ -25,7 +20,7 @@ const SheetOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( , SheetContentProps ->(({ side = "right", className, children, ...props }, ref) => ( +>(({ side = 'right', className, children, ...props }, ref) => ( ) => (
) -SheetHeader.displayName = "SheetHeader" +SheetHeader.displayName = 'SheetHeader' const SheetFooter = ({ className, @@ -98,13 +93,13 @@ const SheetFooter = ({ }: React.HTMLAttributes) => (
) -SheetFooter.displayName = "SheetFooter" +SheetFooter.displayName = 'SheetFooter' const SheetTitle = React.forwardRef< React.ElementRef, @@ -112,7 +107,7 @@ const SheetTitle = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -124,7 +119,7 @@ const SheetDescription = React.forwardRef< >(({ className, ...props }, ref) => ( )) @@ -132,11 +127,13 @@ SheetDescription.displayName = SheetPrimitive.Description.displayName export { Sheet, - SheetTrigger, SheetClose, SheetContent, - SheetHeader, + SheetDescription, SheetFooter, + SheetHeader, + SheetOverlay, + SheetPortal, SheetTitle, - SheetDescription, + SheetTrigger, } diff --git a/components/ui/tabs.tsx b/components/ui/tabs.tsx index 94e4a3e..b0e8fa1 100644 --- a/components/ui/tabs.tsx +++ b/components/ui/tabs.tsx @@ -1,7 +1,8 @@ -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" +'use client' -import { cn } from "~/lib/utils" +import * as TabsPrimitive from '@radix-ui/react-tabs' +import * as React from 'react' +import { cn } from '~/lib/utils' const Tabs = TabsPrimitive.Root @@ -12,7 +13,7 @@ const TabsList = React.forwardRef< { - const { data, error } = await supabase.from('domain_names').insert({ - domain_name: domainName, + await fetchAPI('/domains', 'POST', { + domainName, }) - - if (error) { - throw error - } - - return data }, { async onSuccess() { diff --git a/lib/data/domain-name-delete-mutation.ts b/lib/data/domain-name-delete-mutation.ts new file mode 100644 index 0000000..d034f06 --- /dev/null +++ b/lib/data/domain-name-delete-mutation.ts @@ -0,0 +1,30 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import supabase from '../supabase' + +export interface DomainDeleteVariables { + id: string +} + +export function useDomainDeleteMutation() { + const queryClient = useQueryClient() + + return useMutation( + async ({ id }: DomainDeleteVariables) => { + const { data, error } = await supabase + .from('domain_names') + .delete() + .eq('id', id) + + if (error) { + throw error + } + + return data + }, + { + async onSuccess() { + await queryClient.invalidateQueries(['domain-names']) + }, + } + ) +} diff --git a/lib/data/domain-name-update-mutation.ts b/lib/data/domain-name-update-mutation.ts new file mode 100644 index 0000000..70adbb6 --- /dev/null +++ b/lib/data/domain-name-update-mutation.ts @@ -0,0 +1,39 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import supabase from '../supabase' + +export interface DomainUpdateVariables { + id: string + isOwned?: boolean + statusChangeNotificationsEnabled?: boolean +} + +export function useDomainUpdateMutation() { + const queryClient = useQueryClient() + + return useMutation( + async ({ + id, + isOwned, + statusChangeNotificationsEnabled, + }: DomainUpdateVariables) => { + const { data, error } = await supabase + .from('domain_names') + .update({ + is_owned: isOwned, + status_change_notifications_enabled: statusChangeNotificationsEnabled, + }) + .eq('id', id) + + if (error) { + throw error + } + + return data + }, + { + async onSuccess() { + await queryClient.invalidateQueries(['domain-names']) + }, + } + ) +} diff --git a/lib/data/domain-names-query.ts b/lib/data/domain-names-query.ts index 48e4fdf..4f79aad 100644 --- a/lib/data/domain-names-query.ts +++ b/lib/data/domain-names-query.ts @@ -1,12 +1,15 @@ import { useQuery } from '@tanstack/react-query' import { useIsInitialLoadFinished } from '../contexts/preloaded' import supabase from '../supabase' +import { Database } from '../database.types' + +export type Domain = Database['public']['Tables']['domain_names']['Row'] export async function getDomainNames(signal?: AbortSignal) { const query = supabase .from('domain_names') .select('*') - .order('updated_at', { ascending: false }) + .order('created_at', { ascending: false }) if (signal) { query.abortSignal(signal) } @@ -27,6 +30,13 @@ export function useDomainNamesQuery() { async ({ signal }) => getDomainNames(signal), { enabled: isFinishedLoading, + refetchInterval(data) { + if (data?.some((d) => d.status === 'unknown')) { + return 5000 + } + + return false + }, } ) } diff --git a/lib/data/update-whois-mutation.ts b/lib/data/update-whois-mutation.ts new file mode 100644 index 0000000..f2a01e1 --- /dev/null +++ b/lib/data/update-whois-mutation.ts @@ -0,0 +1,29 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import supabase from '../supabase' + +export interface UpdateWhoisVariables { + id: string +} + +export function useUpdateWhoisMutation() { + const queryClient = useQueryClient() + + return useMutation( + async ({ id }: UpdateWhoisVariables) => { + const { data, error } = await supabase.functions.invoke('update-whois', { + body: { id }, + }) + + if (error) { + throw error + } + + return data + }, + { + async onSuccess() { + await queryClient.invalidateQueries(['domain-names']) + }, + } + ) +} diff --git a/lib/database.types.ts b/lib/database.types.ts index f8d86e0..3bc2f48 100644 --- a/lib/database.types.ts +++ b/lib/database.types.ts @@ -40,43 +40,41 @@ export type Database = { domain_name: string expires_at: string | null id: string - mode: Database["public"]["Enums"]["domain_name_mode"] + is_owned: boolean status: Database["public"]["Enums"]["domain_name_status"] + status_change_notifications_enabled: boolean updated_at: string user_id: string whois_data: Json | null + whois_updated_at: string | null } Insert: { created_at?: string domain_name: string expires_at?: string | null id?: string - mode?: Database["public"]["Enums"]["domain_name_mode"] + is_owned?: boolean status?: Database["public"]["Enums"]["domain_name_status"] + status_change_notifications_enabled?: boolean updated_at?: string user_id?: string whois_data?: Json | null + whois_updated_at?: string | null } Update: { created_at?: string domain_name?: string expires_at?: string | null id?: string - mode?: Database["public"]["Enums"]["domain_name_mode"] + is_owned?: boolean status?: Database["public"]["Enums"]["domain_name_status"] + status_change_notifications_enabled?: boolean updated_at?: string user_id?: string whois_data?: Json | null + whois_updated_at?: string | null } - Relationships: [ - { - foreignKeyName: "domain_names_user_id_fkey" - columns: ["user_id"] - isOneToOne: false - referencedRelation: "users" - referencedColumns: ["id"] - }, - ] + Relationships: [] } user_data: { Row: { @@ -100,15 +98,7 @@ export type Database = { updated_at?: string user_id?: string } - Relationships: [ - { - foreignKeyName: "user_data_user_id_fkey" - columns: ["user_id"] - isOneToOne: true - referencedRelation: "users" - referencedColumns: ["id"] - }, - ] + Relationships: [] } } Views: { @@ -129,7 +119,6 @@ export type Database = { } } Enums: { - domain_name_mode: "watching" | "owned_sales_page" domain_name_status: "unknown" | "registered" | "available" } CompositeTypes: { @@ -535,3 +524,18 @@ export type Enums< ? PublicSchema["Enums"][PublicEnumNameOrOptions] : never +export type CompositeTypes< + PublicCompositeTypeNameOrOptions extends + | keyof PublicSchema["CompositeTypes"] + | { schema: keyof Database }, + CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { + schema: keyof Database + } + ? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] + : never = never, +> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database } + ? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] + : PublicCompositeTypeNameOrOptions extends keyof PublicSchema["CompositeTypes"] + ? PublicSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] + : never + diff --git a/lib/fetcher.tsx b/lib/fetcher.tsx index 552969a..266d84e 100644 --- a/lib/fetcher.tsx +++ b/lib/fetcher.tsx @@ -1,21 +1,6 @@ import { NotFoundError, ValidationError } from './errors' import supabase from './supabase' -const fetcher = async (...args) => { - // @ts-ignore - const res = await fetch(...args) - const json = await res.json() - if (res.status === 200) { - return json - } else { - const error: any = new Error(`${res.status}: ${json.error.message}`) - error.error = json.error - throw error - } -} - -export default fetcher - export async function fetchAPI( path: string, method?: string, diff --git a/lib/query-client.ts b/lib/query-client.ts index 03a1fe1..112408d 100644 --- a/lib/query-client.ts +++ b/lib/query-client.ts @@ -10,6 +10,7 @@ export function getQueryClient() { new QueryClient({ defaultOptions: { queries: { + staleTime: 60 * 1000, // 1 minute retry: (failureCount, error) => { // Don't retry on 404s if (error instanceof NotFoundError) { diff --git a/next.config.js b/next.config.js index d36d8b0..6fe6cb5 100644 --- a/next.config.js +++ b/next.config.js @@ -3,4 +3,5 @@ */ module.exports = { output: 'standalone', + poweredByHeader: false, } diff --git a/package-lock.json b/package-lock.json index 8adb032..7cafad3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,21 @@ { - "name": "domains-api", + "name": "side-domains", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "domains-api", - "license": "MIT", + "name": "side-domains", + "license": "UNLICENSED", "dependencies": { "@hookform/resolvers": "^3.6.0", "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.4", + "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-context-menu": "^2.1.4", - "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-hover-card": "^1.0.6", "@radix-ui/react-label": "^2.0.2", @@ -30,7 +30,7 @@ "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.6", @@ -54,7 +54,6 @@ "react-hook-form": "^7.51.5", "react-hot-toast": "^2.4.1", "resend": "^0.17.2", - "swr": "^2.1.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" @@ -1313,23 +1312,22 @@ } }, "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.4.tgz", - "integrity": "sha512-jbfBCRlKYlhbitueOAv7z74PXYeIQmWpKwm3jllsdkw7fGWNkxqP3v0nY9WmOzcPqpQuoorNtvViBgL46n5gVg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.2.tgz", + "integrity": "sha512-eGSlLzPhKO+TErxkiGcCZGuvbVMnLA1MTnyBksGOeGRGkxHiiJUujsjmNTdWTm4iHVSRaUao9/4Ur671auMghQ==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dialog": "1.0.4", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.2", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -1340,6 +1338,78 @@ } } }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-arrow": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz", @@ -1561,31 +1631,30 @@ } }, "node_modules/@radix-ui/react-dialog": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.4.tgz", - "integrity": "sha512-hJtRy/jPULGQZceSAP2Re6/4NpKo8im6V8P2hUqZsdFiSL8l35kYsw3qbRI6Ay5mQd2+wlLqje770eq+RJ3yZg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-compose-refs": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-dismissable-layer": "1.0.4", - "@radix-ui/react-focus-guards": "1.0.1", - "@radix-ui/react-focus-scope": "1.0.3", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-portal": "1.0.3", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-slot": "1.0.2", - "@radix-ui/react-use-controllable-state": "1.0.1", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz", + "integrity": "sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.5" + "react-remove-scroll": "2.6.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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": { @@ -1596,6 +1665,291 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/react-remove-scroll": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz", + "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.6", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.1.tgz", @@ -2402,25 +2756,150 @@ } }, "node_modules/@radix-ui/react-tabs": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", - "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz", + "integrity": "sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", + "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/primitive": "1.0.1", - "@radix-ui/react-context": "1.0.1", - "@radix-ui/react-direction": "1.0.1", - "@radix-ui/react-id": "1.0.1", - "@radix-ui/react-presence": "1.0.1", - "@radix-ui/react-primitive": "1.0.3", - "@radix-ui/react-roving-focus": "1.0.4", - "@radix-ui/react-use-controllable-state": "1.0.1" + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0", - "react-dom": "^16.8 || ^17.0 || ^18.0" + "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 + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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": { @@ -2431,6 +2910,134 @@ } } }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "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 + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.4.tgz", @@ -8226,9 +8833,9 @@ } }, "node_modules/react-remove-scroll-bar": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.4.tgz", - "integrity": "sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", "dependencies": { "react-style-singleton": "^2.2.1", "tslib": "^2.0.0" @@ -9137,17 +9744,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swr": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.0.tgz", - "integrity": "sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==", - "dependencies": { - "use-sync-external-store": "^1.2.0" - }, - "peerDependencies": { - "react": "^16.11.0 || ^17.0.0 || ^18.0.0" - } - }, "node_modules/synckit": { "version": "0.8.5", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", diff --git a/package.json b/package.json index 7cd2a14..876f512 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "domains-api", - "repository": "https://github.com/vercel/examples.git", - "license": "MIT", + "name": "side-domains", + "repository": "https://github.com/melbourne-tech/side.domains", + "license": "UNLICENSED", "private": true, "scripts": { "dev": "next dev", @@ -13,13 +13,13 @@ "dependencies": { "@hookform/resolvers": "^3.6.0", "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.4", + "@radix-ui/react-alert-dialog": "^1.1.2", "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-avatar": "^1.0.3", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-context-menu": "^2.1.4", - "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-hover-card": "^1.0.6", "@radix-ui/react-label": "^2.0.2", @@ -34,7 +34,7 @@ "@radix-ui/react-slider": "^1.1.2", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.6", @@ -58,7 +58,6 @@ "react-hook-form": "^7.51.5", "react-hot-toast": "^2.4.1", "resend": "^0.17.2", - "swr": "^2.1.0", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.23.8" diff --git a/pages/api/check-domain.ts b/pages/api/check-domain.ts index e4e3b94..3f357e0 100644 --- a/pages/api/check-domain.ts +++ b/pages/api/check-domain.ts @@ -7,9 +7,6 @@ export default async function handler( ) { let { domain } = req.query - console.log('req.headers:', req.headers) - console.log('domain:', domain) - if (typeof domain !== 'string') { return res.status(400).send('Domain must be a string') } @@ -21,10 +18,9 @@ export default async function handler( const { error } = await supabaseAdmin .from('domain_names') .select() - .eq('domain_name', domain) + .eq('domain_name', domain.toLowerCase()) .single() if (error) { - console.log('error:', error) return res.status(404).send('Domain not found') } diff --git a/pages/api/remove-domain.ts b/pages/api/remove-domain.ts deleted file mode 100644 index 741b081..0000000 --- a/pages/api/remove-domain.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { NextApiRequest, NextApiResponse } from 'next' -import { ParseResultType, parseDomain } from 'parse-domain' -import { z } from 'zod' -import supabaseAdmin, { getUserFromRequest } from '~/lib/supabase-admin' - -const schema = z.object({ - domain: z.string().min(1, 'Domain must not be empty'), -}) - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const user = await getUserFromRequest(req) - if (!user) { - return new Response( - JSON.stringify({ - code: 'NOT_AUTHENTICATED', - }), - { - status: 401, - headers: { - 'Content-Type': 'application/json', - }, - } - ) - } - - const result = schema.safeParse(req.query) - if (result.error) { - return res.status(400).json({ - code: 'VALIDATION_ERROR', - errors: result.error.flatten(), - }) - } - - let domainName: string | undefined = undefined - - const parseResult = parseDomain(result.data.domain) - if (parseResult.type === ParseResultType.Listed) { - const { domain, topLevelDomains } = parseResult - domainName = `${domain}.${topLevelDomains.join('.')}` - } - if (domainName === undefined) { - return res.status(400).json({ - code: 'INVALID_DOMAIN', - }) - } - - const { error } = await supabaseAdmin - .from('domain_names') - .delete() - .eq('domain_name', domainName) - .eq('user_id', user.id) - - if (error) { - return res.status(500).json({ - code: 'INTERNAL_SERVER_ERROR', - error: error.message, - }) - } - - // We have to remove the domains in order because of the redirect on the first domain - await fetch( - `https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/www.${domainName}?teamId=${process.env.VERCEL_TEAM_ID}`, - { - headers: { - Authorization: `Bearer ${process.env.VERCEL_AUTH_BEARER_TOKEN}`, - }, - method: 'DELETE', - } - ) - await fetch( - `https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${domainName}?teamId=${process.env.VERCEL_TEAM_ID}`, - { - headers: { - Authorization: `Bearer ${process.env.VERCEL_AUTH_BEARER_TOKEN}`, - }, - method: 'DELETE', - } - ) - - res.status(200).send({ status: 'SUCCESS' }) -} diff --git a/pages/api/verify-domain.js b/pages/api/verify-domain.js deleted file mode 100644 index 610631c..0000000 --- a/pages/api/verify-domain.js +++ /dev/null @@ -1,17 +0,0 @@ -export default async function handler(req, res) { - const { domain } = req.query - - const response = await fetch( - `https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/domains/${domain}/verify?teamId=${process.env.VERCEL_TEAM_ID}`, - { - headers: { - Authorization: `Bearer ${process.env.VERCEL_AUTH_BEARER_TOKEN}`, - 'Content-Type': 'application/json', - }, - method: 'POST', - } - ) - - const data = await response.json() - res.status(response.status).send(data) -} diff --git a/pages/app/index.tsx b/pages/app/index.tsx index 5faab28..ba317df 100644 --- a/pages/app/index.tsx +++ b/pages/app/index.tsx @@ -1,6 +1,9 @@ +import { PostgrestError } from '@supabase/supabase-js' import AddDomain from '~/components/add-domain' import DomainCard from '~/components/domain-card' +import DomainOverviewSkeleton from '~/components/domain-card-skeleton' import AppLayout from '~/components/layouts/AppLayout' +import { Alert, AlertDescription, AlertTitle } from '~/components/ui/alert' import { useDomainNamesQuery } from '~/lib/data/domain-names-query' import { withAuth } from '~/lib/hocs/with-auth' import { withPurchased } from '~/lib/hocs/with-purchased' @@ -12,16 +15,49 @@ const IndexPage: NextPageWithLayout = () => { isLoading, isSuccess, isError, + error, } = useDomainNamesQuery() return ( -
+
- {isSuccess && - domainNames.map((domainName) => ( - - ))} +
+ {isLoading && ( + <> + + + + + + )} + + {isError && ( + + Couldn't load domains + + {(error as PostgrestError)?.message ?? 'Something went wrong'} + + + )} + + {isSuccess && ( + <> + {domainNames.length > 0 ? ( + domainNames.map((domainName) => ( + + )) + ) : ( + + No domains + + You don't have any domains yet. Add one to get started. + + + )} + + )} +
) } diff --git a/supabase/functions/_lib/cors.ts b/supabase/functions/_lib/cors.ts new file mode 100644 index 0000000..6cf31ac --- /dev/null +++ b/supabase/functions/_lib/cors.ts @@ -0,0 +1,5 @@ +export const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': + 'authorization, x-client-info, apikey, content-type', +} diff --git a/supabase/functions/update-whois/index.ts b/supabase/functions/update-whois/index.ts index 72d5d98..34b4f61 100644 --- a/supabase/functions/update-whois/index.ts +++ b/supabase/functions/update-whois/index.ts @@ -1,21 +1,36 @@ -import { createClient } from '@supabase/supabase-js' import 'jsr:@supabase/functions-js/edge-runtime.d.ts' +import { createClient } from '@supabase/supabase-js' +import { corsHeaders } from '../_lib/cors.ts' + Deno.serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response(null, { headers: corsHeaders }) + } + if (req.method !== 'POST') { - return new Response(null, { status: 405 }) + return new Response(null, { status: 405, headers: corsHeaders }) } const { id } = await req.json() + if (typeof id !== 'string') { + return new Response(null, { status: 400, headers: corsHeaders }) + } const supabaseClient = createClient( - Deno.env.get('SUPABASE_URL') ?? '', - Deno.env.get('SUPABASE_ANON_KEY') ?? '', + Deno.env.get('SUPABASE_URL')!, + Deno.env.get('SUPABASE_ANON_KEY')!, { global: { headers: { Authorization: req.headers.get('Authorization')! } }, } ) + const supabaseAdmin = createClient( + Deno.env.get('SUPABASE_URL')!, + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! + ) + + // Fetch the domain name with the user context const { data } = await supabaseClient .from('domain_names') .select() @@ -23,7 +38,7 @@ Deno.serve(async (req) => { .maybeSingle() if (!data) { - return new Response(null, { status: 404 }) + return new Response(null, { status: 404, headers: corsHeaders }) } const whoisResponse = await fetch( @@ -41,12 +56,22 @@ Deno.serve(async (req) => { ? 'available' : 'unknown' - await supabaseClient + const { error } = await supabaseAdmin .from('domain_names') - .update({ whois_data: whoisData, expires_at: expiresAt, status }) + .update({ + whois_data: whoisData, + expires_at: expiresAt, + status, + whois_updated_at: 'now()', + }) .eq('id', id) + if (error) { + console.error('error updating domain name:', error) + return new Response(null, { status: 500, headers: corsHeaders }) + } + return new Response(JSON.stringify({ success: true }), { - headers: { 'Content-Type': 'application/json' }, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, }) }) diff --git a/supabase/migrations/20240917093911_add_domain_info.sql b/supabase/migrations/20240917093911_add_domain_info.sql index 1e34ffb..830a2c6 100644 --- a/supabase/migrations/20240917093911_add_domain_info.sql +++ b/supabase/migrations/20240917093911_add_domain_info.sql @@ -1,20 +1,38 @@ -create policy create_own_domain_names on public.domain_names for insert to authenticated +create policy update_own_domain_names on public.domain_names for +update to authenticated using (auth.uid () = user_id) with check (auth.uid () = user_id); -create type domain_name_mode as enum('watching', 'owned_sales_page'); +create policy delete_own_domain_names on public.domain_names for delete to authenticated using (auth.uid () = user_id); create type domain_name_status as enum('unknown', 'registered', 'available'); alter table public.domain_names -add column "mode" domain_name_mode not null default 'watching', +add column "is_owned" boolean not null default false, +add column "status_change_notifications_enabled" boolean not null default true, add column "status" domain_name_status not null default 'unknown', add column "whois_data" jsonb, +add column "whois_updated_at" timestamptz, add column "expires_at" timestamptz, alter column "user_id" set default auth.uid (); +revoke all on table public.domain_names +from + anon, + authenticated; + +grant +select +, +update ("is_owned", "status_change_notifications_enabled"), +delete on table public.domain_names to authenticated; + +create trigger domain_names_updated_at before +update on public.domain_names for each row +execute function extensions.moddatetime ('updated_at'); + -- All existing domain_names are owned update public.domain_names set - "mode" = 'owned_sales_page'; \ No newline at end of file + "is_owned" = true; \ No newline at end of file diff --git a/supabase/migrations/20240917123202_update_whois_function.sql b/supabase/migrations/20240917123202_update_whois_function.sql index e8163ef..fb4593a 100644 --- a/supabase/migrations/20240917123202_update_whois_function.sql +++ b/supabase/migrations/20240917123202_update_whois_function.sql @@ -1,6 +1,6 @@ create extension pg_cron with - schema extensions; + schema pg_catalog; grant usage on schema cron to postgres;