diff --git a/src/core/session.rs b/src/core/session.rs index a933d7f..6e90d42 100644 --- a/src/core/session.rs +++ b/src/core/session.rs @@ -7,6 +7,7 @@ use crate::{ }, }; use bson::{doc, DateTime}; +use woothee::parser::Parser; use futures::StreamExt; use mongodb::{Client, Collection}; use serde::{Deserialize, Serialize}; @@ -22,6 +23,12 @@ pub struct Session { pub id_token: String, pub refresh_token: String, pub user_agent: String, + pub os: String, + pub os_version: String, + pub vendor: String, + pub device: String, + pub browser: String, + pub browser_version: String, pub is_revoked: bool, pub created_at: DateTime, pub updated_at: DateTime, @@ -39,6 +46,33 @@ impl Session { Err(_) => "".to_string(), }; + let parser = Parser::new(); + + let user_agent_data = parser.parse(user_agent); + + let os = user_agent_data.as_ref().map_or_else(String::new, |result| result.os.to_string()); + + let os_version = user_agent_data + .as_ref() + .map_or_else(String::new, |result| result.os_version.to_string()); + + let vendor = user_agent_data + .as_ref() + .map_or_else(String::new, |result| result.vendor.to_string()); + + let device = user_agent_data + .as_ref() + .map_or_else(String::new, |result| result.category.to_string()); + + let browser = user_agent_data + .as_ref() + .map_or_else(String::new, |result| result.name.to_string()); + + let browser_version = user_agent_data + .as_ref() + .map_or_else(String::new, |result| result.version.to_string()); + + Self { uid: user.uid.to_string(), session_id: Uuid::new_v4().to_string(), @@ -46,6 +80,12 @@ impl Session { id_token, refresh_token, user_agent: user_agent.to_string(), + os, + os_version, + vendor, + device, + browser, + browser_version, is_revoked: false, created_at: DateTime::now(), updated_at: DateTime::now(), @@ -297,6 +337,12 @@ impl Session { session_id: decrypted_session.session_id, email: decrypted_session.email, user_agent: decrypted_session.user_agent, + os: decrypted_session.os, + os_version: decrypted_session.os_version, + vendor: decrypted_session.vendor, + device: decrypted_session.device, + browser: decrypted_session.browser, + browser_version: decrypted_session.browser_version, is_revoked: decrypted_session.is_revoked, created_at: decrypted_session.created_at, updated_at: decrypted_session.updated_at, @@ -396,6 +442,12 @@ impl Session { session_id: decrypted_session.session_id, email: decrypted_session.email, user_agent: decrypted_session.user_agent, + os: decrypted_session.os, + os_version: decrypted_session.os_version, + vendor: decrypted_session.vendor, + device: decrypted_session.device, + browser: decrypted_session.browser, + browser_version: decrypted_session.browser_version, is_revoked: decrypted_session.is_revoked, created_at: decrypted_session.created_at, updated_at: decrypted_session.updated_at, @@ -452,6 +504,12 @@ impl Session { session_id: data.session_id, email: data.email, user_agent: data.user_agent, + os: data.os, + os_version: data.os_version, + vendor: data.vendor, + device: data.device, + browser: data.browser, + browser_version: data.browser_version, is_revoked: data.is_revoked, created_at: data.created_at, updated_at: data.updated_at, diff --git a/src/handlers/overview_handler.rs b/src/handlers/overview_handler.rs index b8a1936..415ae73 100644 --- a/src/handlers/overview_handler.rs +++ b/src/handlers/overview_handler.rs @@ -2,7 +2,6 @@ use axum::{extract::State, Json}; use axum_macros::debug_handler; use bson::doc; use bson::DateTime; -use woothee::parser::{Parser, WootheeResult}; use crate::core::session::Session; use crate::errors::Result; @@ -30,49 +29,21 @@ pub async fn get_all_overview_handler( let active_session_count = all_sessions.iter().filter(|s| !s.is_revoked).count(); let revoked_session_count = all_sessions.iter().filter(|s| s.is_revoked).count(); - // create a user-agent map from all_sessions where is_revoked = false - let user_agents: Vec = all_sessions + let os_types: Vec = all_sessions .iter() - .filter(|s| !s.is_revoked) - .map(|s| s.user_agent.clone()) + .map(|session| session.os.clone()) .collect(); - println!(">> user_agents: {:?}", user_agents); - - let parser = Parser::new(); - - // find out os_types, device_types, browser_types from all_sessions using user-agent-parser - let results: Vec> = - user_agents.iter().map(|ua| parser.parse(ua)).collect(); - - // get os_types as a string[] from results - let os_types: Vec = results - .iter() - .map(|r| { - r.as_ref() - .map_or_else(String::new, |result| result.os.to_string()) - }) - .collect(); - - // get device_types as a string[] from results - let device_types: Vec = results + let device_types: Vec = all_sessions .iter() - .map(|r| { - r.as_ref() - .map_or_else(String::new, |result| result.category.to_string()) - }) + .map(|session| session.device.clone()) .collect(); - // get browser_types as a string[] from results - let browser_types: Vec = results + let browser_types: Vec = all_sessions .iter() - .map(|r| { - r.as_ref() - .map_or_else(String::new, |result| result.name.to_string()) - }) + .map(|session| session.browser.clone()) .collect(); - println!(">> results USER AGENTSSS: {:?}", results); println!(">> os_types: {:?}", os_types); println!(">> device_types: {:?}", device_types); println!(">> browser_types: {:?}", browser_types); diff --git a/src/models/session_model.rs b/src/models/session_model.rs index d8623b8..48d35af 100644 --- a/src/models/session_model.rs +++ b/src/models/session_model.rs @@ -8,13 +8,19 @@ pub struct VerifySession { #[derive(Deserialize, Debug, Clone, Serialize)] pub struct SessionResponse { - pub uid : String, - pub session_id : String, - pub email : String, - pub user_agent : String, - pub is_revoked : bool, - pub created_at : DateTime, - pub updated_at : DateTime, + pub uid: String, + pub session_id: String, + pub email: String, + pub user_agent: String, + pub os: String, + pub os_version: String, + pub vendor: String, + pub device: String, + pub browser: String, + pub browser_version: String, + pub is_revoked: bool, + pub created_at: DateTime, + pub updated_at: DateTime, } #[derive(Deserialize, Debug, Clone)] @@ -54,7 +60,6 @@ pub struct RevokeSessionsResult { pub message: String, } - #[derive(Deserialize, Debug, Clone)] pub struct DeleteAllSessionsPayload { pub uid: String, @@ -80,4 +85,4 @@ pub struct DeleteSessionsResult { pub struct SessionDetailsPayload { pub uid: String, pub session_id: String, -} \ No newline at end of file +} diff --git a/ui/app/api/user/get-recent/route.ts b/ui/app/api/user/get-recent/route.ts new file mode 100644 index 0000000..18b0387 --- /dev/null +++ b/ui/app/api/user/get-recent/route.ts @@ -0,0 +1,25 @@ +export async function POST(req: Request) { + const endPoint: (string | undefined) = `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/user/get-recent`; + + const { limit } = await req.json(); + + if (endPoint) { + try { + const res = await fetch(endPoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', // Set the appropriate content type for your request + 'x-api-key': process.env.X_API_KEY!, + }, + cache: 'no-cache', + body: JSON.stringify({ + limit + }), + }); + const data = await res.json(); + return Response.json({ data }) + } catch (error) { + console.error('Error during request:', error); + } + } +} \ No newline at end of file diff --git a/ui/app/layout.tsx b/ui/app/layout.tsx index f9482b3..96536cb 100644 --- a/ui/app/layout.tsx +++ b/ui/app/layout.tsx @@ -30,7 +30,7 @@ export default function RootLayout({
-
+
{children} diff --git a/ui/components/Overview/Overview.tsx b/ui/components/Overview/Overview.tsx index 4d8a0b4..ce83078 100644 --- a/ui/components/Overview/Overview.tsx +++ b/ui/components/Overview/Overview.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ "use client"; import React, { useEffect, useState } from 'react' import { Loader } from '../custom/Loader'; @@ -5,11 +6,27 @@ import { IOverview } from '@/interfaces/IOverview'; import { DonutChartStats } from '../custom/DonutChartForStats'; import { ChartConfig } from '../ui/chart'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { FaUsersSlash } from 'react-icons/fa'; import { ChartPie } from '../custom/PieChart'; +import { IUser } from '@/interfaces/IUser'; +import { ColumnDef } from '@tanstack/react-table'; +import { LuArrowUpRight } from 'react-icons/lu'; +import { useRouter } from 'next/navigation'; +import { Badge } from '../ui/badge'; +import { TiTick } from 'react-icons/ti'; +import { GoClockFill } from 'react-icons/go'; +import { IoIosWarning, IoMdMore } from 'react-icons/io'; +import { format } from 'date-fns'; +import { toast } from '../ui/use-toast'; +import { Input } from '../ui/input'; +import { Button } from '../ui/button'; +import { DataTable } from '../ui/data-table'; const Overview = () => { const [overview, setOverview] = useState(null) + const [recentUsers, setRecentUsers] = useState([]) const [loading, setLoading] = useState(true) const userChartData = [ @@ -60,7 +77,8 @@ const Overview = () => { const deviceCounts: CountObject = (overview ?? { device_types: [], }).device_types.reduce((acc, device) => { - acc[device] = (acc[device] || 0) + 1; + const sanitizedKey = device.replace(/\s+/g, ''); // Remove whitespace from the key + acc[sanitizedKey] = (acc[sanitizedKey] || 0) + 1; return acc; }, {} as CountObject); @@ -68,7 +86,8 @@ const Overview = () => { const browserCounts: CountObject = (overview ?? { browser_types: [], }).browser_types.reduce((acc, browser) => { - acc[browser] = (acc[browser] || 0) + 1; + const sanitizedKey = browser.replace(/\s+/g, ''); // Remove whitespace from the key + acc[sanitizedKey] = (acc[sanitizedKey] || 0) + 1; return acc; }, {} as CountObject); @@ -76,10 +95,12 @@ const Overview = () => { const osTypeCounts: CountObject = (overview ?? { os_types: [], }).os_types.reduce((acc, os) => { - acc[os] = (acc[os] || 0) + 1; + const sanitizedKey = os.replace(/\s+/g, ''); // Remove whitespace from the key + acc[sanitizedKey] = (acc[sanitizedKey] || 0) + 1; return acc; }, {} as CountObject); + // Define a function to generate colors dynamically const generateColor = (index: number, themeNo: number) => { return `hsl(var(--chart-${index + 1}-${themeNo}))`; @@ -125,6 +146,214 @@ const Overview = () => { const sessionOSChartConfig = generateChartConfig(osTypeCounts, 2); + // delete user function + const deleteUser = async (email: string) => { + try { + setLoading(true) + await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/delete`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email + }), + }); + } catch (error) { + console.error('Error during POST request:', error); + } + setLoading(false) + } + + const router = useRouter(); + + const columns: ColumnDef[] = [ + { + accessorKey: "name", + header: "Name", + cell: ({ row }) => { + const user = row.original; + return ( +
router.push(`/users/${user.uid}`)}> +
{user.name}
+
+ +
+
+ ); + } + }, + { + accessorKey: "email", + header: "Email", + }, + { + accessorKey: "role", + header: "Role", + }, + { + accessorKey: "email_verified", + header: "Email Verification", + cell: ({ row }) => { + return ( + + {row.original?.email_verified ? : } + + {row.original?.email_verified ? "Verified" : "Pending"} + + ) + }, + }, + { + accessorKey: "is_active", + header: "Account Status", + cell: ({ row }) => { + return ( + + {row.original?.is_active ? : } + + {row.original?.is_active ? "Active" : "Suspended"} + + ) + }, + }, + { + accessorKey: "created_at", + header: "Created At", + cell: ({ row }) => { + return ( +
+ {format( + new Date(parseInt(row.original?.created_at.$date.$numberLong!)), + "PP - p" + )} +
+ ) + }, + }, + { + accessorKey: "updated_at", + header: "Action", + cell: ({ row }) => { + const user = row.original; + const [name, setName] = useState(user.name); + const [role, setRole] = useState(user.role) + // edit user function + const editUser = async (email: string, name: string, role: string) => { + if (name === "" || role === "") { + toast({ + title: "Error", + description: "Fill all the fields correctly.", + variant: "destructive" + }); + return; + }; + try { + setLoading(true) + if (role !== user?.role) { + await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/update-role`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email: user?.email, + role + }), + }); + } + if (name !== user?.name) { + await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/update`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + email, + name + }), + }); + } + await getRecentUsers() + } catch (error) { + console.error('Error during POST request:', error); + } + setLoading(false) + } + + return ( +
+ + + + + + + + + Update + + + + Update User Name + +

Name

+ setName(e.target.value)} /> +

Role

+ setRole(e.target.value)} /> +
+
+ + Cancel + + +
+
+
+ + + + Delete + + + + Are you absolutely sure? + + This action cannot be undone. + + + + Cancel + + + + + +
+
+
+ ) + }, + }, + ]; + const getOverview = async () => { try { @@ -144,8 +373,32 @@ const Overview = () => { setLoading(false) } + const getRecentUsers = async () => { + try { + setLoading(true) + const res = await fetch(`${process.env.NEXT_PUBLIC_ENDPOINT}/api/user/get-recent`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ limit: 5 }), + cache: 'no-cache', + }); + const { data } = await res.json(); + setRecentUsers(data); + } catch (error) { + console.error('Error during POST request:', error); + } + setLoading(false) + } + + const fetchAllData = async () => { + await Promise.all([getOverview(), getRecentUsers()]) + } + useEffect(() => { - getOverview() + fetchAllData() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( @@ -164,15 +417,15 @@ const Overview = () => { chartConfig={userChartConfig} key='name' /> - + - Blocked Users + Blocked Users - -

+ + +

{overview?.blocked_user_count}

-
{ key='name' />
+
+

Recent Users

+ +
}
diff --git a/ui/components/custom/DonutChartForStats.tsx b/ui/components/custom/DonutChartForStats.tsx index b463ddd..176598b 100644 --- a/ui/components/custom/DonutChartForStats.tsx +++ b/ui/components/custom/DonutChartForStats.tsx @@ -15,6 +15,7 @@ import { ChartTooltip, ChartTooltipContent, } from "@/components/ui/chart" +import { FaClock, FaUsersSlash } from "react-icons/fa" interface DonutChartProps { title: string; @@ -24,63 +25,70 @@ interface DonutChartProps { } export function DonutChartStats({ title, chartData, chartConfig, key }: DonutChartProps) { - const totalVisitors = React.useMemo(() => { + const totalCount = React.useMemo(() => { return chartData.reduce((acc: any, curr: { count: any }) => acc + curr.count, 0) }, [chartData]) return ( - - {title} + + {title} - - } - /> - - + : + } + /> + + - + + {totalCount.toLocaleString()} + + + {title} + + + ) + } + }} + /> + + } diff --git a/ui/components/custom/PieChart.tsx b/ui/components/custom/PieChart.tsx index 12180d1..3dd4b9f 100644 --- a/ui/components/custom/PieChart.tsx +++ b/ui/components/custom/PieChart.tsx @@ -1,21 +1,22 @@ "use client" -import { Pie, PieChart } from "recharts" - +import { Label, Pie, PieChart, Sector } from "recharts" import { Card, CardContent, CardHeader, CardTitle, -} from "@/components/ui/card" +} from "../ui/card" import { ChartConfig, ChartContainer, - ChartLegend, - ChartLegendContent, + ChartStyle, ChartTooltip, ChartTooltipContent, -} from "@/components/ui/chart" +} from "../ui/chart" +import React from "react"; +import { PieSectorDataItem } from "recharts/types/polar/Pie"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"; interface ChartPieProps { @@ -26,17 +27,63 @@ interface ChartPieProps { } export function ChartPie({ title, chartData, chartConfig, key }: ChartPieProps) { + const id = "pie-interactive" + const [active, setActive] = React.useState(chartData[0]?.name) + const activeIndex = React.useMemo(() => { + const index = (chartData as []).findIndex((item: any) => item.name === active); + return index === -1 ? 0 : index; + }, [active, chartData]); + + const options = React.useMemo(() => chartData.map((item: any) => item.name), [chartData]) + return ( - - - {title} + + + +
+ {title} +
+ {(chartData as []).length > 0 && }
- + - + {(chartData as []).length > 0 ? } @@ -46,14 +93,61 @@ export function ChartPie({ title, chartData, chartConfig, key }: ChartPieProps) dataKey="count" nameKey={key} innerRadius={60} - /> - } - className="-translate-y-2 flex-wrap gap-2 [&>*]:basis-1/4 [&>*]:justify-center" - /> - + strokeWidth={5} + activeIndex={activeIndex} + activeShape={({ + outerRadius = 0, + ...props + }: PieSectorDataItem) => ( + + + + + )} + > + : + +

+ No Data to show +

+
+ }
) -} +} \ No newline at end of file diff --git a/ui/components/session/SessionTableAll.tsx b/ui/components/session/SessionTableAll.tsx index 06ecb07..8ea5132 100644 --- a/ui/components/session/SessionTableAll.tsx +++ b/ui/components/session/SessionTableAll.tsx @@ -20,14 +20,12 @@ import React, { useCallback, useEffect } from "react"; import { IoMdMore } from "react-icons/io"; import { AlertDialogHeader, AlertDialogFooter } from "../ui/alert-dialog"; import { Button } from "../ui/button"; -import { Card, CardHeader, CardTitle, CardContent } from "../ui/card"; import { DataTable } from "../ui/data-table"; -import UAParser from "ua-parser-js"; import { addDays, format } from "date-fns"; const SessionTableAll: React.FC = () => { const [loading, setLoading] = React.useState(false); - const [sessions, setSessions] = React.useState([]); + const [sessions, setSessions] = React.useState([]); // fetch all sessions const fetchAllSessions = useCallback(async () => { @@ -97,24 +95,18 @@ const SessionTableAll: React.FC = () => { fetchAllSessions(); }, [fetchAllSessions]); - const parser = new UAParser("user-agent"); - const sessionColumns: ColumnDef[] = [ { accessorKey: "user_agent", header: "User Agent", cell: ({ row }) => { - parser.setUA(row.original.user_agent); - const result = parser.getResult(); - - console.log(result); - + const session = row.original; return ( // render device type and browser name with logo
- {result.device.type === "mobile" && + {session.device === "smartphone" && ( { className="w-24 h-24" /> )} - {result.device.type === undefined && + {session.device === "pc" && ( device-logo )}
-

{result.device?.vendor} - {result.device?.model}

-

{result.os?.name} - Version: {result.os?.version ? result.os.version : "Unknown"}

+

{session?.device === "smartphone" ? "Smartphone" : session?.device === "pc" ? "Desktop" : ""} ({session.vendor})

+

{session?.os} - Version {session.os_version}

- {result.browser.name?.includes("Chrome") && ( + {session?.browser.includes("Chrome") && ( browser-logo )} - {result.browser.name?.includes("Mozilla") && ( + {session?.browser.includes("Mozilla") && ( browser-logo )} - {result.browser.name?.includes("Safari") && ( + {session?.browser.includes("Safari") && ( browser-logo )} -

{result.browser?.name} - Version: {result.browser?.version}

+

{session?.browser} - Version: {session?.browser_version}

diff --git a/ui/components/session/SessionTableUser.tsx b/ui/components/session/SessionTableUser.tsx index f5b99b5..b0a0f28 100644 --- a/ui/components/session/SessionTableUser.tsx +++ b/ui/components/session/SessionTableUser.tsx @@ -157,17 +157,14 @@ const SessionTable: React.FC = ({ userID }) => { accessorKey: "user_agent", header: "User Agent", cell: ({ row }) => { - parser.setUA(row.original.user_agent); - const result = parser.getResult(); - - console.log(result); + const session = row.original; return ( // render device type and browser name with logo
- {result.device.type === "mobile" && + {session.device === "smartphone" && ( = ({ userID }) => { className="w-24 h-24" /> )} - {result.device.type === undefined && + {session.device === "pc" && ( device-logo )}
-

{result.device?.vendor} - {result.device?.model}

-

{result.os?.name} - Version: {result.os?.version ? result.os.version : "Unknown"}

+

{session?.device === "smartphone" ? "Smartphone" : session?.device === "pc" ? "Desktop" : ""} ({session.vendor})

+

{session?.os} - Version {session.os_version}

- {result.browser.name?.includes("Chrome") && ( + {session?.browser.includes("Chrome") && ( browser-logo )} - {result.browser.name?.includes("Mozilla") && ( + {session?.browser.includes("Mozilla") && ( browser-logo )} - {result.browser.name?.includes("Safari") && ( + {session?.browser.includes("Safari") && ( browser-logo )} -

{result.browser?.name} - Version: {result.browser?.version}

+

{session?.browser} - Version: {session?.browser_version}

diff --git a/ui/components/shared/Navbar.tsx b/ui/components/shared/Navbar.tsx index 6fca5d7..d3ebdf8 100644 --- a/ui/components/shared/Navbar.tsx +++ b/ui/components/shared/Navbar.tsx @@ -38,7 +38,7 @@ const Navbar = () => { } return ( -
+
= ({ item, path, router }) => { if (path.includes("/user/")) { return (item.link === "/user") } + if (path.includes("/users/")) { + return (item.link === "/users") + } return (path === item.link); } diff --git a/ui/components/ui/select.tsx b/ui/components/ui/select.tsx new file mode 100644 index 0000000..cbe5a36 --- /dev/null +++ b/ui/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/ui/interfaces/ISession.ts b/ui/interfaces/ISession.ts index 55903cb..f1556de 100644 --- a/ui/interfaces/ISession.ts +++ b/ui/interfaces/ISession.ts @@ -3,6 +3,12 @@ export interface ISession { email: string; uid: string; user_agent: string; + os: string; + device: string; + browser: string; + browser_version: string; + os_version: String, + vendor: String, is_revoked: boolean; created_at: DateRecord; updated_at: DateRecord; diff --git a/ui/package-lock.json b/ui/package-lock.json index 8a97ce1..45d90f5 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -11,6 +11,7 @@ "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4", @@ -461,6 +462,12 @@ "node": ">=14" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", @@ -945,6 +952,502 @@ } } }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.1.tgz", + "integrity": "sha512-8iRDfyLtzxlprOo9IicnzvpsO1wNCkuwzzCM+Z5Rb5tNOpCdMvcc2AkzX0Fz+Tz9v6NJ5B/7EEgyZveo4FBRfQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@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-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "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-select/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==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", + "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "license": "MIT", + "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-select/node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", + "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "license": "MIT", + "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-select/node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/node_modules/@radix-ui/react-portal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", + "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/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==", + "license": "MIT", + "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-select/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", + "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", + "license": "MIT", + "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-select/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==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-select/node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", diff --git a/ui/package.json b/ui/package.json index 16a7519..1706934 100644 --- a/ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tabs": "^1.0.4",