diff --git a/pages/api/admin/refresh-sessions.ts b/app/api/admin/refresh-sessions/route.ts similarity index 69% rename from pages/api/admin/refresh-sessions.ts rename to app/api/admin/refresh-sessions/route.ts index af22364..fea2fdb 100644 --- a/pages/api/admin/refresh-sessions.ts +++ b/app/api/admin/refresh-sessions/route.ts @@ -1,51 +1,50 @@ -// API route to refresh (fetch+parse+update) session data for a company -import { NextApiRequest, NextApiResponse } from "next"; -import { fetchAndParseCsv } from "../../../lib/csvFetcher"; -import { processQueuedImports } from "../../../lib/importProcessor"; -import { prisma } from "../../../lib/prisma"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - // Check if this is a POST request - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - // Get companyId from body or query - let { companyId } = req.body; - - if (!companyId) { - // Try to get user from prisma based on session cookie - try { - const session = await prisma.session.findFirst({ - orderBy: { createdAt: "desc" }, - where: { - /* Add session check criteria here */ - }, - }); - - if (session) { - companyId = session.companyId; - } - } catch (error) { - // Log error for server-side debugging - const errorMessage = - error instanceof Error ? error.message : String(error); - // Use a server-side logging approach instead of console - process.stderr.write(`Error fetching session: ${errorMessage}\n`); - } - } - - if (!companyId) { - return res.status(400).json({ error: "Company ID is required" }); - } - - const company = await prisma.company.findUnique({ where: { id: companyId } }); - if (!company) return res.status(404).json({ error: "Company not found" }); +import { NextRequest, NextResponse } from "next/server"; +import { fetchAndParseCsv } from "../../../../lib/csvFetcher"; +import { processQueuedImports } from "../../../../lib/importProcessor"; +import { prisma } from "../../../../lib/prisma"; +export async function POST(request: NextRequest) { try { + const body = await request.json(); + let { companyId } = body; + + if (!companyId) { + // Try to get user from prisma based on session cookie + try { + const session = await prisma.session.findFirst({ + orderBy: { createdAt: "desc" }, + where: { + /* Add session check criteria here */ + }, + }); + + if (session) { + companyId = session.companyId; + } + } catch (error) { + // Log error for server-side debugging + const errorMessage = + error instanceof Error ? error.message : String(error); + // Use a server-side logging approach instead of console + process.stderr.write(`Error fetching session: ${errorMessage}\n`); + } + } + + if (!companyId) { + return NextResponse.json( + { error: "Company ID is required" }, + { status: 400 } + ); + } + + const company = await prisma.company.findUnique({ where: { id: companyId } }); + if (!company) { + return NextResponse.json( + { error: "Company not found" }, + { status: 404 } + ); + } + const rawSessionData = await fetchAndParseCsv( company.csvUrl, company.csvUsername as string | undefined, @@ -123,7 +122,7 @@ export default async function handler( where: { companyId: company.id } }); - res.json({ + return NextResponse.json({ ok: true, imported: importedCount, total: rawSessionData.length, @@ -132,6 +131,6 @@ export default async function handler( }); } catch (e) { const error = e instanceof Error ? e.message : "An unknown error occurred"; - res.status(500).json({ error }); + return NextResponse.json({ error }, { status: 500 }); } } diff --git a/pages/api/admin/trigger-processing.ts b/app/api/admin/trigger-processing/route.ts similarity index 68% rename from pages/api/admin/trigger-processing.ts rename to app/api/admin/trigger-processing/route.ts index 9ddc519..cb7c9e7 100644 --- a/pages/api/admin/trigger-processing.ts +++ b/app/api/admin/trigger-processing/route.ts @@ -1,9 +1,9 @@ -import { NextApiRequest, NextApiResponse } from "next"; +import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; -import { authOptions } from "../auth/[...nextauth]"; -import { prisma } from "../../../lib/prisma"; -import { processUnprocessedSessions } from "../../../lib/processingScheduler"; -import { ProcessingStatusManager } from "../../../lib/processingStatusManager"; +import { authOptions } from "../../auth/[...nextauth]/route"; +import { prisma } from "../../../../lib/prisma"; +import { processUnprocessedSessions } from "../../../../lib/processingScheduler"; +import { ProcessingStatusManager } from "../../../../lib/processingStatusManager"; import { ProcessingStage } from "@prisma/client"; interface SessionUser { @@ -15,22 +15,11 @@ interface SessionData { user: SessionUser; } -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method !== "POST") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const session = (await getServerSession( - req, - res, - authOptions - )) as SessionData | null; +export async function POST(request: NextRequest) { + const session = (await getServerSession(authOptions)) as SessionData | null; if (!session?.user) { - return res.status(401).json({ error: "Not logged in" }); + return NextResponse.json({ error: "Not logged in" }, { status: 401 }); } const user = await prisma.user.findUnique({ @@ -39,17 +28,21 @@ export default async function handler( }); if (!user) { - return res.status(401).json({ error: "No user found" }); + return NextResponse.json({ error: "No user found" }, { status: 401 }); } // Check if user has ADMIN role if (user.role !== "ADMIN") { - return res.status(403).json({ error: "Admin access required" }); + return NextResponse.json( + { error: "Admin access required" }, + { status: 403 } + ); } try { // Get optional parameters from request body - const { batchSize, maxConcurrency } = req.body; + const body = await request.json(); + const { batchSize, maxConcurrency } = body; // Validate parameters const validatedBatchSize = batchSize && batchSize > 0 ? parseInt(batchSize) : null; @@ -69,7 +62,7 @@ export default async function handler( const unprocessedCount = companySessionsNeedingAI.length; if (unprocessedCount === 0) { - return res.json({ + return NextResponse.json({ success: true, message: "No sessions requiring AI processing found", unprocessedCount: 0, @@ -90,7 +83,7 @@ export default async function handler( console.error(`[Manual Trigger] Processing failed for company ${user.companyId}:`, error); }); - return res.json({ + return NextResponse.json({ success: true, message: `Started processing ${unprocessedCount} unprocessed sessions`, unprocessedCount, @@ -101,9 +94,12 @@ export default async function handler( } catch (error) { console.error("[Manual Trigger] Error:", error); - return res.status(500).json({ - error: "Failed to trigger processing", - details: error instanceof Error ? error.message : String(error), - }); + return NextResponse.json( + { + error: "Failed to trigger processing", + details: error instanceof Error ? error.message : String(error), + }, + { status: 500 } + ); } } diff --git a/pages/api/auth/[...nextauth].ts b/app/api/auth/[...nextauth]/route.ts similarity index 94% rename from pages/api/auth/[...nextauth].ts rename to app/api/auth/[...nextauth]/route.ts index 348c679..a352535 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,6 +1,6 @@ import NextAuth, { NextAuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; -import { prisma } from "../../../lib/prisma"; +import { prisma } from "../../../../lib/prisma"; import bcrypt from "bcryptjs"; // Define the shape of the JWT token @@ -101,4 +101,6 @@ export const authOptions: NextAuthOptions = { debug: process.env.NODE_ENV === "development", }; -export default NextAuth(authOptions); +const handler = NextAuth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/app/api/dashboard/config/route.ts b/app/api/dashboard/config/route.ts new file mode 100644 index 0000000..8b0eefb --- /dev/null +++ b/app/api/dashboard/config/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { prisma } from "../../../../lib/prisma"; +import { authOptions } from "../../auth/[...nextauth]/route"; + +export async function GET(request: NextRequest) { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: "Not logged in" }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { email: session.user.email as string }, + }); + + if (!user) { + return NextResponse.json({ error: "No user" }, { status: 401 }); + } + + // Get company data + const company = await prisma.company.findUnique({ + where: { id: user.companyId }, + }); + + return NextResponse.json({ company }); +} + +export async function POST(request: NextRequest) { + const session = await getServerSession(authOptions); + if (!session?.user) { + return NextResponse.json({ error: "Not logged in" }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { email: session.user.email as string }, + }); + + if (!user) { + return NextResponse.json({ error: "No user" }, { status: 401 }); + } + + const body = await request.json(); + const { csvUrl } = body; + + await prisma.company.update({ + where: { id: user.companyId }, + data: { csvUrl }, + }); + + return NextResponse.json({ ok: true }); +} diff --git a/pages/api/dashboard/metrics.ts b/app/api/dashboard/metrics/route.ts similarity index 78% rename from pages/api/dashboard/metrics.ts rename to app/api/dashboard/metrics/route.ts index f8501e2..6b6c2cd 100644 --- a/pages/api/dashboard/metrics.ts +++ b/app/api/dashboard/metrics/route.ts @@ -1,10 +1,9 @@ -// API endpoint: return metrics for current company -import { NextApiRequest, NextApiResponse } from "next"; +import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth"; -import { prisma } from "../../../lib/prisma"; -import { sessionMetrics } from "../../../lib/metrics"; -import { authOptions } from "../auth/[...nextauth]"; -import { ChatSession } from "../../../lib/types"; // Import ChatSession +import { prisma } from "../../../../lib/prisma"; +import { sessionMetrics } from "../../../../lib/metrics"; +import { authOptions } from "../../auth/[...nextauth]/route"; +import { ChatSession } from "../../../../lib/types"; interface SessionUser { email: string; @@ -15,26 +14,25 @@ interface SessionData { user: SessionUser; } -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const session = (await getServerSession( - req, - res, - authOptions - )) as SessionData | null; - if (!session?.user) return res.status(401).json({ error: "Not logged in" }); +export async function GET(request: NextRequest) { + const session = (await getServerSession(authOptions)) as SessionData | null; + if (!session?.user) { + return NextResponse.json({ error: "Not logged in" }, { status: 401 }); + } const user = await prisma.user.findUnique({ where: { email: session.user.email }, include: { company: true }, }); - if (!user) return res.status(401).json({ error: "No user" }); + if (!user) { + return NextResponse.json({ error: "No user" }, { status: 401 }); + } // Get date range from query parameters - const { startDate, endDate } = req.query; + const { searchParams } = new URL(request.url); + const startDate = searchParams.get("startDate"); + const endDate = searchParams.get("endDate"); // Build where clause with optional date filtering const whereClause: any = { @@ -43,8 +41,8 @@ export default async function handler( if (startDate && endDate) { whereClause.startTime = { - gte: new Date(startDate as string), - lte: new Date(endDate as string + 'T23:59:59.999Z'), // Include full end date + gte: new Date(startDate), + lte: new Date(endDate + 'T23:59:59.999Z'), // Include full end date }; } @@ -103,7 +101,7 @@ export default async function handler( }; } - res.json({ + return NextResponse.json({ metrics, csvUrl: user.company.csvUrl, company: user.company, diff --git a/pages/api/dashboard/session-filter-options.ts b/app/api/dashboard/session-filter-options/route.ts similarity index 60% rename from pages/api/dashboard/session-filter-options.ts rename to app/api/dashboard/session-filter-options/route.ts index 1277098..9d69a27 100644 --- a/pages/api/dashboard/session-filter-options.ts +++ b/app/api/dashboard/session-filter-options/route.ts @@ -1,23 +1,14 @@ -import { NextApiRequest, NextApiResponse } from "next"; +import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "../auth/[...nextauth]"; -import { prisma } from "../../../lib/prisma"; -import { SessionFilterOptions } from "../../../lib/types"; +import { authOptions } from "../../auth/[...nextauth]/route"; +import { prisma } from "../../../../lib/prisma"; +import { SessionFilterOptions } from "../../../../lib/types"; -export default async function handler( - req: NextApiRequest, - res: NextApiResponse< - SessionFilterOptions | { error: string; details?: string } - > -) { - if (req.method !== "GET") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const authSession = await getServerSession(req, res, authOptions); +export async function GET(request: NextRequest) { + const authSession = await getServerSession(authOptions); if (!authSession || !authSession.user?.companyId) { - return res.status(401).json({ error: "Unauthorized" }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const companyId = authSession.user.companyId; @@ -62,15 +53,19 @@ export default async function handler( .map((s) => s.language) .filter(Boolean) as string[]; // Filter out any nulls and assert as string[] - return res - .status(200) - .json({ categories: distinctCategories, languages: distinctLanguages }); + return NextResponse.json({ + categories: distinctCategories, + languages: distinctLanguages + }); } catch (error) { const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; - return res.status(500).json({ - error: "Failed to fetch filter options", - details: errorMessage, - }); + return NextResponse.json( + { + error: "Failed to fetch filter options", + details: errorMessage, + }, + { status: 500 } + ); } } diff --git a/pages/api/dashboard/session/[id].ts b/app/api/dashboard/session/[id]/route.ts similarity index 78% rename from pages/api/dashboard/session/[id].ts rename to app/api/dashboard/session/[id]/route.ts index 770b1fa..eb65c76 100644 --- a/pages/api/dashboard/session/[id].ts +++ b/app/api/dashboard/session/[id]/route.ts @@ -1,19 +1,18 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import { prisma } from "../../../../lib/prisma"; -import { ChatSession } from "../../../../lib/types"; +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "../../../../../lib/prisma"; +import { ChatSession } from "../../../../../lib/types"; -export default async function handler( - req: NextApiRequest, - res: NextApiResponse +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } ) { - if (req.method !== "GET") { - return res.status(405).json({ error: "Method not allowed" }); - } + const { id } = params; - const { id } = req.query; - - if (!id || typeof id !== "string") { - return res.status(400).json({ error: "Session ID is required" }); + if (!id) { + return NextResponse.json( + { error: "Session ID is required" }, + { status: 400 } + ); } try { @@ -27,7 +26,10 @@ export default async function handler( }); if (!prismaSession) { - return res.status(404).json({ error: "Session not found" }); + return NextResponse.json( + { error: "Session not found" }, + { status: 404 } + ); } // Map Prisma session object to ChatSession type @@ -71,12 +73,13 @@ export default async function handler( })) ?? [], // New field - parsed messages }; - return res.status(200).json({ session }); + return NextResponse.json({ session }); } catch (error) { const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; - return res - .status(500) - .json({ error: "Failed to fetch session", details: errorMessage }); + return NextResponse.json( + { error: "Failed to fetch session", details: errorMessage }, + { status: 500 } + ); } } diff --git a/pages/api/dashboard/sessions.ts b/app/api/dashboard/sessions/route.ts similarity index 69% rename from pages/api/dashboard/sessions.ts rename to app/api/dashboard/sessions/route.ts index 01f482e..068d3a0 100644 --- a/pages/api/dashboard/sessions.ts +++ b/app/api/dashboard/sessions/route.ts @@ -1,40 +1,33 @@ -import { NextApiRequest, NextApiResponse } from "next"; +import { NextRequest, NextResponse } from "next/server"; import { getServerSession } from "next-auth/next"; -import { authOptions } from "../auth/[...nextauth]"; -import { prisma } from "../../../lib/prisma"; +import { authOptions } from "../../auth/[...nextauth]/route"; +import { prisma } from "../../../../lib/prisma"; import { ChatSession, SessionApiResponse, SessionQuery, -} from "../../../lib/types"; +} from "../../../../lib/types"; import { Prisma } from "@prisma/client"; -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method !== "GET") { - return res.status(405).json({ error: "Method not allowed" }); - } - - const authSession = await getServerSession(req, res, authOptions); +export async function GET(request: NextRequest) { + const authSession = await getServerSession(authOptions); if (!authSession || !authSession.user?.companyId) { - return res.status(401).json({ error: "Unauthorized" }); + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } const companyId = authSession.user.companyId; - const { - searchTerm, - category, - language, - startDate, - endDate, - sortKey, - sortOrder, - page: queryPage, - pageSize: queryPageSize, - } = req.query as SessionQuery; + const { searchParams } = new URL(request.url); + + const searchTerm = searchParams.get("searchTerm"); + const category = searchParams.get("category"); + const language = searchParams.get("language"); + const startDate = searchParams.get("startDate"); + const endDate = searchParams.get("endDate"); + const sortKey = searchParams.get("sortKey"); + const sortOrder = searchParams.get("sortOrder"); + const queryPage = searchParams.get("page"); + const queryPageSize = searchParams.get("pageSize"); const page = Number(queryPage) || 1; const pageSize = Number(queryPageSize) || 10; @@ -43,11 +36,7 @@ export default async function handler( const whereClause: Prisma.SessionWhereInput = { companyId }; // Search Term - if ( - searchTerm && - typeof searchTerm === "string" && - searchTerm.trim() !== "" - ) { + if (searchTerm && searchTerm.trim() !== "") { const searchConditions = [ { id: { contains: searchTerm } }, { initialMsg: { contains: searchTerm } }, @@ -57,24 +46,24 @@ export default async function handler( } // Category Filter - if (category && typeof category === "string" && category.trim() !== "") { + if (category && category.trim() !== "") { // Cast to SessionCategory enum if it's a valid value whereClause.category = category as any; } // Language Filter - if (language && typeof language === "string" && language.trim() !== "") { + if (language && language.trim() !== "") { whereClause.language = language; } // Date Range Filter - if (startDate && typeof startDate === "string") { + if (startDate) { whereClause.startTime = { ...((whereClause.startTime as object) || {}), gte: new Date(startDate), }; } - if (endDate && typeof endDate === "string") { + if (endDate) { const inclusiveEndDate = new Date(endDate); inclusiveEndDate.setDate(inclusiveEndDate.getDate() + 1); whereClause.startTime = { @@ -98,7 +87,7 @@ export default async function handler( | Prisma.SessionOrderByWithRelationInput[]; const primarySortField = - sortKey && typeof sortKey === "string" && validSortKeys[sortKey] + sortKey && validSortKeys[sortKey] ? validSortKeys[sortKey] : "startTime"; // Default to startTime field if sortKey is invalid/missing @@ -115,9 +104,6 @@ export default async function handler( { startTime: "desc" }, ]; } - // Note: If sortKey was initially undefined or invalid, primarySortField defaults to "startTime", - // and primarySortOrder defaults to "desc". This makes orderByCondition = { startTime: "desc" }, - // which is the correct overall default sort. const prismaSessions = await prisma.session.findMany({ where: whereClause, @@ -151,12 +137,13 @@ export default async function handler( transcriptContent: null, // Transcript content is now fetched from fullTranscriptUrl when needed })); - return res.status(200).json({ sessions, totalSessions }); + return NextResponse.json({ sessions, totalSessions }); } catch (error) { const errorMessage = error instanceof Error ? error.message : "An unknown error occurred"; - return res - .status(500) - .json({ error: "Failed to fetch sessions", details: errorMessage }); + return NextResponse.json( + { error: "Failed to fetch sessions", details: errorMessage }, + { status: 500 } + ); } } diff --git a/app/api/dashboard/settings/route.ts b/app/api/dashboard/settings/route.ts new file mode 100644 index 0000000..72a9d7a --- /dev/null +++ b/app/api/dashboard/settings/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getServerSession } from "next-auth"; +import { prisma } from "../../../../lib/prisma"; +import { authOptions } from "../../auth/[...nextauth]/route"; + +export async function POST(request: NextRequest) { + const session = await getServerSession(authOptions); + if (!session?.user || session.user.role !== "ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const user = await prisma.user.findUnique({ + where: { email: session.user.email as string }, + }); + + if (!user) { + return NextResponse.json({ error: "No user" }, { status: 401 }); + } + + const body = await request.json(); + const { csvUrl, csvUsername, csvPassword, sentimentThreshold } = body; + + await prisma.company.update({ + where: { id: user.companyId }, + data: { + csvUrl, + csvUsername, + ...(csvPassword ? { csvPassword } : {}), + sentimentAlert: sentimentThreshold + ? parseFloat(sentimentThreshold) + : null, + }, + }); + + return NextResponse.json({ ok: true }); +} diff --git a/app/api/dashboard/users/route.ts b/app/api/dashboard/users/route.ts new file mode 100644 index 0000000..cc00917 --- /dev/null +++ b/app/api/dashboard/users/route.ts @@ -0,0 +1,80 @@ +import { NextRequest, NextResponse } from "next/server"; +import crypto from "crypto"; +import { getServerSession } from "next-auth"; +import { prisma } from "../../../../lib/prisma"; +import bcrypt from "bcryptjs"; +import { authOptions } from "../../auth/[...nextauth]/route"; + +interface UserBasicInfo { + id: string; + email: string; + role: string; +} + +export async function GET(request: NextRequest) { + const session = await getServerSession(authOptions); + if (!session?.user || session.user.role !== "ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const user = await prisma.user.findUnique({ + where: { email: session.user.email as string }, + }); + + if (!user) { + return NextResponse.json({ error: "No user" }, { status: 401 }); + } + + const users = await prisma.user.findMany({ + where: { companyId: user.companyId }, + }); + + const mappedUsers: UserBasicInfo[] = users.map((u) => ({ + id: u.id, + email: u.email, + role: u.role, + })); + + return NextResponse.json({ users: mappedUsers }); +} + +export async function POST(request: NextRequest) { + const session = await getServerSession(authOptions); + if (!session?.user || session.user.role !== "ADMIN") { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + const user = await prisma.user.findUnique({ + where: { email: session.user.email as string }, + }); + + if (!user) { + return NextResponse.json({ error: "No user" }, { status: 401 }); + } + + const body = await request.json(); + const { email, role } = body; + + if (!email || !role) { + return NextResponse.json({ error: "Missing fields" }, { status: 400 }); + } + + const exists = await prisma.user.findUnique({ where: { email } }); + if (exists) { + return NextResponse.json({ error: "Email exists" }, { status: 409 }); + } + + const tempPassword = crypto.randomBytes(12).toString("base64").slice(0, 12); // secure random initial password + + await prisma.user.create({ + data: { + email, + password: await bcrypt.hash(tempPassword, 10), + companyId: user.companyId, + role, + }, + }); + + // TODO: Email user their temp password (stub, for demo) - Implement a robust and secure email sending mechanism. Consider using a transactional email service. + return NextResponse.json({ ok: true, tempPassword }); +} diff --git a/app/api/forgot-password/route.ts b/app/api/forgot-password/route.ts new file mode 100644 index 0000000..382fb82 --- /dev/null +++ b/app/api/forgot-password/route.ts @@ -0,0 +1,28 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "../../../lib/prisma"; +import { sendEmail } from "../../../lib/sendEmail"; +import crypto from "crypto"; + +export async function POST(request: NextRequest) { + const body = await request.json(); + const { email } = body as { email: string }; + + const user = await prisma.user.findUnique({ where: { email } }); + if (!user) { + // Always return 200 for privacy (don't reveal if email exists) + return NextResponse.json({ success: true }, { status: 200 }); + } + + const token = crypto.randomBytes(32).toString("hex"); + const expiry = new Date(Date.now() + 1000 * 60 * 30); // 30 min expiry + + await prisma.user.update({ + where: { email }, + data: { resetToken: token, resetTokenExpiry: expiry }, + }); + + const resetUrl = `${process.env.NEXTAUTH_URL || "http://localhost:3000"}/reset-password?token=${token}`; + await sendEmail(email, "Password Reset", `Reset your password: ${resetUrl}`); + + return NextResponse.json({ success: true }, { status: 200 }); +} diff --git a/app/api/register/route.ts b/app/api/register/route.ts new file mode 100644 index 0000000..bc55e0f --- /dev/null +++ b/app/api/register/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "../../../lib/prisma"; +import bcrypt from "bcryptjs"; + +interface RegisterRequestBody { + email: string; + password: string; + company: string; + csvUrl?: string; +} + +export async function POST(request: NextRequest) { + const body = await request.json(); + const { email, password, company, csvUrl } = body as RegisterRequestBody; + + if (!email || !password || !company) { + return NextResponse.json( + { + success: false, + error: "Missing required fields", + }, + { status: 400 } + ); + } + + // Check if email exists + const exists = await prisma.user.findUnique({ + where: { email }, + }); + + if (exists) { + return NextResponse.json( + { + success: false, + error: "Email already exists", + }, + { status: 409 } + ); + } + + const newCompany = await prisma.company.create({ + data: { name: company, csvUrl: csvUrl || "" }, + }); + + const hashed = await bcrypt.hash(password, 10); + + await prisma.user.create({ + data: { + email, + password: hashed, + companyId: newCompany.id, + role: "ADMIN", + }, + }); + + return NextResponse.json( + { + success: true, + data: { success: true }, + }, + { status: 201 } + ); +} diff --git a/app/api/reset-password/route.ts b/app/api/reset-password/route.ts new file mode 100644 index 0000000..c518bc8 --- /dev/null +++ b/app/api/reset-password/route.ts @@ -0,0 +1,63 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "../../../lib/prisma"; +import bcrypt from "bcryptjs"; + +export async function POST(request: NextRequest) { + const body = await request.json(); + const { token, password } = body as { token?: string; password?: string }; + + if (!token || !password) { + return NextResponse.json( + { error: "Token and password are required." }, + { status: 400 } + ); + } + + if (password.length < 8) { + return NextResponse.json( + { error: "Password must be at least 8 characters long." }, + { status: 400 } + ); + } + + try { + const user = await prisma.user.findFirst({ + where: { + resetToken: token, + resetTokenExpiry: { gte: new Date() }, + }, + }); + + if (!user) { + return NextResponse.json( + { + error: "Invalid or expired token. Please request a new password reset.", + }, + { status: 400 } + ); + } + + const hash = await bcrypt.hash(password, 10); + await prisma.user.update({ + where: { id: user.id }, + data: { + password: hash, + resetToken: null, + resetTokenExpiry: null, + }, + }); + + return NextResponse.json( + { message: "Password has been reset successfully." }, + { status: 200 } + ); + } catch (error) { + console.error("Reset password error:", error); + return NextResponse.json( + { + error: "An internal server error occurred. Please try again later.", + }, + { status: 500 } + ); + } +} diff --git a/app/dashboard/overview/page.tsx b/app/dashboard/overview/page.tsx index 143fab6..dd3961b 100644 --- a/app/dashboard/overview/page.tsx +++ b/app/dashboard/overview/page.tsx @@ -1,40 +1,62 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { signOut, useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; -import { - SessionsLineChart, - CategoriesBarChart, - LanguagePieChart, - TokenUsageChart, -} from "../../../components/Charts"; import { Company, MetricsResult, WordCloudWord } from "../../../lib/types"; -import MetricCard from "../../../components/MetricCard"; -import DonutChart from "../../../components/DonutChart"; +import MetricCard from "../../../components/ui/metric-card"; +import ModernLineChart from "../../../components/charts/line-chart"; +import ModernBarChart from "../../../components/charts/bar-chart"; +import ModernDonutChart from "../../../components/charts/donut-chart"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Separator } from "@/components/ui/separator"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + MessageSquare, + Users, + Clock, + Zap, + Euro, + TrendingUp, + CheckCircle, + RefreshCw, + LogOut, + Calendar, + MoreVertical, + Globe, + MessageCircle, +} from "lucide-react"; import WordCloud from "../../../components/WordCloud"; import GeographicMap from "../../../components/GeographicMap"; import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution"; -import WelcomeBanner from "../../../components/WelcomeBanner"; import DateRangePicker from "../../../components/DateRangePicker"; import TopQuestionsChart from "../../../components/TopQuestionsChart"; // Safely wrapped component with useSession function DashboardContent() { - const { data: session, status } = useSession(); // Add status from useSession - const router = useRouter(); // Initialize useRouter + const { data: session, status } = useSession(); + const router = useRouter(); const [metrics, setMetrics] = useState(null); const [company, setCompany] = useState(null); - const [, setLoading] = useState(false); + const [loading, setLoading] = useState(false); const [refreshing, setRefreshing] = useState(false); const [dateRange, setDateRange] = useState<{ minDate: string; maxDate: string } | null>(null); const [selectedStartDate, setSelectedStartDate] = useState(""); const [selectedEndDate, setSelectedEndDate] = useState(""); + const [isInitialLoad, setIsInitialLoad] = useState(true); const isAuditor = session?.user?.role === "AUDITOR"; // Function to fetch metrics with optional date range - const fetchMetrics = useCallback(async (startDate?: string, endDate?: string) => { + const fetchMetrics = async (startDate?: string, endDate?: string, isInitial = false) => { setLoading(true); try { let url = "/api/dashboard/metrics"; @@ -49,44 +71,44 @@ function DashboardContent() { setCompany(data.company); // Set date range from API response (only on initial load) - if (data.dateRange && !dateRange) { + if (data.dateRange && isInitial) { setDateRange(data.dateRange); setSelectedStartDate(data.dateRange.minDate); setSelectedEndDate(data.dateRange.maxDate); + setIsInitialLoad(false); } } catch (error) { console.error("Error fetching metrics:", error); } finally { setLoading(false); } - }, [dateRange]); + }; // Handle date range changes const handleDateRangeChange = useCallback((startDate: string, endDate: string) => { setSelectedStartDate(startDate); setSelectedEndDate(endDate); fetchMetrics(startDate, endDate); - }, [fetchMetrics]); + }, []); useEffect(() => { // Redirect if not authenticated if (status === "unauthenticated") { router.push("/login"); - return; // Stop further execution in this effect + return; } // Fetch metrics and company on mount if authenticated - if (status === "authenticated") { - fetchMetrics(); + if (status === "authenticated" && isInitialLoad) { + fetchMetrics(undefined, undefined, true); } - }, [status, router, fetchMetrics]); // Add fetchMetrics to dependency array + }, [status, router, isInitialLoad]); async function handleRefresh() { - if (isAuditor) return; // Prevent auditors from refreshing + if (isAuditor) return; try { setRefreshing(true); - // Make sure we have a company ID to send if (!company?.id) { setRefreshing(false); alert("Cannot refresh: Company ID is missing"); @@ -100,7 +122,6 @@ function DashboardContent() { }); if (res.ok) { - // Refetch metrics const metricsRes = await fetch("/api/dashboard/metrics"); const data = await metricsRes.json(); setMetrics(data.metrics); @@ -113,70 +134,129 @@ function DashboardContent() { } } - // Calculate sentiment distribution - const getSentimentData = () => { - if (!metrics) return { positive: 0, neutral: 0, negative: 0 }; - - if ( - metrics.sentimentPositiveCount !== undefined && - metrics.sentimentNeutralCount !== undefined && - metrics.sentimentNegativeCount !== undefined - ) { - return { - positive: metrics.sentimentPositiveCount, - neutral: metrics.sentimentNeutralCount, - negative: metrics.sentimentNegativeCount, - }; - } - - const total = metrics.totalSessions || 1; - return { - positive: Math.round(total * 0.6), - neutral: Math.round(total * 0.3), - negative: Math.round(total * 0.1), - }; - }; - - // Prepare token usage data - const getTokenData = () => { - if (!metrics || !metrics.tokensByDay) { - return { labels: [], values: [], costs: [] }; - } - - const days = Object.keys(metrics.tokensByDay).sort(); - const labels = days.slice(-7); - const values = labels.map((day) => metrics.tokensByDay?.[day] || 0); - const costs = labels.map((day) => metrics.tokensCostByDay?.[day] || 0); - - return { labels, values, costs }; - }; - // Show loading state while session status is being determined if (status === "loading") { - return
Loading session...
; + return ( +
+
+
+

Loading session...

+
+
+ ); } - // If unauthenticated and not redirected yet (should be handled by useEffect, but as a fallback) if (status === "unauthenticated") { - return
Redirecting to login...
; + return ( +
+
+

Redirecting to login...

+
+
+ ); } - if (!metrics || !company) { - return
Loading dashboard...
; + if (loading || !metrics || !company) { + return ( +
+ {/* Header Skeleton */} + + +
+
+ + +
+
+ + +
+
+
+
+ + {/* Metrics Grid Skeleton */} +
+ {Array.from({ length: 8 }).map((_, i) => ( + + ))} +
+ + {/* Charts Skeleton */} +
+ + + + + + + + + + + + + + + + +
+
+ ); } - // Function to prepare word cloud data from metrics.wordCloudData + // Data preparation functions + const getSentimentData = () => { + if (!metrics) return []; + + const sentimentData = { + positive: metrics.sentimentPositiveCount ?? 0, + neutral: metrics.sentimentNeutralCount ?? 0, + negative: metrics.sentimentNegativeCount ?? 0, + }; + + return [ + { name: "Positive", value: sentimentData.positive, color: "hsl(var(--chart-1))" }, + { name: "Neutral", value: sentimentData.neutral, color: "hsl(var(--chart-2))" }, + { name: "Negative", value: sentimentData.negative, color: "hsl(var(--chart-3))" }, + ]; + }; + + const getSessionsOverTimeData = () => { + if (!metrics?.days) return []; + + return Object.entries(metrics.days).map(([date, value]) => ({ + date: new Date(date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), + value: value as number, + })); + }; + + const getCategoriesData = () => { + if (!metrics?.categories) return []; + + return Object.entries(metrics.categories).map(([name, value]) => ({ + name: name.length > 15 ? name.substring(0, 15) + '...' : name, + value: value as number, + })); + }; + + const getLanguagesData = () => { + if (!metrics?.languages) return []; + + return Object.entries(metrics.languages).map(([name, value]) => ({ + name, + value: value as number, + })); + }; + const getWordCloudData = (): WordCloudWord[] => { - if (!metrics || !metrics.wordCloudData) return []; + if (!metrics?.wordCloudData) return []; return metrics.wordCloudData; }; - // Function to prepare country data for the map using actual metrics const getCountryData = () => { - if (!metrics || !metrics.countries) return {}; - - // Convert the countries object from metrics to the format expected by GeographicMap - const result = Object.entries(metrics.countries).reduce( + if (!metrics?.countries) return {}; + return Object.entries(metrics.countries).reduce( (acc, [code, count]) => { if (code && count) { acc[code] = count; @@ -185,11 +265,8 @@ function DashboardContent() { }, {} as Record ); - - return result; }; - // Function to prepare response time distribution data const getResponseTimeData = () => { const avgTime = metrics.avgResponseTime || 1.5; const simulatedData: number[] = []; @@ -204,62 +281,56 @@ function DashboardContent() { return (
- -
-
-

{company.name}

-

- Dashboard updated{" "} - - {new Date(metrics.lastUpdated || Date.now()).toLocaleString()} - -

-
-
- - -
-
+ {/* Modern Header */} + + +
+
+
+

{company.name}

+ + Analytics Dashboard + +
+

+ Last updated{" "} + + {new Date(metrics.lastUpdated || Date.now()).toLocaleString()} + +

+
+ +
+ + + + + + + + signOut({ callbackUrl: "/login" })}> + + Sign out + + + +
+
+
+
- {/* Date Range Picker */} - {dateRange && ( + {/* Date Range Picker - Temporarily disabled to debug infinite loop */} + {/* {dateRange && ( - )} + )} */} -
+ {/* Modern Metrics Grid */} +
- - - } + value={metrics.totalSessions?.toLocaleString()} + icon={} trend={{ value: metrics.sessionTrend ?? 0, isPositive: (metrics.sessionTrend ?? 0) >= 0, }} + variant="primary" /> + - - - } + value={metrics.uniqueUsers?.toLocaleString()} + icon={} trend={{ value: metrics.usersTrend ?? 0, isPositive: (metrics.usersTrend ?? 0) >= 0, }} + variant="success" /> + - - - } + icon={} trend={{ value: metrics.avgSessionTimeTrend ?? 0, isPositive: (metrics.avgSessionTimeTrend ?? 0) >= 0, }} /> + - - - } + icon={} trend={{ value: metrics.avgResponseTimeTrend ?? 0, - isPositive: (metrics.avgResponseTimeTrend ?? 0) <= 0, // Lower response time is better + isPositive: (metrics.avgResponseTimeTrend ?? 0) <= 0, }} + variant="warning" /> + - - - } + icon={} + description="Average per day" /> + - - - } + icon={} + description="Busiest hour" /> + - - - } + icon={} trend={{ value: metrics.resolvedChatsPercentage ?? 0, - isPositive: (metrics.resolvedChatsPercentage ?? 0) >= 80, // 80%+ resolution rate is good + isPositive: (metrics.resolvedChatsPercentage ?? 0) >= 80, }} + variant={metrics.resolvedChatsPercentage && metrics.resolvedChatsPercentage >= 80 ? "success" : "warning"} + /> + + } + description="Languages detected" />
+ {/* Charts Section */}
-
-

- Sessions Over Time -

- -
-
-

- Conversation Sentiment -

- -
+ + +
-
-

- Sessions by Category -

- -
-
-

- Languages Used -

- -
+ + +
+ {/* Geographic and Topics Section */}
-
-

- Geographic Distribution -

- -
+ + + + + Geographic Distribution + + + + + + -
-

- Common Topics -

-
- -
-
+ + + + + Common Topics + + + +
+ +
+
+
{/* Top Questions Chart */} -
-

- Response Time Distribution -

- -
-
-
-

- Token Usage & Costs -

-
-
- Total Tokens: - {metrics.totalTokens?.toLocaleString() || 0} -
-
- Total Cost:€ - {metrics.totalTokensEur?.toFixed(4) || 0} + {/* Response Time Distribution */} + + + Response Time Distribution + + + + + + + {/* Token Usage Summary */} + + +
+ AI Usage & Costs +
+ + Total Tokens: + {metrics.totalTokens?.toLocaleString() || 0} + + + Total Cost: + €{metrics.totalTokensEur?.toFixed(4) || 0} +
-
- -
+ + +
+

Token usage chart will be implemented with historical data

+
+
+
); } -// Our exported component export default function DashboardPage() { return ; } diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 71504ef..4d909f6 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -4,6 +4,19 @@ import { useSession } from "next-auth/react"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { FC } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + BarChart3, + MessageSquare, + Settings, + Users, + ArrowRight, + TrendingUp, + Shield, + Zap, +} from "lucide-react"; const DashboardPage: FC = () => { const { data: session, status } = useSession(); @@ -21,82 +34,223 @@ const DashboardPage: FC = () => { if (loading) { return ( -
-
-
-

Loading dashboard...

+
+
+
+

Loading dashboard...

); } + const navigationCards = [ + { + title: "Analytics Overview", + description: "View comprehensive metrics, charts, and insights from your chat sessions", + icon: , + href: "/dashboard/overview", + variant: "primary" as const, + features: ["Real-time metrics", "Interactive charts", "Trend analysis"], + }, + { + title: "Session Browser", + description: "Browse, search, and analyze individual conversation sessions", + icon: , + href: "/dashboard/sessions", + variant: "success" as const, + features: ["Session search", "Conversation details", "Export data"], + }, + ...(session?.user?.role === "ADMIN" + ? [ + { + title: "Company Settings", + description: "Configure company settings, integrations, and API connections", + icon: , + href: "/dashboard/company", + variant: "warning" as const, + features: ["API configuration", "Integration settings", "Data management"], + adminOnly: true, + }, + { + title: "User Management", + description: "Invite team members and manage user accounts and permissions", + icon: , + href: "/dashboard/users", + variant: "default" as const, + features: ["User invitations", "Role management", "Access control"], + adminOnly: true, + }, + ] + : []), + ]; + + const getCardClasses = (variant: string) => { + switch (variant) { + case "primary": + return "border-primary/20 bg-gradient-to-br from-primary/5 to-primary/10 hover:from-primary/10 hover:to-primary/15"; + case "success": + return "border-green-200 bg-gradient-to-br from-green-50 to-green-100 hover:from-green-100 hover:to-green-150 dark:border-green-800 dark:from-green-950 dark:to-green-900"; + case "warning": + return "border-amber-200 bg-gradient-to-br from-amber-50 to-amber-100 hover:from-amber-100 hover:to-amber-150 dark:border-amber-800 dark:from-amber-950 dark:to-amber-900"; + default: + return "border-border bg-gradient-to-br from-card to-muted/20 hover:from-muted/30 hover:to-muted/40"; + } + }; + + const getIconClasses = (variant: string) => { + switch (variant) { + case "primary": + return "bg-primary/10 text-primary border-primary/20"; + case "success": + return "bg-green-100 text-green-600 border-green-200 dark:bg-green-900 dark:text-green-400 dark:border-green-800"; + case "warning": + return "bg-amber-100 text-amber-600 border-amber-200 dark:bg-amber-900 dark:text-amber-400 dark:border-amber-800"; + default: + return "bg-muted text-muted-foreground border-border"; + } + }; + return ( -
-
-

Dashboard

- -
-
-

Analytics

-

- View your chat session metrics and analytics -

- -
- -
-

Sessions

-

- Browse and analyze conversation sessions -

- -
- - {session?.user?.role === "ADMIN" && ( -
-

- Company Settings -

-

- Configure company settings and integrations +

+ {/* Welcome Header */} + + +
+
+
+

+ Welcome back, {session?.user?.name || "User"}! +

+ + {session?.user?.role} + +
+

+ Choose a section below to explore your analytics dashboard

-
- )} + +
+
+ + Secure Dashboard +
+
+
+
+
- {session?.user?.role === "ADMIN" && ( -
-

- User Management -

-

- Invite and manage user accounts -

- -
- )} -
+ + {card.title === "Analytics Overview" && "View Analytics"} + {card.title === "Session Browser" && "Browse Sessions"} + {card.title === "Company Settings" && "Manage Settings"} + {card.title === "User Management" && "Manage Users"} + + + + + + ))}
+ + {/* Quick Stats */} + + + + + Quick Stats + + + +
+
+
+ + Real-time +
+

Data updates

+
+ +
+
+ + Secure +
+

Data protection

+
+ +
+
+ + Advanced +
+

Analytics

+
+
+
+
); }; diff --git a/app/globals.css b/app/globals.css index f1d8c73..f4c1e9b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1 +1,120 @@ @import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/page.tsx b/app/page.tsx index 20ca100..9ab18d1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; -import { authOptions } from "../pages/api/auth/[...nextauth]"; +import { authOptions } from "./api/auth/[...nextauth]/route"; export default async function HomePage() { const session = await getServerSession(authOptions); diff --git a/app/providers.tsx b/app/providers.tsx index 99b83c6..aecc7f9 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -7,9 +7,9 @@ export function Providers({ children }: { children: ReactNode }) { // Including error handling and refetch interval for better user experience return ( {children} diff --git a/components.json b/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/DateRangePicker.tsx b/components/DateRangePicker.tsx index afd7f40..4b70a0a 100644 --- a/components/DateRangePicker.tsx +++ b/components/DateRangePicker.tsx @@ -21,9 +21,9 @@ export default function DateRangePicker({ const [endDate, setEndDate] = useState(initialEndDate || maxDate); useEffect(() => { - // Notify parent component when dates change + // Only notify parent component when dates change, not when the callback changes onDateRangeChange(startDate, endDate); - }, [startDate, endDate, onDateRangeChange]); + }, [startDate, endDate]); const handleStartDateChange = (newStartDate: string) => { // Ensure start date is not before min date diff --git a/components/ResponseTimeDistribution.tsx b/components/ResponseTimeDistribution.tsx index 2a42daa..dc97668 100644 --- a/components/ResponseTimeDistribution.tsx +++ b/components/ResponseTimeDistribution.tsx @@ -1,10 +1,15 @@ "use client"; -import { useRef, useEffect } from "react"; -import Chart from "chart.js/auto"; -import annotationPlugin from "chartjs-plugin-annotation"; - -Chart.register(annotationPlugin); +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + ReferenceLine, +} from "recharts"; interface ResponseTimeDistributionProps { data: number[]; @@ -12,114 +17,145 @@ interface ResponseTimeDistributionProps { targetResponseTime?: number; } +const CustomTooltip = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+

+ + {payload[0].value} + {" "} + responses +

+
+ ); + } + return null; +}; + export default function ResponseTimeDistribution({ data, average, targetResponseTime, }: ResponseTimeDistributionProps) { - const ref = useRef(null); + if (!data || !data.length) { + return ( +
+ No response time data available +
+ ); + } - useEffect(() => { - if (!ref.current || !data || !data.length) return; + // Create bins for the histogram (0-1s, 1-2s, 2-3s, etc.) + const maxTime = Math.ceil(Math.max(...data)); + const bins = Array(Math.min(maxTime + 1, 10)).fill(0); - const ctx = ref.current.getContext("2d"); - if (!ctx) return; + // Count responses in each bin + data.forEach((time) => { + const binIndex = Math.min(Math.floor(time), bins.length - 1); + bins[binIndex]++; + }); - // Create bins for the histogram (0-1s, 1-2s, 2-3s, etc.) - const maxTime = Math.ceil(Math.max(...data)); - const bins = Array(Math.min(maxTime + 1, 10)).fill(0); + // Create chart data + const chartData = bins.map((count, i) => { + let label; + if (i === bins.length - 1 && bins.length < maxTime + 1) { + label = `${i}+ sec`; + } else { + label = `${i}-${i + 1} sec`; + } - // Count responses in each bin - data.forEach((time) => { - const binIndex = Math.min(Math.floor(time), bins.length - 1); - bins[binIndex]++; - }); + // Determine color based on response time + let color; + if (i <= 2) color = "hsl(var(--chart-1))"; // Green for fast + else if (i <= 5) color = "hsl(var(--chart-4))"; // Yellow for medium + else color = "hsl(var(--chart-3))"; // Red for slow - // Create labels for each bin - const labels = bins.map((_, i) => { - if (i === bins.length - 1 && bins.length < maxTime + 1) { - return `${i}+ seconds`; - } - return `${i}-${i + 1} seconds`; - }); + return { + name: label, + value: count, + color, + }; + }); - const chart = new Chart(ctx, { - type: "bar", - data: { - labels, - datasets: [ - { - label: "Responses", - data: bins, - backgroundColor: bins.map((_, i) => { - // Green for fast, yellow for medium, red for slow - if (i <= 2) return "rgba(34, 197, 94, 0.7)"; // Green - if (i <= 5) return "rgba(250, 204, 21, 0.7)"; // Yellow - return "rgba(239, 68, 68, 0.7)"; // Red - }), - borderWidth: 1, - }, - ], - }, - options: { - responsive: true, - plugins: { - legend: { display: false }, - annotation: { - annotations: { - averageLine: { - type: "line", - yMin: 0, - yMax: Math.max(...bins), - xMin: average, - xMax: average, - borderColor: "rgba(75, 192, 192, 1)", - borderWidth: 2, - label: { - display: true, - content: "Avg: " + average.toFixed(1) + "s", - position: "start", - }, - }, - targetLine: targetResponseTime - ? { - type: "line", - yMin: 0, - yMax: Math.max(...bins), - xMin: targetResponseTime, - xMax: targetResponseTime, - borderColor: "rgba(75, 192, 192, 0.7)", - borderWidth: 2, - label: { - display: true, - content: "Target", - position: "end", - }, - } - : undefined, - }, - }, - }, - scales: { - y: { - beginAtZero: true, - title: { - display: true, - text: "Number of Responses", - }, - }, - x: { - title: { - display: true, - text: "Response Time", - }, - }, - }, - }, - }); + return ( +
+ + + + + + } /> + + + {chartData.map((entry, index) => ( + + ))} + - return () => chart.destroy(); - }, [data, average, targetResponseTime]); + {/* Average line */} + - return ; + {/* Target line (if provided) */} + {targetResponseTime && ( + + )} + + +
+ ); } diff --git a/components/charts/bar-chart.tsx b/components/charts/bar-chart.tsx new file mode 100644 index 0000000..9c1b754 --- /dev/null +++ b/components/charts/bar-chart.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Cell, +} from "recharts"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface BarChartProps { + data: Array<{ name: string; value: number; [key: string]: any }>; + title?: string; + dataKey?: string; + colors?: string[]; + height?: number; + className?: string; +} + +const CustomTooltip = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+

+ + {payload[0].value} + {" "} + sessions +

+
+ ); + } + return null; +}; + +export default function ModernBarChart({ + data, + title, + dataKey = "value", + colors = [ + "hsl(var(--chart-1))", + "hsl(var(--chart-2))", + "hsl(var(--chart-3))", + "hsl(var(--chart-4))", + "hsl(var(--chart-5))", + ], + height = 300, + className, +}: BarChartProps) { + return ( + + {title && ( + + {title} + + )} + + + + + + + } /> + + {data.map((entry, index) => ( + + ))} + + + + + + ); +} diff --git a/components/charts/donut-chart.tsx b/components/charts/donut-chart.tsx new file mode 100644 index 0000000..57f44a7 --- /dev/null +++ b/components/charts/donut-chart.tsx @@ -0,0 +1,122 @@ +"use client"; + +import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip, Legend } from "recharts"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface DonutChartProps { + data: Array<{ name: string; value: number; color?: string }>; + title?: string; + centerText?: { + title: string; + value: string | number; + }; + colors?: string[]; + height?: number; + className?: string; +} + +const CustomTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + const data = payload[0]; + return ( +
+

{data.name}

+

+ + {data.value} + {" "} + sessions ({((data.value / data.payload.total) * 100).toFixed(1)}%) +

+
+ ); + } + return null; +}; + +const CustomLegend = ({ payload }: any) => { + return ( +
+ {payload.map((entry: any, index: number) => ( +
+
+ {entry.value} +
+ ))} +
+ ); +}; + +const CenterLabel = ({ centerText, total }: any) => { + if (!centerText) return null; + + return ( +
+
+

{centerText.value}

+

{centerText.title}

+
+
+ ); +}; + +export default function ModernDonutChart({ + data, + title, + centerText, + colors = [ + "hsl(var(--chart-1))", + "hsl(var(--chart-2))", + "hsl(var(--chart-3))", + "hsl(var(--chart-4))", + "hsl(var(--chart-5))", + ], + height = 300, + className, +}: DonutChartProps) { + const total = data.reduce((sum, item) => sum + item.value, 0); + const dataWithTotal = data.map(item => ({ ...item, total })); + + return ( + + {title && ( + + {title} + + )} + +
+ + + + {dataWithTotal.map((entry, index) => ( + + ))} + + } /> + } /> + + + +
+
+
+ ); +} diff --git a/components/charts/line-chart.tsx b/components/charts/line-chart.tsx new file mode 100644 index 0000000..c3a0f10 --- /dev/null +++ b/components/charts/line-chart.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Area, + AreaChart, +} from "recharts"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; + +interface LineChartProps { + data: Array<{ date: string; value: number; [key: string]: any }>; + title?: string; + dataKey?: string; + color?: string; + gradient?: boolean; + height?: number; + className?: string; +} + +const CustomTooltip = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + return ( +
+

{label}

+

+ + {payload[0].value} + {" "} + sessions +

+
+ ); + } + return null; +}; + +export default function ModernLineChart({ + data, + title, + dataKey = "value", + color = "hsl(var(--primary))", + gradient = true, + height = 300, + className, +}: LineChartProps) { + const ChartComponent = gradient ? AreaChart : LineChart; + + return ( + + {title && ( + + {title} + + )} + + + + + {gradient && ( + + + + + )} + + + + + } /> + + {gradient ? ( + + ) : ( + + )} + + + + + ); +} diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..0205413 --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..a2df8dc --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant, + size, + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..d05bbc6 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,92 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +function Card({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +function CardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { + Card, + CardHeader, + CardFooter, + CardTitle, + CardAction, + CardDescription, + CardContent, +} diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx new file mode 100644 index 0000000..ec51e9c --- /dev/null +++ b/components/ui/dropdown-menu.tsx @@ -0,0 +1,257 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" + +import { cn } from "@/lib/utils" + +function DropdownMenu({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function DropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuItem({ + className, + inset, + variant = "default", + ...props +}: React.ComponentProps & { + inset?: boolean + variant?: "default" | "destructive" +}) { + return ( + + ) +} + +function DropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ) +} + +function DropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + ) +} + +function DropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function DropdownMenuShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ) +} + +function DropdownMenuSub({ + ...props +}: React.ComponentProps) { + return +} + +function DropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean +}) { + return ( + + {children} + + + ) +} + +function DropdownMenuSubContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + DropdownMenu, + DropdownMenuPortal, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuLabel, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuSub, + DropdownMenuSubTrigger, + DropdownMenuSubContent, +} diff --git a/components/ui/metric-card.tsx b/components/ui/metric-card.tsx new file mode 100644 index 0000000..47c98f5 --- /dev/null +++ b/components/ui/metric-card.tsx @@ -0,0 +1,163 @@ +"use client"; + +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn } from "@/lib/utils"; +import { TrendingUp, TrendingDown, Minus } from "lucide-react"; + +interface MetricCardProps { + title: string; + value: string | number | null | undefined; + description?: string; + icon?: React.ReactNode; + trend?: { + value: number; + label?: string; + isPositive?: boolean; + }; + variant?: "default" | "primary" | "success" | "warning" | "danger"; + isLoading?: boolean; + className?: string; +} + +export default function MetricCard({ + title, + value, + description, + icon, + trend, + variant = "default", + isLoading = false, + className, +}: MetricCardProps) { + if (isLoading) { + return ( + + +
+ + +
+
+ + + + +
+ ); + } + + const getVariantClasses = () => { + switch (variant) { + case "primary": + return "border-primary/20 bg-gradient-to-br from-primary/5 to-primary/10"; + case "success": + return "border-green-200 bg-gradient-to-br from-green-50 to-green-100 dark:border-green-800 dark:from-green-950 dark:to-green-900"; + case "warning": + return "border-amber-200 bg-gradient-to-br from-amber-50 to-amber-100 dark:border-amber-800 dark:from-amber-950 dark:to-amber-900"; + case "danger": + return "border-red-200 bg-gradient-to-br from-red-50 to-red-100 dark:border-red-800 dark:from-red-950 dark:to-red-900"; + default: + return "border-border bg-gradient-to-br from-card to-muted/20"; + } + }; + + const getIconClasses = () => { + switch (variant) { + case "primary": + return "bg-primary/10 text-primary border-primary/20"; + case "success": + return "bg-green-100 text-green-600 border-green-200 dark:bg-green-900 dark:text-green-400 dark:border-green-800"; + case "warning": + return "bg-amber-100 text-amber-600 border-amber-200 dark:bg-amber-900 dark:text-amber-400 dark:border-amber-800"; + case "danger": + return "bg-red-100 text-red-600 border-red-200 dark:bg-red-900 dark:text-red-400 dark:border-red-800"; + default: + return "bg-muted text-muted-foreground border-border"; + } + }; + + const getTrendIcon = () => { + if (!trend) return null; + + if (trend.value === 0) { + return ; + } + + return trend.isPositive !== false ? ( + + ) : ( + + ); + }; + + const getTrendColor = () => { + if (!trend || trend.value === 0) return "text-muted-foreground"; + return trend.isPositive !== false ? "text-green-600 dark:text-green-400" : "text-red-600 dark:text-red-400"; + }; + + return ( + + {/* Subtle gradient overlay */} +
+ + +
+
+

+ {title} +

+ {description && ( +

+ {description} +

+ )} +
+ + {icon && ( +
+ {icon} +
+ )} +
+
+ + +
+
+

+ {value ?? "—"} +

+ + {trend && ( + + {getTrendIcon()} + {Math.abs(trend.value).toFixed(1)}% + {trend.label && ` ${trend.label}`} + + )} +
+
+
+ + ); +} diff --git a/components/ui/separator.tsx b/components/ui/separator.tsx new file mode 100644 index 0000000..275381c --- /dev/null +++ b/components/ui/separator.tsx @@ -0,0 +1,28 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +function Separator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Separator } diff --git a/components/ui/skeleton.tsx b/components/ui/skeleton.tsx new file mode 100644 index 0000000..32ea0ef --- /dev/null +++ b/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx new file mode 100644 index 0000000..4ee26b3 --- /dev/null +++ b/components/ui/tooltip.tsx @@ -0,0 +1,61 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function Tooltip({ + ...props +}: React.ComponentProps) { + return ( + + + + ) +} + +function TooltipTrigger({ + ...props +}: React.ComponentProps) { + return +} + +function TooltipContent({ + className, + sideOffset = 0, + children, + ...props +}: React.ComponentProps) { + return ( + + + {children} + + + + ) +} + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/lib/utils.ts b/lib/utils.ts new file mode 100644 index 0000000..bd0c391 --- /dev/null +++ b/lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/package.json b/package.json index a11d9c0..4aad697 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,12 @@ "lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"!.trunk/**\" \"!.venv/**\" \"!node_modules/**\"" }, "dependencies": { + "@prisma/adapter-pg": "^6.10.1", "@prisma/client": "^6.10.1", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-tooltip": "^1.2.7", "@rapideditor/country-coder": "^5.4.0", "@types/d3": "^7.4.3", "@types/d3-cloud": "^1.2.9", @@ -34,8 +39,8 @@ "@types/leaflet": "^1.9.18", "@types/node-fetch": "^2.6.12", "bcryptjs": "^3.0.2", - "chart.js": "^4.0.0", - "chartjs-plugin-annotation": "^3.1.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "csv-parse": "^5.5.0", "d3": "^7.9.0", "d3-cloud": "^1.2.7", @@ -43,16 +48,18 @@ "i18n-iso-countries": "^7.14.0", "iso-639-1": "^3.1.5", "leaflet": "^1.9.4", + "lucide-react": "^0.525.0", "next": "^15.3.2", "next-auth": "^4.24.11", "node-cron": "^4.0.7", "node-fetch": "^3.3.2", "react": "^19.1.0", - "react-chartjs-2": "^5.0.0", "react-dom": "^19.1.0", "react-leaflet": "^5.0.0", "react-markdown": "^10.1.0", - "rehype-raw": "^7.0.0" + "recharts": "^3.0.2", + "rehype-raw": "^7.0.0", + "tailwind-merge": "^3.3.1" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", @@ -82,6 +89,7 @@ "tailwindcss": "^4.1.7", "ts-node": "^10.9.2", "tsx": "^4.20.3", + "tw-animate-css": "^1.3.4", "typescript": "^5.0.0", "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.4" diff --git a/pages/api/dashboard/config.ts b/pages/api/dashboard/config.ts deleted file mode 100644 index 55a1345..0000000 --- a/pages/api/dashboard/config.ts +++ /dev/null @@ -1,36 +0,0 @@ -// API endpoint: update company CSV URL config -import { NextApiRequest, NextApiResponse } from "next"; -import { getServerSession } from "next-auth"; -import { prisma } from "../../../lib/prisma"; -import { authOptions } from "../auth/[...nextauth]"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const session = await getServerSession(req, res, authOptions); - if (!session?.user) return res.status(401).json({ error: "Not logged in" }); - - const user = await prisma.user.findUnique({ - where: { email: session.user.email as string }, - }); - - if (!user) return res.status(401).json({ error: "No user" }); - - if (req.method === "POST") { - const { csvUrl } = req.body; - await prisma.company.update({ - where: { id: user.companyId }, - data: { csvUrl }, - }); - res.json({ ok: true }); - } else if (req.method === "GET") { - // Get company data - const company = await prisma.company.findUnique({ - where: { id: user.companyId }, - }); - res.json({ company }); - } else { - res.status(405).end(); - } -} diff --git a/pages/api/dashboard/settings.ts b/pages/api/dashboard/settings.ts deleted file mode 100644 index 5bd33f8..0000000 --- a/pages/api/dashboard/settings.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import { getServerSession } from "next-auth"; -import { prisma } from "../../../lib/prisma"; -import { authOptions } from "../auth/[...nextauth]"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const session = await getServerSession(req, res, authOptions); - if (!session?.user || session.user.role !== "ADMIN") - return res.status(403).json({ error: "Forbidden" }); - - const user = await prisma.user.findUnique({ - where: { email: session.user.email as string }, - }); - - if (!user) return res.status(401).json({ error: "No user" }); - - if (req.method === "POST") { - const { csvUrl, csvUsername, csvPassword, sentimentThreshold } = req.body; - await prisma.company.update({ - where: { id: user.companyId }, - data: { - csvUrl, - csvUsername, - ...(csvPassword ? { csvPassword } : {}), - sentimentAlert: sentimentThreshold - ? parseFloat(sentimentThreshold) - : null, - }, - }); - res.json({ ok: true }); - } else { - res.status(405).end(); - } -} diff --git a/pages/api/dashboard/users.ts b/pages/api/dashboard/users.ts deleted file mode 100644 index dcdcc6f..0000000 --- a/pages/api/dashboard/users.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import crypto from "crypto"; -import { getServerSession } from "next-auth"; -import { prisma } from "../../../lib/prisma"; -import bcrypt from "bcryptjs"; -import { authOptions } from "../auth/[...nextauth]"; -// User type from prisma is used instead of the one in lib/types - -interface UserBasicInfo { - id: string; - email: string; - role: string; -} - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - const session = await getServerSession(req, res, authOptions); - if (!session?.user || session.user.role !== "ADMIN") - return res.status(403).json({ error: "Forbidden" }); - - const user = await prisma.user.findUnique({ - where: { email: session.user.email as string }, - }); - - if (!user) return res.status(401).json({ error: "No user" }); - - if (req.method === "GET") { - const users = await prisma.user.findMany({ - where: { companyId: user.companyId }, - }); - - const mappedUsers: UserBasicInfo[] = users.map((u) => ({ - id: u.id, - email: u.email, - role: u.role, - })); - - res.json({ users: mappedUsers }); - } else if (req.method === "POST") { - const { email, role } = req.body; - if (!email || !role) - return res.status(400).json({ error: "Missing fields" }); - const exists = await prisma.user.findUnique({ where: { email } }); - if (exists) return res.status(409).json({ error: "Email exists" }); - const tempPassword = crypto.randomBytes(12).toString("base64").slice(0, 12); // secure random initial password - await prisma.user.create({ - data: { - email, - password: await bcrypt.hash(tempPassword, 10), - companyId: user.companyId, - role, - }, - }); - // TODO: Email user their temp password (stub, for demo) - Implement a robust and secure email sending mechanism. Consider using a transactional email service. - res.json({ ok: true, tempPassword }); - } else res.status(405).end(); -} diff --git a/pages/api/forgot-password.ts b/pages/api/forgot-password.ts deleted file mode 100644 index fab8aca..0000000 --- a/pages/api/forgot-password.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { prisma } from "../../lib/prisma"; -import { sendEmail } from "../../lib/sendEmail"; -import crypto from "crypto"; -import type { NextApiRequest, NextApiResponse } from "next"; - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse -) { - if (req.method !== "POST") { - res.setHeader("Allow", ["POST"]); - return res.status(405).end(`Method ${req.method} Not Allowed`); - } - - // Type the body with a type assertion - const { email } = req.body as { email: string }; - - const user = await prisma.user.findUnique({ where: { email } }); - if (!user) return res.status(200).end(); // always 200 for privacy - - const token = crypto.randomBytes(32).toString("hex"); - const expiry = new Date(Date.now() + 1000 * 60 * 30); // 30 min expiry - await prisma.user.update({ - where: { email }, - data: { resetToken: token, resetTokenExpiry: expiry }, - }); - - const resetUrl = `${process.env.NEXTAUTH_URL || "http://localhost:3000"}/reset-password?token=${token}`; - await sendEmail(email, "Password Reset", `Reset your password: ${resetUrl}`); - res.status(200).end(); -} diff --git a/pages/api/register.ts b/pages/api/register.ts deleted file mode 100644 index b56bd12..0000000 --- a/pages/api/register.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NextApiRequest, NextApiResponse } from "next"; -import { prisma } from "../../lib/prisma"; -import bcrypt from "bcryptjs"; -import { ApiResponse } from "../../lib/types"; - -interface RegisterRequestBody { - email: string; - password: string; - company: string; - csvUrl?: string; -} - -export default async function handler( - req: NextApiRequest, - res: NextApiResponse> -) { - if (req.method !== "POST") return res.status(405).end(); - - const { email, password, company, csvUrl } = req.body as RegisterRequestBody; - - if (!email || !password || !company) { - return res.status(400).json({ - success: false, - error: "Missing required fields", - }); - } - - // Check if email exists - const exists = await prisma.user.findUnique({ - where: { email }, - }); - - if (exists) { - return res.status(409).json({ - success: false, - error: "Email already exists", - }); - } - - const newCompany = await prisma.company.create({ - data: { name: company, csvUrl: csvUrl || "" }, - }); - const hashed = await bcrypt.hash(password, 10); - await prisma.user.create({ - data: { - email, - password: hashed, - companyId: newCompany.id, - role: "ADMIN", - }, - }); - res.status(201).json({ - success: true, - data: { success: true }, - }); -} diff --git a/pages/api/reset-password.ts b/pages/api/reset-password.ts deleted file mode 100644 index 86bd4dd..0000000 --- a/pages/api/reset-password.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { prisma } from "../../lib/prisma"; -import bcrypt from "bcryptjs"; -import type { NextApiRequest, NextApiResponse } from "next"; // Import official Next.js types - -export default async function handler( - req: NextApiRequest, // Use official NextApiRequest - res: NextApiResponse // Use official NextApiResponse -) { - if (req.method !== "POST") { - res.setHeader("Allow", ["POST"]); // Good practice to set Allow header for 405 - return res.status(405).end(`Method ${req.method} Not Allowed`); - } - - // It's good practice to explicitly type the expected body for clarity and safety - const { token, password } = req.body as { token?: string; password?: string }; - - if (!token || !password) { - return res.status(400).json({ error: "Token and password are required." }); - } - - if (password.length < 8) { - // Example: Add password complexity rule - return res - .status(400) - .json({ error: "Password must be at least 8 characters long." }); - } - - try { - const user = await prisma.user.findFirst({ - where: { - resetToken: token, - resetTokenExpiry: { gte: new Date() }, - }, - }); - - if (!user) { - return res.status(400).json({ - error: "Invalid or expired token. Please request a new password reset.", - }); - } - - const hash = await bcrypt.hash(password, 10); - await prisma.user.update({ - where: { id: user.id }, - data: { - password: hash, - resetToken: null, - resetTokenExpiry: null, - }, - }); - - // Instead of just res.status(200).end(), send a success message - return res - .status(200) - .json({ message: "Password has been reset successfully." }); - } catch (error) { - console.error("Reset password error:", error); // Log the error for server-side debugging - // Provide a generic error message to the client - return res.status(500).json({ - error: "An internal server error occurred. Please try again later.", - }); - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19b9f08..5c63246 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,24 @@ importers: .: dependencies: + '@prisma/adapter-pg': + specifier: ^6.10.1 + version: 6.10.1(pg@8.16.3) '@prisma/client': specifier: ^6.10.1 version: 6.10.1(prisma@6.10.1(typescript@5.8.3))(typescript@5.8.3) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.15 + version: 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-separator': + specifier: ^1.1.7 + version: 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-tooltip': + specifier: ^1.2.7 + version: 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@rapideditor/country-coder': specifier: ^5.4.0 version: 5.4.0 @@ -35,12 +50,12 @@ importers: bcryptjs: specifier: ^3.0.2 version: 3.0.2 - chart.js: - specifier: ^4.0.0 - version: 4.4.9 - chartjs-plugin-annotation: - specifier: ^3.1.0 - version: 3.1.0(chart.js@4.4.9) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 csv-parse: specifier: ^5.5.0 version: 5.6.0 @@ -62,6 +77,9 @@ importers: leaflet: specifier: ^1.9.4 version: 1.9.4 + lucide-react: + specifier: ^0.525.0 + version: 0.525.0(react@19.1.0) next: specifier: ^15.3.2 version: 15.3.2(@babel/core@7.27.7)(@playwright/test@1.52.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -77,9 +95,6 @@ importers: react: specifier: ^19.1.0 version: 19.1.0 - react-chartjs-2: - specifier: ^5.0.0 - version: 5.3.0(chart.js@4.4.9)(react@19.1.0) react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) @@ -89,9 +104,15 @@ importers: react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.1.5)(react@19.1.0) + recharts: + specifier: ^3.0.2 + version: 3.0.2(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react-is@17.0.2)(react@19.1.0)(redux@5.0.1) rehype-raw: specifier: ^7.0.0 version: 7.0.0 + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 devDependencies: '@eslint/eslintrc': specifier: ^3.3.1 @@ -174,6 +195,9 @@ importers: tsx: specifier: ^4.20.3 version: 4.20.3 + tw-animate-css: + specifier: ^1.3.4 + version: 1.3.4 typescript: specifier: ^5.0.0 version: 5.8.3 @@ -513,6 +537,21 @@ packages: resolution: {integrity: sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.7.1': + resolution: {integrity: sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==} + + '@floating-ui/dom@1.7.1': + resolution: {integrity: sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==} + + '@floating-ui/react-dom@2.1.3': + resolution: {integrity: sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.9': + resolution: {integrity: sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -682,9 +721,6 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@kurkle/color@0.3.4': - resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} - '@napi-rs/wasm-runtime@0.2.10': resolution: {integrity: sha512-bCsCyeZEwVErsGmyPNSzwfwFn4OdxBj0mmv6hOFucB/k81Ojdu68RbZdxYsRQUPc9l6SU5F/cG+bXgWs3oUgsQ==} @@ -774,6 +810,11 @@ packages: engines: {node: '>=18'} hasBin: true + '@prisma/adapter-pg@6.10.1': + resolution: {integrity: sha512-4Kpz5EV1jEOsKDuKYMjfJKMiIIcsuR9Ou1B8zLzehYtB7/oi+1ooDoK1K+T7sMisHkP69aYat5j0dskxvJTgdQ==} + peerDependencies: + pg: ^8.11.3 + '@prisma/client@6.10.1': resolution: {integrity: sha512-Re4pMlcUsQsUTAYMK7EJ4Bw2kg3WfZAAlr8GjORJaK4VOP6LxRQUQ1TuLnxcF42XqGkWQ36q5CQF1yVadANQ6w==} engines: {node: '>=18.18'} @@ -792,6 +833,9 @@ packages: '@prisma/debug@6.10.1': resolution: {integrity: sha512-k2YT53cWxv9OLjW4zSYTZ6Z7j0gPfCzcr2Mj99qsuvlxr8WAKSZ2NcSR0zLf/mP4oxnYG842IMj3utTgcd7CaA==} + '@prisma/driver-adapter-utils@6.10.1': + resolution: {integrity: sha512-MJ7NiiMA5YQUD1aMHiOcLmRpW0U0NTpygyeuLMxHXnKbcq+HX/cy10qilFMLVzpveuIEHuwxziR67z6i0K1MKA==} + '@prisma/engines-version@6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c': resolution: {integrity: sha512-ZJFTsEqapiTYVzXya6TUKYDFnSWCNegfUiG5ik9fleQva5Sk3DNyyUi7X1+0ZxWFHwHDr6BZV5Vm+iwP+LlciA==} @@ -804,6 +848,311 @@ packages: '@prisma/get-platform@6.10.1': resolution: {integrity: sha512-4CY5ndKylcsce9Mv+VWp5obbR2/86SHOLVV053pwIkhVtT9C9A83yqiqI/5kJM9T1v1u1qco/bYjDKycmei9HA==} + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + 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 + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + 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 + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.10': + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + 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 + + '@radix-ui/react-dropdown-menu@2.1.15': + resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} + 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 + + '@radix-ui/react-focus-guards@1.1.2': + resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + 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 + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-menu@2.1.15': + resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} + 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 + + '@radix-ui/react-popper@1.2.7': + resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} + 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 + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + 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 + + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + 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 + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + 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 + + '@radix-ui/react-roving-focus@1.1.10': + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} + 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 + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + 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 + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-tooltip@1.2.7': + resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} + 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 + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + 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 + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rapideditor/country-coder@5.4.0': resolution: {integrity: sha512-5Kjy2hnDcJZnPpRXMrTNY+jTkwhenaniCD4K6oLdZHYnY0GSM8gIIkOmoB3UikVVcot5vhz6i0QVqbTSyxAvrQ==} engines: {node: '>=20'} @@ -815,6 +1164,17 @@ packages: react: ^19.0.0 react-dom: ^19.0.0 + '@reduxjs/toolkit@2.8.2': + resolution: {integrity: sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rolldown/pluginutils@1.0.0-beta.19': resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} @@ -928,6 +1288,12 @@ packages: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -1238,6 +1604,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@typescript-eslint/eslint-plugin@8.32.1': resolution: {integrity: sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1464,6 +1833,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -1610,15 +1983,6 @@ packages: character-reference-invalid@2.0.1: resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - chart.js@4.4.9: - resolution: {integrity: sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==} - engines: {pnpm: '>=8'} - - chartjs-plugin-annotation@3.1.0: - resolution: {integrity: sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==} - peerDependencies: - chart.js: '>=4.0.0' - check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -1627,9 +1991,16 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1859,6 +2230,9 @@ packages: supports-color: optional: true + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + decimal.js@10.5.0: resolution: {integrity: sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==} @@ -1895,6 +2269,9 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -1975,6 +2352,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.39.5: + resolution: {integrity: sha512-z9V0qU4lx1TBXDNFWfAASWk6RNU6c6+TJBKE+FLIg8u0XJ6Yw58Hi0yX8ftEouj6p1QARRlXLFfHbIli93BdQQ==} + esbuild@0.25.5: resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} engines: {node: '>=18'} @@ -2124,6 +2504,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + expect-type@1.2.1: resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} engines: {node: '>=12.0.0'} @@ -2237,6 +2620,10 @@ packages: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + get-proto@1.0.1: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} @@ -2374,6 +2761,9 @@ packages: resolution: {integrity: sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==} engines: {node: '>= 4'} + immer@10.1.1: + resolution: {integrity: sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2725,6 +3115,11 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lucide-react@0.525.0: + resolution: {integrity: sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -3079,6 +3474,40 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + pg-cloudflare@1.2.7: + resolution: {integrity: sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==} + + pg-connection-string@2.9.1: + resolution: {integrity: sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.10.1: + resolution: {integrity: sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.10.3: + resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.16.3: + resolution: {integrity: sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -3116,6 +3545,26 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-array@3.0.4: + resolution: {integrity: sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==} + engines: {node: '>=12'} + + postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + preact-render-to-string@5.2.6: resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} peerDependencies: @@ -3185,12 +3634,6 @@ packages: rbush@2.0.2: resolution: {integrity: sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA==} - react-chartjs-2@5.3.0: - resolution: {integrity: sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==} - peerDependencies: - chart.js: ^4.1.1 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom@19.1.0: resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==} peerDependencies: @@ -3215,14 +3658,72 @@ packages: '@types/react': '>=18' react: '>=18' + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.17.0: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + react@19.1.0: resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==} engines: {node: '>=0.10.0'} + recharts@3.0.2: + resolution: {integrity: sha512-eDc3ile9qJU9Dp/EekSthQPhAVPG48/uM47jk+PF7VBQngxeW3cwQpPHb/GHC1uqwyCRWXcIrDzuHRVrnRryoQ==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3240,6 +3741,9 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3369,6 +3873,10 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} @@ -3469,6 +3977,9 @@ packages: resolution: {integrity: sha512-2pR2ubZSV64f/vqm9eLPz/KOvR9Dm+Co/5ChLgeHl0yEDRc6h5hXHoxEQH8Y5Ljycozd3p1k5TTSVdzYGkPvLw==} engines: {node: ^14.18.0 || >=16.0.0} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwindcss@4.1.7: resolution: {integrity: sha512-kr1o/ErIdNhTz8uzAYL7TpaUuzKIE6QPQ4qmSdxnoX/lo+5wmUHQA6h3L5yIqEImSRnAAURDirLu/BgiXGPAhg==} @@ -3484,6 +3995,9 @@ packages: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3576,6 +4090,9 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tw-animate-css@1.3.4: + resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -3645,6 +4162,31 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -3661,6 +4203,9 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite-node@3.2.4: resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -3829,6 +4374,10 @@ packages: xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -4140,6 +4689,23 @@ snapshots: '@eslint/core': 0.14.0 levn: 0.4.1 + '@floating-ui/core@1.7.1': + dependencies: + '@floating-ui/utils': 0.2.9 + + '@floating-ui/dom@1.7.1': + dependencies: + '@floating-ui/core': 1.7.1 + '@floating-ui/utils': 0.2.9 + + '@floating-ui/react-dom@2.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/dom': 1.7.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + + '@floating-ui/utils@0.2.9': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4271,8 +4837,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@kurkle/color@0.3.4': {} - '@napi-rs/wasm-runtime@0.2.10': dependencies: '@emnapi/core': 1.4.3 @@ -4335,6 +4899,12 @@ snapshots: dependencies: playwright: 1.52.0 + '@prisma/adapter-pg@6.10.1(pg@8.16.3)': + dependencies: + '@prisma/driver-adapter-utils': 6.10.1 + pg: 8.16.3 + postgres-array: 3.0.4 + '@prisma/client@6.10.1(prisma@6.10.1(typescript@5.8.3))(typescript@5.8.3)': optionalDependencies: prisma: 6.10.1(typescript@5.8.3) @@ -4346,6 +4916,10 @@ snapshots: '@prisma/debug@6.10.1': {} + '@prisma/driver-adapter-utils@6.10.1': + dependencies: + '@prisma/debug': 6.10.1 + '@prisma/engines-version@6.10.1-1.9b628578b3b7cae625e8c927178f15a170e74a9c': {} '@prisma/engines@6.10.1': @@ -4365,6 +4939,284 @@ snapshots: dependencies: '@prisma/debug': 6.10.1 + '@radix-ui/primitive@1.1.2': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-context@1.1.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-direction@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-focus-guards@1.1.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-id@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-menu@2.1.15(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + aria-hidden: 1.2.6 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-remove-scroll: 2.7.1(@types/react@19.1.5)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-popper@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@floating-ui/react-dom': 2.1.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-direction': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-slot@1.2.3(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-id': 1.1.1(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@radix-ui/react-slot': 1.2.3(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.1.5)(react@19.1.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.1.5)(react@19.1.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.1.5)(react@19.1.0) + react: 19.1.0 + optionalDependencies: + '@types/react': 19.1.5 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.1.5(@types/react@19.1.5))(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + '@types/react-dom': 19.1.5(@types/react@19.1.5) + + '@radix-ui/rect@1.1.1': {} + '@rapideditor/country-coder@5.4.0': dependencies: which-polygon: 2.2.1 @@ -4375,6 +5227,18 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) + '@reduxjs/toolkit@2.8.2(react-redux@9.2.0(@types/react@19.1.5)(react@19.1.0)(redux@5.0.1))(react@19.1.0)': + dependencies: + '@standard-schema/spec': 1.0.0 + '@standard-schema/utils': 0.3.0 + immer: 10.1.1 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.1.0 + react-redux: 9.2.0(@types/react@19.1.5)(react@19.1.0)(redux@5.0.1) + '@rolldown/pluginutils@1.0.0-beta.19': {} '@rollup/rollup-android-arm-eabi@4.44.1': @@ -4443,6 +5307,10 @@ snapshots: '@sindresorhus/merge-streams@2.3.0': {} + '@standard-schema/spec@1.0.0': {} + + '@standard-schema/utils@0.3.0': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -4766,6 +5634,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@typescript-eslint/eslint-plugin@8.32.1(@typescript-eslint/parser@8.32.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -5006,6 +5876,10 @@ snapshots: argparse@2.0.1: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -5177,20 +6051,18 @@ snapshots: character-reference-invalid@2.0.1: {} - chart.js@4.4.9: - dependencies: - '@kurkle/color': 0.3.4 - - chartjs-plugin-annotation@3.1.0(chart.js@4.4.9): - dependencies: - chart.js: 4.4.9 - check-error@2.1.1: {} chownr@3.0.0: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + client-only@0.0.1: {} + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -5435,6 +6307,8 @@ snapshots: dependencies: ms: 2.1.3 + decimal.js-light@2.5.1: {} + decimal.js@10.5.0: {} decode-named-character-reference@1.1.0: @@ -5467,6 +6341,8 @@ snapshots: detect-libc@2.0.4: {} + detect-node-es@1.1.0: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -5604,6 +6480,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.39.5: {} + esbuild@0.25.5: optionalDependencies: '@esbuild/aix-ppc64': 0.25.5 @@ -5846,6 +6724,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.1: {} + expect-type@1.2.1: {} extend@3.0.2: {} @@ -5965,6 +6845,8 @@ snapshots: hasown: 2.0.2 math-intrinsics: 1.1.0 + get-nonce@1.0.1: {} + get-proto@1.0.1: dependencies: dunder-proto: 1.0.1 @@ -6154,6 +7036,8 @@ snapshots: ignore@7.0.4: {} + immer@10.1.1: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -6500,6 +7384,10 @@ snapshots: dependencies: yallist: 4.0.0 + lucide-react@0.525.0(react@19.1.0): + dependencies: + react: 19.1.0 + lz-string@1.5.0: {} magic-string@0.30.17: @@ -7037,6 +7925,41 @@ snapshots: pathval@2.0.1: {} + pg-cloudflare@1.2.7: + optional: true + + pg-connection-string@2.9.1: {} + + pg-int8@1.0.1: {} + + pg-pool@3.10.1(pg@8.16.3): + dependencies: + pg: 8.16.3 + + pg-protocol@1.10.3: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.16.3: + dependencies: + pg-connection-string: 2.9.1 + pg-pool: 3.10.1(pg@8.16.3) + pg-protocol: 1.10.3 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.2.7 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -7071,6 +7994,18 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-array@3.0.4: {} + + postgres-bytea@1.0.0: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + preact-render-to-string@5.2.6(preact@10.26.6): dependencies: preact: 10.26.6 @@ -7127,11 +8062,6 @@ snapshots: dependencies: quickselect: 1.1.1 - react-chartjs-2@5.3.0(chart.js@4.4.9)(react@19.1.0): - dependencies: - chart.js: 4.4.9 - react: 19.1.0 - react-dom@19.1.0(react@19.1.0): dependencies: react: 19.1.0 @@ -7166,10 +8096,72 @@ snapshots: transitivePeerDependencies: - supports-color + react-redux@9.2.0(@types/react@19.1.5)(react@19.1.0)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.1.0 + use-sync-external-store: 1.5.0(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + redux: 5.0.1 + react-refresh@0.17.0: {} + react-remove-scroll-bar@2.3.8(@types/react@19.1.5)(react@19.1.0): + dependencies: + react: 19.1.0 + react-style-singleton: 2.2.3(@types/react@19.1.5)(react@19.1.0) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.5 + + react-remove-scroll@2.7.1(@types/react@19.1.5)(react@19.1.0): + dependencies: + react: 19.1.0 + react-remove-scroll-bar: 2.3.8(@types/react@19.1.5)(react@19.1.0) + react-style-singleton: 2.2.3(@types/react@19.1.5)(react@19.1.0) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.1.5)(react@19.1.0) + use-sidecar: 1.1.3(@types/react@19.1.5)(react@19.1.0) + optionalDependencies: + '@types/react': 19.1.5 + + react-style-singleton@2.2.3(@types/react@19.1.5)(react@19.1.0): + dependencies: + get-nonce: 1.0.1 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.5 + react@19.1.0: {} + recharts@3.0.2(@types/react@19.1.5)(react-dom@19.1.0(react@19.1.0))(react-is@17.0.2)(react@19.1.0)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.8.2(react-redux@9.2.0(@types/react@19.1.5)(react@19.1.0)(redux@5.0.1))(react@19.1.0) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.39.5 + eventemitter3: 5.0.1 + immer: 10.1.1 + react: 19.1.0 + react-dom: 19.1.0(react@19.1.0) + react-is: 17.0.2 + react-redux: 9.2.0(@types/react@19.1.5)(react@19.1.0)(redux@5.0.1) + reselect: 5.1.1 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.5.0(react@19.1.0) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -7213,6 +8205,8 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + reselect@5.1.1: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -7398,6 +8392,8 @@ snapshots: space-separated-tokens@2.0.2: {} + split2@4.2.0: {} + stable-hash@0.0.5: {} stackback@0.0.2: {} @@ -7516,6 +8512,8 @@ snapshots: dependencies: '@pkgr/core': 0.2.4 + tailwind-merge@3.3.1: {} + tailwindcss@4.1.7: {} tapable@2.2.2: {} @@ -7535,6 +8533,8 @@ snapshots: glob: 10.4.5 minimatch: 9.0.5 + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -7619,6 +8619,8 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tw-animate-css@1.3.4: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -7736,6 +8738,25 @@ snapshots: dependencies: punycode: 2.3.1 + use-callback-ref@1.3.3(@types/react@19.1.5)(react@19.1.0): + dependencies: + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.5 + + use-sidecar@1.1.3(@types/react@19.1.5)(react@19.1.0): + dependencies: + detect-node-es: 1.1.0 + react: 19.1.0 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.1.5 + + use-sync-external-store@1.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + uuid@8.3.2: {} v8-compile-cache-lib@3.0.1: {} @@ -7755,6 +8776,23 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite-node@3.2.4(@types/node@22.15.21)(jiti@2.4.2)(lightningcss@1.30.1)(tsx@4.20.3): dependencies: cac: 6.7.14 @@ -7941,6 +8979,8 @@ snapshots: xmlchars@2.2.0: {} + xtend@4.0.2: {} + yallist@3.1.1: {} yallist@4.0.0: {} diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index cd3d49d..0000000 --- a/tailwind.config.js +++ /dev/null @@ -1,14 +0,0 @@ -/** @type {import('tailwindcss').Config} */ -const config = { - content: [ - "./pages/**/*.{js,ts,jsx,tsx,mdx}", - "./components/**/*.{js,ts,jsx,tsx,mdx}", - "./app/**/*.{js,ts,jsx,tsx,mdx}", - ], - theme: { - extend: {}, - }, - plugins: [], -}; - -export default config;