From 1465016b56f6640d27ea47230ef52643b8dc4f08 Mon Sep 17 00:00:00 2001 From: Hannah_Bagga Date: Thu, 25 Jun 2026 15:15:35 +0200 Subject: [PATCH] ..... --- ai-server/index.mjs | 2 +- database/migrations/012_invites.sql | 10 + package-lock.json | 867 +++++++++++++++------- package.json | 9 +- rust-ai/src/main.rs | 2 +- signaling-server.mjs | 221 ++++++ src/app/(dashboard)/chats/page.tsx | 11 +- src/app/api/auth/jwt/route.ts | 11 + src/app/api/conversations/route.ts | 13 +- src/app/api/invite/generate/route.ts | 25 + src/app/api/users/lookup/route.ts | 38 + src/app/call/[roomId]/page.tsx | 226 ++++++ src/app/join/[token]/page.tsx | 69 ++ src/app/login/page.tsx | 7 +- src/components/chats/voice-call-modal.tsx | 610 ++++++--------- src/data/users.ts | 9 + src/hooks/useWebRTCCall.ts | 508 +++++++++++++ src/lib/auth.ts | 4 +- src/middleware.ts | 1 + src/providers/user-provider.tsx | 1 + src/types/index.ts | 1 + 21 files changed, 1979 insertions(+), 666 deletions(-) create mode 100644 database/migrations/012_invites.sql create mode 100644 signaling-server.mjs create mode 100644 src/app/api/auth/jwt/route.ts create mode 100644 src/app/api/invite/generate/route.ts create mode 100644 src/app/api/users/lookup/route.ts create mode 100644 src/app/call/[roomId]/page.tsx create mode 100644 src/app/join/[token]/page.tsx create mode 100644 src/hooks/useWebRTCCall.ts diff --git a/ai-server/index.mjs b/ai-server/index.mjs index 22f9bed..cd605d7 100644 --- a/ai-server/index.mjs +++ b/ai-server/index.mjs @@ -29,7 +29,7 @@ try { const PORT = parseInt(process.env.AI_PORT || "3001", 10) const HOST = process.env.AI_HOST || "0.0.0.0" const OLLAMA_URL = process.env.OLLAMA_BASE_URL || "http://localhost:11434" -const MODEL = process.env.AI_MODEL || "sam860/dolphin3-llama3.2:3b" +const MODEL = process.env.AI_MODEL || "llama3.2:3b" const DATABASE_URL = process.env.DATABASE_URL const JOBS_PATH = process.env.JOBS_PATH || path.join(ROOT, "data", "ai", "jobs.jsonl") const AI_MD_PATH = process.env.AI_MD_PATH || path.join(ROOT, "data", "ai", "ai.md") diff --git a/database/migrations/012_invites.sql b/database/migrations/012_invites.sql new file mode 100644 index 0000000..016777d --- /dev/null +++ b/database/migrations/012_invites.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS invites ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + token VARCHAR(64) NOT NULL UNIQUE, + phone VARCHAR(50), + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '7 days' +); + +CREATE INDEX IF NOT EXISTS idx_invites_token ON invites(token); diff --git a/package-lock.json b/package-lock.json index 1821189..ef86181 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,22 +25,25 @@ "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", + "@supabase/ssr": "^0.12.0", + "@supabase/supabase-js": "^2.108.2", "@tanstack/react-table": "^8.20.6", "autoprefixer": "^10.4.20", "bcryptjs": "^3.0.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "emoji-mart": "^5.6.0", "framer-motion": "^11.15.0", "jose": "^6.2.3", "lucide-react": "^0.468.0", - "next": "^15.5.19", + "next": "15.0.4", "next-themes": "^0.4.4", "pg": "^8.21.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "recharts": "^2.15.0", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "vaul": "^1.1.2", @@ -53,7 +56,7 @@ "@types/react-dom": "^18", "concurrently": "^10.0.3", "eslint": "^9", - "eslint-config-next": "15.5.19", + "eslint-config-next": "15.0.4", "postcss": "^8.4.49", "tailwindcss": "^3.4.17", "typescript": "^5" @@ -360,20 +363,10 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=18" - } - }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], @@ -389,13 +382,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "cpu": [ "x64" ], @@ -411,13 +404,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], @@ -431,9 +424,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "cpu": [ "x64" ], @@ -447,9 +440,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "cpu": [ "arm" ], @@ -466,9 +459,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ "arm64" ], @@ -484,48 +477,10 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "cpu": [ "s390x" ], @@ -542,9 +497,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "cpu": [ "x64" ], @@ -561,9 +516,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "cpu": [ "arm64" ], @@ -580,9 +535,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], @@ -599,9 +554,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "cpu": [ "arm" ], @@ -620,13 +575,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "cpu": [ "arm64" ], @@ -645,63 +600,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "libc": [ - "glibc" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "cpu": [ "s390x" ], @@ -720,13 +625,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], @@ -745,13 +650,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ "arm64" ], @@ -770,13 +675,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], @@ -795,20 +700,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@emnapi/runtime": "^1.2.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -817,29 +722,10 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "cpu": [ "ia32" ], @@ -856,9 +742,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "cpu": [ "x64" ], @@ -928,15 +814,15 @@ } }, "node_modules/@next/env": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.19.tgz", - "integrity": "sha512-sWWluFvcv5v3Fxznmf2ZfjyoVQt/64oCnYqS90inQWGzMPK1VjvekPiz3OPHKmFT30EnHrjlbyaHLt3M0vWabw==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.4.tgz", + "integrity": "sha512-WNRvtgnRVDD4oM8gbUcRc27IAhaL4eXQ/2ovGbgLnPGUvdyDr8UdXP4Q/IBDdAdojnD2eScryIDirv0YUCjUVw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.19.tgz", - "integrity": "sha512-Ctwb4qYuMbHN/1oXLlTdMchwG8h8Xzwq+wGZZMgF3o6+uwyBKAI2c96bdOsl+C62PaUD0Jkh+QpNkhUeDlam0Q==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.0.4.tgz", + "integrity": "sha512-rbsF17XGzHtR7SDWzWpavSfum3/UdnF8bAaisnKwP//si3KWPTedVUsflAdjyK1zW3rweBjbALfKcavFneLGvg==", "dev": true, "license": "MIT", "dependencies": { @@ -944,9 +830,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.19.tgz", - "integrity": "sha512-jx9wWlTKueHKPvVOndyr7WuaevWCkuYqsQ8gC0TMPKAVWG3MhcdMrjfo9tvIZNXd0QOUYXXvAcZ325y8Uq7uzg==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.0.4.tgz", + "integrity": "sha512-QecQXPD0yRHxSXWL5Ff80nD+A56sUXZG9koUsjWJwA2Z0ZgVQfuy7gd0/otjxoOovPVHR2eVEvPMHbtZP+pf9w==", "cpu": [ "arm64" ], @@ -960,9 +846,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.19.tgz", - "integrity": "sha512-291KFcsIQ3OenRdiUDFOR6W3wezzH4auENXm1gbm1Bjd4ANMMRgxPrWTUztQN43BnVoVuMnHCrLeECIMwgFKbA==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.4.tgz", + "integrity": "sha512-pb7Bye3y1Og3PlCtnz2oO4z+/b3pH2/HSYkLbL0hbVuTGil7fPen8/3pyyLjdiTLcFJ+ymeU3bck5hd4IPFFCA==", "cpu": [ "x64" ], @@ -976,9 +862,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.19.tgz", - "integrity": "sha512-WeH+nelQyyMeE2f8FxBRZNrGipya5zHZV2vjzfCOAYyiI6am+NbnWAAldOBFQBB2w0DjJcsvrKqoFT2b7+5YoA==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.4.tgz", + "integrity": "sha512-12oSaBFjGpB227VHzoXF3gJoK2SlVGmFJMaBJSu5rbpaoT5OjP5OuCLuR9/jnyBF1BAWMs/boa6mLMoJPRriMA==", "cpu": [ "arm64" ], @@ -995,9 +881,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.19.tgz", - "integrity": "sha512-5xTOE0lDlDCSSfp+BAif7j17VRRCjWp//ZPZy6NI0QpdrhxtQnsZguSx0xAAZ0c9XZLrLLwCe/XVe5YPrRilKw==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.4.tgz", + "integrity": "sha512-QARO88fR/a+wg+OFC3dGytJVVviiYFEyjc/Zzkjn/HevUuJ7qGUUAUYy5PGVWY1YgTzeRYz78akQrVQ8r+sMjw==", "cpu": [ "arm64" ], @@ -1014,9 +900,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.19.tgz", - "integrity": "sha512-LTxRmMgqqMv05Had879W00Fm53quiJd3Zuz8h1JSNJ3nGSlbZ/7Tjs1tKyScgN3Au3t3MyPsjPlq60fMmSHLsg==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.4.tgz", + "integrity": "sha512-Z50b0gvYiUU1vLzfAMiChV8Y+6u/T2mdfpXPHraqpypP7yIT2UV9YBBhcwYkxujmCvGEcRTVWOj3EP7XW/wUnw==", "cpu": [ "x64" ], @@ -1033,9 +919,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.19.tgz", - "integrity": "sha512-eoNQSpA5PQfB9wBO4RA47MTDXWz1fizy9Y3Z6e4DetYIF3dvjuu8sj7aIGn/bFCU6lnFzTK34NtCaffP4NsQ7Q==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.4.tgz", + "integrity": "sha512-7H9C4FAsrTAbA/ENzvFWsVytqRYhaJYKa2B3fyQcv96TkOGVMcvyS6s+sj4jZlacxxTcn7ygaMXUPkEk7b78zw==", "cpu": [ "x64" ], @@ -1052,9 +938,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.19.tgz", - "integrity": "sha512-6UNt2dFuCHOe446sm/Kp69nUe8/wIhnh9bm6Xcqw4qEWCOppLMOvhTBVgvM7invVUNr4SPpP6NOQsACtn2IN9Q==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.4.tgz", + "integrity": "sha512-Z/v3WV5xRaeWlgJzN9r4PydWD8sXV35ywc28W63i37G2jnUgScA4OOgS8hQdiXLxE3gqfSuHTicUhr7931OXPQ==", "cpu": [ "arm64" ], @@ -1068,9 +954,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.19.tgz", - "integrity": "sha512-PhmojAHyqMne56HBLGu9dhDnHPuFmEjrXSQMM/nW0J6j849lk3ESrVtqNJcCk8CKOV7brpTTbaYAjwKPzKM69w==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.4.tgz", + "integrity": "sha512-NGLchGruagh8lQpDr98bHLyWJXOBSmkEAfK980OiNBa7vNm6PsNoPvzTfstT78WyOeMRQphEQ455rggd7Eo+Dw==", "cpu": [ "x64" ], @@ -2055,13 +1941,121 @@ "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", "dev": true }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@supabase/auth-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.108.2.tgz", + "integrity": "sha512-tNaQmBgodDZwgB40mRwVbxFy8IDYwjdpcZ0BYrWiwlULCSQoJj4QoG4zgJT7QRPXcqipefNOzvO/qAu4dF98ag==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.108.2.tgz", + "integrity": "sha512-RNUX8EiBy3iLwAX19jtRzLyePnl11/fHcgwDHLnpKcDSXt/5qBnh3LUwAtIjT21Q66QsmNUR2esrHziLCpNubw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/phoenix": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.4.tgz", + "integrity": "sha512-Gt0pqoXuIqX/8dvG0OKp/wMCobXNH3klNbUPBNyOfN0YA1IswrM3HyWFMOPk1Jy+BRaIyDPcFx4jLBwHNmlyfQ==", + "license": "MIT" + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.108.2.tgz", + "integrity": "sha512-GQ28/Y8hk3CFmkb3kXH1h/AQx6JIYSQfO0CJMRVBcEKZoNy6C45cXAZ4fcJvRC5Id0cs6xnkUV0+c0rIocigsw==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.108.2.tgz", + "integrity": "sha512-aAGxCSUemZvQIibnCdvNvgaKib28I4rfrNjKbQ9cG1uBLwUsI7hVpGXgEbypCCDhLjQlDTAiJlu7rgljYUT73g==", + "license": "MIT", + "dependencies": { + "@supabase/phoenix": "^0.4.2", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/ssr": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.12.0.tgz", + "integrity": "sha512-d9XV5XzJvzzZbeAIM7fWTCUYxQJZ2Ru6ny3dJHmHGp/LIrJ+o9FpD7N9Rf/UhhWEvHXSoDe8SI32Z2ouOdMjBg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.2" + }, + "peerDependencies": { + "@supabase/supabase-js": "^2.108.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.108.2.tgz", + "integrity": "sha512-TVZPQxXGxY2+A6yTtm77zUHsh70lBhYUEaJL8RQC+BghcX/ygiMG/rmXrNVBce30/WAeNPa8FiG8HbqlGeV05g==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.108.2", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.108.2.tgz", + "integrity": "sha512-hFhnPveb5JQg4a0QYicM0swT253YHMdfeRAl2BKHOlI5VAzuHxUGSr8RbwNLYNPauWOgQMS1H8sz8bvYlgwUfQ==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.108.2", + "@supabase/functions-js": "2.108.2", + "@supabase/postgrest-js": "2.108.2", + "@supabase/realtime-js": "2.108.2", + "@supabase/storage-js": "2.108.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.13.tgz", + "integrity": "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==", "license": "Apache-2.0", "dependencies": { - "tslib": "^2.8.0" + "tslib": "^2.4.0" } }, "node_modules/@tanstack/react-table": { @@ -2105,6 +2099,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", @@ -2181,7 +2184,6 @@ "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - "dev": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2222,6 +2224,15 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.61.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", @@ -2780,6 +2791,19 @@ "win32" ] }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -3137,6 +3161,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.37", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", @@ -3222,6 +3255,17 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -3397,11 +3441,25 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3413,7 +3471,18 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "devOptional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "node_modules/commander": { "version": "4.1.1", @@ -3480,6 +3549,36 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3682,7 +3781,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -3811,7 +3909,8 @@ "version": "5.6.0", "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", "integrity": "sha512-eJp3QRe79pjwa+duv+n7+5YsNhRcMl812EcFVwrnRvYKoNPoQb5qxU8DG6Bgwji0akHdp6D4Ln6tYLG58MFSow==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -3819,6 +3918,58 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true }, + "node_modules/engine.io": { + "version": "6.6.9", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", + "integrity": "sha512-clKkw4C7nJ22mGgoVcCg6V/W/TxdNyIOTr89k2ONZu81qqkddPFDF0LXcbAwhzPD8DjkiRCjzuiO6Y+fkpD4vg==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -4068,13 +4219,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.19.tgz", - "integrity": "sha512-UZwkuhBCNxVZfo93MSHRDOVNWXooJJGcAUyTAVIp0+9QFhH4SqJxWY0s6Mk9C2kMi777HPMn3dseOrZshWpG9Q==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.0.4.tgz", + "integrity": "sha512-97mLaAhbJKVQYXUBBrenRtEUAA6bNDPxWfaFEd6mEhKfpajP4wJrW4l7BUlHuYWxR8oQa9W014qBJpumpJQwWA==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.5.19", + "@next/eslint-plugin-next": "15.0.4", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -4082,7 +4233,7 @@ "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", - "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react": "^7.35.0", "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { @@ -4877,6 +5028,15 @@ "node": ">= 0.4" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4950,6 +5110,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT", + "optional": true + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -5589,6 +5756,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -5626,8 +5814,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/mz": { "version": "2.7.0", @@ -5678,14 +5865,26 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/next": { - "version": "15.5.19", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.19.tgz", - "integrity": "sha512-xNOW6tYshGX1/Oi3F8uuk4gpDeWsSUE/1Z0G5uUMekIxaQ0xc03UXd9II0VQHYMWviMeA0OHpJFAKsHf8bTYVg==", + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/next/-/next-15.0.4.tgz", + "integrity": "sha512-nuy8FH6M1FG0lktGotamQDCXhh5hZ19Vo0ht1AOIQWrYJLP598TIUagKtvJrfJ5AGwB/WmDqkKaKhMpVifvGPA==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", "license": "MIT", "dependencies": { - "@next/env": "15.5.19", - "@swc/helpers": "0.5.15", + "@next/env": "15.0.4", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.13", + "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -5697,22 +5896,22 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.19", - "@next/swc-darwin-x64": "15.5.19", - "@next/swc-linux-arm64-gnu": "15.5.19", - "@next/swc-linux-arm64-musl": "15.5.19", - "@next/swc-linux-x64-gnu": "15.5.19", - "@next/swc-linux-x64-musl": "15.5.19", - "@next/swc-win32-arm64-msvc": "15.5.19", - "@next/swc-win32-x64-msvc": "15.5.19", - "sharp": "^0.34.3" + "@next/swc-darwin-arm64": "15.0.4", + "@next/swc-darwin-x64": "15.0.4", + "@next/swc-linux-arm64-gnu": "15.0.4", + "@next/swc-linux-arm64-musl": "15.0.4", + "@next/swc-linux-x64-gnu": "15.0.4", + "@next/swc-linux-x64-musl": "15.0.4", + "@next/swc-win32-arm64-msvc": "15.0.4", + "@next/swc-win32-x64-msvc": "15.0.4", + "sharp": "^0.33.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", + "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react": "^18.2.0 || 19.0.0-rc-66855b96-20241106 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-66855b96-20241106 || ^19.0.0", "sass": "^1.3.0" }, "peerDependenciesMeta": { @@ -6856,16 +7055,16 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -6874,30 +7073,25 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, "node_modules/shebang-command": { @@ -7006,6 +7200,72 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz", + "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.21.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/sonner": { "version": "1.7.4", "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", @@ -7050,6 +7310,14 @@ "node": ">= 0.4" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -7631,8 +7899,7 @@ "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" }, "node_modules/unrs-resolver": { "version": "1.12.2", @@ -7756,6 +8023,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vaul": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", @@ -7929,6 +8205,35 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index e687c40..3bd629f 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,10 @@ "private": true, "scripts": { "dev": "npm run dev:precheck & npm run dev:ollama & npm run dev:start", - "dev:start": "concurrently -n AI,BROWSE,NEXT -c cyan,magenta,green \"npm run dev:rust\" \"npm run dev:browser-use\" \"npm run dev:next\"", + "dev:signaling": "node signaling-server.mjs", + "dev:start": "concurrently -n AI,BROWSE,SIGNAL,NEXT -c cyan,magenta,yellow,green \"npm run dev:rust\" \"npm run dev:browser-use\" \"npm run dev:signaling\" \"npm run dev:next\"", "dev:next": "next dev -p 3006", - "dev:precheck": "powershell -NoProfile -Command \"Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in 3001,3006,3008 } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue; Write-Host ('Freed port '+$_.LocalPort) }\"", + "dev:precheck": "powershell -NoProfile -Command \"Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in 3001,3006,3007,3008 } | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue; Write-Host ('Freed port '+$_.LocalPort) }\"", "dev:ollama": "powershell -NoProfile -Command \"$ollama = if (Get-Command ollama -ErrorAction SilentlyContinue) { 'ollama' } else { Join-Path $env:LOCALAPPDATA 'Programs\\Ollama\\ollama.exe' }; if (-not (Get-Process ollama -ErrorAction SilentlyContinue)) { Start-Process $ollama -ArgumentList 'serve' -WindowStyle Hidden; Start-Sleep 3 }; exit 0\"", "dev:rust": "node ai-server/index.mjs", "dev:browser-use": "set FX_PROFILE=C:\\Users\\USER-PC\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles\\h8p11vlj.default-release && cd browser-use-service && python main.py", @@ -32,6 +33,8 @@ "@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tooltip": "^1.1.8", + "@supabase/ssr": "^0.12.0", + "@supabase/supabase-js": "^2.108.2", "@tanstack/react-table": "^8.20.6", "autoprefixer": "^10.4.20", "bcryptjs": "^3.0.3", @@ -47,6 +50,8 @@ "react-dom": "^18.3.1", "react-hook-form": "^7.54.2", "recharts": "^2.15.0", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", "sonner": "^1.7.4", "tailwind-merge": "^2.6.0", "vaul": "^1.1.2", diff --git a/rust-ai/src/main.rs b/rust-ai/src/main.rs index ef33b3f..3f6ea0c 100644 --- a/rust-ai/src/main.rs +++ b/rust-ai/src/main.rs @@ -446,7 +446,7 @@ async fn main() { let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set"); let ollama_url = std::env::var("OLLAMA_BASE_URL").unwrap_or_else(|_| "http://localhost:11434".to_string()); - let model = std::env::var("AI_MODEL").unwrap_or_else(|_| "dolphin-phi".to_string()); + let model = std::env::var("AI_MODEL").unwrap_or_else(|_| "llama3.2:3b".to_string()); let host = std::env::var("AI_HOST").unwrap_or_else(|_| "127.0.0.1".to_string()); let port: u16 = std::env::var("AI_PORT").unwrap_or_else(|_| "3001".to_string()).parse().expect("AI_PORT must be a number"); diff --git a/signaling-server.mjs b/signaling-server.mjs new file mode 100644 index 0000000..7e1ddd6 --- /dev/null +++ b/signaling-server.mjs @@ -0,0 +1,221 @@ +import { createServer } from "http" +import { Server } from "socket.io" +import { SignJWT, jwtVerify } from "jose" +import pg from "pg" + +const PORT = process.env.SIGNALING_PORT || 3007 +const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET || "crm-envr-super-secret-key-2026") +const DATABASE_URL = process.env.DATABASE_URL || "postgres://postgres:postgres@localhost:5432/crm" + +const pool = new pg.Pool({ connectionString: DATABASE_URL }) + +const httpServer = createServer() +const io = new Server(httpServer, { cors: { origin: "*", methods: ["GET", "POST"] } }) + +const onlineUsers = new Map() +const rooms = new Map() + +io.use((socket, next) => { + const token = socket.handshake.auth?.token + if (token) { + jwtVerify(token, JWT_SECRET) + .then(({ payload }) => { + socket.data.userId = payload.userId + socket.data.role = payload.role + next() + }) + .catch(() => { + socket.data.userId = null + socket.data.role = null + next() + }) + } else { + socket.data.userId = null + socket.data.role = null + next() + } +}) + +io.on("connection", (socket) => { + const userId = socket.data.userId + + if (userId) { + onlineUsers.set(userId, socket.id) + socket.broadcast.emit("user:online", { userId }) + } + + if (userId) { + socket.on("user:lookup", async ({ phone }, callback) => { + try { + const result = await pool.query( + `SELECT id, username, first_name, last_name, phone, avatar_url + FROM users + WHERE phone = $1 AND deleted_at IS NULL + LIMIT 1`, + [phone], + ) + if (result.rows.length > 0) { + const user = result.rows[0] + callback({ + found: true, + user: { + id: user.id, + username: user.username, + firstName: user.first_name, + lastName: user.last_name, + phone: user.phone, + avatar: user.avatar_url, + online: onlineUsers.has(user.id), + }, + }) + } else { + callback({ found: false }) + } + } catch { + callback({ found: false, error: "Lookup failed" }) + } + }) + + socket.on("call:offer", async ({ to, sdp, callerInfo }) => { + const targetSocketId = onlineUsers.get(to) + if (targetSocketId) { + let info = callerInfo + if (!info) { + try { + const result = await pool.query( + `SELECT id, username, first_name, last_name, avatar_url + FROM users WHERE id = $1`, + [userId], + ) + if (result.rows.length > 0) { + const u = result.rows[0] + info = { + id: u.id, + username: u.username, + firstName: u.first_name, + lastName: u.last_name, + avatar: u.avatar_url, + } + } + } catch {} + } + io.to(targetSocketId).emit("call:incoming", { + from: userId, + sdp, + callerInfo: info, + }) + } else { + socket.emit("call:user-offline", { userId: to }) + } + }) + + socket.on("call:answer", ({ to, sdp }) => { + const targetSocketId = onlineUsers.get(to) + if (targetSocketId) { + io.to(targetSocketId).emit("call:answered", { from: userId, sdp }) + } + }) + + socket.on("call:ice-candidate", ({ to, candidate }) => { + const targetSocketId = onlineUsers.get(to) + if (targetSocketId) { + io.to(targetSocketId).emit("call:ice-candidate", { from: userId, candidate }) + } + }) + + socket.on("call:end", ({ to }) => { + const targetSocketId = onlineUsers.get(to) + if (targetSocketId) { + io.to(targetSocketId).emit("call:ended", { from: userId }) + } + }) + + socket.on("call:reject", ({ to }) => { + const targetSocketId = onlineUsers.get(to) + if (targetSocketId) { + io.to(targetSocketId).emit("call:rejected", { from: userId }) + } + }) + + socket.on("call:busy", ({ to }) => { + const targetSocketId = onlineUsers.get(to) + if (targetSocketId) { + io.to(targetSocketId).emit("call:busy", { from: userId }) + } + }) + } + + socket.on("room:join", ({ roomId, displayName }) => { + const participantId = userId || `anon-${socket.id}` + const participantLabel = displayName || participantId + + if (!rooms.has(roomId)) { + rooms.set(roomId, []) + } + const participants = rooms.get(roomId) + participants.push({ socketId: socket.id, id: participantId, label: participantLabel }) + socket.join(roomId) + + const list = participants.map(p => ({ id: p.id, label: p.label })) + + if (participants.length === 1) { + socket.emit("room:waiting", { roomId, participants: list }) + } else if (participants.length === 2) { + io.to(participants[0].socketId).emit("room:initiate-offer", { roomId, participants: list }) + io.to(participants[1].socketId).emit("room:participant-joined", { roomId, participants: list }) + } else { + socket.emit("room:participant-joined", { roomId, participants: list }) + socket.to(roomId).emit("room:participant-joined", { roomId, participants: list }) + } + }) + + socket.on("room:offer", ({ roomId, sdp }) => { + socket.to(roomId).emit("room:offer", { sdp }) + }) + + socket.on("room:answer", ({ roomId, sdp }) => { + socket.to(roomId).emit("room:answer", { sdp }) + }) + + socket.on("room:ice-candidate", ({ roomId, candidate }) => { + socket.to(roomId).emit("room:ice-candidate", { candidate }) + }) + + socket.on("room:leave", ({ roomId }) => { + socket.leave(roomId) + const participants = rooms.get(roomId) + if (participants) { + const idx = participants.findIndex(p => p.socketId === socket.id) + if (idx !== -1) participants.splice(idx, 1) + if (participants.length === 0) { + rooms.delete(roomId) + } else { + const list = participants.map(p => ({ id: p.id, label: p.label })) + io.to(roomId).emit("room:participant-left", { roomId, participants: list }) + } + } + }) + + socket.on("disconnect", () => { + for (const [roomId, participants] of rooms) { + const idx = participants.findIndex(p => p.socketId === socket.id) + if (idx !== -1) { + participants.splice(idx, 1) + if (participants.length === 0) { + rooms.delete(roomId) + } else { + const list = participants.map(p => ({ id: p.id, label: p.label })) + io.to(roomId).emit("room:participant-left", { roomId, participants: list }) + } + } + } + if (userId) { + onlineUsers.delete(userId) + socket.broadcast.emit("user:offline", { userId }) + } + }) +}) + +httpServer.listen(PORT, () => { + console.log(`[signaling] server running on port ${PORT}`) +}) diff --git a/src/app/(dashboard)/chats/page.tsx b/src/app/(dashboard)/chats/page.tsx index 9ed8906..a6bceaa 100644 --- a/src/app/(dashboard)/chats/page.tsx +++ b/src/app/(dashboard)/chats/page.tsx @@ -15,7 +15,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { - Search, Send, Phone, Video, MoreHorizontal, Paperclip, + Search, Send, Phone, MoreHorizontal, Paperclip, Smile, Flag, Ban, Trash2, Image, FileIcon, X, Mic, Square, Play, Pause, Check, CheckCheck, CornerDownRight, Forward, Pencil, Download, } from "lucide-react" @@ -28,6 +28,7 @@ import { Textarea } from "@/components/ui/textarea" import { useTheme } from "next-themes" import { useUser } from "@/providers/user-provider" import { toast } from "sonner" +import VoiceCallModal from "@/components/chats/voice-call-modal" import data from "@emoji-mart/data" import Picker from "@emoji-mart/react" @@ -124,6 +125,7 @@ export default function ChatsPage() { const [searchResults, setSearchResults] = useState([]) const [searchingUsers, setSearchingUsers] = useState(false) const [unreadMap, setUnreadMap] = useState>(new Map()) + const [isCallModalOpen, setIsCallModalOpen] = useState(false) const [previewAvatarUrl, setPreviewAvatarUrl] = useState(null) const fileInputRef = useRef(null) const textareaRef = useRef(null) @@ -642,12 +644,9 @@ export default function ChatsPage() {
- -
) } diff --git a/src/app/api/auth/jwt/route.ts b/src/app/api/auth/jwt/route.ts new file mode 100644 index 0000000..12f4c18 --- /dev/null +++ b/src/app/api/auth/jwt/route.ts @@ -0,0 +1,11 @@ +import { NextResponse } from "next/server" +import { cookies } from "next/headers" + +export async function GET() { + const cookieStore = await cookies() + const token = cookieStore.get("session")?.value + if (!token) { + return NextResponse.json({ error: "No session" }, { status: 401 }) + } + return NextResponse.json({ token }) +} diff --git a/src/app/api/conversations/route.ts b/src/app/api/conversations/route.ts index a62c57c..aff4f0b 100644 --- a/src/app/api/conversations/route.ts +++ b/src/app/api/conversations/route.ts @@ -13,10 +13,11 @@ export async function GET() { c.id, c.updated_at, cp_me.last_read_at, - u.id AS other_user_id, - u.first_name || ' ' || u.last_name AS other_user_name, - u.email AS other_user_email, - u.avatar_url AS other_user_avatar_url, + u.id AS other_user_id, + u.first_name || ' ' || u.last_name AS other_user_name, + u.email AS other_user_email, + u.phone AS other_user_phone, + u.avatar_url AS other_user_avatar_url, (SELECT content FROM messages WHERE conversation_id = c.id ORDER BY created_at DESC LIMIT 1) AS last_message, (SELECT created_at FROM messages WHERE conversation_id = c.id ORDER BY created_at DESC LIMIT 1) AS last_message_time, (SELECT count(*) FROM messages WHERE conversation_id = c.id AND sender_id != $1 AND created_at > COALESCE(cp_me.last_read_at, '1970-01-01')) AS unread @@ -39,6 +40,7 @@ export async function GET() { id: row.other_user_id, name: row.other_user_name, email: row.other_user_email, + phone: row.other_user_phone || "", avatar: avatarSvgUrl(row.other_user_name), }, lastMessage: row.last_message || "", @@ -90,7 +92,7 @@ export async function POST(request: NextRequest) { ) const otherUser = await query( - `SELECT id, first_name || ' ' || last_name AS name, email, avatar_url + `SELECT id, first_name || ' ' || last_name AS name, email, phone, avatar_url FROM users WHERE id = $1`, [userId], ) @@ -105,6 +107,7 @@ export async function POST(request: NextRequest) { id: other.id, name: other.name, email: other.email, + phone: other.phone || "", avatar: avatarSvgUrl(other.name), }, lastMessage: "", diff --git a/src/app/api/invite/generate/route.ts b/src/app/api/invite/generate/route.ts new file mode 100644 index 0000000..2b37995 --- /dev/null +++ b/src/app/api/invite/generate/route.ts @@ -0,0 +1,25 @@ +import { NextRequest, NextResponse } from "next/server" +import { query } from "@/lib/db" +import crypto from "crypto" + +export async function POST(request: NextRequest) { + try { + const { phone, token: clientToken } = await request.json() + if (!phone) { + return NextResponse.json({ error: "Phone number required" }, { status: 400 }) + } + + const token = clientToken || crypto.randomBytes(24).toString("hex") + await query( + `INSERT INTO invites (token, phone) VALUES ($1, $2)`, + [token, phone], + ) + + const origin = request.headers.get("origin") || "http://localhost:3000" + const inviteUrl = `${origin}/join/${token}` + + return NextResponse.json({ token, inviteUrl }) + } catch { + return NextResponse.json({ error: "Failed to generate invite" }, { status: 500 }) + } +} diff --git a/src/app/api/users/lookup/route.ts b/src/app/api/users/lookup/route.ts new file mode 100644 index 0000000..c4e6c2e --- /dev/null +++ b/src/app/api/users/lookup/route.ts @@ -0,0 +1,38 @@ +import { NextRequest, NextResponse } from "next/server" +import { query } from "@/lib/db" + +export async function GET(request: NextRequest) { + const phone = request.nextUrl.searchParams.get("phone") + if (!phone) { + return NextResponse.json({ error: "Phone parameter required" }, { status: 400 }) + } + + try { + const result = await query( + `SELECT id, username, first_name, last_name, phone, avatar_url + FROM users + WHERE phone = $1 AND deleted_at IS NULL + LIMIT 1`, + [phone], + ) + + if (result.rows.length === 0) { + return NextResponse.json({ found: false }) + } + + const user = result.rows[0] + return NextResponse.json({ + found: true, + user: { + id: user.id, + username: user.username, + firstName: user.first_name, + lastName: user.last_name, + phone: user.phone, + avatar: user.avatar_url, + }, + }) + } catch { + return NextResponse.json({ error: "Lookup failed" }, { status: 500 }) + } +} diff --git a/src/app/call/[roomId]/page.tsx b/src/app/call/[roomId]/page.tsx new file mode 100644 index 0000000..780d963 --- /dev/null +++ b/src/app/call/[roomId]/page.tsx @@ -0,0 +1,226 @@ +"use client" + +import { useState, useEffect, use, useCallback } from "react" +import { Phone, PhoneOff, Mic, MicOff, Volume2, VolumeX, Users, Loader2 } from "lucide-react" +import { useWebRTCCall } from "@/hooks/useWebRTCCall" + +export default function CallRoomPage({ params }: { params: Promise<{ roomId: string }> }) { + const { roomId } = use(params) + + const { + callState, + isMuted, + isSpeakerOn, + callDuration, + error, + isReady, + participants, + joinRoom, + endCall, + toggleMute, + toggleSpeaker, + formatDuration, + } = useWebRTCCall({ anonymous: true }) + + const [displayName, setDisplayName] = useState("") + const [joined, setJoined] = useState(false) + const [micError, setMicError] = useState(null) + const [connectionTimeout, setConnectionTimeout] = useState(false) + + useEffect(() => { + if (isReady) return + const timer = setTimeout(() => { + if (!isReady) setConnectionTimeout(true) + }, 10000) + return () => clearTimeout(timer) + }, [isReady]) + + const handleJoin = useCallback(async () => { + if (!displayName.trim()) return + setMicError(null) + try { + await navigator.mediaDevices.getUserMedia({ audio: true }) + joinRoom(roomId, displayName.trim() || "Anonymous") + setJoined(true) + } catch { + setMicError("Microphone access is required to join the call. Please allow microphone access in your browser settings.") + } + }, [displayName, roomId, joinRoom]) + + const handleEnd = useCallback(() => { + endCall() + }, [endCall]) + + if (!joined) { + return ( +
+
+ {connectionTimeout ? ( + <> +

This call is no longer available.

+ + ) : !isReady ? ( + <> +
+ +

Connecting to call...

+
+ + ) : ( + <> +

Join Call

+ {micError && ( +

{micError}

+ )} + setDisplayName(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") handleJoin() }} + placeholder="Enter your name" + className="bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E0E0E0] dark:border-[#333333] text-[#111111] dark:text-white placeholder-[#AAAAAA] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full mb-4 outline-none transition-colors focus:border-[#CC0000] dark:focus:border-[#FF4444]" + /> + + + )} +
+
+ ) + } + + return ( +
+
+ {callState === "calling" && ( +
+

Calling

+

Connecting to call room...

+
+ +
+
+ )} + + {callState === "waiting" && ( +
+

Waiting for participant

+

Share the call link to invite someone...

+ {participants.length > 0 && ( +
+
+ + Participants ({participants.length}) +
+ {participants.map((p) => ( +
{p.label}
+ ))} +
+ )} +
+ +
+
+ )} + + {callState === "participant_joined" && ( +
+

Participant joined

+ {participants.length > 0 && ( +
+
+ + Participants ({participants.length}) +
+ {participants.map((p) => ( +
{p.label}
+ ))} +
+ )} +

Connecting...

+
+ +
+
+ )} + + {callState === "connecting" && ( +
+

Connecting

+

Establishing secure connection...

+
+ +
+
+ )} + + {callState === "connected" && ( +
+

Connected

+ {participants.length > 0 && ( +
+
+ + Participants ({participants.length}) +
+ {participants.map((p) => ( +
{p.label}
+ ))} +
+ )} +
+ {formatDuration(callDuration)} +
+
+ + + +
+
+ )} + + {(callState === "ended" || callState === "idle") && ( +
+

Call ended

+ {callDuration > 0 && ( +

Duration: {formatDuration(callDuration)}

+ )} +
+ )} + + {error && ( +

{error}

+ )} +
+
+ ) +} diff --git a/src/app/join/[token]/page.tsx b/src/app/join/[token]/page.tsx new file mode 100644 index 0000000..fb1fb49 --- /dev/null +++ b/src/app/join/[token]/page.tsx @@ -0,0 +1,69 @@ +import { query } from "@/lib/db" +import { redirect } from "next/navigation" + +interface Props { + params: Promise<{ token: string }> +} + +function ErrorPage({ message }: { message: string }) { + return ( +
+
+

+ {message} +

+
+
+ ) +} + +export default async function JoinPage({ params }: Props) { + const { token } = await params + + const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i + + if (UUID_REGEX.test(token)) { + redirect(`/call/${token}`) + } + + const result = await query( + `SELECT phone, created_at, expires_at FROM invites WHERE token = $1 LIMIT 1`, + [token], + ) + + if (result.rows.length === 0) { + return + } + + const invite = result.rows[0] + + if (new Date(invite.expires_at) <= new Date()) { + return + } + + return ( +
+
+

+ You're Invited! +

+

+ Someone wants to connect with you on our platform. +

+
+

Contact Number

+

{invite.phone}

+
+

+ Create an account to start making free voice calls to this person and others on the platform. +

+ + Create Account + +
+
+ ) +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 3f027d6..18ccddf 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,7 +1,7 @@ "use client" import { useState, useEffect, useRef } from "react" -import { useRouter } from "next/navigation" +import { useSearchParams } from "next/navigation" import { COMPANY_NAME } from "@/lib/constants" import { Eye, EyeOff, Loader2 } from "lucide-react" @@ -14,7 +14,8 @@ const waves = [ ] export default function LoginPage() { - const router = useRouter() + const searchParams = useSearchParams() + const redirectTo = searchParams.get("redirect") || "/dashboard" const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [showPassword, setShowPassword] = useState(false) @@ -215,7 +216,7 @@ export default function LoginPage() { }) if (res.ok) { - router.push("/dashboard") + window.location.href = redirectTo } else { const data = await res.json().catch(() => ({})) setError(data.error || "Invalid email or password.") diff --git a/src/components/chats/voice-call-modal.tsx b/src/components/chats/voice-call-modal.tsx index 317b0e2..e72a39a 100644 --- a/src/components/chats/voice-call-modal.tsx +++ b/src/components/chats/voice-call-modal.tsx @@ -1,7 +1,7 @@ "use client" import { useState, useEffect, useCallback } from "react" -import { Phone, PhoneOff, Mic, MicOff, Volume2, VolumeX, X } from "lucide-react" +import { Phone, X, Copy, MessageSquare } from "lucide-react" import { getSupabase } from "@/lib/supabase" import { useWebRTCCall } from "@/hooks/useWebRTCCall" @@ -17,6 +17,14 @@ interface VoiceCallModalProps { onClose: () => void } +function formatPhoneForWhatsApp(phone: string): string { + const digits = phone.replace(/[^0-9]/g, "") + if (!digits) return "" + if (digits.startsWith("27")) return digits + if (digits.startsWith("0")) return "27" + digits.slice(1) + return digits +} + export default function VoiceCallModal({ open, onClose }: VoiceCallModalProps) { const [contacts, setContacts] = useState([]) const [contactsLoading, setContactsLoading] = useState(false) @@ -24,69 +32,94 @@ export default function VoiceCallModal({ open, onClose }: VoiceCallModalProps) { const [searchQuery, setSearchQuery] = useState("") const [phoneNumber, setPhoneNumber] = useState("") const [phoneError, setPhoneError] = useState(false) - const [lookupMessage, setLookupMessage] = useState(null) - const setLookupMessageWithReset = useCallback((msg: string | null) => { - setLookupMessage(msg) - if (msg !== "This phone number is not registered on the platform.") { - setInviteUrl(null) - } - }, []) - const [inviteUrl, setInviteUrl] = useState(null) - const [inviteLoading, setInviteLoading] = useState(false) - const generateInvite = useCallback(async (phone: string) => { - setInviteLoading(true) + const [callLink, setCallLink] = useState(null) + const [shareSent, setShareSent] = useState(false) + const [shareError, setShareError] = useState(null) + const [targetPhone, setTargetPhone] = useState("") + const [showWaUrl, setShowWaUrl] = useState(null) + + const { createRoom } = useWebRTCCall() + + const handleCall = useCallback((phone?: string) => { + setShareSent(false) + setShareError(null) + const { roomId, link } = createRoom() + setCallLink(link) + setTargetPhone(phone || "") + console.log("[call] room created, roomId:", roomId) + console.log("[call] join link:", link) + console.log("[call] target phone:", phone || "none") + }, [createRoom]) + + const copyLink = useCallback(async () => { + if (!callLink) return try { - const res = await fetch("/api/invite/generate", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ phone }), - }) - const data = await res.json() - if (data.inviteUrl) { - setInviteUrl(data.inviteUrl) - } + await navigator.clipboard.writeText(callLink) + setShareSent(true) + setShareError(null) + console.log("[share] link copied") } catch { - // fallback: generate client-side - const token = crypto.randomUUID?.() || Math.random().toString(36).slice(2) - setInviteUrl(`${window.location.origin}/join/${token}`) - } finally { - setInviteLoading(false) + setShareError("Failed to copy URL") + console.error("[share] copy failed") } - }, []) + }, [callLink]) - const { - isReady, - isCallActive, - isIncoming, - isMuted, - isSpeakerOn, - callState, - callDuration, - error: callError, - callerInfo, - makeCall, - acceptCall, - rejectCall, - endCall, - toggleMute, - toggleSpeaker, - formatDuration, - } = useWebRTCCall() + const sendWhatsApp = useCallback(() => { + if (!callLink) return - useEffect(() => { - if (!open && callState !== "idle") { - endCall() + const originalPhone = targetPhone + const formattedPhone = formatPhoneForWhatsApp(originalPhone) + const generatedCallLink = callLink + + console.log("[wa] original phone:", originalPhone) + console.log("[wa] formatted phone:", formattedPhone) + console.log("[wa] call link:", generatedCallLink) + + if (!formattedPhone) { + setShareError("Invalid phone number.") + console.error("[wa] invalid phone number") + return } - }, [open, callState, endCall]) + + const msg = encodeURIComponent(`Hi! Join me on this link so we can start our call:\n\n${generatedCallLink}`) + const waUrl = `https://wa.me/${formattedPhone}?text=${msg}` + + console.log("[wa] WhatsApp URL:", waUrl) + setShowWaUrl(waUrl) + + const w = window.open(waUrl, "_blank") + if (!w || w.closed) { + setShareError("Could not open WhatsApp. Please check your browser allows popups.") + console.error("[wa] failed: window.open returned null or closed") + } else { + setShareSent(true) + setShareError(null) + console.log("[wa] success: WhatsApp opened") + } + }, [callLink, targetPhone]) useEffect(() => { if (!open) { setSearchQuery("") setPhoneNumber("") setPhoneError(false) - return + setCallLink(null) + setShareSent(false) + setShareError(null) + setTargetPhone("") + setShowWaUrl(null) } + }, [open]) + + const FALLBACK_CONTACTS: Contact[] = [ + { id: "fallback-dillen", name: "Dillen", phone: "0799158142", avatar_url: null }, + { id: "fallback-ewan", name: "Ewan", phone: "0845172665", avatar_url: null }, + { id: "fallback-caitlin", name: "Caitlin", phone: "0612729281", avatar_url: null }, + ] + + useEffect(() => { + if (!open) return const fetchContacts = async () => { setContactsLoading(true) setContactsError(false) @@ -97,9 +130,9 @@ export default function VoiceCallModal({ open, onClose }: VoiceCallModalProps) { .select("id, name, phone, avatar_url") .order("name", { ascending: true }) if (error) throw error - setContacts(data || []) + setContacts([...(data || []), ...FALLBACK_CONTACTS]) } catch { - setContactsError(true) + setContacts(FALLBACK_CONTACTS) } finally { setContactsLoading(false) } @@ -138,334 +171,177 @@ export default function VoiceCallModal({ open, onClose }: VoiceCallModalProps) { -

Start a Call

-

Search contacts or dial a number

+ {callLink ? ( + shareSent ? ( + <> +

Call link copied.

+

Waiting for participant

+

+ The recipient must receive and open the call link. +

-

- CONTACTS -

- - setSearchQuery(e.target.value)} - placeholder="Search contacts..." - className="bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E0E0E0] dark:border-[#333333] text-[#111111] dark:text-white placeholder-[#AAAAAA] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full mb-3 outline-none transition-colors focus:border-[#CC0000] dark:focus:border-[#FF4444]" - /> - -
- {contactsLoading && ( -

Loading contacts...

- )} - {contactsError && ( -

Could not load contacts

- )} - {!contactsLoading && !contactsError && filteredContacts.length === 0 && ( -

No contacts found

- )} - {!contactsLoading && !contactsError && filteredContacts.map((contact) => ( -
-
- {contact.avatar_url ? ( - {contact.name} - ) : ( - contact.name.charAt(0).toUpperCase() - )} +
+ {callLink}
-
-

{contact.name}

-

{contact.phone}

+ +
+ +
- -
- ))} -
+ Open Call + + + ) : ( + <> +

Choose how to notify this person

+

+ Select a method to send the call link +

-
-
- OR -
-
- -

- DIAL A NUMBER -

- - { setPhoneNumber(e.target.value); setPhoneError(false) }} - placeholder="+27 000 000 0000" - className="bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E0E0E0] dark:border-[#333333] text-[#111111] dark:text-white placeholder-[#AAAAAA] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full outline-none transition-colors focus:border-[#CC0000] dark:focus:border-[#FF4444]" - /> - {phoneError && ( -

Please enter a phone number

- )} - - {isIncoming ? ( -
-
- Incoming Call -
-
-
- {callerInfo?.firstName?.charAt(0).toUpperCase()} +
+ {callLink}
-
-

- {callerInfo?.firstName} {callerInfo?.lastName} -

-

{callerInfo?.username}

+ + {showWaUrl && ( +
+ {showWaUrl} +
+ )} + +
+ +
-
-

Ringing...

-
- - -
-
- ) : isCallActive ? ( -
- {callState === "calling" && ( - <> -
- {phoneNumber} -
-

Calling...

-
- -
- - )} - {callState === "ringing" && ( - <> -
- {phoneNumber} -
-

Ringing...

-
- -
- - )} - {callState === "connecting" && ( - <> -
- {phoneNumber} -
-

Connecting...

-
- -
- - )} - {callState === "connected" && ( - <> -
- {phoneNumber} -
-
- {formatDuration(callDuration)} -
-
- - - -
- - )} - {(callState === "ended" || callState === "missed" || callState === "rejected" || callState === "offline" || callState === "busy") && ( - <> -
- {phoneNumber} -
-

- {callState === "missed" && "This phone number is not registered on the platform."} - {callState === "offline" && "User is offline"} - {callState === "rejected" && "Call rejected"} - {callState === "busy" && "User is busy"} - {callState === "ended" && "Call ended"} -

- {callState === "missed" && ( -
- {!inviteUrl ? ( - - ) : ( -
-

- {inviteUrl} -

-
- - -
-
- )} -
- )} - - )} -
+ + {shareError && ( +

{shareError}

+ )} + + ) ) : ( <> - {lookupMessage ? ( -
-

{lookupMessage}

- {lookupMessage === "This phone number is not registered on the platform." && ( -
- {!inviteUrl ? ( - +

Start a Call

+

Search contacts or dial a number

+ +

+ CONTACTS +

+ + setSearchQuery(e.target.value)} + placeholder="Search contacts..." + className="bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E0E0E0] dark:border-[#333333] text-[#111111] dark:text-white placeholder-[#AAAAAA] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full mb-3 outline-none transition-colors focus:border-[#CC0000] dark:focus:border-[#FF4444]" + /> + +
+ {contactsLoading && ( +

Loading contacts...

+ )} + {contactsError && ( +

Could not load contacts

+ )} + {!contactsLoading && !contactsError && filteredContacts.length === 0 && ( +

No contacts found

+ )} + {!contactsLoading && !contactsError && filteredContacts.map((contact) => ( +
+
+ {contact.avatar_url ? ( + {contact.name} ) : ( -
-

- {inviteUrl} -

-
- - -
-
+ contact.name.charAt(0).toUpperCase() )}
- )} -
- ) : ( - <> - - {callError && ( -

{callError}

- )} - +
+

{contact.name}

+

{contact.phone}

+
+ +
+ ))} +
+ +
+
+ OR +
+
+ +

+ DIAL A NUMBER +

+ + { setPhoneNumber(e.target.value); setPhoneError(false) }} + placeholder="+27 000 000 0000" + className="bg-[#F5F5F5] dark:bg-[#1A1A1A] border border-[#E0E0E0] dark:border-[#333333] text-[#111111] dark:text-white placeholder-[#AAAAAA] dark:placeholder-[#555555] rounded-xl px-4 py-2.5 text-sm w-full outline-none transition-colors focus:border-[#CC0000] dark:focus:border-[#FF4444]" + /> + {phoneError && ( +

Please enter a phone number

)} + + )}
diff --git a/src/data/users.ts b/src/data/users.ts index 227d07f..16af249 100644 --- a/src/data/users.ts +++ b/src/data/users.ts @@ -5,6 +5,7 @@ export const users: User[] = [ id: "u-super", name: "Jason Oosthuizen", email: "jason@coastalit.com", + phone: "+27 82 555 0100", role: "super_admin", active: true, avatar: "https://ui-avatars.com/api/?name=Jason+Oosthuizen&background=0f172a&color=fff&size=128", @@ -14,6 +15,7 @@ export const users: User[] = [ id: "u1", name: "Sarah Chen", email: "SarahChen@coastit.co.za", + phone: "+27 72 555 0101", role: "admin", active: true, avatar: "https://ui-avatars.com/api/?name=Sarah+Chen&background=1d4ed8&color=fff&size=128", @@ -23,6 +25,7 @@ export const users: User[] = [ id: "u2", name: "Marcus Johnson", email: "marcus@coastalit.com", + phone: "+27 62 555 0102", role: "sales", active: true, avatar: "https://ui-avatars.com/api/?name=Marcus+Johnson&background=2563eb&color=fff&size=128", @@ -32,6 +35,7 @@ export const users: User[] = [ id: "u3", name: "Emily Rodriguez", email: "emily@coastalit.com", + phone: "+27 73 555 0103", role: "sales", active: true, avatar: "https://ui-avatars.com/api/?name=Emily+Rodriguez&background=3b82f6&color=fff&size=128", @@ -41,6 +45,7 @@ export const users: User[] = [ id: "u4", name: "David Kim", email: "david@coastalit.com", + phone: "+27 64 555 0104", role: "sales", active: true, avatar: "https://ui-avatars.com/api/?name=David+Kim&background=60a5fa&color=fff&size=128", @@ -50,6 +55,7 @@ export const users: User[] = [ id: "u5", name: "Jessica Patel", email: "jessica@coastalit.com", + phone: "+27 82 555 0105", role: "sales", active: false, avatar: "https://ui-avatars.com/api/?name=Jessica+Patel&background=93c5fd&color=fff&size=128", @@ -59,6 +65,7 @@ export const users: User[] = [ id: "u6", name: "Alex Thompson", email: "alex@coastalit.com", + phone: "+27 72 555 0106", role: "sales", active: true, avatar: "https://ui-avatars.com/api/?name=Alex+Thompson&background=1d4ed8&color=fff&size=128", @@ -68,6 +75,7 @@ export const users: User[] = [ id: "u7", name: "Rachel Williams", email: "rachel@coastalit.com", + phone: "+27 64 555 0107", role: "sales", active: true, avatar: "https://ui-avatars.com/api/?name=Rachel+Williams&background=2563eb&color=fff&size=128", @@ -77,6 +85,7 @@ export const users: User[] = [ id: "u8", name: "Tom Nakamura", email: "tom@coastalit.com", + phone: "+27 83 555 0108", role: "admin", active: true, avatar: "https://ui-avatars.com/api/?name=Tom+Nakamura&background=3b82f6&color=fff&size=128", diff --git a/src/hooks/useWebRTCCall.ts b/src/hooks/useWebRTCCall.ts new file mode 100644 index 0000000..fbc2bc9 --- /dev/null +++ b/src/hooks/useWebRTCCall.ts @@ -0,0 +1,508 @@ +"use client" + +import { useState, useEffect, useRef, useCallback } from "react" +import { io, Socket } from "socket.io-client" + +const STUN_SERVERS = { + iceServers: [ + { urls: "stun:stun.l.google.com:19302" }, + { urls: "stun:stun1.l.google.com:19302" }, + ], +} + +const SIGNALING_URL = process.env.NEXT_PUBLIC_SIGNALING_URL || "http://localhost:3007" + +export type CallState = "idle" | "calling" | "ringing" | "connecting" | "connected" | "ended" | "missed" | "rejected" | "offline" | "busy" | "waiting" | "participant_joined" + +export interface PlatformUser { + id: string + username: string + firstName: string + lastName: string + phone: string | null + avatar: string | null + online: boolean +} + +export interface CallerInfo { + id: string + username: string + firstName: string + lastName: string + avatar: string | null +} + +export interface Participant { + id: string + label: string +} + +export function useWebRTCCall(options?: { anonymous?: boolean }) { + const isAnonymous = options?.anonymous ?? false + + const [callState, setCallState] = useState("idle") + const [isMuted, setIsMuted] = useState(false) + const [isSpeakerOn, setIsSpeakerOn] = useState(false) + const [callDuration, setCallDuration] = useState(0) + const [error, setError] = useState(null) + const [isReady, setIsReady] = useState(false) + const [remoteStream, setRemoteStream] = useState(null) + const [callerInfo, setCallerInfo] = useState(null) + const [calleeNumber, setCalleeNumber] = useState("") + const [tokenLoading, setTokenLoading] = useState(true) + const [participants, setParticipants] = useState([]) + + const socketRef = useRef(null) + const pcRef = useRef(null) + const localStreamRef = useRef(null) + const audioRef = useRef(null) + const timerRef = useRef(null) + const pendingCallRef = useRef<{ to: string; sdp: any } | null>(null) + const roomIdRef = useRef(null) + + const startTimer = useCallback(() => { + setCallDuration(0) + if (timerRef.current) clearInterval(timerRef.current) + timerRef.current = setInterval(() => { + setCallDuration((prev) => prev + 1) + }, 1000) + }, []) + + useEffect(() => { + let socket: Socket | null = null + let cancelled = false + + async function init() { + try { + let authToken: string | undefined + + if (!isAnonymous) { + const res = await fetch("/api/auth/jwt") + if (res.ok) { + const data = await res.json() + authToken = data.token + } + if (cancelled) return + } + + socket = io(SIGNALING_URL, { + auth: authToken ? { token: authToken } : undefined, + transports: ["websocket", "polling"], + }) + + socket.on("connect", () => setIsReady(true)) + socket.on("disconnect", () => setIsReady(false)) + + if (!isAnonymous) { + socket.on("call:incoming", ({ from, sdp, callerInfo: info }) => { + setCallerInfo(info) + setCallState("ringing") + pendingCallRef.current = { to: from, sdp } + }) + + socket.on("call:answered", async ({ from, sdp }) => { + setCallState("connecting") + if (pcRef.current && sdp) { + try { + await pcRef.current.setRemoteDescription(new RTCSessionDescription(sdp)) + } catch {} + } + }) + + socket.on("call:ice-candidate", async ({ from, candidate }) => { + if (pcRef.current && candidate) { + try { + await pcRef.current.addIceCandidate(new RTCIceCandidate(candidate)) + } catch {} + } + }) + + socket.on("call:ended", ({ from }) => { + setCallState("ended") + cleanupCall() + }) + + socket.on("call:rejected", ({ from }) => { + setCallState("rejected") + cleanupCall() + }) + + socket.on("call:busy", ({ from }) => { + setCallState("busy") + cleanupCall() + }) + + socket.on("call:user-offline", ({ userId }) => { + setCallState("offline") + cleanupCall() + }) + } + + socket.on("room:waiting", ({ roomId, participants: pList }) => { + setParticipants(pList || []) + setCallState("waiting") + }) + + socket.on("room:participant-joined", ({ roomId, participants: pList }) => { + setParticipants(pList || []) + setCallState("participant_joined") + }) + + socket.on("room:initiate-offer", async ({ roomId, participants: pList }) => { + setParticipants(pList || []) + setCallState("participant_joined") + try { + const pc = new RTCPeerConnection(STUN_SERVERS) + pc.onicecandidate = (e) => { + if (e.candidate && roomIdRef.current) { + socket?.emit("room:ice-candidate", { + roomId: roomIdRef.current, + candidate: e.candidate.toJSON(), + }) + } + } + pc.ontrack = (e) => { + setRemoteStream(e.streams[0]) + const audio = new Audio() + audio.srcObject = e.streams[0] + audio.autoplay = true + audioRef.current = audio + audio.play().catch(() => {}) + } + pc.oniceconnectionstatechange = () => { + if (pc.iceConnectionState === "disconnected" || pc.iceConnectionState === "failed") { + setCallState("ended") + cleanupCall() + } + } + const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }) + localStreamRef.current = stream + stream.getTracks().forEach((track) => pc.addTrack(track, stream)) + pcRef.current = pc + + const offer = await pc.createOffer() + await pc.setLocalDescription(offer) + setCallState("connecting") + socket?.emit("room:offer", { roomId, sdp: pc.localDescription }) + } catch (e) { + setError("Failed to start call") + cleanupCall() + } + }) + + socket.on("room:offer", async ({ sdp }) => { + try { + setCallState("connecting") + const pc = new RTCPeerConnection(STUN_SERVERS) + pc.onicecandidate = (e) => { + if (e.candidate && roomIdRef.current) { + socket?.emit("room:ice-candidate", { + roomId: roomIdRef.current, + candidate: e.candidate.toJSON(), + }) + } + } + pc.ontrack = (e) => { + setRemoteStream(e.streams[0]) + const audio = new Audio() + audio.srcObject = e.streams[0] + audio.autoplay = true + audioRef.current = audio + audio.play().catch(() => {}) + } + pc.oniceconnectionstatechange = () => { + if (pc.iceConnectionState === "disconnected" || pc.iceConnectionState === "failed") { + setCallState("ended") + cleanupCall() + } + } + const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }) + localStreamRef.current = stream + stream.getTracks().forEach((track) => pc.addTrack(track, stream)) + pcRef.current = pc + + await pc.setRemoteDescription(new RTCSessionDescription(sdp)) + const answer = await pc.createAnswer() + await pc.setLocalDescription(answer) + socket?.emit("room:answer", { roomId: roomIdRef.current, sdp: pc.localDescription }) + setCallState("connected") + startTimer() + } catch (e) { + setError("Failed to join call") + cleanupCall() + } + }) + + socket.on("room:answer", async ({ sdp }) => { + if (pcRef.current) { + try { + await pcRef.current.setRemoteDescription(new RTCSessionDescription(sdp)) + setCallState("connected") + startTimer() + } catch {} + } + }) + + socket.on("room:ice-candidate", async ({ candidate }) => { + if (pcRef.current) { + try { + await pcRef.current.addIceCandidate(new RTCIceCandidate(candidate)) + } catch {} + } + }) + + socket.on("room:participant-left", ({ roomId, participants: pList }) => { + setParticipants(pList || []) + setCallState("ended") + cleanupCall() + }) + + socketRef.current = socket + } finally { + setTokenLoading(false) + } + } + + init() + + return () => { + cancelled = true + if (socket) socket.disconnect() + cleanupCall() + } + }, [isAnonymous]) + + const cleanupCall = useCallback(() => { + if (timerRef.current) { + clearInterval(timerRef.current) + timerRef.current = null + } + if (pcRef.current) { + pcRef.current.close() + pcRef.current = null + } + if (localStreamRef.current) { + localStreamRef.current.getTracks().forEach((t) => t.stop()) + localStreamRef.current = null + } + setRemoteStream(null) + setIsMuted(false) + setIsSpeakerOn(false) + if (audioRef.current) { + audioRef.current.pause() + audioRef.current.srcObject = null + audioRef.current = null + } + setCallDuration(0) + pendingCallRef.current = null + roomIdRef.current = null + setParticipants([]) + }, []) + + const createPeerConnection = useCallback(async () => { + const pc = new RTCPeerConnection(STUN_SERVERS) + + pc.onicecandidate = (e) => { + if (e.candidate && pendingCallRef.current) { + socketRef.current?.emit("call:ice-candidate", { + to: pendingCallRef.current.to, + candidate: e.candidate.toJSON(), + }) + } + } + + pc.ontrack = (e) => { + setRemoteStream(e.streams[0]) + const audio = new Audio() + audio.srcObject = e.streams[0] + audio.autoplay = true + audioRef.current = audio + audio.play().catch(() => {}) + } + + pc.oniceconnectionstatechange = () => { + if (pc.iceConnectionState === "disconnected" || pc.iceConnectionState === "failed") { + setCallState("ended") + cleanupCall() + } + } + + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false }) + localStreamRef.current = stream + stream.getTracks().forEach((track) => pc.addTrack(track, stream)) + } catch { + setError("Microphone access denied") + } + + pcRef.current = pc + return pc + }, [cleanupCall]) + + const lookupUser = useCallback(async (phone: string): Promise => { + return new Promise((resolve) => { + socketRef.current?.emit("user:lookup", { phone }, (result: any) => { + if (result?.found) { + resolve(result.user) + } else { + resolve(null) + } + }) + }) + }, []) + + const makeCall = useCallback(async (phoneNumber: string) => { + try { + setError(null) + setCalleeNumber(phoneNumber) + + const user = await lookupUser(phoneNumber) + if (!user) { + setCallState("missed") + setError("Not on Platform") + return + } + if (!user.online) { + setCallState("offline") + setError("User is offline") + return + } + + const pc = await createPeerConnection() + setCallState("calling") + + const offer = await pc.createOffer() + await pc.setLocalDescription(offer) + + socketRef.current?.emit("call:offer", { + to: user.id, + sdp: pc.localDescription, + callerInfo: null, + }) + pendingCallRef.current = { to: user.id, sdp: null } + + setCallState("ringing") + } catch { + setError("Failed to start call") + cleanupCall() + } + }, [createPeerConnection, lookupUser, cleanupCall]) + + const acceptCall = useCallback(async () => { + if (!pendingCallRef.current) return + setCallState("connecting") + + try { + const pc = await createPeerConnection() + + await pc.setRemoteDescription(new RTCSessionDescription(pendingCallRef.current.sdp)) + const answer = await pc.createAnswer() + await pc.setLocalDescription(answer) + + socketRef.current?.emit("call:answer", { + to: pendingCallRef.current.to, + sdp: pc.localDescription, + }) + + setCallState("connected") + startTimer() + } catch { + setError("Failed to accept call") + cleanupCall() + } + }, [createPeerConnection, cleanupCall, startTimer]) + + const rejectCall = useCallback(() => { + if (pendingCallRef.current) { + socketRef.current?.emit("call:reject", { to: pendingCallRef.current.to }) + } + setCallState("idle") + cleanupCall() + }, [cleanupCall]) + + const endCall = useCallback(() => { + if (roomIdRef.current) { + socketRef.current?.emit("room:leave", { roomId: roomIdRef.current }) + } + if (!isAnonymous) { + if (callState === "ringing" && pendingCallRef.current) { + socketRef.current?.emit("call:reject", { to: pendingCallRef.current.to }) + } + if (callState === "connected" || callState === "connecting" || callState === "calling") { + const to = pendingCallRef.current?.to + if (to) { + socketRef.current?.emit("call:end", { to }) + } + } + } + setCallState("ended") + cleanupCall() + }, [callState, cleanupCall, isAnonymous]) + + const toggleSpeaker = useCallback(() => { + setIsSpeakerOn((prev) => !prev) + }, []) + + const toggleMute = useCallback(() => { + if (localStreamRef.current) { + localStreamRef.current.getAudioTracks().forEach((track) => { + track.enabled = isMuted + }) + setIsMuted(!isMuted) + } + }, [isMuted]) + + const formatDuration = useCallback((seconds: number) => { + const m = Math.floor(seconds / 60).toString().padStart(2, "0") + const s = (seconds % 60).toString().padStart(2, "0") + return `${m}:${s}` + }, []) + + const createRoom = useCallback(() => { + const roomId = crypto.randomUUID?.() || Math.random().toString(36).slice(2, 14) + const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || (typeof window !== "undefined" ? window.location.origin : "http://localhost:3000") + const link = `${baseUrl}/join/${roomId}` + return { roomId, link } + }, []) + + const joinRoom = useCallback((roomId: string, displayName?: string) => { + roomIdRef.current = roomId + setCallState("calling") + socketRef.current?.emit("room:join", { roomId, displayName: displayName || "" }) + }, []) + + const leaveRoom = useCallback(() => { + if (roomIdRef.current) { + socketRef.current?.emit("room:leave", { roomId: roomIdRef.current }) + } + setCallState("ended") + cleanupCall() + }, [cleanupCall]) + + return { + callState, + isCallActive: callState === "connected" || callState === "connecting" || callState === "ringing" || callState === "calling" || callState === "waiting" || callState === "participant_joined", + isIncoming: callState === "ringing" && !!callerInfo, + isMuted, + isSpeakerOn, + callDuration, + error, + isReady, + remoteStream, + callerInfo, + calleeNumber, + participants, + createRoom, + joinRoom, + leaveRoom, + makeCall, + acceptCall, + rejectCall, + endCall, + toggleMute, + toggleSpeaker, + lookupUser, + formatDuration, + tokenLoading, + } +} diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 2202d12..7618ddc 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -17,6 +17,7 @@ export interface SessionUser { id: string; username: string; email: string; + phone: string | null; firstName: string; lastName: string; role: string; @@ -85,7 +86,7 @@ export async function getUserByUsername(username: string) { export async function getUserById(id: string) { const result = await query( - ` SELECT u.id, u.username, u.email, u.first_name, u.last_name, + ` SELECT u.id, u.username, u.email, u.phone, u.first_name, u.last_name, u.is_active, u.avatar_url, r.name AS role_name FROM users u @@ -185,6 +186,7 @@ export function mapDbUserToSessionUser( id: dbUser.id as string, username: dbUser.username as string, email: dbUser.email as string, + phone: (dbUser.phone as string) || null, firstName: dbUser.first_name as string, lastName: dbUser.last_name as string, role: roleMapping[roleName] || roleName.toLowerCase(), diff --git a/src/middleware.ts b/src/middleware.ts index 20a75b2..dc89512 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -10,6 +10,7 @@ const JWT_SECRET = new TextEncoder().encode(RAW_SECRET) const publicRoutes = [ "/login", + "/join", "/api/auth/login", "/api/auth/logout", "/_next/static", diff --git a/src/providers/user-provider.tsx b/src/providers/user-provider.tsx index 3dae191..ddfb8b8 100644 --- a/src/providers/user-provider.tsx +++ b/src/providers/user-provider.tsx @@ -30,6 +30,7 @@ export function UserProvider({ children }: { children: ReactNode }) { id: u.id, name: `${u.firstName} ${u.lastName}`, email: u.email, + phone: u.phone || "", role: u.role, active: true, avatar: u.avatar, diff --git a/src/types/index.ts b/src/types/index.ts index 072f2e6..0ced48c 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -10,6 +10,7 @@ export interface User { id: string; name: string; email: string; + phone?: string; role: UserRole; active: boolean; avatar: string;