Merge pull request #13 from Code-Victor/feat/profile-page
Feat/profile page
This commit is contained in:
commit
39993af4a3
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@ -5,5 +5,9 @@
|
||||
"source.fixAll": "explicit",
|
||||
"source.organizeImports": "explicit"
|
||||
},
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||
["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
|
||||
],
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
|
@ -5,7 +5,13 @@ import { z } from "zod"
|
||||
|
||||
import { and, eq, sql } from "drizzle-orm"
|
||||
import * as schema from "./schema"
|
||||
import { sandbox, user, usersToSandboxes } from "./schema"
|
||||
import {
|
||||
Sandbox,
|
||||
sandbox,
|
||||
sandboxLikes,
|
||||
user,
|
||||
usersToSandboxes,
|
||||
} from "./schema"
|
||||
|
||||
export interface Env {
|
||||
DB: D1Database
|
||||
@ -18,6 +24,13 @@ export interface Env {
|
||||
|
||||
// npm run generate
|
||||
// npx wrangler d1 execute d1-sandbox --local --file=./drizzle/<FILE>
|
||||
interface SandboxWithLiked extends Sandbox {
|
||||
liked: boolean
|
||||
}
|
||||
|
||||
interface UserResponse extends Omit<schema.User, "sandbox"> {
|
||||
sandbox: SandboxWithLiked[]
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(
|
||||
@ -238,15 +251,59 @@ export default {
|
||||
|
||||
if (params.has("id")) {
|
||||
const id = params.get("id") as string
|
||||
|
||||
const res = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.id, id),
|
||||
with: {
|
||||
sandbox: {
|
||||
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
|
||||
with: {
|
||||
likes: true,
|
||||
},
|
||||
},
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
if (res) {
|
||||
const transformedUser: UserResponse = {
|
||||
...res,
|
||||
sandbox: res.sandbox.map(
|
||||
(sb): SandboxWithLiked => ({
|
||||
...sb,
|
||||
liked: sb.likes.some((like) => like.userId === id),
|
||||
})
|
||||
),
|
||||
}
|
||||
return json(transformedUser)
|
||||
}
|
||||
return json(res ?? {})
|
||||
} else if (params.has("username")) {
|
||||
const username = params.get("username") as string
|
||||
const userId = params.get("currentUserId")
|
||||
const res = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.username, username),
|
||||
with: {
|
||||
sandbox: {
|
||||
orderBy: (sandbox, { desc }) => [desc(sandbox.createdAt)],
|
||||
with: {
|
||||
likes: true,
|
||||
},
|
||||
},
|
||||
usersToSandboxes: true,
|
||||
},
|
||||
})
|
||||
if (res) {
|
||||
const transformedUser: UserResponse = {
|
||||
...res,
|
||||
sandbox: res.sandbox.map(
|
||||
(sb): SandboxWithLiked => ({
|
||||
...sb,
|
||||
liked: sb.likes.some((like) => like.userId === userId),
|
||||
})
|
||||
),
|
||||
}
|
||||
return json(transformedUser)
|
||||
}
|
||||
return json(res ?? {})
|
||||
} else {
|
||||
const res = await db.select().from(user).all()
|
||||
@ -267,8 +324,8 @@ export default {
|
||||
})
|
||||
|
||||
const body = await request.json()
|
||||
const { id, name, email, username, avatarUrl, createdAt, generations, tier, tierExpiresAt, lastResetDate } = userSchema.parse(body)
|
||||
|
||||
const { id, name, email, username, avatarUrl, createdAt, generations, tier, tierExpiresAt, lastResetDate } = userSchema.parse(body)
|
||||
const res = await db
|
||||
.insert(user)
|
||||
.values({
|
||||
@ -293,6 +350,57 @@ export default {
|
||||
await db.delete(user).where(eq(user.id, id))
|
||||
return success
|
||||
} else return invalidRequest
|
||||
} else if (method === "PUT") {
|
||||
const updateUserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
username: z.string().optional(),
|
||||
avatarUrl: z.string().optional(),
|
||||
generations: z.number().optional(),
|
||||
})
|
||||
|
||||
try {
|
||||
const body = await request.json()
|
||||
const validatedData = updateUserSchema.parse(body)
|
||||
|
||||
const { id, username, ...updateData } = validatedData
|
||||
|
||||
// If username is being updated, check for existing username
|
||||
if (username) {
|
||||
const existingUser = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.username, username))
|
||||
.get()
|
||||
if (existingUser && existingUser.id !== id) {
|
||||
return json({ error: "Username already exists" }, { status: 409 })
|
||||
}
|
||||
}
|
||||
|
||||
const cleanUpdateData = {
|
||||
...updateData,
|
||||
...(username ? { username } : {}),
|
||||
}
|
||||
|
||||
const res = await db
|
||||
.update(user)
|
||||
.set(cleanUpdateData)
|
||||
.where(eq(user.id, id))
|
||||
.returning()
|
||||
.get()
|
||||
|
||||
if (!res) {
|
||||
return json({ error: "User not found" }, { status: 404 })
|
||||
}
|
||||
|
||||
return json({ res })
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return json({ error: error.errors }, { status: 400 })
|
||||
}
|
||||
return json({ error: "Internal server error" }, { status: 500 })
|
||||
}
|
||||
} else {
|
||||
return methodNotAllowed
|
||||
}
|
||||
@ -304,7 +412,7 @@ export default {
|
||||
if (!username) return invalidRequest
|
||||
|
||||
const exists = await db.query.user.findFirst({
|
||||
where: (user, { eq }) => eq(user.username, username)
|
||||
where: (user, { eq }) => eq(user.username, username),
|
||||
})
|
||||
|
||||
return json({ exists: !!exists })
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { createId } from "@paralleldrive/cuid2"
|
||||
import { relations } from "drizzle-orm"
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
import { sql } from "drizzle-orm"
|
||||
import { relations, sql } from "drizzle-orm"
|
||||
import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"
|
||||
|
||||
// #region Tables
|
||||
export const user = sqliteTable("user", {
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
@ -12,8 +12,9 @@ export const user = sqliteTable("user", {
|
||||
email: text("email").notNull(),
|
||||
username: text("username").notNull().unique(),
|
||||
avatarUrl: text("avatarUrl"),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" })
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
generations: integer("generations").default(0),
|
||||
tier: text("tier", { enum: ["FREE", "PRO", "ENTERPRISE"] }).default("FREE"),
|
||||
tierExpiresAt: integer("tierExpiresAt"),
|
||||
@ -22,11 +23,6 @@ export const user = sqliteTable("user", {
|
||||
|
||||
export type User = typeof user.$inferSelect
|
||||
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sandbox: many(sandbox),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
}))
|
||||
|
||||
export const sandbox = sqliteTable("sandbox", {
|
||||
id: text("id")
|
||||
.$defaultFn(() => createId())
|
||||
@ -35,8 +31,9 @@ export const sandbox = sqliteTable("sandbox", {
|
||||
name: text("name").notNull(),
|
||||
type: text("type").notNull(),
|
||||
visibility: text("visibility", { enum: ["public", "private"] }),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" })
|
||||
.default(sql`CURRENT_TIMESTAMP`),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
@ -46,13 +43,23 @@ export const sandbox = sqliteTable("sandbox", {
|
||||
|
||||
export type Sandbox = typeof sandbox.$inferSelect
|
||||
|
||||
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
|
||||
author: one(user, {
|
||||
fields: [sandbox.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
}))
|
||||
export const sandboxLikes = sqliteTable(
|
||||
"sandbox_likes",
|
||||
{
|
||||
userId: text("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
sandboxId: text("sandbox_id")
|
||||
.notNull()
|
||||
.references(() => sandbox.id),
|
||||
createdAt: integer("createdAt", { mode: "timestamp_ms" }).default(
|
||||
sql`CURRENT_TIMESTAMP`
|
||||
),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.sandboxId, table.userId] }),
|
||||
})
|
||||
)
|
||||
|
||||
export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
|
||||
userId: text("userId")
|
||||
@ -64,6 +71,33 @@ export const usersToSandboxes = sqliteTable("users_to_sandboxes", {
|
||||
sharedOn: integer("sharedOn", { mode: "timestamp_ms" }),
|
||||
})
|
||||
|
||||
// #region Relations
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sandbox: many(sandbox),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
}))
|
||||
|
||||
export const sandboxRelations = relations(sandbox, ({ one, many }) => ({
|
||||
author: one(user, {
|
||||
fields: [sandbox.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
usersToSandboxes: many(usersToSandboxes),
|
||||
likes: many(sandboxLikes),
|
||||
}))
|
||||
|
||||
export const sandboxLikesRelations = relations(sandboxLikes, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [sandboxLikes.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
sandbox: one(sandbox, {
|
||||
fields: [sandboxLikes.sandboxId],
|
||||
references: [sandbox.id],
|
||||
}),
|
||||
}))
|
||||
|
||||
export const usersToSandboxesRelations = relations(
|
||||
usersToSandboxes,
|
||||
({ one }) => ({
|
||||
@ -77,3 +111,5 @@ export const usersToSandboxesRelations = relations(
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
// #endregion
|
||||
|
65
frontend/app/[username]/page.tsx
Normal file
65
frontend/app/[username]/page.tsx
Normal file
@ -0,0 +1,65 @@
|
||||
import ProfilePage from "@/components/profile"
|
||||
import ProfileNavbar from "@/components/profile/navbar"
|
||||
import { SandboxWithLiked, User } from "@/lib/types"
|
||||
import { currentUser } from "@clerk/nextjs"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
export default async function Page({
|
||||
params: { username: rawUsername },
|
||||
}: {
|
||||
params: { username: string }
|
||||
}) {
|
||||
const username = decodeURIComponent(rawUsername).replace("@", "")
|
||||
const loggedInClerkUser = await currentUser()
|
||||
|
||||
const [profileOwnerResponse, loggedInUserResponse] = await Promise.all([
|
||||
fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?username=${username}¤tUserId=${loggedInClerkUser?.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
},
|
||||
}
|
||||
),
|
||||
fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user?id=${loggedInClerkUser?.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
},
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
const profileOwner = (await profileOwnerResponse.json()) as User
|
||||
const loggedInUser = (await loggedInUserResponse.json()) as User
|
||||
|
||||
if (!Boolean(profileOwner?.id)) {
|
||||
notFound()
|
||||
}
|
||||
const publicSandboxes: SandboxWithLiked[] = []
|
||||
const privateSandboxes: SandboxWithLiked[] = []
|
||||
|
||||
profileOwner?.sandbox?.forEach((sandbox) => {
|
||||
if (sandbox.visibility === "public") {
|
||||
publicSandboxes.push(sandbox as SandboxWithLiked)
|
||||
} else if (sandbox.visibility === "private") {
|
||||
privateSandboxes.push(sandbox as SandboxWithLiked)
|
||||
}
|
||||
})
|
||||
|
||||
const isUserLoggedIn = Boolean(loggedInUser?.id)
|
||||
return (
|
||||
<section>
|
||||
<ProfileNavbar userData={loggedInUser} />
|
||||
<ProfilePage
|
||||
publicSandboxes={publicSandboxes}
|
||||
privateSandboxes={
|
||||
profileOwner?.id === loggedInUser.id ? privateSandboxes : []
|
||||
}
|
||||
profileOwner={profileOwner}
|
||||
loggedInUser={isUserLoggedIn ? loggedInUser : null}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
@ -8,7 +8,7 @@ import DashboardNavbarSearch from "./search"
|
||||
|
||||
export default function DashboardNavbar({ userData }: { userData: User }) {
|
||||
return (
|
||||
<div className="h-16 px-4 w-full flex items-center justify-between border-b border-border">
|
||||
<div className=" py-2 px-4 w-full flex items-center justify-between border-b border-border">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/"
|
||||
|
@ -11,13 +11,13 @@ import {
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
|
||||
export default function ProjectCardDropdown({
|
||||
sandbox,
|
||||
visibility,
|
||||
onVisibilityChange,
|
||||
onDelete,
|
||||
}: {
|
||||
sandbox: Sandbox
|
||||
onVisibilityChange: (sandbox: Sandbox) => void
|
||||
onDelete: (sandbox: Sandbox) => void
|
||||
visibility: Sandbox["visibility"]
|
||||
onVisibilityChange: () => void
|
||||
onDelete: () => void
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenu modal={false}>
|
||||
@ -26,7 +26,7 @@ export default function ProjectCardDropdown({
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}}
|
||||
className="h-6 w-6 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
|
||||
className="h-6 w-6 z-10 flex items-center justify-center transition-colors bg-transparent hover:bg-muted-foreground/25 rounded-sm outline-foreground"
|
||||
>
|
||||
<Ellipsis className="w-4 h-4" />
|
||||
</DropdownMenuTrigger>
|
||||
@ -34,11 +34,11 @@ export default function ProjectCardDropdown({
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onVisibilityChange(sandbox)
|
||||
onVisibilityChange()
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
{sandbox.visibility === "public" ? (
|
||||
{visibility === "public" ? (
|
||||
<>
|
||||
<Lock className="mr-2 h-4 w-4" />
|
||||
<span>Make Private</span>
|
||||
@ -53,7 +53,7 @@ export default function ProjectCardDropdown({
|
||||
<DropdownMenuItem
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete(sandbox)
|
||||
onDelete()
|
||||
}}
|
||||
className="!text-destructive cursor-pointer"
|
||||
>
|
||||
|
@ -1,59 +1,235 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { toggleLike } from "@/lib/actions"
|
||||
import { projectTemplates } from "@/lib/data"
|
||||
import { Sandbox } from "@/lib/types"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useUser } from "@clerk/nextjs"
|
||||
import { AnimatePresence, motion } from "framer-motion"
|
||||
import { Clock, Globe, Lock } from "lucide-react"
|
||||
import { Clock, Eye, Globe, Heart, Lock } from "lucide-react"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
memo,
|
||||
MouseEventHandler,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useOptimistic,
|
||||
useState,
|
||||
useTransition,
|
||||
} from "react"
|
||||
import ProjectCardDropdown from "./dropdown"
|
||||
import { CanvasRevealEffect } from "./revealEffect"
|
||||
|
||||
export default function ProjectCard({
|
||||
children,
|
||||
sandbox,
|
||||
onVisibilityChange,
|
||||
onDelete,
|
||||
deletingId,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
sandbox: Sandbox
|
||||
onVisibilityChange: (sandbox: Sandbox) => void
|
||||
onDelete: (sandbox: Sandbox) => void
|
||||
type BaseProjectCardProps = {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
visibility: "public" | "private"
|
||||
createdAt: Date
|
||||
likeCount: number
|
||||
liked?: boolean
|
||||
viewCount: number
|
||||
}
|
||||
|
||||
type AuthenticatedProjectCardProps = BaseProjectCardProps & {
|
||||
isAuthenticated: true
|
||||
onVisibilityChange: (
|
||||
sandbox: Pick<Sandbox, "id" | "name" | "visibility">
|
||||
) => void
|
||||
onDelete: (sandbox: Pick<Sandbox, "id" | "name">) => void
|
||||
deletingId: string
|
||||
}) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const [date, setDate] = useState<string>()
|
||||
const router = useRouter()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const createdAt = new Date(sandbox.createdAt)
|
||||
type UnauthenticatedProjectCardProps = BaseProjectCardProps & {
|
||||
isAuthenticated: false
|
||||
}
|
||||
|
||||
type ProjectCardProps =
|
||||
| AuthenticatedProjectCardProps
|
||||
| UnauthenticatedProjectCardProps
|
||||
|
||||
const StatItem = memo(({ icon: Icon, value }: { icon: any; value: number }) => (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Icon className="size-4" />
|
||||
<span className="text-xs">{value}</span>
|
||||
</div>
|
||||
))
|
||||
|
||||
StatItem.displayName = "StatItem"
|
||||
|
||||
const formatDate = (date: Date): string => {
|
||||
const now = new Date()
|
||||
const diffInMinutes = Math.floor(
|
||||
(now.getTime() - createdAt.getTime()) / 60000
|
||||
const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / 60000)
|
||||
|
||||
if (diffInMinutes < 1) return "Now"
|
||||
if (diffInMinutes < 60) return `${diffInMinutes}m ago`
|
||||
if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h ago`
|
||||
return `${Math.floor(diffInMinutes / 1440)}d ago`
|
||||
}
|
||||
|
||||
const ProjectMetadata = memo(
|
||||
({
|
||||
id,
|
||||
visibility,
|
||||
createdAt,
|
||||
likeCount,
|
||||
liked,
|
||||
viewCount,
|
||||
}: Pick<
|
||||
BaseProjectCardProps,
|
||||
"visibility" | "createdAt" | "likeCount" | "liked" | "viewCount" | "id"
|
||||
>) => {
|
||||
const { user } = useUser()
|
||||
const [date, setDate] = useState<string>()
|
||||
const Icon = visibility === "private" ? Lock : Globe
|
||||
useEffect(() => {
|
||||
setDate(formatDate(new Date(createdAt)))
|
||||
}, [createdAt])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col text-muted-foreground space-y-2 text-sm z-10">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon className="size-4" />
|
||||
<span className="text-xs">
|
||||
{visibility === "private" ? "Private" : "Public"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Clock className="size-4" /> <span className="text-xs">{date}</span>
|
||||
</div>
|
||||
<LikeButton
|
||||
sandboxId={id}
|
||||
initialIsLiked={!!liked}
|
||||
initialLikeCount={likeCount}
|
||||
userId={user?.id ?? null}
|
||||
/>
|
||||
<StatItem icon={Eye} value={viewCount} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (diffInMinutes < 1) {
|
||||
setDate("Now")
|
||||
} else if (diffInMinutes < 60) {
|
||||
setDate(`${diffInMinutes}m ago`)
|
||||
} else if (diffInMinutes < 1440) {
|
||||
setDate(`${Math.floor(diffInMinutes / 60)}h ago`)
|
||||
} else {
|
||||
setDate(`${Math.floor(diffInMinutes / 1440)}d ago`)
|
||||
ProjectMetadata.displayName = "ProjectMetadata"
|
||||
|
||||
interface LikeButtonProps {
|
||||
sandboxId: string
|
||||
userId: string | null
|
||||
initialLikeCount: number
|
||||
initialIsLiked: boolean
|
||||
}
|
||||
}, [sandbox])
|
||||
const projectIcon =
|
||||
projectTemplates.find((p) => p.id === sandbox.type)?.icon ??
|
||||
"/project-icons/node.svg"
|
||||
|
||||
export function LikeButton({
|
||||
sandboxId,
|
||||
userId,
|
||||
initialLikeCount,
|
||||
initialIsLiked,
|
||||
}: LikeButtonProps) {
|
||||
// Optimistic state for like status and count
|
||||
const [{ isLiked, likeCount }, optimisticUpdateLike] = useOptimistic(
|
||||
{ isLiked: initialIsLiked, likeCount: initialLikeCount },
|
||||
(state, optimisticValue: boolean) => {
|
||||
return {
|
||||
isLiked: optimisticValue,
|
||||
likeCount: state.likeCount + (optimisticValue ? 1 : -1),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const [isPending, startTransition] = useTransition()
|
||||
|
||||
const handleLike: MouseEventHandler<HTMLButtonElement> = async (e) => {
|
||||
e.stopPropagation() // Prevent click event from bubbling up which leads to navigation to /code/:id
|
||||
if (!userId) return
|
||||
|
||||
startTransition(async () => {
|
||||
const newLikeState = !isLiked
|
||||
try {
|
||||
optimisticUpdateLike(newLikeState)
|
||||
await toggleLike(sandboxId, userId)
|
||||
} catch (error) {
|
||||
console.log("error", error)
|
||||
optimisticUpdateLike(!newLikeState)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={!userId || isPending}
|
||||
onClick={handleLike}
|
||||
className="gap-1 px-1 rounded-full"
|
||||
>
|
||||
<Heart
|
||||
className={cn("size-4", isLiked ? "stroke-red-500 fill-red-500" : "")}
|
||||
/>
|
||||
<span className="text-xs">{likeCount}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
function ProjectCardComponent({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
visibility,
|
||||
createdAt,
|
||||
likeCount,
|
||||
viewCount,
|
||||
...props
|
||||
}: ProjectCardProps) {
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const projectIcon = useMemo(
|
||||
() =>
|
||||
projectTemplates.find((p) => p.id === type)?.icon ??
|
||||
"/project-icons/node.svg",
|
||||
[type]
|
||||
)
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (props.isAuthenticated) {
|
||||
props.onVisibilityChange({
|
||||
id,
|
||||
name,
|
||||
visibility,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
if (props.isAuthenticated) {
|
||||
props.onDelete({
|
||||
id,
|
||||
name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
tabIndex={0}
|
||||
onClick={() => router.push(`/code/${sandbox.id}`)}
|
||||
onClick={() => router.push(`/code/${id}`)}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
className={`group/canvas-card p-4 h-48 flex flex-col justify-between items-start hover:border-muted-foreground/50 relative overflow-hidden transition-all`}
|
||||
className={`
|
||||
group/canvas-card p-4 h-48 flex flex-col justify-between items-start
|
||||
hover:border-muted-foreground/50 relative overflow-hidden transition-all
|
||||
${
|
||||
props.isAuthenticated && props.deletingId === id
|
||||
? "opacity-50 pointer-events-none cursor-events-none"
|
||||
: "cursor-pointer"
|
||||
}
|
||||
`}
|
||||
>
|
||||
<AnimatePresence>
|
||||
{hovered && (
|
||||
@ -62,38 +238,64 @@ export default function ProjectCard({
|
||||
animate={{ opacity: 1 }}
|
||||
className="h-full w-full absolute inset-0"
|
||||
>
|
||||
{children}
|
||||
<CanvasRevealEffect
|
||||
animationSpeed={3}
|
||||
containerClassName="bg-black"
|
||||
colors={colors[type]}
|
||||
dotSize={2}
|
||||
/>
|
||||
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<div className="space-x-2 flex items-center justify-start w-full z-10">
|
||||
<Image alt="" src={projectIcon} width={20} height={20} />
|
||||
<div className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden">
|
||||
{sandbox.name}
|
||||
</div>
|
||||
<ProjectCardDropdown
|
||||
sandbox={sandbox}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
onDelete={onDelete}
|
||||
<Image
|
||||
alt={`${type} project icon`}
|
||||
src={projectIcon}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
<Link
|
||||
href={`/code/${id}`}
|
||||
className="font-medium static whitespace-nowrap w-full text-ellipsis overflow-hidden before:content-[''] before:absolute before:z-0 before:top-0 before:left-0 before:w-full before:h-full before:rounded-xl"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
{props.isAuthenticated && (
|
||||
<ProjectCardDropdown
|
||||
onVisibilityChange={handleVisibilityChange}
|
||||
onDelete={handleDelete}
|
||||
visibility={visibility}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col text-muted-foreground space-y-0.5 text-sm z-10">
|
||||
<div className="flex items-center">
|
||||
{sandbox.visibility === "private" ? (
|
||||
<>
|
||||
<Lock className="w-3 h-3 mr-2" /> Private
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Globe className="w-3 h-3 mr-2" /> Public
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Clock className="w-3 h-3 mr-2" /> {date}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectMetadata
|
||||
visibility={visibility}
|
||||
createdAt={createdAt}
|
||||
likeCount={likeCount}
|
||||
viewCount={viewCount}
|
||||
id={id}
|
||||
liked={props.liked}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
ProjectCardComponent.displayName = "ProjectCard"
|
||||
|
||||
const ProjectCard = memo(ProjectCardComponent)
|
||||
|
||||
export default ProjectCard
|
||||
|
||||
const colors: { [key: string]: number[][] } = {
|
||||
react: [
|
||||
[71, 207, 237],
|
||||
[30, 126, 148],
|
||||
],
|
||||
node: [
|
||||
[86, 184, 72],
|
||||
[59, 112, 52],
|
||||
],
|
||||
}
|
||||
|
@ -2,11 +2,9 @@
|
||||
|
||||
import { deleteSandbox, updateSandbox } from "@/lib/actions"
|
||||
import { Sandbox } from "@/lib/types"
|
||||
import Link from "next/link"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
import ProjectCard from "./projectCard"
|
||||
import { CanvasRevealEffect } from "./projectCard/revealEffect"
|
||||
|
||||
const colors: { [key: string]: number[][] } = {
|
||||
react: [
|
||||
@ -28,11 +26,27 @@ export default function DashboardProjects({
|
||||
}) {
|
||||
const [deletingId, setDeletingId] = useState<string>("")
|
||||
|
||||
const onDelete = async (sandbox: Sandbox) => {
|
||||
const onVisibilityChange = useMemo(
|
||||
() => async (sandbox: Pick<Sandbox, "id" | "name" | "visibility">) => {
|
||||
const newVisibility =
|
||||
sandbox.visibility === "public" ? "private" : "public"
|
||||
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
|
||||
await updateSandbox({
|
||||
id: sandbox.id,
|
||||
visibility: newVisibility,
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const onDelete = useMemo(
|
||||
() => async (sandbox: Pick<Sandbox, "id" | "name">) => {
|
||||
setDeletingId(sandbox.id)
|
||||
toast(`Project ${sandbox.name} deleted.`)
|
||||
await deleteSandbox(sandbox.id)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (deletingId) {
|
||||
@ -40,15 +54,6 @@ export default function DashboardProjects({
|
||||
}
|
||||
}, [sandboxes])
|
||||
|
||||
const onVisibilityChange = async (sandbox: Sandbox) => {
|
||||
const newVisibility = sandbox.visibility === "public" ? "private" : "public"
|
||||
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
|
||||
await updateSandbox({
|
||||
id: sandbox.id,
|
||||
visibility: newVisibility,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grow p-4 flex flex-col">
|
||||
<div className="text-xl font-medium mb-8">
|
||||
@ -64,30 +69,14 @@ export default function DashboardProjects({
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
key={sandbox.id}
|
||||
href={`/code/${sandbox.id}`}
|
||||
className={`${
|
||||
deletingId === sandbox.id
|
||||
? "pointer-events-none opacity-50 cursor-events-none"
|
||||
: "cursor-pointer"
|
||||
} transition-all focus-visible:outline-none focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:ring-2 focus-visible:ring-ring rounded-lg`}
|
||||
>
|
||||
<ProjectCard
|
||||
sandbox={sandbox}
|
||||
key={sandbox.id}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
onDelete={onDelete}
|
||||
deletingId={deletingId}
|
||||
>
|
||||
<CanvasRevealEffect
|
||||
animationSpeed={3}
|
||||
containerClassName="bg-black"
|
||||
colors={colors[sandbox.type]}
|
||||
dotSize={2}
|
||||
isAuthenticated
|
||||
{...sandbox}
|
||||
/>
|
||||
<div className="absolute inset-0 [mask-image:radial-gradient(400px_at_center,white,transparent)] bg-background/75" />
|
||||
</ProjectCard>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
477
frontend/components/profile/index.tsx
Normal file
477
frontend/components/profile/index.tsx
Normal file
@ -0,0 +1,477 @@
|
||||
"use client"
|
||||
|
||||
import NewProjectModal from "@/components/dashboard/newProject"
|
||||
import ProjectCard from "@/components/dashboard/projectCard/"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||||
import { deleteSandbox, updateSandbox, updateUser } from "@/lib/actions"
|
||||
import { MAX_FREE_GENERATION } from "@/lib/constant"
|
||||
import { SandboxWithLiked, User } from "@/lib/types"
|
||||
import { useUser } from "@clerk/nextjs"
|
||||
import {
|
||||
Edit,
|
||||
Heart,
|
||||
Info,
|
||||
Loader2,
|
||||
LucideIcon,
|
||||
Package2,
|
||||
PlusCircle,
|
||||
Sparkles,
|
||||
X,
|
||||
} from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useFormState, useFormStatus } from "react-dom"
|
||||
import { toast } from "sonner"
|
||||
import Avatar from "../ui/avatar"
|
||||
import { Badge } from "../ui/badge"
|
||||
import { Input } from "../ui/input"
|
||||
import { Progress } from "../ui/progress"
|
||||
|
||||
// #region Profile Page
|
||||
export default function ProfilePage({
|
||||
publicSandboxes,
|
||||
privateSandboxes,
|
||||
profileOwner,
|
||||
loggedInUser,
|
||||
}: {
|
||||
publicSandboxes: SandboxWithLiked[]
|
||||
privateSandboxes: SandboxWithLiked[]
|
||||
profileOwner: User
|
||||
loggedInUser: User | null
|
||||
}) {
|
||||
const isOwnProfile = profileOwner.id === loggedInUser?.id
|
||||
|
||||
const sandboxes = useMemo(() => {
|
||||
const allSandboxes = isOwnProfile
|
||||
? [...publicSandboxes, ...privateSandboxes]
|
||||
: publicSandboxes
|
||||
|
||||
return allSandboxes
|
||||
}, [isOwnProfile, publicSandboxes, privateSandboxes])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto p-6 grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="md:col-span-1">
|
||||
<ProfileCard
|
||||
name={profileOwner.name}
|
||||
username={profileOwner.username}
|
||||
avatarUrl={profileOwner.avatarUrl}
|
||||
sandboxes={sandboxes}
|
||||
joinedDate={profileOwner.createdAt}
|
||||
generations={isOwnProfile ? loggedInUser.generations : undefined}
|
||||
isOwnProfile={isOwnProfile}
|
||||
/>
|
||||
</div>
|
||||
<div className="md:col-span-2">
|
||||
<SandboxesPanel
|
||||
{...{
|
||||
publicSandboxes,
|
||||
privateSandboxes,
|
||||
isOwnProfile,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Profile Card
|
||||
function ProfileCard({
|
||||
name,
|
||||
username,
|
||||
avatarUrl,
|
||||
sandboxes,
|
||||
joinedDate,
|
||||
generations,
|
||||
isOwnProfile,
|
||||
}: {
|
||||
name: string
|
||||
username: string
|
||||
avatarUrl: string | null
|
||||
sandboxes: SandboxWithLiked[]
|
||||
joinedDate: Date
|
||||
generations?: number
|
||||
isOwnProfile: boolean
|
||||
}) {
|
||||
const { user } = useUser()
|
||||
const router = useRouter()
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [formState, formAction] = useFormState(updateUser, {})
|
||||
const joinedAt = useMemo(() => {
|
||||
const date = new Date(joinedDate).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})
|
||||
return `Joined ${date}`
|
||||
}, [joinedDate])
|
||||
const toggleEdit = useCallback(() => {
|
||||
setIsEditing((s) => !s)
|
||||
}, [])
|
||||
const stats = useMemo(() => {
|
||||
const totalSandboxes = sandboxes.length
|
||||
const totalLikes = sandboxes.reduce(
|
||||
(sum, sandbox) => sum + sandbox.likeCount,
|
||||
0
|
||||
)
|
||||
|
||||
return {
|
||||
sandboxes:
|
||||
totalSandboxes === 1 ? "1 sandbox" : `${totalSandboxes} sandboxes`,
|
||||
likes: totalLikes === 1 ? "1 like" : `${totalLikes} likes`,
|
||||
}
|
||||
}, [sandboxes])
|
||||
|
||||
useEffect(() => {
|
||||
if ("message" in formState) {
|
||||
toast.success(formState.message as String)
|
||||
toggleEdit()
|
||||
if ("newRoute" in formState && typeof formState.newRoute === "string") {
|
||||
router.replace(formState.newRoute)
|
||||
}
|
||||
}
|
||||
if ("error" in formState) {
|
||||
const error = formState.error
|
||||
if (typeof error === "string") {
|
||||
toast.error(error)
|
||||
} else {
|
||||
toast.error("An Error Occured")
|
||||
}
|
||||
}
|
||||
}, [formState])
|
||||
return (
|
||||
<Card className="mb-6 md:mb-0 sticky top-6">
|
||||
{isOwnProfile && (
|
||||
<Button
|
||||
onClick={toggleEdit}
|
||||
aria-label={isEditing ? "close edit form" : "open edit form"}
|
||||
size="smIcon"
|
||||
variant="secondary"
|
||||
className="rounded-full absolute top-2 right-2"
|
||||
>
|
||||
{isEditing ? <X className="size-4" /> : <Edit className="size-4" />}
|
||||
</Button>
|
||||
)}
|
||||
<CardContent className="flex flex-col gap-4 items-center pt-6">
|
||||
<Avatar name={name} avatarUrl={avatarUrl} className="size-36" />
|
||||
|
||||
{!isEditing ? (
|
||||
<div className="space-y-2">
|
||||
<CardTitle className="text-2xl text-center">{name}</CardTitle>
|
||||
<CardDescription className="text-center">{`@${username}`}</CardDescription>
|
||||
</div>
|
||||
) : (
|
||||
<form action={formAction} className="flex flex-col gap-2">
|
||||
<Input
|
||||
name="id"
|
||||
placeholder="ID"
|
||||
className="hidden "
|
||||
value={user?.id}
|
||||
/>
|
||||
<Input
|
||||
name="oldUsername"
|
||||
placeholder="ID"
|
||||
className="hidden "
|
||||
value={user?.username ?? undefined}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="input-name">Name</Label>
|
||||
<Input
|
||||
id="input-name"
|
||||
name="name"
|
||||
placeholder="Name"
|
||||
defaultValue={name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="input-username">User name</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id="input-username"
|
||||
className="peer ps-6"
|
||||
type="text"
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
defaultValue={username}
|
||||
/>
|
||||
<span className="pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-2 text-sm text-muted-foreground peer-disabled:opacity-50">
|
||||
@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SubmitButton />
|
||||
</form>
|
||||
)}
|
||||
{!isEditing && (
|
||||
<>
|
||||
<div className="flex gap-6">
|
||||
<StatsItem icon={Package2} label={stats.sandboxes} />
|
||||
<StatsItem icon={Heart} label={stats.likes} />
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<p className="text-xs text-muted-foreground">{joinedAt}</p>
|
||||
{typeof generations === "number" && (
|
||||
<SubscriptionBadge generations={generations} />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<Button size="sm" type="submit" className="w-full mt-2" disabled={pending}>
|
||||
{pending && <Loader2 className="animate-spin mr-2 h-4 w-4" />}
|
||||
Save
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Sandboxes Panel
|
||||
function SandboxesPanel({
|
||||
publicSandboxes,
|
||||
privateSandboxes,
|
||||
isOwnProfile,
|
||||
}: {
|
||||
publicSandboxes: SandboxWithLiked[]
|
||||
privateSandboxes: SandboxWithLiked[]
|
||||
isOwnProfile: boolean
|
||||
}) {
|
||||
const [deletingId, setDeletingId] = useState<string>("")
|
||||
const hasPublicSandboxes = publicSandboxes.length > 0
|
||||
const hasPrivateSandboxes = privateSandboxes.length > 0
|
||||
|
||||
const onVisibilityChange = useMemo(
|
||||
() =>
|
||||
async (sandbox: Pick<SandboxWithLiked, "id" | "name" | "visibility">) => {
|
||||
const newVisibility =
|
||||
sandbox.visibility === "public" ? "private" : "public"
|
||||
toast(`Project ${sandbox.name} is now ${newVisibility}.`)
|
||||
await updateSandbox({
|
||||
id: sandbox.id,
|
||||
visibility: newVisibility,
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const onDelete = useMemo(
|
||||
() => async (sandbox: Pick<SandboxWithLiked, "id" | "name">) => {
|
||||
setDeletingId(sandbox.id)
|
||||
toast(`Project ${sandbox.name} deleted.`)
|
||||
await deleteSandbox(sandbox.id)
|
||||
setDeletingId("")
|
||||
},
|
||||
[]
|
||||
)
|
||||
if (!isOwnProfile) {
|
||||
return (
|
||||
<div className="">
|
||||
{hasPublicSandboxes ? (
|
||||
<>
|
||||
<h2 className="font-semibold text-xl mb-4">Sandboxes</h2>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{publicSandboxes.map((sandbox) => {
|
||||
return (
|
||||
<Fragment key={sandbox.id}>
|
||||
{isOwnProfile ? (
|
||||
<ProjectCard
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
onDelete={onDelete}
|
||||
deletingId={deletingId}
|
||||
isAuthenticated
|
||||
{...sandbox}
|
||||
/>
|
||||
) : (
|
||||
<ProjectCard isAuthenticated={false} {...sandbox} />
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<EmptyState type="private" isOwnProfile={isOwnProfile} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Tabs defaultValue="public">
|
||||
<TabsList className="mb-4">
|
||||
<TabsTrigger value="public">Public</TabsTrigger>
|
||||
<TabsTrigger value="private">Private</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="public">
|
||||
{hasPublicSandboxes ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{publicSandboxes.map((sandbox) => {
|
||||
return (
|
||||
<Fragment key={sandbox.id}>
|
||||
{isOwnProfile ? (
|
||||
<ProjectCard
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
onDelete={onDelete}
|
||||
deletingId={deletingId}
|
||||
isAuthenticated
|
||||
{...sandbox}
|
||||
/>
|
||||
) : (
|
||||
<ProjectCard isAuthenticated={false} {...sandbox} />
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState type="public" isOwnProfile={isOwnProfile} />
|
||||
)}
|
||||
</TabsContent>
|
||||
<TabsContent value="private">
|
||||
{hasPrivateSandboxes ? (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{privateSandboxes.map((sandbox) => (
|
||||
<ProjectCard
|
||||
key={sandbox.id}
|
||||
onVisibilityChange={onVisibilityChange}
|
||||
onDelete={onDelete}
|
||||
deletingId={deletingId}
|
||||
isAuthenticated
|
||||
{...sandbox}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState type="private" isOwnProfile={isOwnProfile} />
|
||||
)}
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region Empty State
|
||||
function EmptyState({
|
||||
type,
|
||||
isOwnProfile,
|
||||
}: {
|
||||
type: "public" | "private"
|
||||
isOwnProfile: boolean
|
||||
}) {
|
||||
const [newProjectModalOpen, setNewProjectModalOpen] = useState(false)
|
||||
|
||||
const text = useMemo(() => {
|
||||
let title: string
|
||||
let description: string
|
||||
switch (type) {
|
||||
case "public":
|
||||
title = "No public sandboxes yet"
|
||||
description = isOwnProfile
|
||||
? "Create your first public sandbox to share your work with the world!"
|
||||
: "user has no public sandboxes"
|
||||
|
||||
case "private":
|
||||
title = "No private sandboxes yet"
|
||||
description = isOwnProfile
|
||||
? "Create your first private sandbox to start working on your personal projects!"
|
||||
: "user has no private sandboxes"
|
||||
}
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
}
|
||||
}, [type, isOwnProfile])
|
||||
const openModal = useCallback(() => setNewProjectModalOpen(true), [])
|
||||
return (
|
||||
<>
|
||||
<Card className="flex flex-col items-center justify-center p-6 text-center h-[300px]">
|
||||
<PlusCircle className="h-12 w-12 text-muted-foreground mb-4" />
|
||||
<CardTitle className="text-xl mb-2">{text.title}</CardTitle>
|
||||
<CardDescription className="mb-4">{text.description}</CardDescription>
|
||||
{isOwnProfile && (
|
||||
<Button onClick={openModal}>
|
||||
<PlusCircle className="h-4 w-4 mr-2" />
|
||||
Create Sandbox
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
<NewProjectModal
|
||||
open={newProjectModalOpen}
|
||||
setOpen={setNewProjectModalOpen}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
// #endregion
|
||||
|
||||
// #region StatsItem
|
||||
interface StatsItemProps {
|
||||
icon: LucideIcon
|
||||
label: string
|
||||
}
|
||||
|
||||
const StatsItem = ({ icon: Icon, label }: StatsItemProps) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon size={18} />
|
||||
<span className="text-sm text-muted-foreground">{label}</span>
|
||||
</div>
|
||||
)
|
||||
// #endregion
|
||||
|
||||
// #region Sub Badge
|
||||
const SubscriptionBadge = ({ generations }: { generations: number }) => {
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Badge variant="secondary" className="text-sm cursor-pointer">
|
||||
Free
|
||||
</Badge>
|
||||
<HoverCard>
|
||||
<HoverCardTrigger>
|
||||
<Button variant="ghost" size="smIcon">
|
||||
<Info size={20} />
|
||||
</Button>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent>
|
||||
<div className="w-full space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="font-medium">AI Generations</span>
|
||||
<span>{`${generations} / ${MAX_FREE_GENERATION}`}</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={generations}
|
||||
max={MAX_FREE_GENERATION}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" className="w-full mt-4">
|
||||
<Sparkles className="mr-2 h-4 w-4" /> Upgrade to Pro
|
||||
</Button>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
// #endregion
|
33
frontend/components/profile/navbar.tsx
Normal file
33
frontend/components/profile/navbar.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import Logo from "@/assets/logo.svg"
|
||||
import { ThemeSwitcher } from "@/components/ui/theme-switcher"
|
||||
import UserButton from "@/components/ui/userButton"
|
||||
import { User } from "@/lib/types"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { Button } from "../ui/button"
|
||||
|
||||
export default function ProfileNavbar({ userData }: { userData: User }) {
|
||||
return (
|
||||
<nav className=" py-2 px-4 w-full flex items-center justify-between border-b border-border">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Link
|
||||
href="/"
|
||||
className="ring-offset-2 ring-offset-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none rounded-sm"
|
||||
>
|
||||
<Image src={Logo} alt="Logo" width={36} height={36} />
|
||||
</Link>
|
||||
<div className="text-sm font-medium flex items-center">Sandbox</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<ThemeSwitcher />
|
||||
{Boolean(userData?.id) ? (
|
||||
<UserButton userData={userData} />
|
||||
) : (
|
||||
<Link href="/sign-in">
|
||||
<Button>Login</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
import Image from "next/image"
|
||||
|
||||
export default function Avatar({
|
||||
name,
|
||||
@ -22,12 +21,12 @@ export default function Avatar({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
className,
|
||||
"w-9 h-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium"
|
||||
"size-9 font-mono rounded-full overflow-hidden bg-gradient-to-t from-neutral-800 to-neutral-600 flex items-center justify-center text-sm font-medium",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{avatarUrl ? (
|
||||
<Image
|
||||
<img
|
||||
src={avatarUrl}
|
||||
alt={name || "User"}
|
||||
width={20}
|
||||
|
36
frontend/components/ui/badge.tsx
Normal file
36
frontend/components/ui/badge.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
@ -5,7 +5,7 @@ import * as React from "react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition active:scale-[0.99] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
|
29
frontend/components/ui/hover-card.tsx
Normal file
29
frontend/components/ui/hover-card.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as HoverCardPrimitive from "@radix-ui/react-hover-card"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const HoverCard = HoverCardPrimitive.Root
|
||||
|
||||
const HoverCardTrigger = HoverCardPrimitive.Trigger
|
||||
|
||||
const HoverCardContent = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<HoverCardPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName
|
||||
|
||||
export { HoverCard, HoverCardTrigger, HoverCardContent }
|
@ -18,7 +18,7 @@ const Progress = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
className="h-full w-full flex-1 bg-primary transition-all rounded-full"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
|
55
frontend/components/ui/tabs.tsx
Normal file
55
frontend/components/ui/tabs.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
@ -17,7 +17,7 @@ export function ThemeSwitcher() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="icon" className="text-muted-foreground">
|
||||
<Button variant="outline" size="icon" className="text-foreground">
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
|
@ -7,9 +7,18 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu"
|
||||
import { MAX_FREE_GENERATION } from "@/lib/constant"
|
||||
import { User } from "@/lib/types"
|
||||
import { useClerk } from "@clerk/nextjs"
|
||||
import { Crown, LogOut, Sparkles } from "lucide-react"
|
||||
import {
|
||||
Crown,
|
||||
LayoutDashboard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
User as UserIcon,
|
||||
} from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
import Avatar from "./avatar"
|
||||
@ -90,6 +99,23 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
|
||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
||||
<Link href={"/dashboard"}>
|
||||
<LayoutDashboard className="mr-2 size-4" />
|
||||
<span>Dashboard</span>
|
||||
<DropdownMenuSeparator />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer" asChild>
|
||||
<Link href={`/@${userData.username}`}>
|
||||
<UserIcon className="mr-2 size-4" />
|
||||
<span>Profile</span>
|
||||
<DropdownMenuSeparator />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
|
||||
<div className="py-1.5 px-2 w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
@ -110,8 +136,9 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
</div>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<div className="py-1.5 px-2 w-full">
|
||||
<DropdownMenuItem>
|
||||
<Sparkles className="size-4 mr-2 text-indigo-500" />
|
||||
<div className="w-full">
|
||||
<div className="flex items-center justify-between text-sm text-muted-foreground mb-2">
|
||||
<span>AI Usage</span>
|
||||
<span>{userData.generations}/{tierInfo.limit}</span>
|
||||
@ -130,18 +157,18 @@ export default function UserButton({ userData: initialUserData }: { userData: Us
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{/* <DropdownMenuItem className="cursor-pointer">
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
<Pencil className="mr-2 size-4" />
|
||||
<span>Edit Profile</span>
|
||||
</DropdownMenuItem> */}
|
||||
<DropdownMenuItem
|
||||
onClick={() => signOut(() => router.push("/"))}
|
||||
className="!text-destructive cursor-pointer"
|
||||
>
|
||||
<LogOut className="mr-2 h-4 w-4" />
|
||||
<LogOut className="mr-2 size-4" />
|
||||
<span>Log Out</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
@ -1,6 +1,7 @@
|
||||
"use server"
|
||||
|
||||
import { revalidatePath } from "next/cache"
|
||||
import { z } from "zod"
|
||||
|
||||
export async function createSandbox(body: {
|
||||
type: string
|
||||
@ -91,3 +92,95 @@ export async function unshareSandbox(sandboxId: string, userId: string) {
|
||||
|
||||
revalidatePath(`/code/${sandboxId}`)
|
||||
}
|
||||
|
||||
export async function toggleLike(sandboxId: string, userId: string) {
|
||||
await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/sandbox/like`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({ sandboxId, userId }),
|
||||
}
|
||||
)
|
||||
revalidatePath(`/[username]`, "page")
|
||||
revalidatePath(`/dashboard`, "page")
|
||||
}
|
||||
|
||||
const UpdateErrorSchema = z.object({
|
||||
error: z
|
||||
.union([
|
||||
z.string(),
|
||||
z.array(
|
||||
z.object({
|
||||
path: z.array(z.string()),
|
||||
message: z.string(),
|
||||
})
|
||||
),
|
||||
])
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export async function updateUser(prevState: any, formData: FormData) {
|
||||
const data = Object.fromEntries(formData)
|
||||
|
||||
const schema = z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
oldUsername: z.string(),
|
||||
name: z.string(),
|
||||
})
|
||||
console.log(data)
|
||||
|
||||
try {
|
||||
const validatedData = schema.parse(data)
|
||||
|
||||
const changedUsername = validatedData.username !== validatedData.oldUsername
|
||||
const res = await fetch(
|
||||
`${process.env.NEXT_PUBLIC_DATABASE_WORKER_URL}/api/user`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `${process.env.NEXT_PUBLIC_WORKERS_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: validatedData.id,
|
||||
username: data.username ?? undefined,
|
||||
name: data.name ?? undefined,
|
||||
}),
|
||||
}
|
||||
)
|
||||
|
||||
const responseData = await res.json()
|
||||
|
||||
// Validate the response using our error schema
|
||||
const parseResult = UpdateErrorSchema.safeParse(responseData)
|
||||
|
||||
if (!parseResult.success) {
|
||||
return { error: "Unexpected error occurred" }
|
||||
}
|
||||
|
||||
if (parseResult.data.error) {
|
||||
return parseResult.data
|
||||
}
|
||||
|
||||
if (changedUsername) {
|
||||
const newRoute = `/@${validatedData.username}`
|
||||
return { message: "Successfully updated", newRoute }
|
||||
}
|
||||
revalidatePath(`/[username]`, "page")
|
||||
return { message: "Successfully updated" }
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.log(error)
|
||||
return {
|
||||
error: error.errors?.[0].message,
|
||||
}
|
||||
}
|
||||
|
||||
return { error: "An unexpected error occurred" }
|
||||
}
|
||||
}
|
||||
|
1
frontend/lib/constant.ts
Normal file
1
frontend/lib/constant.ts
Normal file
@ -0,0 +1 @@
|
||||
export const MAX_FREE_GENERATION = 1000
|
@ -22,9 +22,13 @@ export type Sandbox = {
|
||||
visibility: "public" | "private"
|
||||
createdAt: Date
|
||||
userId: string
|
||||
likeCount: number
|
||||
viewCount: number
|
||||
usersToSandboxes: UsersToSandboxes[]
|
||||
}
|
||||
|
||||
export type SandboxWithLiked = Sandbox & {
|
||||
liked: boolean
|
||||
}
|
||||
export type UsersToSandboxes = {
|
||||
userId: string
|
||||
sandboxId: string
|
||||
|
749
frontend/package-lock.json
generated
749
frontend/package-lock.json
generated
@ -21,9 +21,11 @@
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-context-menu": "^2.1.5",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.2",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
@ -31,6 +33,7 @@
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@react-three/fiber": "^8.16.6",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.5",
|
||||
@ -1384,6 +1387,126 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.1.tgz",
|
||||
"integrity": "sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
||||
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
|
||||
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
|
||||
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.3.tgz",
|
||||
@ -1623,6 +1746,348 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.2.tgz",
|
||||
"integrity": "sha512-Y5w0qGhysvmqsIy6nQxaPa6mXNKznfoGjOfBgzOjocLxr2XlSjqBMYQQL+FfyogsMuX+m8cZyQGYhJxvxUzO4w==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.1.1",
|
||||
"@radix-ui/react-popper": "1.2.0",
|
||||
"@radix-ui/react-portal": "1.1.2",
|
||||
"@radix-ui/react-presence": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-arrow": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
|
||||
"integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
||||
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-dismissable-layer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz",
|
||||
"integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-escape-keydown": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-popper": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz",
|
||||
"integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==",
|
||||
"dependencies": {
|
||||
"@floating-ui/react-dom": "^2.0.0",
|
||||
"@radix-ui/react-arrow": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0",
|
||||
"@radix-ui/react-use-rect": "1.1.0",
|
||||
"@radix-ui/react-use-size": "1.1.0",
|
||||
"@radix-ui/rect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
||||
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz",
|
||||
"integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
|
||||
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
|
||||
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
|
||||
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-escape-keydown": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
|
||||
"integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
|
||||
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-rect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
|
||||
"integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
|
||||
"dependencies": {
|
||||
"@radix-ui/rect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-use-size": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
|
||||
"integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/rect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
|
||||
"integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-icons": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
|
||||
@ -2231,7 +2696,6 @@
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.0.tgz",
|
||||
"integrity": "sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0"
|
||||
@ -2443,6 +2907,289 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz",
|
||||
"integrity": "sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.1",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-roving-focus": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz",
|
||||
"integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA=="
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz",
|
||||
"integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-slot": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
||||
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-compose-refs": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
|
||||
"integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
|
||||
"integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-direction": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
|
||||
"integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-id": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
|
||||
"integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz",
|
||||
"integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz",
|
||||
"integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz",
|
||||
"integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.0",
|
||||
"@radix-ui/react-collection": "1.1.0",
|
||||
"@radix-ui/react-compose-refs": "1.1.0",
|
||||
"@radix-ui/react-context": "1.1.0",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.0",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz",
|
||||
"integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
|
||||
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
"integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-controllable-state": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
|
||||
"integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
|
||||
"integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tooltip": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.3.tgz",
|
||||
|
@ -22,9 +22,11 @@
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||
"@radix-ui/react-avatar": "^1.1.1",
|
||||
"@radix-ui/react-context-menu": "^2.1.5",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.2",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.1.1",
|
||||
@ -32,6 +34,7 @@
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tabs": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@react-three/fiber": "^8.16.6",
|
||||
"@uiw/codemirror-theme-vscode": "^4.23.5",
|
||||
|
Loading…
x
Reference in New Issue
Block a user