chore: Import project cards and deps

This commit is contained in:
icld
2025-03-19 15:24:30 -07:00
parent 206132088f
commit ca47c2c220
42 changed files with 1605 additions and 4 deletions
+44
View File
@@ -0,0 +1,44 @@
[server]
host = "127.0.0.1"
port = 8000
gqlPath = "/graphql"
[server.session]
secret = "ANY234234oldstslklting"
# Frontend webapp URL origin
appOriginUrl = "http://localhost:3000"
# Set to true if server running behind proxy
trustProxy = false
# Backend URL hostname
domain = "localhost"
[database]
dbPath = "db/snowball"
[gitHub]
webhookUrl = "https://cd91-75-164-238-42.ngrok-free.app"
[gitHub.oAuth]
clientId = "Ov23li29Afs0s2Hw2VV1"
clientSecret = "90389f168b7e07547a2c8796f95404f5b58a2913"
[registryConfig]
fetchDeploymentRecordDelay = 5000
# restEndpoint = "http://localhost:1317"
# gqlEndpoint = "http://localhost:9473/api"
restEndpoint = "https://audubon.app"
gqlEndpoint = "https://audubon.app/api"
# chainId = "laconic-testnet-2"
chainId = "laconic-testnet-2"
privateKey = "0xe374dcccb706f6b2922a1ef9d81e10fe22913b82e5c9ce8fb3917f6e5a5b52db"
bondId = ""
authority = "" # TODO: Add authority ID if needed
[registryConfig.fee]
gas = ""
fees = ""
gasPrice = "1alnt"
# Durations are set to 2 mins as deployers may take time with ongoing deployments and auctions
[auction]
commitFee = "100000"
commitsDuration = "120s"
revealFee = "100000"
revealsDuration = "120s"
denom = "alnt"
+6
View File
@@ -49,23 +49,28 @@
"@radix-ui/react-tooltip": "^1.1.8",
"@radix-ui/react-visually-hidden": "^1.1.2",
"@workspace/ui": "workspace:*",
"axios": "^1.8.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "1.0.4",
"date-fns": "^4.1.0",
"downshift": "^9.0.9",
"embla-carousel-react": "^8.5.2",
"input-otp": "^1.4.2",
"lucide-react": "0.477.0",
"next": "^15.2.1",
"next-themes": "^0.4.4",
"octokit": "^3.1.2",
"react": "^19.0.0",
"react-day-picker": "8.10.1",
"react-dom": "^19.0.0",
"react-hook-form": "^7.54.2",
"react-resizable-panels": "^2.1.7",
"recharts": "^2.15.1",
"siwe": "^3.0.0",
"sonner": "^2.0.1",
"tailwind-merge": "^3.0.2",
"usehooks-ts": "^3.1.1",
"vaul": "^1.1.2",
"zod": "^3.23.8",
"zustand": "^5.0.3"
@@ -74,6 +79,7 @@
"@types/node": "^20",
"@types/react": "18.3.0",
"@types/react-dom": "18.3.1",
"@workspace/gql-client": "workspace:*",
"@workspace/typescript-config": "workspace:*",
"dotenv": "^16.4.7",
"postcss": "^8",
@@ -0,0 +1,59 @@
'use client'
import { PageWrapper } from '@/components/foundation'
import CheckBalanceIframe from '@/components/iframe/check-balance-iframe/CheckBalanceIframe'
import type { Project } from '@octokit/webhooks-types'
import { useParams } from 'next/navigation'
import { useState } from 'react'
export default function ProjectsPage() {
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const [projects, setProjects] = useState<Project[]>([])
// const { isReady } = useWallet()
// const client = useGQLClient()
const { orgSlug } = useParams()
// const fetchProjects = useCallback(async () => {
// if (!orgSlug) return
// const { projectsInOrganization } =
// await client.getProjectsInOrganization(orgSlug)
// setProjects(projectsInOrganization)
// // }, [orgSlug, client])
// }, [orgSlug])
// useEffect(() => {
// if (isReady && orgSlug) {
// fetchProjects()
// }
// }, [fetchProjects, orgSlug, isReady])
// // }, [fetchProjects, orgSlug, isReady])
// useEffect(() => {
// if (isBalanceSufficient === false) {
// router.push('/buy-prepaid-service')
// }
// }, [isBalanceSufficient, navigate])
return (
<PageWrapper
header={{
title: 'Projects',
actions: [{ label: 'Create Project', href: '/projects/create' }]
}}
>
<div className="grid grid-flow-row grid-cols-[repeat(auto-fill,_minmax(280px,_1fr))] gap-4">
{projects.length > 0 &&
projects.map((project) => (
// <ProjectCard project={project} key={project.id} />
<div key={project.id}>{project.name}</div>
))}
</div>
<CheckBalanceIframe
onBalanceChange={setIsBalanceSufficient}
isPollingEnabled={false}
amount="1"
/>
</PageWrapper>
)
}
@@ -0,0 +1,87 @@
'use client'
import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
import { Dialog } from '@workspace/ui/components/dialog'
import { useEffect, useState } from 'react'
import useCheckBalance from './useCheckBalance'
const CHECK_BALANCE_INTERVAL = 5000
const IFRAME_ID = 'checkBalanceIframe'
/**
* CheckBalanceIframe component that checks the balance using an iframe.
* @param {Object} props - The component props.
* @param {function} props.onBalanceChange - Callback function to be called when the balance changes.
* @param {boolean} props.isPollingEnabled - Determines whether to poll the balance periodically.
* @param {string} props.amount - The amount to check against the balance.
* @returns {JSX.Element} - The CheckBalanceIframe component.
*/
const CheckBalanceIframe = ({
onBalanceChange,
isPollingEnabled,
amount
}: {
onBalanceChange: (value: boolean | undefined) => void
isPollingEnabled: boolean
amount: string
}) => {
const { isBalanceSufficient, checkBalance } = useCheckBalance(
amount,
IFRAME_ID
)
const [isLoaded, setIsLoaded] = useState(false)
/**
* useEffect hook that calls checkBalance when the component is loaded or the amount changes.
*/
useEffect(() => {
if (!isLoaded) {
return
}
checkBalance()
}, [checkBalance, isLoaded])
/**
* useEffect hook that sets up an interval to poll the balance if polling is enabled.
* Clears the interval when the component unmounts, balance is sufficient, or polling is disabled.
*/
useEffect(() => {
if (!isPollingEnabled || !isLoaded || isBalanceSufficient) {
return
}
const interval = setInterval(() => {
checkBalance()
}, CHECK_BALANCE_INTERVAL)
return () => {
clearInterval(interval)
}
}, [isBalanceSufficient, isPollingEnabled, checkBalance, isLoaded])
/**
* useEffect hook that calls the onBalanceChange callback when the isBalanceSufficient state changes.
*/
useEffect(() => {
onBalanceChange(isBalanceSufficient)
}, [isBalanceSufficient, onBalanceChange])
return (
<Dialog open={false}>
<VisuallyHidden>
<iframe
title="Check Balance"
onLoad={() => setIsLoaded(true)}
id={IFRAME_ID}
src={process.env.NEXT_PUBLIC_WALLET_IFRAME_URL}
width="100%"
height="100%"
sandbox="allow-scripts allow-same-origin"
className="border rounded-md shadow-sm"
/>
</VisuallyHidden>
</Dialog>
)
}
export default CheckBalanceIframe
@@ -0,0 +1,77 @@
import { useCallback, useEffect, useState } from 'react'
import { toast } from 'sonner'
/**
* `useCheckBalance` is a custom React hook that checks if the balance in a wallet iframe is sufficient for a given amount.
* It communicates with the iframe to request a balance check and updates the state based on the response.
*
* @param amount - The amount to check against the wallet balance. This should be a string representation of the amount.
* @param iframeId - The ID of the iframe element containing the wallet. This ID is used to locate the iframe in the DOM.
* @returns An object containing:
* - `isBalanceSufficient`: A boolean state variable indicating whether the balance is sufficient (true) or not (false). It is initially undefined.
* - `checkBalance`: A function that triggers the balance check by sending a message to the wallet iframe.
*
* @example
* ```tsx
* const { isBalanceSufficient, checkBalance } = useCheckBalance("100", "myWalletIframe");
*
* // To trigger the balance check:
* checkBalance();
*
* // To conditionally render based on balance:
* {isBalanceSufficient === true ? <p>Balance is sufficient!</p> : isBalanceSufficient === false ? <p>Balance is insufficient.</p> : <p>Checking balance...</p>}
* ```
*
* @remarks
* - The hook uses `useState` to manage the `isBalanceSufficient` state.
* - The `checkBalance` function uses `useCallback` to prevent unnecessary re-renders.
* - The hook uses `useEffect` to listen for messages from the wallet iframe and update the `isBalanceSufficient` state accordingly.
* - It expects the iframe to send a message of type 'IS_SUFFICIENT' with a boolean data field.
* - It uses `VITE_LACONICD_CHAIN_ID` and `VITE_WALLET_IFRAME_URL` from the environment variables for the chain ID and iframe URL, respectively.
* - Error logging is included if the iframe is not found or not loaded.
*/
const useCheckBalance = (amount: string, iframeId: string) => {
const [isBalanceSufficient, setIsBalanceSufficient] = useState<boolean>()
const iframeUrl = String(process.env.NEXT_PUBLIC_WALLET_IFRAME_URL) || ''
const chainId = String(process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID) || ''
const checkBalance = useCallback(() => {
const iframe = document.getElementById(iframeId) as HTMLIFrameElement
if (!iframe || !iframe.contentWindow) {
console.error(`Iframe with ID "${iframeId}" not found or not loaded`)
return
}
iframe.contentWindow.postMessage(
{
type: 'CHECK_BALANCE',
chainId,
amount
},
iframeUrl
)
}, [iframeId, amount, iframeUrl, chainId])
useEffect(() => {
toast.info('Checking balance from useEffect...')
const handleMessage = (event: MessageEvent) => {
if (event.origin !== iframeUrl) return
toast.info('Is Sufficient?', event.data.type)
if (event.data.type !== 'IS_SUFFICIENT') return
setIsBalanceSufficient(event.data.data)
}
window.addEventListener('message', handleMessage)
return () => {
toast.info('Removing event listener from useEffect...')
window.removeEventListener('message', handleMessage)
}
}, [iframeUrl])
return { isBalanceSufficient, checkBalance }
}
export default useCheckBalance
@@ -0,0 +1,128 @@
import { getInitials } from '@/utils/getInitials'
import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar'
import type { Project } from '@workspace/gql-client'
import { Card, CardContent, CardHeader } from '@workspace/ui/components/card'
import { AlertTriangle } from 'lucide-react'
import { useRouter } from 'next/router'
import React, { type ComponentPropsWithoutRef, useCallback } from 'react'
import { ProjectCardActions } from './ProjectCardActions'
import { ProjectDeploymentInfo } from './ProjectDeploymentInfo'
import { ProjectStatusDot } from './ProjectStatusDot'
/**
* Status types for project deployment status
*/
export type ProjectStatus = 'success' | 'in-progress' | 'failure' | 'pending'
/**
* Props for the ProjectCard component
*
* @property {Project} project - The project data to display
* @property {ProjectStatus} [status='failure'] - The current deployment status of the project
*/
interface ProjectCardProps extends ComponentPropsWithoutRef<'div'> {
project: Project
status?: ProjectStatus
}
/**
* ProjectCard component
*
* Displays a card with project information including:
* - Project name and icon
* - Domain URL (if available)
* - Deployment status
* - Latest commit information
* - Timestamp and branch information
* - Actions menu for project settings and deletion
*
* The card is clickable and navigates to the project details page.
*
* @example
* ```tsx
* <ProjectCard
* project={projectData}
* status="success"
* />
* ```
*/
export const ProjectCard = ({
className,
project,
status = 'failure',
...props
}: ProjectCardProps) => {
const hasError = status === 'failure'
const router = useRouter()
/**
* Handles click on the card to navigate to project details
*/
const handleClick = React.useCallback(() => {
router.push(`projects/${project.id}`)
}, [project.id, router])
/**
* Handles click on the settings menu item
* Prevents event propagation to avoid triggering card click
*/
const handleSettingsClick = React.useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
router.push(`projects/${project.id}/settings`)
},
[project.id, router]
)
/**
* Handles click on the delete menu item
* Prevents event propagation to avoid triggering card click
*/
const handleDeleteClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
router.push(`projects/${project.id}/settings`)
},
[project.id, router]
)
return (
<Card className="w-full" onClick={handleClick} {...props}>
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2">
<div className="flex gap-2.5">
<Avatar className="h-10 w-10">
<AvatarImage src={project.icon} alt={project.name} />
<AvatarFallback>{getInitials(project.name)}</AvatarFallback>
</Avatar>
<div className="flex flex-col gap-1.5">
<p className="text-sm font-semibold text-foreground leading-none">
{project.name}
</p>
<p className="text-sm text-muted-foreground leading-5">
{project.deployments[0]?.applicationDeploymentRecordData?.url ??
'No domain'}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{hasError && <AlertTriangle className="text-destructive h-4 w-4" />}
<ProjectCardActions
onSettingsClick={handleSettingsClick}
onDeleteClick={handleDeleteClick}
/>
</div>
</CardHeader>
<CardContent className="flex flex-col gap-3 pt-2">
<div className="flex items-center gap-2">
<ProjectStatusDot status={status} />
<ProjectDeploymentInfo project={project} />
</div>
</CardContent>
</Card>
)
}
@@ -0,0 +1,73 @@
import { Button } from '@workspace/ui/components/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@workspace/ui/components/dropdown-menu'
import { ExternalLink, MoreHorizontal, Trash } from 'lucide-react'
import type React from 'react'
import type { ComponentPropsWithoutRef } from 'react'
/**
* Props for the ProjectCardActions component
*
* @property {Function} onSettingsClick - Callback function triggered when the settings option is clicked
* @property {Function} onDeleteClick - Callback function triggered when the delete option is clicked
*/
interface ProjectCardActionsProps extends ComponentPropsWithoutRef<'div'> {
onSettingsClick: (e: React.MouseEvent) => void
onDeleteClick: (e: React.MouseEvent) => void
}
/**
* ProjectCardActions component
*
* Displays a dropdown menu with actions that can be performed on a project:
* - Project settings: Navigates to the project settings page
* - Delete project: Initiates the project deletion process
*
* The component uses a three-dot menu icon that expands to show available actions.
* Each action has an associated icon for better visual recognition.
*
* @example
* ```tsx
* <ProjectCardActions
* onSettingsClick={handleSettingsClick}
* onDeleteClick={handleDeleteClick}
* />
* ```
*/
export const ProjectCardActions = ({
onSettingsClick,
onDeleteClick,
...props
}: ProjectCardActionsProps) => {
return (
<div {...props}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-8 w-8">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuItem onClick={onSettingsClick}>
<ExternalLink className="mr-2 h-4 w-4" />
<span>Project settings</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={onDeleteClick}
className="text-destructive"
>
<Trash className="mr-2 h-4 w-4" />
<span>Delete project</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
}
@@ -0,0 +1,71 @@
import { relativeTimeMs } from '@/utils/time'
import type { Project } from '@workspace/gql-client'
import { Clock, GitBranch } from 'lucide-react'
import type { ComponentPropsWithoutRef } from 'react'
/**
* Props for the ProjectDeploymentInfo component
*
* @property {Project} project - The project data containing deployment information
*/
interface ProjectDeploymentInfoProps extends ComponentPropsWithoutRef<'div'> {
project: Project
}
/**
* ProjectDeploymentInfo component
*
* Displays information about the latest deployment for a project, including:
* - Commit message (or "No production deployment" if none exists)
* - Relative time since deployment or project creation
* - Branch name (if a deployment exists)
*
* The component handles both cases where a project has deployments and where it doesn't,
* displaying appropriate information in each case.
*
* @example
* ```tsx
* <ProjectDeploymentInfo project={projectData} />
* ```
*/
export const ProjectDeploymentInfo = ({
project,
...props
}: ProjectDeploymentInfoProps) => {
const hasDeployment = project.deployments.length > 0
const latestDeployment = hasDeployment ? project.deployments[0] : null
return (
<div className="flex flex-col gap-3" {...props}>
{/* Commit message or no deployment message */}
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
{hasDeployment
? latestDeployment?.commitMessage
: 'No production deployment'}
</span>
</div>
{/* Timestamp and branch information */}
<div className="flex items-center gap-1">
<div className="flex items-center gap-1">
<Clock className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{hasDeployment
? relativeTimeMs(latestDeployment?.createdAt ?? '')
: relativeTimeMs(project.createdAt)}{' '}
on
</span>
</div>
{hasDeployment && (
<div className="flex items-center gap-1">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{latestDeployment?.branch}
</span>
</div>
)}
</div>
</div>
)
}
@@ -0,0 +1,56 @@
import { cn } from '@/lib/utils'
import type { ComponentPropsWithoutRef } from 'react'
import type { ProjectStatus } from './ProjectCard'
/**
* Mapping of status values to their corresponding CSS classes for styling
*
* - success: Green color for successful deployments
* - in-progress: Orange color for deployments in progress
* - failure: Red color for failed deployments
* - pending: Gray color for pending deployments
*/
const statusStyles: Record<ProjectStatus, string> = {
success: 'bg-emerald-500',
'in-progress': 'bg-orange-400',
failure: 'bg-destructive',
pending: 'bg-muted'
}
/**
* Props for the ProjectStatusDot component
*
* @property {ProjectStatus} status - The current status of the project deployment
*/
interface ProjectStatusDotProps extends ComponentPropsWithoutRef<'div'> {
status: ProjectStatus
}
/**
* ProjectStatusDot component
*
* A visual indicator that displays the current status of a project deployment.
* The color of the dot changes based on the status:
* - Green for success
* - Orange for in-progress
* - Red for failure
* - Gray for pending
*
* @example
* ```tsx
* <ProjectStatusDot status="success" />
* ```
*/
export const ProjectStatusDot = ({
status,
className,
...props
}: ProjectStatusDotProps) => {
return (
<div
className={cn('h-2 w-2 rounded-full', statusStyles[status], className)}
aria-label={`Deployment status: ${status}`}
{...props}
/>
)
}
@@ -0,0 +1 @@
export * from './ProjectCard'
@@ -0,0 +1,94 @@
import type { Project } from '@workspace/gql-client'
import { useCombobox } from 'downshift'
import { useCallback, useEffect, useState } from 'react'
import { useDebounceValue } from 'usehooks-ts'
import { SearchBar } from '@/components/core/search-bar'
import { useGQLClient } from '@/context/GQLClientContext'
import { cn } from '@/lib/utils'
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty'
import { ProjectSearchBarItem } from './ProjectSearchBarItem'
interface ProjectSearchBarProps {
onChange?: (data: Project) => void
}
export const ProjectSearchBar = ({ onChange }: ProjectSearchBarProps) => {
const [items, setItems] = useState<Project[]>([])
const [selectedItem, setSelectedItem] = useState<Project | null>(null)
const client = useGQLClient()
const {
isOpen,
getMenuProps,
getInputProps,
getItemProps,
highlightedIndex,
inputValue
} = useCombobox({
items,
itemToString(item) {
return item ? item.name : ''
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem)
if (onChange) {
onChange(newSelectedItem)
}
}
}
})
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300)
const fetchProjects = useCallback(
async (inputValue: string) => {
const { searchProjects } = await client.searchProjects(inputValue)
setItems(searchProjects)
},
[client]
)
useEffect(() => {
if (debouncedInputValue) {
fetchProjects(debouncedInputValue)
}
}, [fetchProjects, debouncedInputValue])
return (
<div className="relative w-full lg:w-fit dark:bg-overlay">
<SearchBar {...getInputProps()} />
<div
{...getMenuProps({}, { suppressRefError: true })}
className={cn(
'flex flex-col shadow-dropdown rounded-xl dark:bg-overlay2 bg-surface-card absolute w-[459px] max-h-52 overflow-y-auto px-2 py-2 gap-1 z-50',
{ hidden: !inputValue || !isOpen }
)}
>
{items.length ? (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
{items.map((item, index) => (
<ProjectSearchBarItem
{...getItemProps({ item, index })}
key={item.id}
item={item}
active={highlightedIndex === index || selectedItem === item}
/>
))}
</>
) : (
<ProjectSearchBarEmpty />
)}
</div>
</div>
)
}
@@ -0,0 +1,138 @@
import { ProjectSearchBarItem } from '@/components/projects/project/ProjectSearchBar/ProjectSearchBarItem'
import { useGQLClient } from '@/context/GQLClientContext'
import * as Dialog from '@radix-ui/react-dialog'
import type { Project } from '@workspace/gql-client'
import { Button } from '@workspace/ui/components/button'
import { Input } from '@workspace/ui/components/input'
import { useCombobox } from 'downshift'
import { Search, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useDebounceValue } from 'usehooks-ts'
import { ProjectSearchBarEmpty } from './ProjectSearchBarEmpty'
interface ProjectSearchBarDialogProps extends Dialog.DialogProps {
open?: boolean
onClose?: () => void
onClickItem?: (data: Project) => void
}
export const ProjectSearchBarDialog = ({
onClose,
onClickItem,
...props
}: ProjectSearchBarDialogProps) => {
const [items, setItems] = useState<Project[]>([])
const [selectedItem, setSelectedItem] = useState<Project | null>(null)
const client = useGQLClient()
const router = useRouter()
const inputRef = useRef<HTMLInputElement>(null)
const {
getInputProps,
getItemProps,
getMenuProps,
inputValue,
setInputValue
} = useCombobox({
items,
itemToString(item) {
return item ? item.name : ''
},
selectedItem,
onSelectedItemChange: ({ selectedItem: newSelectedItem }) => {
if (newSelectedItem) {
setSelectedItem(newSelectedItem)
onClickItem?.(newSelectedItem)
router.push(
`/${newSelectedItem.organization.slug}/projects/${newSelectedItem.id}`
)
}
}
})
const [debouncedInputValue, _] = useDebounceValue<string>(inputValue, 300)
const fetchProjects = useCallback(
async (inputValue: string) => {
const { searchProjects } = await client.searchProjects(inputValue)
setItems(searchProjects)
},
[client]
)
useEffect(() => {
if (debouncedInputValue) {
fetchProjects(debouncedInputValue)
}
}, [fetchProjects, debouncedInputValue])
const handleClose = () => {
setInputValue('')
setItems([])
onClose?.()
}
return (
<Dialog.Root {...props}>
<Dialog.Portal>
<Dialog.Overlay className="bg-base-bg md:hidden fixed inset-0 overflow-y-auto" />
<Dialog.Content>
<div className="fixed inset-0 top-0 flex flex-col h-full">
<div className="py-2.5 px-4 flex items-center justify-between border-b border-border-separator/[0.06]">
<div className="relative flex-1">
<Search className="left-2 top-1/2 text-muted-foreground absolute w-4 h-4 -translate-y-1/2" />
<Input
{...getInputProps(
{ ref: inputRef },
{ suppressRefError: true }
)}
className="pl-8"
placeholder="Search"
autoFocus
type="text"
/>
</div>
<Button
variant="ghost"
size="icon"
onClick={handleClose}
type="button"
>
<X className="w-4 h-4" />
</Button>
</div>
<div
className="flex flex-col gap-1 px-2 py-2"
{...getMenuProps(
{},
{
suppressRefError: true
}
)}
>
{items.length > 0 ? (
<>
<div className="px-2 py-2">
<p className="text-elements-mid-em text-xs font-medium">
Suggestions
</p>
</div>
{items.map((item, index) => (
<ProjectSearchBarItem
key={item.id}
item={item}
{...getItemProps({ item, index })}
/>
))}
</>
) : (
inputValue && <ProjectSearchBarEmpty />
)}
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}
@@ -0,0 +1,24 @@
import { cn } from '@/lib/utils'
import { Search } from 'lucide-react'
import type { ComponentPropsWithoutRef } from 'react'
interface ProjectSearchBarEmptyProps extends ComponentPropsWithoutRef<'div'> {}
export const ProjectSearchBarEmpty = ({
className,
...props
}: ProjectSearchBarEmptyProps) => {
return (
<div
{...props}
className={cn('flex items-center px-2 py-2 gap-3', className)}
>
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-orange-50 text-elements-warning dark:bg-red-50 text-error">
<Search size={16} />
</div>
<p className="text-elements-low-em text-sm dark:text-foreground-secondary tracking-[-0.006em]">
No projects matching this name
</p>
</div>
)
}
@@ -0,0 +1,62 @@
import { cn } from '@/lib/utils'
import type { OmitCommon } from '@/types/common'
import { getInitials } from '@/utils/getInitials'
import type { Project } from '@workspace/gql-client'
import {
Avatar,
AvatarFallback,
AvatarImage
} from '@workspace/ui/components/avatar'
import type { Overwrite, UseComboboxGetItemPropsReturnValue } from 'downshift'
import { type ComponentPropsWithoutRef, forwardRef } from 'react'
/**
* Represents a type that merges ComponentPropsWithoutRef<'li'> with certain exclusions.
* @type {MergedComponentPropsWithoutRef}
*/
type MergedComponentPropsWithoutRef = OmitCommon<
ComponentPropsWithoutRef<'button'>,
Omit<
Overwrite<UseComboboxGetItemPropsReturnValue, Project[]>,
'index' | 'item'
>
>
interface ProjectSearchBarItemProps extends MergedComponentPropsWithoutRef {
item: Project
active?: boolean
}
const ProjectSearchBarItem = forwardRef<
HTMLButtonElement,
ProjectSearchBarItemProps
>(({ item, active, ...props }, ref) => {
return (
<button
{...props}
ref={ref}
key={item.id}
className={cn(
'px-2 py-2 flex items-center gap-3 rounded-lg text-left hover:bg-base-bg-emphasized',
{
'bg-base-bg-emphasized': active
}
)}
>
<Avatar>
<AvatarImage src={item.icon} />
<AvatarFallback>{getInitials(item.name)}</AvatarFallback>
</Avatar>
<div className="flex flex-col flex-1">
<p className="text-sm tracking-[-0.006em] text-elements-high-em">
{item.name}
</p>
<p className="text-xs text-elements-low-em">{item.organization.name}</p>
</div>
</button>
)
})
ProjectSearchBarItem.displayName = 'ProjectSearchBarItem'
export { ProjectSearchBarItem }
@@ -0,0 +1,2 @@
export * from './ProjectSearchBar'
export * from './ProjectSearchBarDialog'
@@ -0,0 +1,41 @@
import type { GQLClient } from '@workspace/gql-client'
import { type ReactNode, createContext, useContext } from 'react'
/**
* @const GQLClientContext
* @description Creates a context for managing the GQLClient instance.
*/
const GQLClientContext = createContext({} as GQLClient)
/**
* @component GQLClientProvider
* @description Provides the GQLClientContext to its children.
* @param {Object} props - The component props
* @param {ReactNode} props.children - The children to render.
* @param {GQLClient} props.client - The GQLClient instance.
*/
export const GQLClientProvider = ({
client,
children
}: {
children: ReactNode
client: GQLClient
}) => (
<GQLClientContext.Provider value={client}>
{children}
</GQLClientContext.Provider>
)
/**
* @function useGQLClient
* @description A hook that provides access to the GQLClientContext.
* @returns {GQLClient} The GQLClient instance.
* @throws {Error} If used outside of a GQLClientProvider.
*/
export const useGQLClient = () => {
const client = useContext(GQLClientContext)
if (!client) {
throw new Error('useGQLClient must be used within a GQLClientProvider')
}
return client
}
@@ -0,0 +1,156 @@
import { Octokit, RequestError } from 'octokit'
import { useDebounceCallback } from 'usehooks-ts'
import { useParams, useRouter } from 'next/navigation'
import {
type ReactNode,
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState
} from 'react'
import { toast } from 'sonner'
import { useGQLClient } from './GQLClientContext'
const UNAUTHORIZED_ERROR_CODE = 401
/**
* @interface ContextValue
* @description Defines the structure of the OctokitContext value.
* @property {Octokit} octokit - The Octokit instance.
* @property {boolean} isAuth - Indicates if the user is authenticated with GitHub.
* @property {function} updateAuth - Function to update the authentication status.
*/
interface ContextValue {
octokit: Octokit
isAuth: boolean
updateAuth: () => void
}
/**
* @const OctokitContext
* @description Creates a context for managing Octokit and authentication state.
*/
const OctokitContext = createContext<ContextValue>({
octokit: new Octokit(),
isAuth: false,
updateAuth: () => {}
})
/**
* @component OctokitProvider
* @description Provides the OctokitContext to its children.
* @param {Object} props - Component props
* @param {ReactNode} props.children - The children to render.
* @param {Function} [props.navigate] - Optional navigation function. If not provided, useNavigate will be used.
*/
export const OctokitProvider = ({
children
}: {
children: ReactNode
navigate?: (to: string) => void
}) => {
const [authToken, setAuthToken] = useState<string | null>(null)
const [isAuth, setIsAuth] = useState(false)
// const navigate = externalNavigate || internalNavigateconst
const router = useRouter()
const { orgSlug } = useParams()
// const { toast, dismiss } = useToast()
const client = useGQLClient()
/**
* @function fetchUser
* @description Fetches the user's GitHub token from the GQLClient.
*/
const fetchUser = useCallback(async () => {
const { user } = await client.getUser()
setAuthToken(user.gitHubToken)
}, [client])
/**
* @function updateAuth
* @description Updates the authentication status by fetching the user.
*/
const updateAuth = useCallback(() => {
fetchUser()
}, [fetchUser])
const octokit = useMemo(() => {
if (!authToken) {
setIsAuth(false)
return new Octokit()
}
setIsAuth(true)
return new Octokit({ auth: authToken })
}, [authToken])
useEffect(() => {
fetchUser()
}, [fetchUser])
const debouncedUnauthorizedGithubHandler = useDebounceCallback(
useCallback(
(error: RequestError) => {
toast.error(`GitHub authentication error: ${error.message}`, {
// id: 'unauthorized-github-token',
// variant: 'error',
// onDismiss: dismiss
})
router.push(`/${orgSlug}/projects/create`)
},
[orgSlug, router]
),
500
)
useEffect(() => {
// TODO: Handle React component error
const interceptor = async (error: RequestError | Error) => {
if (
error instanceof RequestError &&
error.status === UNAUTHORIZED_ERROR_CODE
) {
await client.unauthenticateGithub()
await fetchUser()
debouncedUnauthorizedGithubHandler(error)
}
throw error
}
octokit.hook.error('request', interceptor)
return () => {
// Remove the interceptor when the component unmounts
octokit.hook.remove('request', interceptor)
}
}, [octokit, client, debouncedUnauthorizedGithubHandler, fetchUser])
return (
<OctokitContext.Provider value={{ octokit, updateAuth, isAuth }}>
{children}
</OctokitContext.Provider>
)
}
/**
* @function useOctokit
* @description A hook that provides access to the OctokitContext.
* @returns {object} An object containing the Octokit instance, updateAuth function, and isAuth status.
*/
export const useOctokit = () => {
const context = useContext(OctokitContext)
if (!context) {
throw new Error('useOctokit must be used within an OctokitProvider')
}
return context
}
@@ -0,0 +1,18 @@
import { useRouter } from 'next/navigation'
import type { ReactNode } from 'react'
import { OctokitProvider } from './OctokitContext'
/**
* @component OctokitProviderWithRouter
* @description A wrapper component that provides the OctokitProvider with a navigation function from the router.
* @param {ReactNode} children - The children to render.
*/
export const OctokitProviderWithRouter = ({
children
}: {
children: ReactNode
}) => {
const router = useRouter()
return <OctokitProvider navigate={router.push}>{children}</OctokitProvider>
}
@@ -0,0 +1,118 @@
import type React from 'react'
import {
type ReactNode,
createContext,
useContext,
useEffect,
useState
} from 'react'
import { toast } from 'sonner'
/**
* @interface WalletContextType
* @description Defines the structure of the WalletContext value.
* @property {object | null} wallet - The wallet object containing id and address.
* @property {boolean} isConnected - Indicates if the wallet is connected.
* @property {function} connect - Function to connect the wallet.
* @property {function} disconnect - Function to disconnect the wallet.
*/
interface WalletContextType {
wallet: {
id: string
address?: string
} | null
isConnected: boolean
connect: () => Promise<void>
disconnect: () => void
}
/**
* @const WalletContext
* @description Creates a context for managing wallet connection state.
*/
const WalletContext = createContext<WalletContextType | undefined>(undefined)
/**
* @component WalletProvider
* @description Provides the WalletContext to its children.
* @param {Object} props - Component props
* @param {ReactNode} props.children - The children to render.
*/
export const WalletProvider: React.FC<{ children: ReactNode }> = ({
children
}) => {
const [wallet, setWallet] = useState<WalletContextType['wallet']>(null)
const [isConnected, setIsConnected] = useState(false)
useEffect(() => {
const handleWalletMessage = (event: MessageEvent) => {
if (event.origin !== process.env.NEXT_PUBLIC_WALLET_IFRAME_URL) return
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
const address = event.data.data[0].address
setWallet({
id: address,
address: address
})
setIsConnected(true)
toast.success('Wallet Connected', {
// variant: 'success',
duration: 3000
// id: '',
})
}
}
window.addEventListener('message', handleWalletMessage)
return () => window.removeEventListener('message', handleWalletMessage)
}, [])
const connect = async () => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement
if (iframe?.contentWindow) {
iframe.contentWindow.postMessage(
{
type: 'REQUEST_WALLET_ACCOUNTS',
chainId: process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID
},
process.env.NEXT_PUBLIC_WALLET_IFRAME_URL ?? ''
)
} else {
toast.error('Wallet Connection Failed', {
// description: 'Wallet iframe not found or not loaded',
// variant: 'error',
duration: 3000
})
}
}
const disconnect = () => {
setWallet(null)
setIsConnected(false)
toast.info('Wallet Disconnected', {
duration: 3000
})
}
return (
<WalletContext.Provider
value={{ wallet, isConnected, connect, disconnect }}
>
{children}
</WalletContext.Provider>
)
}
/**
* @function useWallet
* @description A hook that provides access to the WalletContext.
* @returns {WalletContextType} The wallet context value.
* @throws {Error} If used outside of a WalletProvider.
*/
export const useWallet = () => {
const context = useContext(WalletContext)
if (context === undefined) {
throw new Error('useWallet must be used within a WalletProvider')
}
return context
}
@@ -0,0 +1,246 @@
import { AutoSignInIFrameModal } from '@/components/iframe/auto-sign-in'
import axios from 'axios'
import { usePathname, useRouter } from 'next/navigation'
import type React from 'react'
import {
type ReactNode,
createContext,
useContext,
useEffect,
useState
} from 'react'
import { SiweMessage, generateNonce } from 'siwe'
import { toast } from 'sonner'
const axiosInstance = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
withCredentials: true
})
/**
* @interface WalletContextType
* @description Defines the structure of the WalletContext value.
* @property {object | null} wallet - The wallet object containing id and address.
* @property {boolean} isConnected - Indicates if the wallet is connected.
* @property {boolean} isReady - Indicates if the app is ready to make API calls.
* @property {function} connect - Function to connect the wallet.
* @property {function} disconnect - Function to disconnect the wallet.
*/
interface WalletContextType {
wallet: {
id: string
address?: string
} | null
isConnected: boolean
isReady: boolean
connect: () => Promise<void>
disconnect: () => void
}
/**
* @const WalletContext
* @description Creates a context for managing wallet connection state.
*/
const WalletContext = createContext<WalletContextType | undefined>(undefined)
/**
* @component WalletContextProvider
* @description Provides the WalletContext to its children.
* @param {Object} props - Component props
* @param {ReactNode} props.children - The children to render.
*/
export const WalletContextProvider: React.FC<{ children: ReactNode }> = ({
children
}) => {
const [wallet, setWallet] = useState<WalletContextType['wallet']>(null)
const [isConnected, setIsConnected] = useState(false)
const [isReady, setIsReady] = useState(false)
const [accountAddress, setAccountAddress] = useState<string>()
const router = useRouter()
const pathname = usePathname()
const baseUrl = process.env.NEXT_PUBLIC_API_URL
// Update isReady state when connection changes
useEffect(() => {
if (isConnected) {
// Add a small delay to ensure session is fully established
const timer = setTimeout(() => {
setIsReady(true)
console.log('Wallet is now ready for API calls')
}, 500)
return () => clearTimeout(timer)
}
setIsReady(false)
}, [isConnected])
// Check session status on mount
useEffect(() => {
fetch(`${baseUrl}/auth/session`, {
credentials: 'include'
}).then((res) => {
const path = pathname
console.log(res)
if (res.status !== 200) {
setIsConnected(false)
localStorage.clear()
if (path !== '/login') {
router.push('/login')
}
} else {
setIsConnected(true)
if (path === '/login') {
router.push('/')
}
}
})
}, [pathname, router, baseUrl])
// Handle wallet messages for account data
useEffect(() => {
const handleWalletMessage = (event: MessageEvent) => {
if (event.origin !== process.env.NEXT_PUBLIC_WALLET_IFRAME_URL) return
console.log(event)
if (event.data.type === 'WALLET_ACCOUNTS_DATA') {
const address = event.data.data[0].address
setWallet({
id: address,
address: address
})
setAccountAddress(address)
setIsConnected(true)
toast.success('Wallet Connected', {
// variant: 'success',
duration: 3000
// id: '',
})
}
}
window.addEventListener('message', handleWalletMessage)
return () => window.removeEventListener('message', handleWalletMessage)
}, [])
// Handle sign-in response from the wallet iframe
useEffect(() => {
const handleSignInResponse = async (event: MessageEvent) => {
if (event.origin !== process.env.NEXT_PUBLIC_WALLET_IFRAME_URL) return
if (event.data.type === 'SIGN_IN_RESPONSE') {
try {
const { success } = (
await axiosInstance.post('/auth/validate', {
message: event.data.data.message,
signature: event.data.data.signature
})
).data
if (success === true) {
setIsConnected(true)
if (pathname === '/login') {
router.push('/')
}
}
} catch (error) {
console.error('Error signing in:', error)
}
}
}
window.addEventListener('message', handleSignInResponse)
return () => {
window.removeEventListener('message', handleSignInResponse)
}
}, [router, pathname])
// Initiate auto sign-in when account address is available
useEffect(() => {
const initiateAutoSignIn = async () => {
if (!accountAddress) return
const iframe = document.getElementById(
'walletAuthFrame'
) as HTMLIFrameElement
if (!iframe?.contentWindow) {
console.error('Iframe not found or not loaded')
return
}
const message = new SiweMessage({
version: '1',
domain: window.location.host,
uri: window.location.origin,
chainId: 1,
address: accountAddress,
nonce: generateNonce(),
statement: 'Sign in With Ethereum.'
}).prepareMessage()
iframe.contentWindow.postMessage(
{
type: 'AUTO_SIGN_IN',
chainId: '1',
message
},
process.env.NEXT_PUBLIC_WALLET_IFRAME_URL ?? ''
)
}
initiateAutoSignIn()
}, [accountAddress])
const connect = async () => {
const iframe = document.getElementById('walletIframe') as HTMLIFrameElement
if (iframe?.contentWindow) {
iframe.contentWindow.postMessage(
{
type: 'REQUEST_WALLET_ACCOUNTS',
chainId: process.env.NEXT_PUBLIC_LACONICD_CHAIN_ID
},
process.env.NEXT_PUBLIC_WALLET_IFRAME_URL ?? ''
)
} else {
toast.error('Wallet Connection Failed', {
// description: 'Wallet iframe not found or not loaded',
// variant: 'error',
duration: 3000
})
}
}
const disconnect = () => {
setWallet(null)
setIsConnected(false)
toast.info('Wallet Disconnected', {
duration: 3000
})
}
return (
<WalletContext.Provider
value={{ wallet, isConnected, isReady, connect, disconnect }}
>
{children}
{!isConnected && <AutoSignInIFrameModal />}
</WalletContext.Provider>
)
}
/**
* @function useWallet
* @description A hook that provides access to the WalletContext.
* @returns {WalletContextType} The wallet context value.
* @throws {Error} If used outside of a WalletContextProvider.
*/
export const useWallet = () => {
const context = useContext(WalletContext)
if (context === undefined) {
throw new Error('useWallet must be used within a WalletContextProvider')
}
return context
}
+4
View File
@@ -0,0 +1,4 @@
export * from './GQLClientContext'
export * from './OctokitContext'
export * from './OctokitProviderWithRouter'
export * from './WalletContextProvider'
+27
View File
@@ -0,0 +1,27 @@
import {
type ComponentPropsWithoutRef,
type ElementType,
forwardRef
} from 'react'
/**
* Construct a type by excluding common keys from one type to another.
* @template T - The type from which to omit properties.
* @template U - The type whose properties to omit from T.
* @param {T} - The source type.
* @param {U} - The target type.
* @returns A new type that includes all properties from T except those that are common with U.
*/
export type OmitCommon<T, U> = Pick<T, Exclude<keyof T, keyof U>>
export type PolymorphicProps<Element extends ElementType, Props> = Props &
Omit<ComponentPropsWithoutRef<Element>, 'as'> & {
as?: Element
}
// taken from : https://github.com/total-typescript/react-typescript-tutorial/blob/main/src/08-advanced-patterns/72-as-prop-with-forward-ref.solution.tsx
type FixedForwardRef = <T, P = object>(
render: (props: P, ref: React.Ref<T>) => React.ReactNode
) => (props: P & React.RefAttributes<T>) => JSX.Element
export const fixedForwardRef = forwardRef as FixedForwardRef
+73 -4
View File
@@ -261,6 +261,9 @@ importers:
'@workspace/ui':
specifier: workspace:*
version: link:../../services/ui
axios:
specifier: ^1.8.4
version: 1.8.4
class-variance-authority:
specifier: ^0.7.0
version: 0.7.1
@@ -273,6 +276,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
downshift:
specifier: ^9.0.9
version: 9.0.9(react@19.0.0)
embla-carousel-react:
specifier: ^8.5.2
version: 8.5.2(react@19.0.0)
@@ -288,6 +294,9 @@ importers:
next-themes:
specifier: ^0.4.4
version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
octokit:
specifier: ^3.1.2
version: 3.2.1
react:
specifier: ^19.0.0
version: 19.0.0
@@ -306,12 +315,18 @@ importers:
recharts:
specifier: ^2.15.1
version: 2.15.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
siwe:
specifier: ^3.0.0
version: 3.0.0(ethers@5.8.0)
sonner:
specifier: ^2.0.1
version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
tailwind-merge:
specifier: ^3.0.2
version: 3.0.2
usehooks-ts:
specifier: ^3.1.1
version: 3.1.1(react@19.0.0)
vaul:
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@@ -331,6 +346,9 @@ importers:
'@types/react-dom':
specifier: 18.3.1
version: 18.3.1
'@workspace/gql-client':
specifier: workspace:*
version: link:../../services/gql-client
'@workspace/typescript-config':
specifier: workspace:*
version: link:../../services/typescript-config
@@ -378,7 +396,7 @@ importers:
version: 1.9.4
'@biomejs/monorepo':
specifier: github:biomejs/biome
version: https://codeload.github.com/biomejs/biome/tar.gz/8a832f29581970bd2dab0aa004f0df2ec26c4e96
version: https://codeload.github.com/biomejs/biome/tar.gz/e9e82674a1a294da75195b46705695b6e0f3e088
'@hookform/resolvers':
specifier: ^4.1.2
version: 4.1.3(react-hook-form@7.54.2(react@19.0.0))
@@ -699,8 +717,8 @@ packages:
cpu: [x64]
os: [win32]
'@biomejs/monorepo@https://codeload.github.com/biomejs/biome/tar.gz/8a832f29581970bd2dab0aa004f0df2ec26c4e96':
resolution: {tarball: https://codeload.github.com/biomejs/biome/tar.gz/8a832f29581970bd2dab0aa004f0df2ec26c4e96}
'@biomejs/monorepo@https://codeload.github.com/biomejs/biome/tar.gz/e9e82674a1a294da75195b46705695b6e0f3e088':
resolution: {tarball: https://codeload.github.com/biomejs/biome/tar.gz/e9e82674a1a294da75195b46705695b6e0f3e088}
version: 0.0.0
'@cerc-io/laconic-registry-cli@0.2.10':
@@ -2814,6 +2832,9 @@ packages:
axios@1.8.2:
resolution: {integrity: sha512-ls4GYBm5aig9vWx8AWDSGLpnpDQRtWAfrjU+EuytuODrFBkqesN2RkOQCBzrA1RQNHw1SmRMSDDDSwzNAYQ6Rg==}
axios@1.8.4:
resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -3083,6 +3104,9 @@ packages:
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
engines: {node: '>= 6'}
compute-scroll-into-view@3.1.1:
resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -3343,6 +3367,11 @@ packages:
resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==}
engines: {node: '>=12'}
downshift@9.0.9:
resolution: {integrity: sha512-ygOT8blgiz5liDuEFAIaPeU4dDEa+w9p6PHVUisPIjrkF5wfR59a52HpGWAVVMoWnoFO8po2mZSScKZueihS7g==}
peerDependencies:
react: '>=16.12.0'
dset@3.1.4:
resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
engines: {node: '>=4'}
@@ -4079,6 +4108,9 @@ packages:
lodash-clean@2.2.3:
resolution: {integrity: sha512-ioRhn/L0NNKq220nba58FPvjZ+bTdlUCb37+mhlDe4kzIzuPC/prUHLwDM9izeicr/rcnWrn0EanzNxhAbo8oA==}
lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
@@ -4658,6 +4690,9 @@ packages:
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-is@18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
@@ -5440,6 +5475,12 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
usehooks-ts@3.1.1:
resolution: {integrity: sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==}
engines: {node: '>=16.15.0'}
peerDependencies:
react: ^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -5777,7 +5818,7 @@ snapshots:
'@biomejs/cli-win32-x64@1.9.4':
optional: true
'@biomejs/monorepo@https://codeload.github.com/biomejs/biome/tar.gz/8a832f29581970bd2dab0aa004f0df2ec26c4e96': {}
'@biomejs/monorepo@https://codeload.github.com/biomejs/biome/tar.gz/e9e82674a1a294da75195b46705695b6e0f3e088': {}
'@cerc-io/laconic-registry-cli@0.2.10':
dependencies:
@@ -8318,6 +8359,14 @@ snapshots:
transitivePeerDependencies:
- debug
axios@1.8.4:
dependencies:
follow-redirects: 1.15.9(debug@4.4.0)
form-data: 4.0.2
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
balanced-match@1.0.2: {}
base-x@3.0.10:
@@ -8630,6 +8679,8 @@ snapshots:
commander@4.1.1: {}
compute-scroll-into-view@3.1.1: {}
concat-map@0.0.1: {}
consola@3.4.0: {}
@@ -8880,6 +8931,15 @@ snapshots:
dotenv@16.4.7: {}
downshift@9.0.9(react@19.0.0):
dependencies:
'@babel/runtime': 7.26.9
compute-scroll-into-view: 3.1.1
prop-types: 15.8.1
react: 19.0.0
react-is: 18.2.0
tslib: 2.8.1
dset@3.1.4: {}
dunder-proto@1.0.1:
@@ -9729,6 +9789,8 @@ snapshots:
dependencies:
lodash: 4.17.21
lodash.debounce@4.0.8: {}
lodash.get@4.4.2: {}
lodash.includes@4.3.0: {}
@@ -10293,6 +10355,8 @@ snapshots:
react-is@16.13.1: {}
react-is@18.2.0: {}
react-is@18.3.1: {}
react-remove-scroll-bar@2.3.8(@types/react@18.3.0)(react@19.0.0):
@@ -11144,6 +11208,11 @@ snapshots:
dependencies:
react: 19.0.0
usehooks-ts@3.1.1(react@19.0.0):
dependencies:
lodash.debounce: 4.0.8
react: 19.0.0
util-deprecate@1.0.2: {}
utils-merge@1.0.1: {}