diff --git a/app/(team)/team/accept-invite/route.ts b/app/(team)/team/accept-invite/route.ts
new file mode 100644
index 000000000..9abec8889
--- /dev/null
+++ b/app/(team)/team/accept-invite/route.ts
@@ -0,0 +1,66 @@
+import { cookies } from "next/headers";
+import { NextResponse } from "next/server";
+import { CookieOptions, createServerClient } from "@supabase/ssr";
+
+export async function GET(request: Request) {
+ const { searchParams, origin } = new URL(request.url);
+ const token = searchParams.get("token");
+ const next = searchParams.get("next") ?? "/dashboard";
+
+ if (token) {
+ const cookieStore = cookies();
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name: string) {
+ return cookieStore.get(name)?.value;
+ },
+ set(name: string, value: string, options: CookieOptions) {
+ cookieStore.set({ name, value, ...options });
+ },
+ remove(name: string, options: CookieOptions) {
+ cookieStore.delete({ name, ...options });
+ },
+ },
+ },
+ );
+
+ const {
+ data: { session },
+ } = await supabase.auth.getSession();
+
+ if (!session) {
+ return NextResponse.redirect(
+ `${origin}/signin?next=/team/accept-invite?token=${token}`,
+ );
+ }
+
+ try {
+ const response = await fetch(
+ `${process.env.PEARAI_SERVER_URL}/team/accept-invite`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${session.access_token}`,
+ },
+ body: JSON.stringify({ token }),
+ },
+ );
+
+ if (!response.ok) {
+ throw new Error("Failed to accept invitation");
+ }
+
+ return NextResponse.redirect(`${origin}${next}`);
+ } catch (error) {
+ return NextResponse.redirect(
+ `${origin}/team/invite-error?error=accept-failed`,
+ );
+ }
+ }
+
+ return NextResponse.redirect(`${origin}/team/invite-error?error=no-token`);
+}
diff --git a/app/api/create-checkout-session/route.ts b/app/api/create-checkout-session/route.ts
index 86b472c59..e5fb3ee0d 100644
--- a/app/api/create-checkout-session/route.ts
+++ b/app/api/create-checkout-session/route.ts
@@ -10,7 +10,7 @@ async function createCheckoutSession(request: NextRequest & { user: User }) {
const supabase = createClient();
try {
- const { priceId } = await request.json();
+ const { priceId, teamName } = await request.json();
const {
data: { session },
} = await supabase.auth.getSession();
@@ -65,7 +65,7 @@ async function createCheckoutSession(request: NextRequest & { user: User }) {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
- body: JSON.stringify({ priceId }),
+ body: JSON.stringify({ priceId, teamName }),
});
if (!response.ok) {
diff --git a/app/api/get-requests-usage/route.ts b/app/api/get-requests-usage/route.ts
index d0b4e7402..2e8e6367d 100644
--- a/app/api/get-requests-usage/route.ts
+++ b/app/api/get-requests-usage/route.ts
@@ -3,22 +3,37 @@ import { withAuth } from "@/utils/withAuth";
import { createClient } from "@/utils/supabase/server";
const getRequestsUsage = async (request: NextRequest) => {
+ console.log("Starting getRequestsUsage function");
const supabase = createClient();
try {
+ console.log("Attempting to get session");
const {
data: { session },
} = await supabase.auth.getSession();
if (!session) {
+ console.log("No session found");
return NextResponse.json(
{ error: "Failed to get session" },
{ status: 401 },
);
}
+ console.log("Session found, extracting token and user metadata");
const token = session.access_token;
- const res = await fetch(`${process.env.PEARAI_SERVER_URL}/get-usage`, {
+ // const isTeamOwner = session.user.user_metadata.is_team_owner;
+ const isTeamOwner = false; // TODO: fix this
+
+ let endpoint = `${process.env.PEARAI_SERVER_URL}/user-quota-usage`;
+ if (isTeamOwner) {
+ console.log("User is team owner, using team quota endpoint");
+ endpoint = `${process.env.PEARAI_SERVER_URL}/team-quota-usage`;
+ }
+ console.log(`Using endpoint: ${endpoint}`);
+
+ console.log("Sending request to server");
+ const res = await fetch(endpoint, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -27,6 +42,7 @@ const getRequestsUsage = async (request: NextRequest) => {
});
if (!res.ok) {
+ console.log(`Server responded with status: ${res.status}`);
if (res.status === 401) {
return NextResponse.json(
{ error: "Unauthorized. Please log in again." },
@@ -39,9 +55,12 @@ const getRequestsUsage = async (request: NextRequest) => {
);
}
+ console.log("Successfully received data from server");
const data = await res.json();
+ console.log("Data:", data);
return NextResponse.json(data);
} catch (error) {
+ console.error("Error in getRequestsUsage:", error);
return NextResponse.json(
{ error: "Error getting requests usage" },
{ status: 500 },
diff --git a/app/api/team/route.ts b/app/api/team/route.ts
new file mode 100644
index 000000000..415ff6162
--- /dev/null
+++ b/app/api/team/route.ts
@@ -0,0 +1,84 @@
+import { NextRequest, NextResponse } from "next/server";
+import { createClient } from "@/utils/supabase/server";
+
+export async function POST(request: NextRequest) {
+ const supabase = createClient();
+ const {
+ data: { session },
+ } = await supabase.auth.getSession();
+
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { teamId, email, role } = await request.json();
+
+ try {
+ const response = await fetch(
+ `${process.env.PEARAI_SERVER_URL}/team/invite`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${session.access_token}`,
+ },
+ body: JSON.stringify({ team_id: teamId, email, role }),
+ },
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.detail || "Failed to send invitation");
+ }
+
+ const data = await response.json();
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error("Error sending invitation:", error);
+ return NextResponse.json(
+ { error: "Failed to send invitation" },
+ { status: 500 },
+ );
+ }
+}
+
+export async function DELETE(request: NextRequest) {
+ const supabase = createClient();
+ const {
+ data: { session },
+ } = await supabase.auth.getSession();
+
+ if (!session) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const { teamId, memberId } = await request.json();
+
+ try {
+ const response = await fetch(
+ `${process.env.PEARAI_SERVER_URL}/team/member`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${session.access_token}`,
+ },
+ body: JSON.stringify({ team_id: teamId, member_id: memberId }),
+ },
+ );
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.detail || "Failed to delete member");
+ }
+
+ const data = await response.json();
+ return NextResponse.json(data);
+ } catch (error) {
+ console.error("Error deleting member:", error);
+ return NextResponse.json(
+ { error: "Failed to delete member" },
+ { status: 500 },
+ );
+ }
+}
diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx
index b28b49ceb..471e17c5b 100644
--- a/app/dashboard/page.tsx
+++ b/app/dashboard/page.tsx
@@ -1,5 +1,5 @@
import DashboardPage from "@/components/dashboard";
-import { getUserAndSubscription } from "@/lib/data-fetching";
+import { getUserAndSubscription, getTeamData } from "@/lib/data-fetching";
import { redirect } from "next/navigation";
import { constructMetadata } from "@/lib/utils";
import { Metadata } from "next/types";
@@ -22,11 +22,28 @@ export default async function Dashboard() {
return redirect(redirectTo ?? "/signin");
}
+ let team = null;
+ let isTeamOwner = false;
+
+ if (subscription && subscription.team_id) {
+ const { team: fetchedTeam, error } = await getTeamData(
+ subscription.team_id,
+ );
+ if (fetchedTeam) {
+ team = fetchedTeam;
+ isTeamOwner = team.owner_id === user.id;
+ } else if (error) {
+ console.error("Error fetching team data:", error);
+ }
+ }
+
return (
);
}
diff --git a/app/dashboard/team/page.tsx b/app/dashboard/team/page.tsx
new file mode 100644
index 000000000..005cef47e
--- /dev/null
+++ b/app/dashboard/team/page.tsx
@@ -0,0 +1,29 @@
+import EnterpriseDashboard from "@/components/team/enterprise-dashboard";
+import { constructMetadata } from "@/lib/utils";
+import { Metadata } from "next/types";
+import { getUserAndSubscription, getTeamData } from "@/lib/data-fetching";
+import { redirect } from "next/navigation";
+
+export const metadata: Metadata = constructMetadata({
+ title: "Team Dashboard",
+ description: "Manage your team, members, and roles.",
+ canonical: "/team/dashboard",
+});
+
+export default async function EnterpriseDashboardPage() {
+ const { user, subscription } = await getUserAndSubscription();
+
+ if (!user || !subscription || !subscription.team_id) {
+ redirect("/dashboard?error=unauthorized");
+ }
+
+ const { team, error } = await getTeamData(subscription.team_id);
+
+ if (error || !team) {
+ redirect("/dashboard?error=team-not-found");
+ }
+
+ return (
+
+ );
+}
diff --git a/app/team/new-team/page.tsx b/app/team/new-team/page.tsx
new file mode 100644
index 000000000..8fa4e223b
--- /dev/null
+++ b/app/team/new-team/page.tsx
@@ -0,0 +1,45 @@
+import { createClient } from "@/utils/supabase/server";
+import CreateTeamForm from "@/components/team/create-team-form";
+import { redirect } from "next/navigation";
+
+export default async function NewTeamPage({
+ searchParams,
+}: {
+ searchParams: { yearly?: string };
+}) {
+ const supabase = createClient();
+ const {
+ data: { user },
+ } = await supabase.auth.getUser();
+
+ const initialYearly = searchParams.yearly === "true";
+
+ if (!user) {
+ return
Please log in to create a team
;
+ }
+
+ const handleSignOut = async () => {
+ "use server";
+ const supabase = createClient();
+ await supabase.auth.signOut();
+ redirect("/signin");
+ };
+
+ return (
+
+ );
+}
diff --git a/components/dashboard.tsx b/components/dashboard.tsx
index cc28e3369..2b41270bc 100644
--- a/components/dashboard.tsx
+++ b/components/dashboard.tsx
@@ -7,28 +7,43 @@ import { toast } from "sonner";
import ProfileCard from "@/components/dashboard/profile-card";
import SubscriptionCard from "@/components/dashboard/subscription-card";
import FreeTrialCard from "@/components/dashboard/freetrial-card";
+import { Team } from "@/types/team";
type DashboardPageProps = {
subscription: Subscription | null;
openAppQueryParams: string;
user: User;
+ team: Team | null;
+ isTeamOwner: boolean;
};
export type UsageType = {
- percent_credit_used: number | null;
+ chat_usage: {
+ max_quota: number;
+ used_quota: number;
+ quota_remaining: number;
+ };
+ autocomplete_usage: {
+ max_quota: number;
+ used_quota: number;
+ quota_remaining: number;
+ };
+ percent_credit_used: {
+ percent_credit_used: number;
+ };
};
export default function DashboardPage({
subscription,
openAppQueryParams,
user,
+ team,
+ isTeamOwner,
}: DashboardPageProps) {
const searchParams = useSearchParams();
const router = useRouter();
const [loading, setLoading] = useState(true);
- const [usage, setUsage] = useState({
- percent_credit_used: null,
- });
+ const [usage, setUsage] = useState(null);
useEffect(() => {
const handleCallbackForApp = async () => {
@@ -66,8 +81,8 @@ export default function DashboardPage({
toast.error("Failed to fetch requests usage.");
return;
}
- const usage = await response.json();
- setUsage(usage);
+ const usageData: UsageType = await response.json();
+ setUsage(usageData);
} catch (error) {
toast.error(`Error fetching requests usage: ${error}`);
} finally {
@@ -91,7 +106,6 @@ export default function DashboardPage({
- {/* Below commented out until we implement Free Trial */}
{subscription ? (
) : (
PearAI Credits
- {loading ? (
+ {loading || !usage ? (
"-"
) : (
- {usage?.percent_credit_used != null
- ? `${usage.percent_credit_used}%`
+ {usage.percent_credit_used?.percent_credit_used != null
+ ? `${usage.percent_credit_used.percent_credit_used}%`
: "Cannot find used percentage. Please contact PearAI support."}
)}
@@ -53,13 +53,15 @@ export default function FreeTrialCard({
- {loading ? "-" : usage.percent_credit_used}% of free trial PearAI
- Credits used
+ {loading || !usage
+ ? "-"
+ : `${usage.percent_credit_used?.percent_credit_used ?? 0}%`}{" "}
+ of free trial PearAI Credits used
diff --git a/components/dashboard/subscription-card.tsx b/components/dashboard/subscription-card.tsx
index f0e8e366b..69f1a526f 100644
--- a/components/dashboard/subscription-card.tsx
+++ b/components/dashboard/subscription-card.tsx
@@ -1,6 +1,6 @@
import Link from "next/link";
import { Subscription } from "@/types/subscription";
-import { capitalizeInital } from "@/lib/utils";
+import { capitalizeInitial } from "@/lib/utils";
import {
Card,
CardHeader,
@@ -23,15 +23,17 @@ import {
import { useState } from "react";
import { useCancelSubscription } from "@/hooks/useCancelSubscription";
import { User } from "@supabase/supabase-js";
-import { Info } from "lucide-react";
+import { Info, Users } from "lucide-react";
import { UsageType } from "../dashboard";
type SubscriptionCardProps = {
subscription: Subscription | null;
- usage?: UsageType;
+ usage: UsageType | null;
openAppQueryParams?: string;
user: User;
loading: boolean;
+ teamName?: string;
+ isTeamOwner?: boolean;
};
const DEFAULT_OPEN_APP_CALLBACK = "pearai://pearai.pearai/auth";
@@ -42,10 +44,17 @@ export default function SubscriptionCard({
openAppQueryParams,
user,
loading,
+ teamName,
+ isTeamOwner,
}: SubscriptionCardProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false);
const { handleCancelSubscription, isCanceling, isCanceled } =
useCancelSubscription(user, subscription);
+ const percentUsed = usage?.percent_credit_used.percent_credit_used ?? 0;
+ const chatUsed = usage?.chat_usage.used_quota ?? 0;
+ const chatRemaining = usage?.chat_usage.quota_remaining ?? 0;
+ const autocompleteUsed = usage?.autocomplete_usage.used_quota ?? 0;
+ const autocompleteRemaining = usage?.autocomplete_usage.quota_remaining ?? 0;
const handleCancelClick = () => {
if (isCanceled) {
@@ -92,6 +101,8 @@ export default function SubscriptionCard({
);
}
+ const isEnterprise = subscription?.pricing_tier.startsWith("enterprise_");
+
return (
@@ -104,7 +115,9 @@ export default function SubscriptionCard({
variant="secondary"
className="border-primary-800 bg-primary-800/10 px-2 py-1 text-xs text-primary-800"
>
- Pro - {capitalizeInital(subscription.pricing_tier)}
+ {isEnterprise
+ ? `Enterprise - ${subscription.pricing_tier.includes("monthly") ? "Monthly" : "Yearly"}`
+ : `Pro - ${capitalizeInitial(subscription.pricing_tier)}`}
@@ -112,55 +125,67 @@ export default function SubscriptionCard({
{usage && (
-
Requests
+
PearAI Credits
{loading ? (
"-"
) : (
- {usage?.percent_credit_used != null
- ? `${usage.percent_credit_used}%`
+ {usage.percent_credit_used.percent_credit_used != null
+ ? `${usage.percent_credit_used.percent_credit_used}%`
: "Cannot find remaining percentage. Please contact PearAI support."}
)}
{loading
? "-"
- : `${usage.percent_credit_used ?? 0}% of PearAI Credits used`}
+ : `${usage.percent_credit_used.percent_credit_used ?? 0}% of PearAI Credits used`}
)}
-
-
-
Current Plan
-
- {capitalizeInital(subscription.pricing_tier)}
-
-
-
-
-
-
Current Period
-
- {new Date(
- subscription.current_period_start * 1000,
- ).toLocaleDateString()}{" "}
- -{" "}
- {subscription.current_period_end
- ? new Date(
- subscription.current_period_end * 1000,
- ).toLocaleDateString()
- : "Now"}
-
+ {/* {isEnterprise && (
+
+
+
Team Name
+
{teamName || "N/A"}
+
-
+ )} */}
+ {!isEnterprise && (
+ <>
+
+
+
Current Plan
+
+ {capitalizeInitial(subscription.pricing_tier)}
+
+
+
+
+
+
Current Period
+
+ {new Date(
+ subscription.current_period_start * 1000,
+ ).toLocaleDateString()}{" "}
+ -{" "}
+ {subscription.current_period_end
+ ? new Date(
+ subscription.current_period_end * 1000,
+ ).toLocaleDateString()
+ : "Now"}
+
+
+
+ >
+ )}
@@ -172,49 +197,60 @@ export default function SubscriptionCard({
-
-
-
- {isCanceling
- ? "Canceling..."
- : isCanceled
- ? "Subscription canceled, reactivate?"
- : "Cancel Subscription"}
-
-
-
-
- Cancel Subscription
-
- Are you sure you want to cancel your subscription?
- You'll lose access to premium features at the end of
- your current billing period.
-
-
-
- setIsDialogOpen(false)}
- className="mt-2"
- >
- Keep Subscription
-
+ {isEnterprise && isTeamOwner && (
+
+
+ Enterprise Dashboard
+
+
+
+ )}
+
+ {!isEnterprise && (
+
+
- {isCanceling ? "Canceling..." : "Confirm Cancellation"}
+ {isCanceling
+ ? "Canceling..."
+ : isCanceled
+ ? "Subscription canceled, reactivate?"
+ : "Cancel Subscription"}
-
-
-
+
+
+
+ Cancel Subscription
+
+ Are you sure you want to cancel your subscription?
+ You'll lose access to premium features at the end of
+ your current billing period.
+
+
+
+ setIsDialogOpen(false)}
+ className="mt-2"
+ >
+ Keep Subscription
+
+
+ {isCanceling ? "Canceling..." : "Confirm Cancellation"}
+
+
+
+
+ )}
diff --git a/components/pricing.tsx b/components/pricing.tsx
index cc8549555..b1df4cc74 100755
--- a/components/pricing.tsx
+++ b/components/pricing.tsx
@@ -45,6 +45,7 @@ const PricingTier: React.FC = ({
features,
buttonText,
isFree = false,
+ isEnterprise = false,
priceId,
user,
index,
@@ -78,6 +79,11 @@ const PricingTier: React.FC = ({
}
: {};
+ const handleEnterpriseClick = (isYearly: boolean) => {
+ console.log("Create team:", { isYearly });
+ router.push(`/team/new-team?yearly=${isYearly}`);
+ };
+
const handleDownload = async (os_type: string) => {
setIsDownloading(true);
try {
@@ -310,9 +316,16 @@ const PricingTier: React.FC = ({
{!isFree && (
<>
- {disabled ? (
-
- Coming Soon
+ {isEnterprise ? (
+
+ handleEnterpriseClick(
+ title.toLowerCase().includes("yearly"),
+ )
+ }
+ >
+ {buttonText}
) : (
= ({ user }) => {
user={user}
index={index}
priceUnit="/month/user"
- disabled
/>
))}
diff --git a/components/team/create-team-form.tsx b/components/team/create-team-form.tsx
new file mode 100644
index 000000000..926ed087c
--- /dev/null
+++ b/components/team/create-team-form.tsx
@@ -0,0 +1,186 @@
+"use client";
+
+import React from "react";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useCheckout } from "@/hooks/useCheckout";
+import { PRICING_TIERS, STRIPE_PRICE_IDS } from "@/utils/constants";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { Loader2, CreditCard } from "lucide-react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipProvider,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+import { CreateTeamFormData, createTeamSchema } from "@/utils/form-schema";
+import { CreateTeamFormProps } from "@/types/team";
+
+export default function CreateTeamForm({
+ user,
+ initialYearly,
+ handleSignOut,
+}: CreateTeamFormProps) {
+ const { handleCheckout, isSubmitting } = useCheckout(user);
+
+ const form = useForm
({
+ resolver: zodResolver(createTeamSchema),
+ defaultValues: {
+ teamName: "",
+ isYearly: initialYearly,
+ },
+ });
+
+ const isYearly = form.watch("isYearly");
+
+ const tierData = PRICING_TIERS.enterprise.find(
+ (tier) => tier.title.toLowerCase() === (isYearly ? "yearly" : "monthly"),
+ );
+ const price = tierData?.price || "0";
+ const priceId = isYearly
+ ? STRIPE_PRICE_IDS.ENTERPRISE_ANNUAL
+ : STRIPE_PRICE_IDS.ENTERPRISE_MONTHLY;
+
+ const handleSubmit = async (data: CreateTeamFormData) => {
+ try {
+ await handleCheckout(priceId, data.teamName);
+ console.log("Team created successfully! Redirecting to checkout...");
+ } catch (error) {
+ console.error("Checkout failed:", error);
+ }
+ };
+
+ const annualSavings = (
+ parseFloat(price) * 12 -
+ parseFloat(price) * 10
+ ).toFixed(2);
+
+ return (
+ <>
+
+
+
+ Create a Team
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/components/team/enterprise-dashboard.tsx b/components/team/enterprise-dashboard.tsx
new file mode 100644
index 000000000..17522cc58
--- /dev/null
+++ b/components/team/enterprise-dashboard.tsx
@@ -0,0 +1,341 @@
+"use client";
+import React, { useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { PlusIcon, Pencil1Icon, TrashIcon } from "@radix-ui/react-icons";
+import { Badge } from "@/components/ui/badge";
+import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+import { Team, TeamMember, TeamInvite } from "@/types/team";
+import { Subscription } from "@/types/subscription";
+import { User } from "@supabase/auth-js";
+import { capitalizeInitial } from "@/lib/utils";
+import { toast } from "sonner";
+
+type EnterpriseDashboardProps = {
+ team: Team;
+ user: User;
+ subscription: Subscription;
+};
+
+export default function EnterpriseDashboard({
+ team,
+ user,
+ subscription,
+}: EnterpriseDashboardProps) {
+ const [teamName, setTeamName] = useState(team.name);
+ const [isEditingTeamName, setIsEditingTeamName] = useState(false);
+ const [members, setMembers] = useState(team.members);
+ const [invites, setInvites] = useState(team.invites);
+ const [newMember, setNewMember] = useState({
+ email: "",
+ role: "member" as "admin" | "member",
+ });
+ const [editingId, setEditingId] = useState(null);
+ const [teamUsage, setTeamUsage] = useState(null);
+
+ const isOwner = (memberId: string) => {
+ return members.find((m) => m.id === memberId)?.role === "owner";
+ };
+
+ const isCurrentUser = (memberId: string) => {
+ return memberId === user.id;
+ };
+
+ const canEditMember = (memberId: string) => {
+ return !isOwner(memberId) && !isCurrentUser(memberId);
+ };
+
+ const addMember = async () => {
+ try {
+ const response = await fetch("/api/team", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ teamId: team.id,
+ email: newMember.email,
+ role: newMember.role,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to send invitation");
+ }
+
+ const newInvite: TeamInvite = {
+ id: Date.now().toString(), // Temporary ID, should be replaced with the one from the server
+ ...newMember,
+ status: "pending",
+ };
+ setInvites((prevInvites) => [...(prevInvites || []), newInvite]);
+ setNewMember({ email: "", role: "member" });
+ // Show success message to user
+ toast.success("Invitation sent successfully");
+ } catch (error) {
+ console.error("Error sending invitation:", error);
+ // Show error message to user
+ toast.error("Failed to send invitation");
+ }
+ };
+ const updateMember = (id: string) => {
+ // In a real app, this would call an API to update the member
+ setMembers(
+ members.map((member) =>
+ member.id === id
+ ? { ...member, role: newMember.role as "owner" | "admin" | "member" }
+ : member,
+ ),
+ );
+ setEditingId(null);
+ setNewMember({ email: "", role: "member" });
+ };
+
+ const deleteMember = async (id: string) => {
+ try {
+ const response = await fetch("/api/team/member", {
+ method: "DELETE",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ teamId: team.id,
+ memberId: id,
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to delete member");
+ }
+
+ setMembers(members.filter((member) => member.id !== id));
+ toast.success("Member removed successfully");
+ } catch (error) {
+ console.error("Error deleting member:", error);
+ toast.error("Failed to remove member");
+ }
+ };
+
+ const deleteInvite = (id: string) => {
+ // In a real app, this would call an API to cancel the invitation
+ setInvites(invites.filter((invite) => invite.id !== id));
+ };
+ useEffect(() => {
+ const getTeamUsage = async () => {
+ try {
+ const response = await fetch("/api/get-requests-usage", {
+ method: "GET",
+ headers: { "Content-Type": "application/json" },
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch team usage");
+ }
+
+ const usageData = await response.json();
+ setTeamUsage(usageData);
+ } catch (error) {
+ console.error("Error fetching team usage:", error);
+ toast.error("Failed to fetch team usage");
+ }
+ };
+
+ getTeamUsage();
+ }, []);
+ return (
+
+
+
+
+ {isEditingTeamName ? (
+ setTeamName(e.target.value)}
+ className="max-w-sm"
+ />
+ ) : (
+ teamName
+ )}
+ setIsEditingTeamName(!isEditingTeamName)}
+ >
+ {isEditingTeamName ? "Save" : "Edit"}
+
+
+
+
+
+
+ Manage your team members and their roles
+
+
+
+ Enterprise -{" "}
+ {subscription.pricing_tier.includes("monthly")
+ ? "Monthly"
+ : "Yearly"}
+
+
+ Next billing date:{" "}
+ {new Date(
+ subscription.current_period_end * 1000,
+ ).toLocaleDateString()}
+
+
+
+
+
+
+
+
Team Members
+
+
+
+ Name
+ Email
+ Role
+ Actions
+
+
+
+ {members.map((member) => (
+
+ {member.name}
+ {member.email}
+
+ {editingId === member.id ? (
+
+ setNewMember({ ...newMember, role: value })
+ }
+ >
+
+
+
+
+ Admin
+ Member
+
+
+ ) : (
+ capitalizeInitial(member.role)
+ )}
+
+
+ {editingId === member.id ? (
+ updateMember(member.id)}>
+ Save
+
+ ) : (
+ <>
+ {canEditMember(member.id) && (
+ <>
+ {
+ setEditingId(member.id);
+ setNewMember({
+ email: member.email,
+ role: member.role,
+ });
+ }}
+ >
+
+
+ deleteMember(member.id)}
+ >
+
+
+ >
+ )}
+ >
+ )}
+
+
+ ))}
+
+
+
+
+
+
Invite New Member
+
+
+ setNewMember({ ...newMember, email: e.target.value })
+ }
+ />
+
+ setNewMember({ ...newMember, role: value })
+ }
+ >
+
+
+
+
+ Admin
+ Member
+
+
+
+ Invite Member
+
+
+
+ {invites.length > 0 && (
+
+
Pending Invitations
+
+
+
+ Email
+ Role
+ Actions
+
+
+
+ {invites.map((invite) => (
+
+ {invite.email}
+ {invite.role}
+
+ deleteInvite(invite.id)}
+ >
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+ );
+}
diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx
new file mode 100644
index 000000000..416157cc2
--- /dev/null
+++ b/components/ui/alert.tsx
@@ -0,0 +1,60 @@
+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",
+ info: "rounded-md ring-1 ring-gray-400/40 bg-gray-300/10 backdrop-blur-md",
+ destructive:
+ "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ },
+);
+
+const Alert = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps
+>(({ className, variant, ...props }, ref) => (
+
+));
+Alert.displayName = "Alert";
+
+const AlertTitle = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertTitle.displayName = "AlertTitle";
+
+const AlertDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = "AlertDescription";
+
+export { Alert, AlertTitle, AlertDescription };
diff --git a/components/ui/select.tsx b/components/ui/select.tsx
new file mode 100644
index 000000000..9218f68ba
--- /dev/null
+++ b/components/ui/select.tsx
@@ -0,0 +1,160 @@
+"use client";
+
+import * as React from "react";
+import * as SelectPrimitive from "@radix-ui/react-select";
+import { Check, ChevronDown, ChevronUp } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+
+const Select = SelectPrimitive.Root;
+
+const SelectGroup = SelectPrimitive.Group;
+
+const SelectValue = SelectPrimitive.Value;
+
+const SelectTrigger = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+ span]:line-clamp-1",
+ className,
+ )}
+ {...props}
+ >
+ {children}
+
+
+
+
+));
+SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
+
+const SelectScrollUpButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
+
+const SelectScrollDownButton = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+SelectScrollDownButton.displayName =
+ SelectPrimitive.ScrollDownButton.displayName;
+
+const SelectContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, position = "popper", ...props }, ref) => (
+
+
+
+
+ {children}
+
+
+
+
+));
+SelectContent.displayName = SelectPrimitive.Content.displayName;
+
+const SelectLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SelectLabel.displayName = SelectPrimitive.Label.displayName;
+
+const SelectItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+
+ {children}
+
+));
+SelectItem.displayName = SelectPrimitive.Item.displayName;
+
+const SelectSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
+
+export {
+ Select,
+ SelectGroup,
+ SelectValue,
+ SelectTrigger,
+ SelectContent,
+ SelectLabel,
+ SelectItem,
+ SelectSeparator,
+ SelectScrollUpButton,
+ SelectScrollDownButton,
+};
diff --git a/components/ui/table.tsx b/components/ui/table.tsx
new file mode 100644
index 000000000..73cf6b4ad
--- /dev/null
+++ b/components/ui/table.tsx
@@ -0,0 +1,117 @@
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const Table = React.forwardRef<
+ HTMLTableElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+Table.displayName = "Table";
+
+const TableHeader = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableHeader.displayName = "TableHeader";
+
+const TableBody = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableBody.displayName = "TableBody";
+
+const TableFooter = React.forwardRef<
+ HTMLTableSectionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+ tr]:last:border-b-0",
+ className,
+ )}
+ {...props}
+ />
+));
+TableFooter.displayName = "TableFooter";
+
+const TableRow = React.forwardRef<
+ HTMLTableRowElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableRow.displayName = "TableRow";
+
+const TableHead = React.forwardRef<
+ HTMLTableCellElement,
+ React.ThHTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableHead.displayName = "TableHead";
+
+const TableCell = React.forwardRef<
+ HTMLTableCellElement,
+ React.TdHTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCell.displayName = "TableCell";
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+TableCaption.displayName = "TableCaption";
+
+export {
+ Table,
+ TableHeader,
+ TableBody,
+ TableFooter,
+ TableHead,
+ TableRow,
+ TableCell,
+ TableCaption,
+};
diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx
index 20021083f..12406bc9b 100644
--- a/components/ui/tooltip.tsx
+++ b/components/ui/tooltip.tsx
@@ -32,7 +32,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
- "z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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",
+ "z-50 overflow-hidden rounded-md border border-border bg-background px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-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}
diff --git a/hooks/useCheckout.ts b/hooks/useCheckout.ts
index d3ee89d52..51ea8e175 100644
--- a/hooks/useCheckout.ts
+++ b/hooks/useCheckout.ts
@@ -7,7 +7,10 @@ export const useCheckout = (user: User | null) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
- const handleCheckout = async (priceId: string) => {
+ const handleCheckout = async (
+ priceId: string,
+ teamName: string | null = null,
+ ) => {
if (!user) {
toast.error("Please log in to subscribe to this plan.");
router.push("/signin");
@@ -21,7 +24,7 @@ export const useCheckout = (user: User | null) => {
const response = await fetch("/api/create-checkout-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ priceId }),
+ body: JSON.stringify({ priceId, teamName }),
});
if (!response.ok) {
diff --git a/lib/data-fetching.ts b/lib/data-fetching.ts
index c2c0219aa..652b8bd09 100644
--- a/lib/data-fetching.ts
+++ b/lib/data-fetching.ts
@@ -1,6 +1,7 @@
import { Subscription } from "@/types/subscription";
import { createClient } from "@/utils/supabase/server";
import { User } from "@supabase/auth-js";
+import { Team, TeamInvite, TeamMember } from "@/types/team";
type GetUserSubscriptionResult = {
user: User | null;
@@ -9,6 +10,11 @@ type GetUserSubscriptionResult = {
redirect: string | null;
};
+type GetTeamDataResult = {
+ team: Team | null;
+ error: string | null;
+};
+
export async function getUserAndSubscription(): Promise {
const supabase = createClient();
@@ -24,22 +30,65 @@ export async function getUserAndSubscription(): Promise {
+ const supabase = createClient();
+
+ try {
+ // Fetch team data
+ const { data: teamData, error: teamError } = await supabase
+ .from("teams")
+ .select("id, name, owner_id")
+ .eq("id", teamId)
+ .single();
+
+ if (teamError) throw teamError;
+
+ // Fetch active team members
+ const { data: membersData, error: membersError } = await supabase
+ .from("team_members")
+ .select("id, user_id, role")
+ .eq("team_id", teamId);
+
+ if (membersError) throw membersError;
+
+ // Fetch pending invites
+ const { data: invitesData, error: invitesError } = await supabase
+ .from("team_invites")
+ .select("id, email, role")
+ .eq("team_id", teamId)
+ .eq("status", "pending");
+
+ if (invitesError) throw invitesError;
+
+ // Fetch user details for active members
+ const memberPromises = membersData.map(async (member) => {
+ const { data: userData, error: userError } = await supabase
+ .from("profiles")
+ .select("first_name, last_name, email")
+ .eq("user_id", member.user_id)
+ .single();
+
+ if (userError) throw userError;
+
+ const lastName = userData.last_name ?? "";
+ const fullName = `${userData.first_name} ${lastName}`.trim();
+
+ return {
+ id: member.id,
+ name: fullName,
+ email: userData.email,
+ role: member.role as "owner" | "admin" | "member",
+ };
+ });
+
+ const members = await Promise.all(memberPromises);
+
+ const invites: TeamInvite[] = invitesData.map((invite) => ({
+ id: invite.id,
+ email: invite.email,
+ role: invite.role as "admin" | "member",
+ status: "pending",
+ }));
+
+ const team: Team = {
+ ...teamData,
+ members,
+ invites,
+ };
+
+ return { team, error: null };
+ } catch (error) {
+ console.error("Error fetching team data:", error);
+ return { team: null, error: "Failed to fetch team data" };
+ }
+}
diff --git a/lib/utils.ts b/lib/utils.ts
index 122465764..8969c3a2e 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -94,7 +94,7 @@ export const getURL = () => {
return url;
};
-export function capitalizeInital(input: unknown): string | undefined {
+export function capitalizeInitial(input: unknown): string | undefined {
if (typeof input !== "string") {
return "";
}
diff --git a/package.json b/package.json
index 7085caf79..93936d45a 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
+ "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-navigation-menu": "^1.2.0",
"@radix-ui/react-popover": "^1.0.7",
diff --git a/types/pricing.ts b/types/pricing.ts
index 457319086..36947e387 100644
--- a/types/pricing.ts
+++ b/types/pricing.ts
@@ -12,6 +12,7 @@ export interface PricingTierProps {
user: User | null;
index: number;
priceUnit?: string; // Added new field
+ isEnterprise?: boolean; // Added new field
}
export interface PricingPageProps {
diff --git a/types/subscription.ts b/types/subscription.ts
index bb6062816..af9e9a558 100644
--- a/types/subscription.ts
+++ b/types/subscription.ts
@@ -9,4 +9,5 @@ export interface Subscription {
canceled_at?: string;
created_at?: string;
updated_at?: string;
+ team_id?: string;
}
diff --git a/types/team.ts b/types/team.ts
new file mode 100644
index 000000000..924c7c652
--- /dev/null
+++ b/types/team.ts
@@ -0,0 +1,29 @@
+import { User } from "@supabase/supabase-js";
+
+export interface TeamMember {
+ id: string;
+ name: string;
+ email: string;
+ role: "owner" | "admin" | "member";
+}
+
+export interface Team {
+ id: string;
+ name: string;
+ owner_id: string;
+ members: TeamMember[];
+ invites?: TeamInvite[];
+}
+
+export interface TeamInvite {
+ id: string;
+ email: string;
+ role: "admin" | "member";
+ status: "pending";
+}
+
+export interface CreateTeamFormProps {
+ user: User;
+ initialYearly: boolean;
+ handleSignOut: () => Promise;
+}
diff --git a/utils/constants.ts b/utils/constants.ts
index 5032a1c28..56ed99803 100644
--- a/utils/constants.ts
+++ b/utils/constants.ts
@@ -16,11 +16,21 @@ const NEXT_PUBLIC_STRIPE_WAITLIST_PRICE_ID = "price_1PZ9X608N4O93LU5yqMbGDtu";
const NEXT_PUBLIC_STRIPE_WAITLIST_PRICE_ID_TEST =
"price_1PZUT208N4O93LU5jItKoEYu";
const NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID = "price_1PoZiZ08N4O93LU5kCrdrXvI";
+// const NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID_TEST =
+// "price_1Ppa9408N4O93LU5irNxLp5p";
const NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID_TEST =
- "price_1Ppa9408N4O93LU5irNxLp5p";
+ "price_1PaMw2DipRqiTgC40064Io1w";
const NEXT_PUBLIC_STRIPE_ANNUAL_PRICE_ID = "price_1PpZUO08N4O93LU5FYFUyh43";
+// const NEXT_PUBLIC_STRIPE_ANNUAL_PRICE_ID_TEST =
+// "price_1PZUSi08N4O93LU5UVdlkfp2";
const NEXT_PUBLIC_STRIPE_ANNUAL_PRICE_ID_TEST =
- "price_1PZUSi08N4O93LU5UVdlkfp2";
+ "price_1PaMx7DipRqiTgC4vL0KbVm7";
+const NEXT_PUBLIC_STRIPE_ENTERPRISE_MONTHLY_PRICE_ID = "";
+const NEXT_PUBLIC_STRIPE_ENTERPRISE_MONTHLY_PRICE_ID_TEST =
+ "price_1PxJtDDipRqiTgC47WFM22pv";
+const NEXT_PUBLIC_STRIPE_ENTERPRISE_ANNUAL_PRICE_ID = "";
+const NEXT_PUBLIC_STRIPE_ENTERPRISE_ANNUAL_PRICE_ID_TEST =
+ "price_1PxJu0DipRqiTgC4VirNSxfJ";
export const STRIPE_PRICE_IDS = {
WAITLIST: TEST_MODE_ENABLED
@@ -32,6 +42,12 @@ export const STRIPE_PRICE_IDS = {
ANNUAL: TEST_MODE_ENABLED
? NEXT_PUBLIC_STRIPE_ANNUAL_PRICE_ID_TEST
: NEXT_PUBLIC_STRIPE_ANNUAL_PRICE_ID,
+ ENTERPRISE_MONTHLY: TEST_MODE_ENABLED
+ ? NEXT_PUBLIC_STRIPE_ENTERPRISE_MONTHLY_PRICE_ID_TEST
+ : NEXT_PUBLIC_STRIPE_ENTERPRISE_MONTHLY_PRICE_ID,
+ ENTERPRISE_ANNUAL: TEST_MODE_ENABLED
+ ? NEXT_PUBLIC_STRIPE_ENTERPRISE_ANNUAL_PRICE_ID_TEST
+ : NEXT_PUBLIC_STRIPE_ENTERPRISE_ANNUAL_PRICE_ID,
};
export const PRICING_TIERS: {
@@ -94,8 +110,9 @@ export const PRICING_TIERS: {
"Private Discord Channel",
],
buttonText: "Get Started",
- priceId: STRIPE_PRICE_IDS.MONTHLY,
+ priceId: STRIPE_PRICE_IDS.ENTERPRISE_MONTHLY,
index: 0,
+ isEnterprise: true,
},
{
title: "Yearly",
@@ -104,8 +121,9 @@ export const PRICING_TIERS: {
description: "Pay one lump sum yearly for our highest priority tier.",
features: ["Everything from monthly", "Priority Customer Support"],
buttonText: "Get Started",
- priceId: STRIPE_PRICE_IDS.ANNUAL,
+ priceId: STRIPE_PRICE_IDS.ENTERPRISE_ANNUAL,
index: 1,
+ isEnterprise: true,
},
],
};
diff --git a/utils/form-schema.ts b/utils/form-schema.ts
index a4e358957..22cc61e09 100644
--- a/utils/form-schema.ts
+++ b/utils/form-schema.ts
@@ -56,7 +56,16 @@ export const updatePasswordSchema = z
path: ["confirmPassword"],
});
+export const createTeamSchema = z.object({
+ teamName: z
+ .string()
+ .min(3, "Team name must be at least 3 characters long")
+ .max(30, "Team name must be at most 30 characters long"),
+ isYearly: z.boolean().default(false),
+});
+
export type SignUpFormData = z.infer;
export type SignInFormData = z.infer;
export type ResetPasswordFormData = z.infer;
export type UpdatePasswordFormData = z.infer;
+export type CreateTeamFormData = z.infer;
diff --git a/yarn.lock b/yarn.lock
index 2f206b7bf..8707f9142 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1950,6 +1950,11 @@
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
+"@radix-ui/react-icons@^1.3.0":
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.3.0.tgz#c61af8f323d87682c5ca76b856d60c2312dbcb69"
+ integrity sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==
+
"@radix-ui/react-id@1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
@@ -2093,7 +2098,7 @@
"@radix-ui/react-select@^2.1.1":
version "2.1.1"
- resolved "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz#df05cb0b29d3deaef83b505917c4042e0e418a9f"
+ resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.1.1.tgz#df05cb0b29d3deaef83b505917c4042e0e418a9f"
integrity sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==
dependencies:
"@radix-ui/number" "1.1.0"