mirror of
https://github.com/sern-handler/frontpage-bot
synced 2026-06-05 17:06:51 +00:00
feat: admin panel
This commit is contained in:
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"printWidth": 800,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"semi": false
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@sern/poster": "^1.2.6",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
|
||||
2
prisma/migrations/20240518221152_is_admin/migration.sql
Normal file
2
prisma/migrations/20240518221152_is_admin/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "isAdmin" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Bot" ALTER COLUMN "inviteLink" DROP NOT NULL;
|
||||
@@ -17,6 +17,7 @@ model User {
|
||||
id String @id @default(cuid())
|
||||
username String @unique
|
||||
hashed_password String
|
||||
isAdmin Boolean @default(false)
|
||||
sessions Session[]
|
||||
bots Bot[]
|
||||
}
|
||||
@@ -36,8 +37,8 @@ model Bot {
|
||||
name String
|
||||
description String
|
||||
verified Boolean @default(false)
|
||||
inviteLink String
|
||||
pfpLink String
|
||||
inviteLink String?
|
||||
srcLink String?
|
||||
botId String
|
||||
}
|
||||
|
||||
24
src/app/admin/page.tsx
Normal file
24
src/app/admin/page.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import UserCard from "@/components/app/UserCard/UserCard";
|
||||
import { validateRequest } from "@/lib/auth";
|
||||
import prisma from "@/lib/db";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page() {
|
||||
const { user } = await validateRequest()
|
||||
if (!user) return redirect('/auth/signIn')
|
||||
if (!user.isAdmin) return (
|
||||
<h1 className="text-center font-extrabold text-6xl">Sorry</h1>
|
||||
)
|
||||
|
||||
const getBots = await prisma.bot.findMany()
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-center font-extrabold text-4xl">wow an admin panel</h1>
|
||||
<div className="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 p-4">
|
||||
{getBots.map((bot) => (
|
||||
<UserCard key={bot.id} {...bot} isAdminPanel />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -22,6 +22,8 @@
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--destructive: 360 80.95% 45.49%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--warning: 41deg, 86%, 83%;
|
||||
--warning-foreground: 41deg, 86%, 83%;
|
||||
--ring: 342 59% 54%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
@@ -45,6 +47,8 @@
|
||||
--accent-foreground: 0 0% 100%;
|
||||
--destructive: 360 69.52% 39.02%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
--warning: 41deg, 86%, 83%;
|
||||
--warning-foreground: 41deg, 86%, 83%;
|
||||
--ring: 342 59% 54%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function Home() {
|
||||
</div>
|
||||
<div className="space-x-4">
|
||||
<Link href="/dashboard">
|
||||
<Button>Start right NOW!</Button>
|
||||
<Button>Let's go!</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import SubmitButton from "../SubmitButton/SubmitButton";
|
||||
import { submitBotData } from "@/lib/actions";
|
||||
import { revalidatePathServer, submitBotData, updateBotProfilePicture } from "@/lib/actions";
|
||||
import { useFormState } from "react-dom";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function BotForm(props: Props) {
|
||||
const router = useRouter()
|
||||
const [submitData, submitDataAction] = useFormState(submitBotData, null);
|
||||
const [regenLoading, setRegenLoading] = useState(false)
|
||||
useEffect(() => {
|
||||
if (submitData?.error) {
|
||||
toast.error(submitData.error)
|
||||
@@ -34,15 +40,47 @@ export default function BotForm(props: Props) {
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="inviteLink">Invite link</Label>
|
||||
<Input name="inviteLink" id="inviteLink" required type="text" defaultValue={props?.inviteLink} />
|
||||
<Input name="inviteLink" id="inviteLink" type="text" defaultValue={props?.inviteLink} />
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="srcLink">Repo link (optional)</Label>
|
||||
<Input name="srcLink" id="srcLink" type="text" defaultValue={props?.srcLink || ''} />
|
||||
</div>
|
||||
</div>
|
||||
<SubmitButton buttonText="Submit" />
|
||||
<div className="flex items-center justify-center p-2 gap-2">
|
||||
{props.botId && (
|
||||
<Button variant="secondary" loading={regenLoading} type="button" onClick={() => {
|
||||
setRegenLoading(true)
|
||||
updateBotProfilePicture({ botId: props.botId! }).then((res) => {
|
||||
setRegenLoading(false)
|
||||
if (res.error) {
|
||||
toast.error(res.error)
|
||||
}
|
||||
if (res.success) {
|
||||
toast.success(res.message)
|
||||
revalidatePathServer('/dashboard')
|
||||
router.push('/dashboard')
|
||||
}
|
||||
})
|
||||
}}>
|
||||
Regenerate profile picture
|
||||
</Button>
|
||||
)}
|
||||
<SubmitButton buttonText="Submit" />
|
||||
</div>
|
||||
</div>
|
||||
{props.id && (
|
||||
<div className="flex items-center justify-center p-2">
|
||||
<Alert variant={"warning"} className="w-[400px]">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Warning</AlertTitle>
|
||||
<AlertDescription className="whitespace-pre-line">
|
||||
When resubmitting, the bot will be unverified until the devteam verifies it again.{'\n'}
|
||||
This doesn't apply when it's just regenerating the profile picture
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
11
src/components/app/CheckedLoading/CheckedLoading.tsx
Normal file
11
src/components/app/CheckedLoading/CheckedLoading.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import { LoaderIcon } from "lucide-react";
|
||||
|
||||
export default function CheckedLoading(props: Props) {
|
||||
return props.loading && <LoaderIcon className="animate-spin" />
|
||||
}
|
||||
|
||||
interface Props {
|
||||
loading: boolean;
|
||||
}
|
||||
@@ -63,6 +63,13 @@ export default function Navbar() {
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{user.isAdmin && (
|
||||
<DropdownMenuItem>
|
||||
<Link href="/admin">
|
||||
Admin panel
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem className="cursor-pointer" onClick={() => {
|
||||
logoutAction()
|
||||
}}>
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
import { Avatar, AvatarImage } from "@/components/ui/avatar"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import type { Bot } from "@prisma/client"
|
||||
import Link from "next/link"
|
||||
import VerifiedSwitch from "../VerifiedSwitch/VerifiedSwitch"
|
||||
|
||||
export default function UserCard(props: Bot) {
|
||||
const yes = <span className="text-green-500">Yes</span>
|
||||
const no = <span className="text-red-500">No</span>
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg border bg-card text-card-foreground shadow-sm p-4">
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<Avatar className="w-14 h-14">
|
||||
<AvatarImage src={props.pfpLink} alt="sernbot" />
|
||||
</Avatar>
|
||||
<h1 className="flex-grow text-center font-extrabold text-2xl">{props.name}</h1>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<p>{props.description}</p>
|
||||
<p className="text-sm">Verified: {props.verified ? yes : no}</p>
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 gap-4">
|
||||
{props.srcLink && (
|
||||
<Link href={props.srcLink} target="_blank">
|
||||
<Button variant={'secondaryFilledLink'}>Source</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={props.inviteLink} target="_blank">
|
||||
<Button variant={'secondaryFilledLink'}>Invite</Button>
|
||||
</Link>
|
||||
<Link href={`/dashboard/${props.id}`}>
|
||||
<Button>Settings</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default function UserCard(props: Bot & { isAdminPanel?: boolean }) {
|
||||
const yes = <span className="text-green-500">Yes</span>
|
||||
const no = <span className="text-red-500">No</span>
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-lg border bg-card text-card-foreground shadow-sm p-4">
|
||||
<div className="flex items-center justify-center space-x-4">
|
||||
<Avatar className="w-14 h-14">
|
||||
<AvatarImage src={props.pfpLink} alt="sernbot" />
|
||||
</Avatar>
|
||||
<h1 className="flex-grow text-center font-extrabold text-2xl">
|
||||
{props.name}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="pt-4">
|
||||
<p>{props.description}</p>
|
||||
{!props.isAdminPanel && <p className="text-sm">Verified: {props.verified ? yes : no}</p>}
|
||||
{props.isAdminPanel && <p className="text-sm">Bot ID: <code className="bg-zinc-800 p-0.5">{props.botId}</code></p>}
|
||||
</div>
|
||||
<div className="flex justify-end mt-4 gap-4">
|
||||
{props.srcLink && (
|
||||
<Link href={props.srcLink} target="_blank">
|
||||
<Button variant={"secondaryFilledLink"}>Source</Button>
|
||||
</Link>
|
||||
)}
|
||||
{props.inviteLink && (
|
||||
<Link href={props.inviteLink} target="_blank">
|
||||
<Button variant={"secondaryFilledLink"}>Invite</Button>
|
||||
</Link>
|
||||
)}
|
||||
{props.isAdminPanel && <VerifiedSwitch id={props.id} verified={props.verified} />}
|
||||
{!props.isAdminPanel && (
|
||||
<>
|
||||
<Link href={`/dashboard/${props.id}`}>
|
||||
<Button>Settings</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
43
src/components/app/VerifiedSwitch/VerifiedSwitch.tsx
Normal file
43
src/components/app/VerifiedSwitch/VerifiedSwitch.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client'
|
||||
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import CheckedLoading from '../CheckedLoading/CheckedLoading'
|
||||
import { handleBotVerificationSwitch } from '@/lib/actions'
|
||||
import { useState } from 'react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export default function VerifiedSwitch(props: Props) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [checked, setChecked] = useState(props.verified)
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="verified-switch">Verified</Label>
|
||||
<Switch
|
||||
id="verified-switch"
|
||||
name="verified"
|
||||
checked={checked}
|
||||
onCheckedChange={(c) => {
|
||||
setLoading(true)
|
||||
handleBotVerificationSwitch({ id: props.id, verified: c }).then((ver) => {
|
||||
setLoading(false)
|
||||
// sets to the opposite of the current value, like a normal switch
|
||||
setChecked(!checked)
|
||||
if (ver.error) {
|
||||
toast.error(ver.error)
|
||||
}
|
||||
if (ver.success) {
|
||||
toast.success(ver.message)
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<CheckedLoading loading={loading} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
verified: boolean
|
||||
}
|
||||
61
src/components/ui/alert.tsx
Normal file
61
src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
warning:
|
||||
"border-warning/50 text-warning dark:border-warning dark:text-warning",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
))
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName
|
||||
|
||||
export { Switch }
|
||||
@@ -3,8 +3,9 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { validateRequest } from "./auth";
|
||||
import prisma from "./db";
|
||||
import { botSubmitSchema } from "./zod";
|
||||
import { botSubmitSchema, botVerificationSwitchSchema } from "./zod";
|
||||
import poster from '@sern/poster';
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function submitBotData(prev: any, formData: FormData): Promise<DefaultActionResponse> {
|
||||
const parsedData = await botSubmitSchema.safeParseAsync(Object.fromEntries(formData.entries()));
|
||||
@@ -58,6 +59,83 @@ export async function submitBotData(prev: any, formData: FormData): Promise<Defa
|
||||
redirect('/dashboard')
|
||||
}
|
||||
|
||||
export async function handleBotVerificationSwitch(data: { id: string, verified: boolean }): Promise<DefaultActionResponse> {
|
||||
const parsedData = await botVerificationSwitchSchema.safeParseAsync(data);
|
||||
const { user } = await validateRequest();
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
error: "You must be logged in to perform this action",
|
||||
};
|
||||
}
|
||||
if (!parsedData.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: `From ${parsedData.error.errors[0].path[0]}: ${parsedData.error.errors[0].message}`,
|
||||
};
|
||||
}
|
||||
console.log(parsedData.data.id, parsedData.data.verified)
|
||||
|
||||
const botUpdate = await prisma.bot.update({
|
||||
where: {
|
||||
id: parsedData.data.id,
|
||||
},
|
||||
data: {
|
||||
verified: parsedData.data.verified,
|
||||
},
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
message: `Bot verification status for ${botUpdate.name} updated`,
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateBotProfilePicture(data: { botId: string }): Promise<DefaultActionResponse> {
|
||||
const { user } = await validateRequest()
|
||||
if (!user) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'You must be logged in to perform this action',
|
||||
}
|
||||
}
|
||||
const bot = await prisma.bot.findFirst({
|
||||
where: {
|
||||
botId: data.botId,
|
||||
},
|
||||
})
|
||||
if (!bot) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Bot not found',
|
||||
}
|
||||
}
|
||||
const botClient = await poster.client(process.env.DSC_TOKEN!)
|
||||
const userAvatarHash = (await (await botClient('user/get', { user_id: bot.botId })).json()).avatar
|
||||
if (!userAvatarHash) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Bot not found on Discord',
|
||||
}
|
||||
}
|
||||
const userAvatar = `https://cdn.discordapp.com/avatars/${bot.botId}/${userAvatarHash}.webp`
|
||||
await prisma.bot.update({
|
||||
where: {
|
||||
id: bot.id,
|
||||
},
|
||||
data: {
|
||||
pfpLink: userAvatar,
|
||||
},
|
||||
})
|
||||
return {
|
||||
success: true,
|
||||
message: 'Profile picture updated',
|
||||
}
|
||||
}
|
||||
|
||||
export async function revalidatePathServer(path: string) {
|
||||
return revalidatePath(path)
|
||||
}
|
||||
|
||||
interface DefaultActionResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
|
||||
@@ -18,7 +18,8 @@ export const lucia = new Lucia(adapter, {
|
||||
},
|
||||
getUserAttributes: (attributes) => {
|
||||
return {
|
||||
username: attributes.username
|
||||
username: attributes.username,
|
||||
isAdmin: attributes.isAdmin
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -68,4 +69,5 @@ declare module "lucia" {
|
||||
|
||||
interface DatabaseUserAttributes {
|
||||
username: string;
|
||||
isAdmin: boolean;
|
||||
}
|
||||
@@ -6,6 +6,11 @@ export const botSubmitSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
botId: z.string(),
|
||||
inviteLink: z.string(),
|
||||
inviteLink: z.string().nullable().optional(),
|
||||
srcLink: z.string().nullable().optional()
|
||||
})
|
||||
|
||||
export const botVerificationSwitchSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
verified: z.boolean()
|
||||
})
|
||||
@@ -36,6 +36,10 @@ const config = {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: "hsl(var(--warning))",
|
||||
foreground: "hsl(var(--warning-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
|
||||
21
yarn.lock
21
yarn.lock
@@ -682,6 +682,20 @@
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
|
||||
"@radix-ui/react-switch@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e"
|
||||
integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.1"
|
||||
"@radix-ui/react-context" "1.0.1"
|
||||
"@radix-ui/react-primitive" "1.0.3"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.1"
|
||||
"@radix-ui/react-use-previous" "1.0.1"
|
||||
"@radix-ui/react-use-size" "1.0.1"
|
||||
|
||||
"@radix-ui/react-use-callback-ref@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a"
|
||||
@@ -712,6 +726,13 @@
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-previous@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz#b595c087b07317a4f143696c6a01de43b0d0ec66"
|
||||
integrity sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
|
||||
"@radix-ui/react-use-rect@1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2"
|
||||
|
||||
Reference in New Issue
Block a user