mirror of
https://github.com/QWRK-ORG/qwrk-laconic-core.git
synced 2026-09-09 05:14:07 +00:00
chore: Import project cards and deps
This commit is contained in:
@@ -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"
|
||||
@@ -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",
|
||||
|
||||
+59
@@ -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>
|
||||
)
|
||||
}
|
||||
+138
@@ -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>
|
||||
)
|
||||
}
|
||||
+24
@@ -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>
|
||||
)
|
||||
}
|
||||
+62
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './GQLClientContext'
|
||||
export * from './OctokitContext'
|
||||
export * from './OctokitProviderWithRouter'
|
||||
export * from './WalletContextProvider'
|
||||
@@ -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
|
||||
Generated
+73
-4
@@ -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: {}
|
||||
|
||||
Reference in New Issue
Block a user