diff --git a/PHASE4-CALLS.md b/PHASE4-CALLS.md
index 9784424..e15a3d7 100644
--- a/PHASE4-CALLS.md
+++ b/PHASE4-CALLS.md
@@ -202,7 +202,7 @@ Get temporary TURN server credentials.
```json
{
"credentials": {
- "urls": ["turn:turn.example.com:3478"],
+ "urls": ["turn:turn.example.com:3000"],
"username": "1736517600:username",
"credential": "hmac-sha1-hash"
},
@@ -427,7 +427,7 @@ Edit `/etc/turnserver.conf`:
```conf
# Listening port
-listening-port=3478
+listening-port=3000
tls-listening-port=5349
# External IP (replace with your server IP)
@@ -467,7 +467,7 @@ Add to `.env`:
```env
# TURN Server Configuration
TURN_SERVER_HOST=turn.yourdomain.com
-TURN_SERVER_PORT=3478
+TURN_SERVER_PORT=3000
TURN_SECRET=your-turn-secret-key
TURN_TTL=86400
```
@@ -476,8 +476,8 @@ TURN_TTL=86400
```bash
# Allow TURN ports
-sudo ufw allow 3478/tcp
-sudo ufw allow 3478/udp
+sudo ufw allow 3000/tcp
+sudo ufw allow 3000/udp
sudo ufw allow 5349/tcp
sudo ufw allow 5349/udp
@@ -496,7 +496,7 @@ sudo systemctl status coturn
Use the [Trickle ICE](https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/) test page:
-1. Add your TURN server URL: `turn:YOUR_SERVER_IP:3478`
+1. Add your TURN server URL: `turn:YOUR_SERVER_IP:3000`
2. Generate TURN credentials using the HMAC method
3. Click "Gather candidates"
4. Verify `relay` candidates appear
diff --git a/PHASE4-QUICK-START.md b/PHASE4-QUICK-START.md
index c252869..898ead3 100644
--- a/PHASE4-QUICK-START.md
+++ b/PHASE4-QUICK-START.md
@@ -97,7 +97,7 @@ Add to `.env`:
```env
# TURN Server Configuration
TURN_SERVER_HOST=turn.example.com
-TURN_SERVER_PORT=3478
+TURN_SERVER_PORT=3000
TURN_SECRET=your-turn-secret-key
TURN_TTL=86400
```
diff --git a/SAVE-POINT-2026-01-12.md b/SAVE-POINT-2026-01-12.md
index 7c4250c..0c5aa93 100644
--- a/SAVE-POINT-2026-01-12.md
+++ b/SAVE-POINT-2026-01-12.md
@@ -106,12 +106,12 @@ packages/ui/styles/tokens.ts # Complete redesign with dark theme
http://localhost:3000
http://localhost:3000/health
-# Port 4321 - Astro Landing Site
-http://localhost:4321
+# Port 3000 - Astro Landing Site
+http://localhost:3000
cd astro-site && npm run dev
-# Port 5173 - React Frontend (Vite)
-http://localhost:5173
+# Port 3000 - React Frontend (Vite)
+http://localhost:3000
cd src/frontend && npm run dev
```
@@ -327,7 +327,7 @@ cd src/frontend && npm run dev # React (Terminal 3)
### 2. Check Status
- Visit http://localhost:5173 (React app)
-- Visit http://localhost:4321 (Astro landing)
+- Visit http://localhost:3000 (Astro landing)
- Check git status: `git status`
### 3. Continue Development
diff --git a/astro-site/package.json b/astro-site/package.json
index 6e9e626..88e05e0 100644
--- a/astro-site/package.json
+++ b/astro-site/package.json
@@ -3,7 +3,7 @@
"version": "1.0.0",
"private": true,
"scripts": {
- "dev": "astro dev --port 4321",
+ "dev": "astro dev --port 3000",
"build": "astro build",
"preview": "astro preview"
},
diff --git a/astro-site/src/components/ReactAppIsland.jsx b/astro-site/src/components/ReactAppIsland.jsx
new file mode 100644
index 0000000..ed490c9
--- /dev/null
+++ b/astro-site/src/components/ReactAppIsland.jsx
@@ -0,0 +1,7 @@
+import React from "react";
+
+import Demo from "../react-app/Demo";
+
+export default function ReactAppIsland() {
+ return ;
+}
diff --git a/astro-site/src/components/auth/SupabaseLogin.jsx b/astro-site/src/components/auth/SupabaseLogin.jsx
new file mode 100644
index 0000000..2300916
--- /dev/null
+++ b/astro-site/src/components/auth/SupabaseLogin.jsx
@@ -0,0 +1,70 @@
+import React, { useState } from "react";
+import { createClient } from "@supabase/supabase-js";
+
+const supabase = createClient(
+ import.meta.env.PUBLIC_SUPABASE_URL,
+ import.meta.env.PUBLIC_SUPABASE_ANON_KEY
+);
+
+export default function SupabaseLogin() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [success, setSuccess] = useState(false);
+
+ const handleLogin = async (e) => {
+ e.preventDefault();
+ setLoading(true);
+ setError(null);
+ setSuccess(false);
+ const { error } = await supabase.auth.signInWithPassword({ email, password });
+ setLoading(false);
+ if (error) {
+ setError(error.message);
+ } else {
+ setSuccess(true);
+ // Optionally redirect or reload
+ window.location.href = "/app";
+ }
+ };
+
+ return (
+
+
+
+
AeThex Connect
+
Sign in to your account
+ {error &&
{error}
}
+ {success &&
Login successful!
}
+
+
+
+
+
+ );
+}
diff --git a/astro-site/src/pages/_mockup.jsx b/astro-site/src/pages/_mockup.jsx
index d96cc66..84ec9d5 100644
--- a/astro-site/src/pages/_mockup.jsx
+++ b/astro-site/src/pages/_mockup.jsx
@@ -1,7 +1,2 @@
-import React from "react";
-import MainLayout from "../components/mockup/MainLayout";
-import "../components/mockup/global.css";
-export default function MockupPage() {
- return ;
-}
\ No newline at end of file
+// Removed: This page is deprecated. Use /app for the full platform UI.
\ No newline at end of file
diff --git a/astro-site/src/pages/app.astro b/astro-site/src/pages/app.astro
new file mode 100644
index 0000000..2e1b199
--- /dev/null
+++ b/astro-site/src/pages/app.astro
@@ -0,0 +1,6 @@
+
+---
+import ReactAppIsland from '../components/ReactAppIsland.jsx';
+---
+
+
diff --git a/astro-site/src/pages/index.astro b/astro-site/src/pages/index.astro
index c058222..8f021d3 100644
--- a/astro-site/src/pages/index.astro
+++ b/astro-site/src/pages/index.astro
@@ -9,6 +9,8 @@ import Layout from '../layouts/Layout.astro';
AeThex Connect
Next-generation voice & chat for gamers. Own your identity. Connect everywhere.
Get Started
+ Open AeThex Connect Platform
+ Legacy Mockup
diff --git a/astro-site/src/pages/login.astro b/astro-site/src/pages/login.astro
index 81bd75c..475475e 100644
--- a/astro-site/src/pages/login.astro
+++ b/astro-site/src/pages/login.astro
@@ -1,9 +1,9 @@
---
import Layout from '../layouts/Layout.astro';
-import LoginIsland from '../components/auth/LoginIsland.jsx';
+import SupabaseLogin from '../components/auth/SupabaseLogin.jsx';
---
-
+
diff --git a/astro-site/src/pages/mockup.astro b/astro-site/src/pages/mockup.astro
new file mode 100644
index 0000000..38b665f
--- /dev/null
+++ b/astro-site/src/pages/mockup.astro
@@ -0,0 +1,14 @@
+
+
+
+
+ AeThex Connect - Metaverse Communication
+
+
+
+
+
+
+
diff --git a/astro-site/src/pages/mockup.jsx b/astro-site/src/pages/mockup.jsx
index 4c6d94d..31a45bf 100644
--- a/astro-site/src/pages/mockup.jsx
+++ b/astro-site/src/pages/mockup.jsx
@@ -1,7 +1,2 @@
-import React from "react";
-import MainLayout from "../components/mockup/MainLayout";
-import "../components/mockup/global.css";
-export default function MockupPage() {
- return ;
-}
+// Removed: This page is deprecated. Use /app for the full platform UI.
diff --git a/astro-site/src/react-app/App.css b/astro-site/src/react-app/App.css
new file mode 100644
index 0000000..608e005
--- /dev/null
+++ b/astro-site/src/react-app/App.css
@@ -0,0 +1,189 @@
+/* Sleek Dark Gaming Theme - BitChat/Root Inspired */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', 'Roboto', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background: #000000;
+ min-height: 100vh;
+}
+
+code {
+ font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
+}
+
+.app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background: #000000;
+}
+
+.app-header {
+ background: rgba(10, 10, 15, 0.8);
+ backdrop-filter: blur(20px);
+ padding: 20px 32px;
+ text-align: center;
+ border-bottom: 1px solid rgba(0, 217, 255, 0.1);
+ box-shadow: 0 4px 24px rgba(0, 217, 255, 0.05);
+}
+
+.app-header h1 {
+ margin: 0 0 8px 0;
+ color: #00d9ff;
+ font-size: 32px;
+ font-weight: 700;
+ text-shadow: 0 0 30px rgba(0, 217, 255, 0.3);
+}
+
+.app-header p {
+ margin: 0;
+ color: #a1a1aa;
+ font-size: 16px;
+}
+
+.app-main {
+ flex: 1;
+ padding: 40px 24px;
+ max-width: 1200px;
+ width: 100%;
+ margin: 0 auto;
+}
+
+.user-profile {
+ background: rgba(10, 10, 15, 0.6);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(0, 217, 255, 0.1);
+ border-radius: 16px;
+ padding: 32px;
+ margin-bottom: 32px;
+ box-shadow: 0 8px 32px rgba(0, 217, 255, 0.1);
+}
+
+.profile-header {
+ text-align: center;
+ padding-bottom: 24px;
+ border-bottom: 1px solid rgba(0, 217, 255, 0.1);
+ margin-bottom: 24px;
+}
+
+.profile-header h2 {
+ margin: 0 0 8px 0;
+ color: #ffffff;
+ font-size: 28px;
+ font-weight: 600;
+}
+
+.profile-header p {
+ margin: 0 0 16px 0;
+ color: #a1a1aa;
+ font-size: 16px;
+}
+
+.profile-section {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.toggle-button {
+ padding: 12px 32px;
+ background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
+ color: #000000;
+ border: none;
+ border-radius: 12px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s;
+ align-self: center;
+ box-shadow: 0 4px 20px rgba(0, 217, 255, 0.3);
+}
+
+.toggle-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 30px rgba(0, 217, 255, 0.4), 0 0 40px rgba(0, 255, 136, 0.2);
+}
+
+.verification-container {
+ margin-top: 8px;
+}
+
+.info-section {
+ background: rgba(10, 10, 15, 0.6);
+ backdrop-filter: blur(20px);
+ border: 1px solid rgba(0, 217, 255, 0.1);
+ border-radius: 16px;
+ padding: 32px;
+ box-shadow: 0 8px 32px rgba(0, 217, 255, 0.1);
+}
+
+.info-section h3 {
+ margin: 0 0 16px 0;
+ color: #00d9ff;
+ font-size: 24px;
+ font-weight: 600;
+}
+
+.info-section p {
+ margin: 0 0 16px 0;
+ color: #d4d4d8;
+ line-height: 1.6;
+}
+
+.info-section ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.info-section li {
+ padding: 12px 16px;
+ color: #d4d4d8;
+ font-size: 16px;
+ background: rgba(0, 217, 255, 0.05);
+ border-radius: 8px;
+ margin-bottom: 8px;
+ border-left: 3px solid #00d9ff;
+}
+
+.app-footer {
+ background: rgba(10, 10, 15, 0.8);
+ backdrop-filter: blur(20px);
+ color: #a1a1aa;
+ text-align: center;
+ padding: 24px;
+ margin-top: auto;
+ border-top: 1px solid rgba(0, 217, 255, 0.1);
+}
+
+.app-footer p {
+ margin: 0;
+ font-size: 14px;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .app-main {
+ padding: 24px 16px;
+ }
+
+ .user-profile,
+ .info-section {
+ padding: 24px;
+ }
+
+ .app-header h1 {
+ font-size: 24px;
+ }
+
+ .profile-header h2 {
+ font-size: 24px;
+ }
+}
diff --git a/astro-site/src/react-app/App.jsx b/astro-site/src/react-app/App.jsx
new file mode 100644
index 0000000..366d5e5
--- /dev/null
+++ b/astro-site/src/react-app/App.jsx
@@ -0,0 +1,84 @@
+import React from 'react';
+import { AuthProvider, useAuth } from './contexts/AuthContext';
+import DomainVerification from './components/DomainVerification';
+import VerifiedDomainBadge from './components/VerifiedDomainBadge';
+import './App.css';
+
+/**
+ * Main application component
+ * Demo of domain verification feature
+ */
+function App() {
+ const { user, loading } = useAuth();
+ const [showVerification, setShowVerification] = React.useState(false);
+
+ if (loading) {
+ return Loading...
;
+ }
+
+ return (
+
+
+ AeThex Passport
+ Domain Verification
+
+
+
+ {user && (
+
+
+
{user.email}
+ {user.verifiedDomain && (
+
+ )}
+
+
+
+
setShowVerification(!showVerification)}
+ className="toggle-button"
+ >
+ {showVerification ? 'Hide' : 'Show'} Domain Verification
+
+
+ {showVerification && (
+
+
+
+ )}
+
+
+ )}
+
+
+
About Domain Verification
+
+ Domain verification allows you to prove ownership of a domain by adding
+ a DNS TXT record or connecting a wallet that owns a .aethex blockchain domain.
+
+
+ ✓ Verify traditional domains via DNS TXT records
+ ✓ Verify .aethex domains via blockchain
+ ✓ Display verified domain on your profile
+ ✓ Prevent domain impersonation
+
+
+
+
+
+
+ );
+}
+
+export default function AppWrapper() {
+ return (
+
+
+
+ );
+}
diff --git a/astro-site/src/react-app/Demo.css b/astro-site/src/react-app/Demo.css
new file mode 100644
index 0000000..a573ce9
--- /dev/null
+++ b/astro-site/src/react-app/Demo.css
@@ -0,0 +1,579 @@
+/* Demo App Styles - Sleek Dark Gaming Theme (BitChat/Root Inspired) */
+
+.demo-app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background: #000000;
+ color: #e4e4e7;
+}
+
+/* Loading Screen */
+.loading-screen {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-cont#0a0a0f;
+ color: #e4e4e7;
+}
+
+.loading-spinner {
+ font-size: 4rem;
+ animation: pulse 2s ease-in-out infinite;
+ margin-bottom: 1rem;
+ filter: drop-shadow(0 0 20px rgba(139, 92, 246, 0.6));
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.1);
+ opacity: 0.8;
+ }
+}
+
+.loading-screen p {
+ font-size: 1.2rem;
+ opacity: 0.7;
+ font-weight: 500;
+}
+
+/* Header */
+.demo-header {
+ background: #18181b;
+ border-bottom: 1px solid #27272a;
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
+ padding: 10 2px 10px rgba(0, 0, 0, 0.1);
+ padding: 1.5rem 2rem;
+}
+
+.header-content {
+ max-width: 1400px;
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.logo-section h1 {
+ margin: 0;
+ font-size: 1.75rem;
+ color: #00d9ff;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ text-shadow: 0 0 30px rgba(0, 217, 255, 0.3);
+}
+
+.tagline {
+ margin: 0.25rem 0 0 0;
+ color: #71717a;
+ font-size: 0.875rem;
+ font-weight: 500;
+}
+
+.user-section {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.user-info {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+}
+
+.user-name {
+ font-weight: 600;
+ color: #e4e4e7;
+}
+
+.user-email {
+ font-size: 0.8rem;
+ color: #71717a;
+}
+
+/* Navigation */
+.demo-nav {
+ background: #18181b;
+ border-bottom: 1px solid #27272a;
+ display: flex;
+ gap: 0.5rem;
+ padding: 0.75rem 2rem;
+ overflow-x: auto;
+ scrollbar-width: thin;
+ scrollbar-color: #3f3f46 transparent;
+}
+
+.demo-nav::-webkit-scrollbar {
+ height: 6px;
+}
+
+.demo-nav::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.demo-nav::-webkit-scrollbar-thumb {
+ background: #3f3f46;
+ border-radius: 3px;
+}
+
+.nav-tab {
+ background: #09090b;
+ border: 1px solid #27272a;
+ padding: 0.625rem 1.25rem;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ white-space: nowrap;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.25rem;
+ min-width: 120px;
+ color: #a1a1aa;
+ font-size: 0.875rem;
+}
+
+.nav-tab:hover {
+ background: #18181b;
+ border-color: #3f3f46;
+ color: #e4e4e7;
+ transform: translateY(-1px);
+}
+
+.nav-tab.active {
+ background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
+ border-color: transparent;
+ color: #000000;
+ font-weight: 700;
+ box-shadow: 0 4px 12px rgba(0, 217, 255, 0.4);
+}
+
+.tab-label {
+ font-weight: 600;
+ font-size: 0.8rem;
+}
+
+.tab-phase {
+ font-size: 0.65rem;
+ opacity: 0.7;
+ background: rgba(255, 255, 255, 0.05);
+ padding: 2px 6px;
+ border-radius: 4px;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+}
+
+.nav-tab.active .tab-phase {
+ background: rgba(255, 255, 255, 0.15);
+ opacity: 0.9;
+}
+
+/* Main Content */
+.demo-main {
+ flex: 1;
+ max-width: 1400px;
+ width: 100%;
+ margin: 0 auto;
+ padding: 2rem;
+}
+
+/* Overview Section */
+.overview-section {
+ background: #18181b;
+ border: 1px solid #27272a;
+ border-radius: 12px;
+ padding: 2rem;
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
+}
+
+.overview-section h2 {
+ margin: 0 0 0.75rem 0;
+ color: #e4e4e7;
+ font-size: 2rem;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+
+.intro {
+ color: #a1a1aa;
+ font-size: 1rem;
+ margin-bottom: 2rem;
+ line-height: 1.6;
+}
+
+/* Feature Grid */
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 1.25rem;
+ margin: 2rem 0;
+}
+
+.feature-card {
+ background: #09090b;
+ border: 1px solid #27272a;
+ border-radius: 12px;
+ padding: 1.5rem;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ position: relative;
+ overflow: hidden;
+}
+
+.feature-card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, #00d9ff, #00ff88);
+ opacity: 0;
+ transition: opacity 0.3s;
+}
+
+.feature-card:hover {
+ transform: translateY(-4px);
+ border-color: #3f3f46;
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(139, 92, 246, 0.1);
+}
+
+.feature-card:hover::before {
+ opacity: 1;
+}
+
+.feature-icon {
+ font-size: 2.5rem;
+ margin-bottom: 1rem;
+ filter: drop-shadow(0 4px 8px rgba(139, 92, 246, 0.3));
+}
+
+.feature-card h3 {
+ margin: 0.5rem 0;
+ color: #e4e4e7;
+ font-size: 1.25rem;
+ font-weight: 600;
+ letter-spacing: -0.01em;
+}
+
+.feature-card p {
+ color: #71717a;
+ margin: 0.5rem 0 1rem 0;
+ line-height: 1.5;
+ font-size: 0.9rem;
+}
+
+.feature-card ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.feature-card ul li {
+ padding: 0.4rem 0;
+ color: #a1a1aa;
+ font-size: 0.875rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.feature-card ul li:before {
+ content: "✓";
+ color: #00d9ff;
+ font-weight: bold;
+ font-size: 0.875rem;
+}
+
+/* Badges */
+.badge {
+ display: inline-block;
+ padding: 0.25rem 0.625rem;
+ border-radius: 6px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.phase-1 { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border: 1px solid rgba(59, 130, 246, 0.3); }
+.phase-2 { background: rgba(168, 85, 247, 0.15); color: #c084fc; border: 1px solid rgba(168, 85, 247, 0.3); }
+.phase-3 { background: rgba(34, 197, 94, 0.15); color: #4ade80; border: 1px solid rgba(34, 197, 94, 0.3); }
+.phase-4 { background: rgba(251, 146, 60, 0.15); color: #fb923c; border: 1px solid rgba(251, 146, 60, 0.3); }
+.phase-5 { background: rgba(236, 72, 153, 0.15); color: #f472b6; border: 1px solid rgba(236, 72, 153, 0.3); }
+.phase-6 { background: rgba(234, 179, 8, 0.15); color: #fbbf24; border: 1px solid rgba(234, 179, 8, 0.3); }
+
+/* Status Section */
+.status-section {
+ background: linear-gradient(135deg, #18181b 0%, #27272a 100%);
+ border: 1px solid #3f3f46;
+ color: #e4e4e7;
+ padding: 2rem;
+ border-radius: 12px;
+ margin: 2rem 0;
+ position: relative;
+ overflow: hidden;
+}
+
+.status-section::before {
+ content: '';
+ position: absolute;
+ top: -50%;
+ right: -50%;
+ width: 200%;
+ height: 200%;
+ background: radial-gradient(circle, rgba(139, 92, 246, 0.1) 0%, transparent 70%);
+ animation: rotate 20s linear infinite;
+}
+
+@keyframes rotate {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+.status-section h3 {
+ margin: 0 0 1rem 0;
+ font-size: 1.5rem;
+ position: relative;
+ z-index: 1;
+}
+
+.status-section p {
+ margin: 0.5rem 0;
+ opacity: 0.9;
+ position: relative;
+ z-index: 1;
+}
+
+.platform-badges {
+ display: flex;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+ margin: 1rem 0;
+ position: relative;
+ z-index: 1;
+}
+
+.platform-badge {
+ background: rgba(24, 24, 27, 0.6);
+ border: 1px solid #3f3f46;
+ padding: 0.5rem 1rem;
+ border-radius: 8px;
+ font-size: 0.875rem;
+ backdrop-filter: blur(10px);
+ transition: all 0.2s;
+}
+
+.platform-badge:hover {
+ background: rgba(0, 217, 255, 0.15);
+ border-color: #00d9ff;
+ transform: translateY(-2px);
+}
+
+.timeline {
+ font-style: italic;
+ opacity: 0.8;
+ margin-top: 1rem;
+ position: relative;
+ z-index: 1;
+}
+
+/* Quick Stats */
+.quick-stats {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 1.25rem;
+ margin-top: 2rem;
+}
+
+.stat {
+ text-align: center;
+ padding: 1.5rem;
+ background: #09090b;
+ border: 1px solid #27272a;
+ border-radius: 12px;
+ transition: all 0.3s;
+ position: relative;
+ overflow: hidden;
+}
+
+.stat::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 2px;
+ background: linear-gradient(90deg, #00d9ff, #00ff88);
+ transform: scaleX(0);
+ transition: transform 0.3s;
+}
+
+.stat:hover {
+ border-color: #3f3f46;
+ transform: translateY(-4px);
+}
+
+.stat:hover::before {
+ transform: scaleX(1);
+}
+
+.stat-value {
+ font-size: 2.5rem;
+ font-weight: 700;
+ line-height: 1;
+ background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.stat-label {
+ font-size: 0.875rem;
+ color: #71717a;
+ margin-top: 0.5rem;
+ font-weight: 500;
+}
+
+/* Feature Section */
+.feature-section {
+ background: #18181b;
+ border: 1px solid #27272a;
+ border-radius: 12px;
+ padding: 2rem;
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
+ min-height: 500px;
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ padding-bottom: 1rem;
+ border-bottom: 1px solid #27272a;
+}
+
+.section-header h2 {
+ margin: 0;
+ color: #e4e4e7;
+ font-size: 1.75rem;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+}
+
+.section-description {
+ color: #a1a1aa;
+ margin-bottom: 2rem;
+ line-height: 1.6;
+ font-size: 0.95rem;
+}
+
+/* Footer */
+.demo-footer {
+ background: #18181b;
+ border-top: 1px solid #27272a;
+ margin-top: auto;
+ padding: 2rem;
+}
+
+.footer-content {
+ max-width: 1400px;
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 2rem;
+ margin-bottom: 1.5rem;
+}
+
+.footer-section h4 {
+ margin: 0 0 1rem 0;
+ background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ font-size: 1rem;
+ font-weight: 600;
+}
+
+.footer-section p {
+ color: #71717a;
+ margin: 0;
+ font-size: 0.875rem;
+ line-height: 1.5;
+}
+
+.footer-section ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+
+.footer-section ul li {
+ padding: 0.375rem 0;
+ font-size: 0.875rem;
+ color: #71717a;
+ transition: color 0.2s;
+}
+
+.footer-section ul li:hover {
+ color: #a1a1aa;
+}
+
+.footer-section ul li a {
+ color: inherit;
+ text-decoration: none;
+ transition: color 0.2s;
+}
+
+.footer-section ul li a:hover {
+ color: #00d9ff;
+}
+
+.footer-bottom {
+ text-align: center;
+ padding-top: 1.5rem;
+ border-top: 1px solid #27272a;
+}
+
+.footer-bottom p {
+ margin: 0;
+ color: #52525b;
+ font-size: 0.8rem;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .header-content {
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .user-info {
+ align-items: center;
+ }
+
+ .demo-nav {
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ }
+
+ .feature-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .quick-stats {
+ grid-template-columns: repeat(2, 1fr);
+ }
+
+ .footer-content {
+ grid-template-columns: 1fr;
+ text-align: center;
+ }
+}
diff --git a/astro-site/src/react-app/Demo.jsx b/astro-site/src/react-app/Demo.jsx
new file mode 100644
index 0000000..0f70b43
--- /dev/null
+++ b/astro-site/src/react-app/Demo.jsx
@@ -0,0 +1,311 @@
+import React, { useState } from 'react';
+import { SocketProvider } from './contexts/SocketContext';
+import { AuthProvider, useAuth } from './contexts/AuthContext';
+import DomainVerification from './components/DomainVerification';
+import VerifiedDomainBadge from './components/VerifiedDomainBadge';
+import Chat from './components/Chat/Chat';
+import Call from './components/Call';
+import GameForgeChat from './components/GameForgeChat';
+import UpgradeFlow from './components/Premium';
+import './App.css';
+
+/**
+ * Comprehensive demo showcasing all AeThex Connect features
+ * Phases 1-6 implementation
+ */
+function DemoContent() {
+ const [activeTab, setActiveTab] = useState('overview');
+ const { user, loading } = useAuth();
+
+ // Show loading state while auth initializes
+ if (loading || !user) {
+ return (
+
+
{String.fromCodePoint(0x1F680)}
+
Loading AeThex Connect...
+
+ );
+ }
+
+ const tabs = [
+ { id: 'overview', label: '🏠 Overview', icon: '🏠' },
+ { id: 'domain', label: '🌐 Domain Verification', phase: 'Phase 1' },
+ { id: 'messaging', label: '💬 Real-time Chat', phase: 'Phase 2' },
+ { id: 'gameforge', label: '🎮 GameForge', phase: 'Phase 3' },
+ { id: 'calls', label: '📞 Voice/Video', phase: 'Phase 4' },
+ { id: 'premium', label: '⭐ Premium', phase: 'Phase 6' }
+ ];
+
+ return (
+
+
+
+
+
+
🚀 AeThex Connect
+
Next-Gen Communication for Gamers
+
+
+
+ {user.name}
+ {user.email}
+
+ {user.verifiedDomain && (
+
+ )}
+
+
+
+
+
+ {tabs.map(tab => (
+ setActiveTab(tab.id)}
+ >
+ {tab.label}
+ {tab.phase && {tab.phase} }
+
+ ))}
+
+
+
+ {activeTab === 'overview' && (
+
+
Welcome to AeThex Connect
+
+ A comprehensive communication platform built specifically for gamers and game developers.
+ Explore each feature using the tabs above.
+
+
+
+
+
🌐
+
Domain Verification
+
Phase 1
+
Verify ownership of traditional domains (DNS) or blockchain .aethex domains
+
+ DNS TXT record verification
+ Blockchain domain integration
+ Verified profile badges
+
+
+
+
+
💬
+
Real-time Messaging
+
Phase 2
+
Instant, encrypted messaging with WebSocket connections
+
+ Private conversations
+ Message history
+ Read receipts
+ Typing indicators
+
+
+
+
+
🎮
+
GameForge Integration
+
Phase 3
+
Built-in chat for game development teams
+
+ Project channels
+ Team collaboration
+ Build notifications
+ Asset sharing
+
+
+
+
+
📞
+
Voice & Video Calls
+
Phase 4
+
High-quality WebRTC calls with screen sharing
+
+ 1-on-1 voice calls
+ Video conferencing
+ Screen sharing
+ Call recording
+
+
+
+
+
🔗
+
Nexus Engine
+
Phase 5
+
Cross-game identity and social features
+
+ Unified player profiles
+ Friend system
+ Game lobbies
+ Rich presence
+
+
+
+
+
⭐
+
Premium Subscriptions
+
Phase 6
+
Monetization with blockchain domains
+
+ .aethex domain marketplace
+ Premium tiers ($10/mo)
+ Enterprise plans
+ Stripe integration
+
+
+
+
+
+
🚀 Phase 7: Full Platform (In Progress)
+
Transform AeThex Connect into cross-platform apps:
+
+ 🌐 Progressive Web App
+ 📱 iOS & Android
+ 💻 Windows, macOS, Linux
+
+
Expected completion: May 2026 (5 months)
+
+
+
+
+
+
1
+
Phase In Progress
+
+
+
+
+
+ )}
+
+ {activeTab === 'domain' && (
+
+
+
🌐 Domain Verification
+ Phase 1
+
+
+ Prove ownership of your domain to display it on your profile and prevent impersonation.
+ Supports both traditional domains (via DNS) and blockchain .aethex domains.
+
+
+
+ )}
+
+ {activeTab === 'messaging' && (
+
+
+
💬 Real-time Messaging
+ Phase 2
+
+
+ Private encrypted conversations with real-time delivery. Messages sync across all devices.
+
+
+
+ )}
+
+ {activeTab === 'gameforge' && (
+
+
+
🎮 GameForge Integration
+ Phase 3
+
+
+ Collaborate with your game development team. Channels auto-provision with your GameForge projects.
+
+
+
+ )}
+
+ {activeTab === 'calls' && (
+
+
+
📞 Voice & Video Calls
+ Phase 4
+
+
+ Crystal-clear WebRTC calls with screen sharing. Perfect for co-op gaming or team standups.
+
+
+
+ )}
+
+ {activeTab === 'premium' && (
+
+
+
⭐ Premium Subscriptions
+ Phase 6
+
+
+ Upgrade to unlock blockchain .aethex domains, increased storage, and advanced features.
+
+
+
+ )}
+
+
+
+
+
+ );
+}
+
+function Demo() {
+ return (
+
+
+
+ );
+}
+
+export default Demo;
diff --git a/astro-site/src/react-app/components/Call/Call.css b/astro-site/src/react-app/components/Call/Call.css
new file mode 100644
index 0000000..24f2704
--- /dev/null
+++ b/astro-site/src/react-app/components/Call/Call.css
@@ -0,0 +1,345 @@
+.call-container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #1a1a1a;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+}
+
+.call-error {
+ position: absolute;
+ top: 20px;
+ left: 50%;
+ transform: translateX(-50%);
+ background-color: #f44336;
+ color: white;
+ padding: 12px 20px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ z-index: 1001;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+}
+
+.call-error button {
+ background: none;
+ border: none;
+ color: white;
+ font-size: 20px;
+ cursor: pointer;
+ padding: 0;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.call-header {
+ padding: 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background-color: rgba(0, 0, 0, 0.5);
+}
+
+.call-status {
+ color: white;
+ font-size: 18px;
+ font-weight: 500;
+}
+
+.quality-indicator {
+ padding: 6px 12px;
+ border-radius: 20px;
+ color: white;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+}
+
+.video-container {
+ flex: 1;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.remote-videos {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ gap: 10px;
+ padding: 10px;
+}
+
+/* Grid layouts for different participant counts */
+.remote-videos:has(.remote-video-wrapper:nth-child(1):last-child) {
+ grid-template-columns: 1fr;
+}
+
+.remote-videos:has(.remote-video-wrapper:nth-child(2)) {
+ grid-template-columns: repeat(2, 1fr);
+}
+
+.remote-videos:has(.remote-video-wrapper:nth-child(3)),
+.remote-videos:has(.remote-video-wrapper:nth-child(4)) {
+ grid-template-columns: repeat(2, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+}
+
+.remote-videos:has(.remote-video-wrapper:nth-child(5)),
+.remote-videos:has(.remote-video-wrapper:nth-child(6)) {
+ grid-template-columns: repeat(3, 1fr);
+ grid-template-rows: repeat(2, 1fr);
+}
+
+.remote-videos:has(.remote-video-wrapper:nth-child(7)) {
+ grid-template-columns: repeat(3, 1fr);
+ grid-template-rows: repeat(3, 1fr);
+}
+
+.remote-video-wrapper {
+ position: relative;
+ background-color: #2c2c2c;
+ border-radius: 12px;
+ overflow: hidden;
+ min-height: 200px;
+}
+
+.remote-video {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.participant-name {
+ position: absolute;
+ bottom: 12px;
+ left: 12px;
+ background-color: rgba(0, 0, 0, 0.7);
+ color: white;
+ padding: 6px 12px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.local-video-wrapper {
+ position: absolute;
+ bottom: 100px;
+ right: 20px;
+ width: 200px;
+ height: 150px;
+ background-color: #2c2c2c;
+ border-radius: 12px;
+ overflow: hidden;
+ border: 2px solid #ffffff20;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
+}
+
+.local-video {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ transform: scaleX(-1); /* Mirror effect for local video */
+}
+
+.local-label {
+ position: absolute;
+ bottom: 8px;
+ left: 8px;
+ background-color: rgba(0, 0, 0, 0.7);
+ color: white;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.call-controls {
+ position: absolute;
+ bottom: 30px;
+ left: 50%;
+ transform: translateX(-50%);
+ display: flex;
+ gap: 16px;
+ background-color: rgba(0, 0, 0, 0.7);
+ padding: 16px 24px;
+ border-radius: 50px;
+ backdrop-filter: blur(10px);
+}
+
+.control-btn {
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ border: none;
+ background-color: #3c3c3c;
+ color: white;
+ cursor: pointer;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ font-size: 24px;
+ transition: all 0.2s ease;
+ position: relative;
+}
+
+.control-btn:hover {
+ transform: scale(1.1);
+ background-color: #4c4c4c;
+}
+
+.control-btn:active {
+ transform: scale(0.95);
+}
+
+.control-btn.active {
+ background-color: #4CAF50;
+}
+
+.control-btn.inactive {
+ background-color: #f44336;
+}
+
+.control-btn.accept-btn {
+ background-color: #4CAF50;
+ width: 120px;
+ border-radius: 28px;
+ font-size: 16px;
+ gap: 8px;
+}
+
+.control-btn.accept-btn .icon {
+ font-size: 20px;
+}
+
+.control-btn.reject-btn {
+ background-color: #f44336;
+ width: 120px;
+ border-radius: 28px;
+ font-size: 16px;
+ gap: 8px;
+}
+
+.control-btn.reject-btn .icon {
+ font-size: 20px;
+}
+
+.control-btn.end-btn {
+ background-color: #f44336;
+}
+
+.control-btn.end-btn:hover {
+ background-color: #d32f2f;
+}
+
+.call-actions {
+ display: flex;
+ gap: 20px;
+ justify-content: center;
+ padding: 40px;
+}
+
+.start-call-btn {
+ padding: 16px 32px;
+ border: none;
+ border-radius: 12px;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ transition: all 0.2s ease;
+ color: white;
+}
+
+.start-call-btn.audio {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+
+.start-call-btn.video {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+}
+
+.start-call-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
+}
+
+.start-call-btn:active {
+ transform: translateY(0);
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+ .local-video-wrapper {
+ width: 120px;
+ height: 90px;
+ bottom: 110px;
+ right: 10px;
+ }
+
+ .call-controls {
+ gap: 12px;
+ padding: 12px 16px;
+ }
+
+ .control-btn {
+ width: 48px;
+ height: 48px;
+ font-size: 20px;
+ }
+
+ .control-btn.accept-btn,
+ .control-btn.reject-btn {
+ width: 100px;
+ font-size: 14px;
+ }
+
+ .remote-videos {
+ gap: 5px;
+ padding: 5px;
+ }
+
+ .participant-name {
+ font-size: 12px;
+ padding: 4px 8px;
+ }
+
+ .call-actions {
+ flex-direction: column;
+ padding: 20px;
+ }
+
+ .start-call-btn {
+ width: 100%;
+ justify-content: center;
+ }
+}
+
+/* Animation for ringing */
+@keyframes pulse {
+ 0%, 100% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 50% {
+ transform: scale(1.05);
+ opacity: 0.8;
+ }
+}
+
+.call-status:has(:contains("Calling")) {
+ animation: pulse 2s ease-in-out infinite;
+}
diff --git a/astro-site/src/react-app/components/Call/index.jsx b/astro-site/src/react-app/components/Call/index.jsx
new file mode 100644
index 0000000..2df769f
--- /dev/null
+++ b/astro-site/src/react-app/components/Call/index.jsx
@@ -0,0 +1,556 @@
+import React, { useState, useEffect, useRef } from 'react';
+import axios from 'axios';
+import WebRTCManager from '../../utils/webrtc';
+import './Call.css';
+
+const Call = ({ socket, conversationId, participants, onCallEnd }) => {
+ const [callId, setCallId] = useState(null);
+ const [callStatus, setCallStatus] = useState('idle'); // idle, initiating, ringing, connected, ended
+ const [isAudioEnabled, setIsAudioEnabled] = useState(true);
+ const [isVideoEnabled, setIsVideoEnabled] = useState(true);
+ const [isScreenSharing, setIsScreenSharing] = useState(false);
+ const [callDuration, setCallDuration] = useState(0);
+ const [connectionQuality, setConnectionQuality] = useState('good'); // good, fair, poor
+ const [remoteParticipants, setRemoteParticipants] = useState([]);
+ const [error, setError] = useState(null);
+
+ const webrtcManager = useRef(null);
+ const localVideoRef = useRef(null);
+ const remoteVideosRef = useRef(new Map());
+ const callStartTime = useRef(null);
+ const durationInterval = useRef(null);
+ const statsInterval = useRef(null);
+
+ /**
+ * Initialize WebRTC manager
+ */
+ useEffect(() => {
+ if (!socket) return;
+
+ webrtcManager.current = new WebRTCManager(socket);
+
+ // Setup event handlers
+ webrtcManager.current.onRemoteStream = handleRemoteStream;
+ webrtcManager.current.onRemoteStreamRemoved = handleRemoteStreamRemoved;
+ webrtcManager.current.onConnectionStateChange = handleConnectionStateChange;
+
+ return () => {
+ if (webrtcManager.current) {
+ webrtcManager.current.cleanup();
+ }
+ clearInterval(durationInterval.current);
+ clearInterval(statsInterval.current);
+ };
+ }, [socket]);
+
+ /**
+ * Listen for incoming calls
+ */
+ useEffect(() => {
+ if (!socket) return;
+
+ socket.on('call:incoming', handleIncomingCall);
+ socket.on('call:ended', handleCallEnded);
+
+ return () => {
+ socket.off('call:incoming', handleIncomingCall);
+ socket.off('call:ended', handleCallEnded);
+ };
+ }, [socket]);
+
+ /**
+ * Update call duration timer
+ */
+ useEffect(() => {
+ if (callStatus === 'connected' && !durationInterval.current) {
+ callStartTime.current = Date.now();
+ durationInterval.current = setInterval(() => {
+ const duration = Math.floor((Date.now() - callStartTime.current) / 1000);
+ setCallDuration(duration);
+ }, 1000);
+ } else if (callStatus !== 'connected' && durationInterval.current) {
+ clearInterval(durationInterval.current);
+ durationInterval.current = null;
+ }
+
+ return () => {
+ if (durationInterval.current) {
+ clearInterval(durationInterval.current);
+ }
+ };
+ }, [callStatus]);
+
+ /**
+ * Monitor connection quality
+ */
+ useEffect(() => {
+ if (callStatus === 'connected' && !statsInterval.current) {
+ statsInterval.current = setInterval(async () => {
+ if (webrtcManager.current && remoteParticipants.length > 0) {
+ const firstParticipant = remoteParticipants[0];
+ const stats = await webrtcManager.current.getConnectionStats(firstParticipant.userId);
+
+ if (stats && stats.connection) {
+ const rtt = stats.connection.roundTripTime || 0;
+ const bitrate = stats.connection.availableOutgoingBitrate || 0;
+
+ // Determine quality based on RTT and bitrate
+ if (rtt < 0.1 && bitrate > 500000) {
+ setConnectionQuality('good');
+ } else if (rtt < 0.3 && bitrate > 200000) {
+ setConnectionQuality('fair');
+ } else {
+ setConnectionQuality('poor');
+ }
+ }
+ }
+ }, 3000);
+ } else if (callStatus !== 'connected' && statsInterval.current) {
+ clearInterval(statsInterval.current);
+ statsInterval.current = null;
+ }
+
+ return () => {
+ if (statsInterval.current) {
+ clearInterval(statsInterval.current);
+ }
+ };
+ }, [callStatus, remoteParticipants]);
+
+ /**
+ * Handle incoming call
+ */
+ const handleIncomingCall = async (data) => {
+ console.log('Incoming call:', data);
+ setCallId(data.callId);
+ setCallStatus('ringing');
+ setRemoteParticipants(data.participants || []);
+ };
+
+ /**
+ * Handle remote stream received
+ */
+ const handleRemoteStream = (userId, stream) => {
+ console.log('Remote stream received from:', userId);
+
+ // Get or create video element for this user
+ const videoElement = remoteVideosRef.current.get(userId);
+ if (videoElement) {
+ videoElement.srcObject = stream;
+ }
+ };
+
+ /**
+ * Handle remote stream removed
+ */
+ const handleRemoteStreamRemoved = (userId) => {
+ console.log('Remote stream removed from:', userId);
+ setRemoteParticipants(prev => prev.filter(p => p.userId !== userId));
+ };
+
+ /**
+ * Handle connection state change
+ */
+ const handleConnectionStateChange = (userId, state) => {
+ console.log(`Connection state with ${userId}:`, state);
+
+ if (state === 'connected') {
+ setCallStatus('connected');
+ } else if (state === 'failed' || state === 'disconnected') {
+ setError(`Connection ${state} with user ${userId}`);
+ }
+ };
+
+ /**
+ * Handle call ended
+ */
+ const handleCallEnded = (data) => {
+ console.log('Call ended:', data);
+ setCallStatus('ended');
+
+ if (webrtcManager.current) {
+ webrtcManager.current.cleanup();
+ }
+
+ if (onCallEnd) {
+ onCallEnd(data);
+ }
+ };
+
+ /**
+ * Initiate a new call
+ */
+ const initiateCall = async (type = 'video') => {
+ try {
+ setCallStatus('initiating');
+ setError(null);
+
+ // Get TURN credentials
+ const turnResponse = await axios.get('/api/calls/turn-credentials', {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+
+ if (webrtcManager.current && turnResponse.data.credentials) {
+ await webrtcManager.current.setTurnCredentials(turnResponse.data.credentials);
+ }
+
+ // Initialize local media stream
+ const audioEnabled = true;
+ const videoEnabled = type === 'video';
+
+ if (webrtcManager.current) {
+ const localStream = await webrtcManager.current.initializeLocalStream(audioEnabled, videoEnabled);
+
+ // Display local video
+ if (localVideoRef.current) {
+ localVideoRef.current.srcObject = localStream;
+ }
+ }
+
+ // Initiate call via API
+ const response = await axios.post('/api/calls/initiate', {
+ conversationId: conversationId,
+ type: type,
+ participantIds: participants.map(p => p.userId)
+ }, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+
+ const { callId: newCallId } = response.data;
+ setCallId(newCallId);
+ setCallStatus('ringing');
+
+ if (webrtcManager.current) {
+ webrtcManager.current.currentCallId = newCallId;
+ webrtcManager.current.isInitiator = true;
+
+ // Create peer connections for each participant
+ for (const participant of participants) {
+ await webrtcManager.current.initiateCallToUser(participant.userId);
+ }
+ }
+
+ setRemoteParticipants(participants);
+ } catch (err) {
+ console.error('Error initiating call:', err);
+ setError(err.response?.data?.message || err.message || 'Failed to initiate call');
+ setCallStatus('idle');
+ }
+ };
+
+ /**
+ * Answer incoming call
+ */
+ const answerCall = async () => {
+ try {
+ setError(null);
+
+ // Get TURN credentials
+ const turnResponse = await axios.get('/api/calls/turn-credentials', {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+
+ if (webrtcManager.current && turnResponse.data.credentials) {
+ await webrtcManager.current.setTurnCredentials(turnResponse.data.credentials);
+ }
+
+ // Initialize local media stream
+ if (webrtcManager.current) {
+ const localStream = await webrtcManager.current.initializeLocalStream(true, true);
+
+ // Display local video
+ if (localVideoRef.current) {
+ localVideoRef.current.srcObject = localStream;
+ }
+ }
+
+ // Answer call via API
+ await axios.post(`/api/calls/${callId}/answer`, {}, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+
+ setCallStatus('connected');
+ } catch (err) {
+ console.error('Error answering call:', err);
+ setError(err.response?.data?.message || err.message || 'Failed to answer call');
+ setCallStatus('idle');
+ }
+ };
+
+ /**
+ * Reject incoming call
+ */
+ const rejectCall = async () => {
+ try {
+ await axios.post(`/api/calls/${callId}/reject`, {}, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+
+ setCallStatus('idle');
+ setCallId(null);
+ } catch (err) {
+ console.error('Error rejecting call:', err);
+ setError(err.response?.data?.message || err.message || 'Failed to reject call');
+ }
+ };
+
+ /**
+ * End active call
+ */
+ const endCall = async () => {
+ try {
+ if (callId) {
+ await axios.post(`/api/calls/${callId}/end`, {}, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ }
+
+ if (webrtcManager.current) {
+ webrtcManager.current.cleanup();
+ }
+
+ setCallStatus('ended');
+ setCallId(null);
+
+ if (onCallEnd) {
+ onCallEnd({ reason: 'ended-by-user' });
+ }
+ } catch (err) {
+ console.error('Error ending call:', err);
+ setError(err.response?.data?.message || err.message || 'Failed to end call');
+ }
+ };
+
+ /**
+ * Toggle audio on/off
+ */
+ const toggleAudio = async () => {
+ if (webrtcManager.current) {
+ const enabled = !isAudioEnabled;
+ webrtcManager.current.toggleAudio(enabled);
+ setIsAudioEnabled(enabled);
+
+ // Update media state via API
+ if (callId) {
+ try {
+ await axios.patch(`/api/calls/${callId}/media`, {
+ audioEnabled: enabled
+ }, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ } catch (err) {
+ console.error('Error updating media state:', err);
+ }
+ }
+ }
+ };
+
+ /**
+ * Toggle video on/off
+ */
+ const toggleVideo = async () => {
+ if (webrtcManager.current) {
+ const enabled = !isVideoEnabled;
+ webrtcManager.current.toggleVideo(enabled);
+ setIsVideoEnabled(enabled);
+
+ // Update media state via API
+ if (callId) {
+ try {
+ await axios.patch(`/api/calls/${callId}/media`, {
+ videoEnabled: enabled
+ }, {
+ headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }
+ });
+ } catch (err) {
+ console.error('Error updating media state:', err);
+ }
+ }
+ }
+ };
+
+ /**
+ * Toggle screen sharing
+ */
+ const toggleScreenShare = async () => {
+ if (webrtcManager.current) {
+ try {
+ if (isScreenSharing) {
+ webrtcManager.current.stopScreenShare();
+ setIsScreenSharing(false);
+
+ // Restore local video
+ if (localVideoRef.current && webrtcManager.current.getLocalStream()) {
+ localVideoRef.current.srcObject = webrtcManager.current.getLocalStream();
+ }
+ } else {
+ const screenStream = await webrtcManager.current.startScreenShare();
+ setIsScreenSharing(true);
+
+ // Display screen in local video
+ if (localVideoRef.current) {
+ localVideoRef.current.srcObject = screenStream;
+ }
+ }
+ } catch (err) {
+ console.error('Error toggling screen share:', err);
+ setError('Failed to share screen');
+ }
+ }
+ };
+
+ /**
+ * Format call duration (HH:MM:SS or MM:SS)
+ */
+ const formatDuration = (seconds) => {
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+ const secs = seconds % 60;
+
+ if (hours > 0) {
+ return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
+ }
+ return `${minutes}:${secs.toString().padStart(2, '0')}`;
+ };
+
+ /**
+ * Render call controls
+ */
+ const renderControls = () => {
+ if (callStatus === 'ringing' && !webrtcManager.current?.isInitiator) {
+ return (
+
+
+ 📞
+ Answer
+
+
+ 📵
+ Reject
+
+
+ );
+ }
+
+ if (callStatus === 'connected' || callStatus === 'ringing') {
+ return (
+
+
+ {isAudioEnabled ? '🎤' : '🔇'}
+
+
+
+ {isVideoEnabled ? '📹' : '🚫'}
+
+
+
+ 🖥️
+
+
+
+ 📵
+ End
+
+
+ );
+ }
+
+ return null;
+ };
+
+ /**
+ * Render connection quality indicator
+ */
+ const renderQualityIndicator = () => {
+ if (callStatus !== 'connected') return null;
+
+ const colors = {
+ good: '#4CAF50',
+ fair: '#FFC107',
+ poor: '#F44336'
+ };
+
+ return (
+
+ {connectionQuality}
+
+ );
+ };
+
+ return (
+
+ {error && (
+
+ {error}
+ setError(null)}>×
+
+ )}
+
+
+
+ {callStatus === 'ringing' && 'Calling...'}
+ {callStatus === 'connected' && `Call Duration: ${formatDuration(callDuration)}`}
+ {callStatus === 'ended' && 'Call Ended'}
+
+ {renderQualityIndicator()}
+
+
+
+ {/* Remote videos */}
+
+ {remoteParticipants.map(participant => (
+
+
{
+ if (el) remoteVideosRef.current.set(participant.userId, el);
+ }}
+ autoPlay
+ playsInline
+ className="remote-video"
+ />
+ {participant.userName || participant.userIdentifier}
+
+ ))}
+
+
+ {/* Local video */}
+ {(callStatus === 'ringing' || callStatus === 'connected') && (
+
+ )}
+
+
+ {renderControls()}
+
+ {callStatus === 'idle' && (
+
+ initiateCall('audio')}>
+ 🎤 Start Audio Call
+
+ initiateCall('video')}>
+ 📹 Start Video Call
+
+
+ )}
+
+ );
+};
+
+export default Call;
diff --git a/astro-site/src/react-app/components/Chat/Chat.css b/astro-site/src/react-app/components/Chat/Chat.css
new file mode 100644
index 0000000..4d79929
--- /dev/null
+++ b/astro-site/src/react-app/components/Chat/Chat.css
@@ -0,0 +1,156 @@
+/* Chat Container - Dark Gaming Theme */
+.chat-container {
+ display: flex;
+ height: 100vh;
+ background: #000000;
+ position: relative;
+}
+
+.chat-status {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 100;
+}
+
+.status-indicator {
+ padding: 6px 12px;
+ border-radius: 20px;
+ font-size: 0.875rem;
+ font-weight: 500;
+}
+
+.status-indicator.online {
+ background: rgba(0, 255, 136, 0.2);
+ color: #00ff88;
+ border: 1px solid #00ff88;
+}
+
+.status-indicator.offline {
+ background: rgba(239, 68, 68, 0.2);
+ color: #ef4444;
+ border: 1px solid #ef4444;
+}
+
+/* Main Chat Area */
+.chat-main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: rgba(10, 10, 15, 0.6);
+ backdrop-filter: blur(20px);
+ border-left: 1px solid rgba(0, 217, 255, 0.1);
+}
+
+.chat-header {
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid rgba(0, 217, 255, 0.1);
+ background: rgba(10, 10, 15, 0.8);
+ backdrop-filter: blur(20px);
+}
+
+.conversation-info {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.conversation-avatar {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-weight: 600;
+ font-size: 1.25rem;
+}
+
+.conversation-info h3 {
+ margin: 0;
+ font-size: 1.125rem;
+ font-weight: 600;
+}
+
+.participant-info {
+ margin: 0.25rem 0 0 0;
+ font-size: 0.875rem;
+ color: #6b7280;
+}
+
+/* No Conversation Selected */
+.no-conversation-selected {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ color: #9ca3af;
+}
+
+.no-conversation-selected p {
+ margin: 0.5rem 0;
+}
+
+.no-conversation-selected .hint {
+ font-size: 0.875rem;
+ color: #d1d5db;
+}
+
+/* Loading/Error States */
+.chat-loading,
+.chat-error {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100vh;
+ gap: 1rem;
+}
+
+.spinner {
+ width: 40px;
+ height: 40px;
+ border: 4px solid #e5e7eb;
+ border-top-color: #3b82f6;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+.chat-error p {
+ color: #dc2626;
+ font-weight: 500;
+}
+
+.chat-error button {
+ padding: 0.5rem 1rem;
+ background: #3b82f6;
+ color: white;
+ border: none;
+ border-radius: 0.5rem;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.chat-error button:hover {
+ background: #2563eb;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .chat-container {
+ flex-direction: column;
+ }
+
+ .conversation-list {
+ max-width: 100%;
+ }
+}
diff --git a/astro-site/src/react-app/components/Chat/Chat.jsx b/astro-site/src/react-app/components/Chat/Chat.jsx
new file mode 100644
index 0000000..b22419a
--- /dev/null
+++ b/astro-site/src/react-app/components/Chat/Chat.jsx
@@ -0,0 +1,425 @@
+/**
+ * Chat Component - Main messaging interface
+ */
+
+import React, { useState, useEffect, useRef } from 'react';
+import { useSocket } from '../../contexts/SocketContext';
+import ConversationList from './ConversationList';
+import MessageList from './MessageList';
+import MessageInput from './MessageInput';
+import './Chat.css';
+
+export default function Chat() {
+ const { socket, connected } = useSocket();
+ const [conversations, setConversations] = useState([]);
+ const [activeConversation, setActiveConversation] = useState(null);
+ const [messages, setMessages] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [typingUsers, setTypingUsers] = useState(new Set());
+ const [error, setError] = useState(null);
+
+ const typingTimeoutRef = useRef(null);
+
+ // Load conversations on mount
+ useEffect(() => {
+ loadConversations();
+ }, []);
+
+ // Socket event listeners
+ useEffect(() => {
+ if (!socket || !connected) return;
+
+ // New message
+ socket.on('new_message', handleNewMessage);
+
+ // Message edited
+ socket.on('message_edited', handleMessageEdited);
+
+ // Message deleted
+ socket.on('message_deleted', handleMessageDeleted);
+
+ // Reaction added
+ socket.on('reaction_added', handleReactionAdded);
+
+ // Reaction removed
+ socket.on('reaction_removed', handleReactionRemoved);
+
+ // Typing indicators
+ socket.on('user_typing', handleTypingStart);
+ socket.on('user_stopped_typing', handleTypingStop);
+
+ // User status changed
+ socket.on('user_status_changed', handleStatusChange);
+
+ return () => {
+ socket.off('new_message', handleNewMessage);
+ socket.off('message_edited', handleMessageEdited);
+ socket.off('message_deleted', handleMessageDeleted);
+ socket.off('reaction_added', handleReactionAdded);
+ socket.off('reaction_removed', handleReactionRemoved);
+ socket.off('user_typing', handleTypingStart);
+ socket.off('user_stopped_typing', handleTypingStop);
+ socket.off('user_status_changed', handleStatusChange);
+ };
+ }, [socket, connected, activeConversation]);
+
+ // Load conversations from API
+ const loadConversations = async () => {
+ try {
+ const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:3000'}/api/messaging/conversations`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ setConversations(data.conversations);
+ } else {
+ setError(data.error);
+ }
+ } catch (error) {
+ console.error('Failed to load conversations:', error);
+ setError('Failed to load conversations');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Load messages for a conversation
+ const loadMessages = async (conversationId) => {
+ try {
+ const response = await fetch(
+ `${import.meta.env.VITE_API_URL || 'http://localhost:3000'}/api/messaging/conversations/${conversationId}/messages`,
+ {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ }
+ );
+
+ const data = await response.json();
+
+ if (data.success) {
+ setMessages(data.messages);
+
+ // Mark as read
+ if (data.messages.length > 0) {
+ markAsRead(conversationId);
+ }
+ }
+ } catch (error) {
+ console.error('Failed to load messages:', error);
+ }
+ };
+
+ // Select conversation
+ const selectConversation = async (conversation) => {
+ setActiveConversation(conversation);
+ await loadMessages(conversation.id);
+ };
+
+ // Handle new message
+ const handleNewMessage = (message) => {
+ // If this conversation is active, add message
+ if (activeConversation && activeConversation.id === message.conversationId) {
+ setMessages(prev => [...prev, message]);
+
+ // Mark as read
+ markAsRead(message.conversationId);
+ }
+
+ // Update conversation list (move to top, update last message)
+ setConversations(prev => {
+ const updated = prev.map(conv => {
+ if (conv.id === message.conversationId) {
+ return {
+ ...conv,
+ lastMessage: message,
+ updatedAt: message.createdAt,
+ unreadCount: activeConversation?.id === message.conversationId ? 0 : conv.unreadCount + 1
+ };
+ }
+ return conv;
+ });
+
+ // Sort by updated_at
+ return updated.sort((a, b) =>
+ new Date(b.updatedAt) - new Date(a.updatedAt)
+ );
+ });
+ };
+
+ // Handle message edited
+ const handleMessageEdited = (data) => {
+ const { messageId, content, editedAt } = data;
+
+ setMessages(prev => prev.map(msg =>
+ msg.id === messageId
+ ? { ...msg, content: content, editedAt: editedAt }
+ : msg
+ ));
+ };
+
+ // Handle message deleted
+ const handleMessageDeleted = (data) => {
+ const { messageId } = data;
+ setMessages(prev => prev.filter(msg => msg.id !== messageId));
+ };
+
+ // Handle reaction added
+ const handleReactionAdded = (data) => {
+ const { messageId, emoji, userId } = data;
+
+ setMessages(prev => prev.map(msg => {
+ if (msg.id === messageId) {
+ const reactions = [...(msg.reactions || [])];
+ const existing = reactions.find(r => r.emoji === emoji);
+
+ if (existing) {
+ if (!existing.users.includes(userId)) {
+ existing.users.push(userId);
+ }
+ } else {
+ reactions.push({
+ emoji: emoji,
+ users: [userId]
+ });
+ }
+
+ return { ...msg, reactions: reactions };
+ }
+ return msg;
+ }));
+ };
+
+ // Handle reaction removed
+ const handleReactionRemoved = (data) => {
+ const { messageId, emoji, userId } = data;
+
+ setMessages(prev => prev.map(msg => {
+ if (msg.id === messageId) {
+ const reactions = (msg.reactions || [])
+ .map(r => {
+ if (r.emoji === emoji) {
+ return {
+ ...r,
+ users: r.users.filter(u => u !== userId)
+ };
+ }
+ return r;
+ })
+ .filter(r => r.users.length > 0);
+
+ return { ...msg, reactions: reactions };
+ }
+ return msg;
+ }));
+ };
+
+ // Handle typing start
+ const handleTypingStart = (data) => {
+ const { conversationId, userId } = data;
+
+ if (activeConversation && activeConversation.id === conversationId) {
+ setTypingUsers(prev => new Set([...prev, userId]));
+ }
+ };
+
+ // Handle typing stop
+ const handleTypingStop = (data) => {
+ const { conversationId, userId } = data;
+
+ if (activeConversation && activeConversation.id === conversationId) {
+ setTypingUsers(prev => {
+ const updated = new Set(prev);
+ updated.delete(userId);
+ return updated;
+ });
+ }
+ };
+
+ // Handle user status change
+ const handleStatusChange = (data) => {
+ const { userId, status } = data;
+
+ // Update conversation participants
+ setConversations(prev => prev.map(conv => ({
+ ...conv,
+ participants: conv.participants?.map(p =>
+ p.id === userId ? { ...p, status: status } : p
+ )
+ })));
+
+ // Update active conversation
+ if (activeConversation) {
+ setActiveConversation(prev => ({
+ ...prev,
+ participants: prev.participants?.map(p =>
+ p.id === userId ? { ...p, status: status } : p
+ )
+ }));
+ }
+ };
+
+ // Send message
+ const sendMessage = async (content) => {
+ if (!activeConversation || !content.trim()) return;
+
+ try {
+ const response = await fetch(
+ `${import.meta.env.VITE_API_URL || 'http://localhost:3000'}/api/messaging/conversations/${activeConversation.id}/messages`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({
+ content: content,
+ contentType: 'text'
+ })
+ }
+ );
+
+ const data = await response.json();
+
+ if (!data.success) {
+ throw new Error(data.error);
+ }
+
+ // Message will be received via socket event
+ } catch (error) {
+ console.error('Failed to send message:', error);
+ setError('Failed to send message');
+ }
+ };
+
+ // Start typing indicator
+ const startTyping = () => {
+ if (!activeConversation || !socket) return;
+
+ socket.emit('typing_start', {
+ conversationId: activeConversation.id
+ });
+
+ // Auto-stop after 3 seconds
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ }
+
+ typingTimeoutRef.current = setTimeout(() => {
+ stopTyping();
+ }, 3000);
+ };
+
+ // Stop typing indicator
+ const stopTyping = () => {
+ if (!activeConversation || !socket) return;
+
+ socket.emit('typing_stop', {
+ conversationId: activeConversation.id
+ });
+
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ typingTimeoutRef.current = null;
+ }
+ };
+
+ // Mark conversation as read
+ const markAsRead = async (conversationId) => {
+ try {
+ await fetch(
+ `${import.meta.env.VITE_API_URL || 'http://localhost:3000'}/api/messaging/conversations/${conversationId}/read`,
+ {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ }
+ );
+
+ // Update local state
+ setConversations(prev => prev.map(conv =>
+ conv.id === conversationId ? { ...conv, unreadCount: 0 } : conv
+ ));
+ } catch (error) {
+ console.error('Failed to mark as read:', error);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
Loading conversations...
+
+ );
+ }
+
+ if (error && conversations.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+ {connected ? (
+ ● Connected
+ ) : (
+ ○ Disconnected
+ )}
+
+
+
+
+
+ {activeConversation ? (
+ <>
+
+
+
+ {activeConversation.title?.[0] || '?'}
+
+
+
{activeConversation.title || 'Direct Message'}
+
+ {activeConversation.otherParticipants?.length || 0} participants
+
+
+
+
+
+
+
+
+ >
+ ) : (
+
+
Select a conversation to start messaging
+
or create a new conversation
+
+ )}
+
+
+ );
+}
diff --git a/astro-site/src/react-app/components/Chat/ConversationList.css b/astro-site/src/react-app/components/Chat/ConversationList.css
new file mode 100644
index 0000000..bf025cf
--- /dev/null
+++ b/astro-site/src/react-app/components/Chat/ConversationList.css
@@ -0,0 +1,201 @@
+/* Conversation List Sidebar - Dark Gaming Theme */
+.conversation-list {
+ width: 320px;
+ background: rgba(10, 10, 15, 0.8);
+ backdrop-filter: blur(20px);
+ border-right: 1px solid rgba(0, 217, 255, 0.1);
+ display: flex;
+ flex-direction: column;
+}
+
+.conversation-list-header {
+ padding: 1rem 1.5rem;
+ border-bottom: 1px solid rgba(0, 217, 255, 0.1);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.conversation-list-header h2 {
+ margin: 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+ color: #ffffff;
+}
+
+.btn-new-conversation {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
+ color: #000000;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.3s;
+ box-shadow: 0 4px 12px rgba(0, 217, 255, 0.3);
+}
+
+.btn-new-conversation:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(0, 217, 255, 0.4);
+}
+
+/* Conversation Items */
+.conversation-list-items {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.no-conversations {
+ padding: 2rem 1.5rem;
+ text-align: center;
+ color: #9ca3af;
+}
+
+.no-conversations p {
+ margin: 0.5rem 0;
+}
+
+.no-conversations .hint {
+ font-size: 0.875rem;
+ color: #d1d5db;
+}
+
+.conversation-item {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1rem 1.5rem;
+ cursor: pointer;
+ transition: background 0.15s;
+ border-bottom: 1px solid #f3f4f6;
+}
+
+.conversation-item:hover {
+ background: #f9fafb;
+}
+
+.conversation-item.active {
+ background: #eff6ff;
+ border-left: 3px solid #3b82f6;
+}
+
+/* Conversation Avatar */
+.conversation-avatar-container {
+ position: relative;
+ flex-shrink: 0;
+}
+
+.conversation-avatar-img {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.conversation-avatar-placeholder {
+ width: 48px;
+ height: 48px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-weight: 600;
+ font-size: 1.25rem;
+}
+
+.online-indicator {
+ position: absolute;
+ bottom: 2px;
+ right: 2px;
+ width: 12px;
+ height: 12px;
+ background: #10b981;
+ border: 2px solid white;
+ border-radius: 50%;
+}
+
+/* Conversation Details */
+.conversation-details {
+ flex: 1;
+ min-width: 0;
+}
+
+.conversation-header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 0.25rem;
+}
+
+.conversation-title {
+ margin: 0;
+ font-size: 0.9375rem;
+ font-weight: 500;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.conversation-time {
+ font-size: 0.75rem;
+ color: #9ca3af;
+ flex-shrink: 0;
+ margin-left: 0.5rem;
+}
+
+.conversation-last-message {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+}
+
+.last-message-text {
+ margin: 0;
+ font-size: 0.875rem;
+ color: #6b7280;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex: 1;
+}
+
+.unread-badge {
+ flex-shrink: 0;
+ min-width: 20px;
+ height: 20px;
+ padding: 0 6px;
+ background: #3b82f6;
+ color: white;
+ border-radius: 10px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+/* Scrollbar Styling */
+.conversation-list-items::-webkit-scrollbar {
+ width: 6px;
+}
+
+.conversation-list-items::-webkit-scrollbar-track {
+ background: #f3f4f6;
+}
+
+.conversation-list-items::-webkit-scrollbar-thumb {
+ background: #d1d5db;
+ border-radius: 3px;
+}
+
+.conversation-list-items::-webkit-scrollbar-thumb:hover {
+ background: #9ca3af;
+}
diff --git a/astro-site/src/react-app/components/Chat/ConversationList.jsx b/astro-site/src/react-app/components/Chat/ConversationList.jsx
new file mode 100644
index 0000000..f768b9a
--- /dev/null
+++ b/astro-site/src/react-app/components/Chat/ConversationList.jsx
@@ -0,0 +1,110 @@
+/**
+ * ConversationList Component
+ * Displays list of conversations in sidebar
+ */
+
+import React from 'react';
+import './ConversationList.css';
+
+export default function ConversationList({ conversations, activeConversation, onSelectConversation }) {
+
+ const formatTime = (timestamp) => {
+ const date = new Date(timestamp);
+ const now = new Date();
+ const diffMs = now - date;
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ if (diffMins < 1) return 'Just now';
+ if (diffMins < 60) return `${diffMins}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ if (diffDays < 7) return `${diffDays}d ago`;
+ return date.toLocaleDateString();
+ };
+
+ const getConversationTitle = (conv) => {
+ if (conv.title) return conv.title;
+
+ // For direct conversations, show other participant's domain
+ if (conv.otherParticipants && conv.otherParticipants.length > 0) {
+ return conv.otherParticipants[0].verified_domain || conv.otherParticipants[0].username;
+ }
+
+ return 'Unknown';
+ };
+
+ const getConversationAvatar = (conv) => {
+ if (conv.avatarUrl) return conv.avatarUrl;
+
+ // For direct conversations, show other participant's avatar
+ if (conv.otherParticipants && conv.otherParticipants.length > 0) {
+ return conv.otherParticipants[0].avatar_url;
+ }
+
+ return null;
+ };
+
+ return (
+
+
+
Messages
+
+ +
+
+
+
+
+ {conversations.length === 0 ? (
+
+
No conversations yet
+
Start a new conversation to get started
+
+ ) : (
+ conversations.map(conv => (
+
onSelectConversation(conv)}
+ >
+
+ {getConversationAvatar(conv) ? (
+
+ ) : (
+
+ {getConversationTitle(conv)[0]?.toUpperCase()}
+
+ )}
+ {conv.otherParticipants?.[0]?.status === 'online' && (
+
+ )}
+
+
+
+
+
{getConversationTitle(conv)}
+
+ {formatTime(conv.updatedAt)}
+
+
+
+
+
+ {conv.lastMessage?.content || 'No messages yet'}
+
+ {conv.unreadCount > 0 && (
+
{conv.unreadCount}
+ )}
+
+
+
+ ))
+ )}
+
+
+ );
+}
diff --git a/astro-site/src/react-app/components/Chat/MessageInput.css b/astro-site/src/react-app/components/Chat/MessageInput.css
new file mode 100644
index 0000000..b5c4a02
--- /dev/null
+++ b/astro-site/src/react-app/components/Chat/MessageInput.css
@@ -0,0 +1,117 @@
+/* Message Input Container - Dark Gaming Theme */
+.message-input {
+ padding: 1rem 1.5rem;
+ background: rgba(10, 10, 15, 0.8);
+ backdrop-filter: blur(20px);
+ border-top: 1px solid rgba(0, 217, 255, 0.1);
+ display: flex;
+ align-items: flex-end;
+ gap: 0.75rem;
+}
+
+/* Buttons */
+.btn-attach,
+.btn-emoji {
+ width: 36px;
+ height: 36px;
+ border: none;
+ background: rgba(0, 217, 255, 0.1);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 1.25rem;
+ transition: all 0.3s;
+ flex-shrink: 0;
+ color: #00d9ff;
+}
+
+.btn-attach:hover,
+.btn-emoji:hover {
+ background: rgba(0, 217, 255, 0.2);
+ transform: scale(1.05);
+}
+
+.btn-attach:disabled,
+.btn-emoji:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Textarea */
+.message-textarea {
+ flex: 1;
+ min-height: 36px;
+ max-height: 120px;
+ padding: 0.5rem 0.75rem;
+ border: 1px solid rgba(0, 217, 255, 0.2);
+ background: rgba(0, 0, 0, 0.5);
+ color: #ffffff;
+ border-radius: 18px;
+ font-size: 0.9375rem;
+ font-family: inherit;
+ resize: none;
+ outline: none;
+ transition: border-color 0.2s;
+}
+
+.message-textarea:focus {
+ border-color: #00d9ff;
+ box-shadow: 0 0 0 2px rgba(0, 217, 255, 0.1);
+}
+
+.message-textarea:disabled {
+ background: rgba(0, 0, 0, 0.3);
+ cursor: not-allowed;
+ opacity: 0.5;
+}
+
+.message-textarea::placeholder {
+ color: #71717a;
+}
+
+/* Send Button */
+.btn-send {
+ padding: 0.5rem 1.5rem;
+ background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
+ color: #000000;
+ border: none;
+ border-radius: 18px;
+ font-size: 0.9375rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s;
+ flex-shrink: 0;
+ box-shadow: 0 4px 12px rgba(0, 217, 255, 0.3);
+}
+
+.btn-send:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 6px 20px rgba(0, 217, 255, 0.4);
+}
+
+.btn-send:disabled {
+ background: #9ca3af;
+ cursor: not-allowed;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .message-input {
+ padding: 0.75rem 1rem;
+ gap: 0.5rem;
+ }
+
+ .btn-attach,
+ .btn-emoji {
+ width: 32px;
+ height: 32px;
+ font-size: 1.125rem;
+ }
+
+ .btn-send {
+ padding: 0.5rem 1rem;
+ font-size: 0.875rem;
+ }
+}
diff --git a/astro-site/src/react-app/components/Chat/MessageInput.jsx b/astro-site/src/react-app/components/Chat/MessageInput.jsx
new file mode 100644
index 0000000..ef74ec0
--- /dev/null
+++ b/astro-site/src/react-app/components/Chat/MessageInput.jsx
@@ -0,0 +1,134 @@
+/**
+ * MessageInput Component
+ * Input field for sending messages
+ */
+
+import React, { useState, useRef } from 'react';
+import './MessageInput.css';
+
+export default function MessageInput({ onSend, onTyping, onStopTyping }) {
+ const [message, setMessage] = useState('');
+ const [uploading, setUploading] = useState(false);
+ const fileInputRef = useRef(null);
+ const typingTimeoutRef = useRef(null);
+
+ const handleChange = (e) => {
+ setMessage(e.target.value);
+
+ // Trigger typing indicator
+ if (onTyping) onTyping();
+
+ // Reset stop-typing timeout
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ }
+
+ typingTimeoutRef.current = setTimeout(() => {
+ if (onStopTyping) onStopTyping();
+ }, 1000);
+ };
+
+ const handleSubmit = (e) => {
+ e.preventDefault();
+
+ if (!message.trim()) return;
+
+ onSend(message);
+ setMessage('');
+
+ if (onStopTyping) onStopTyping();
+
+ if (typingTimeoutRef.current) {
+ clearTimeout(typingTimeoutRef.current);
+ }
+ };
+
+ const handleKeyPress = (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit(e);
+ }
+ };
+
+ const handleFileUpload = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ setUploading(true);
+
+ try {
+ const formData = new FormData();
+ formData.append('file', file);
+
+ const response = await fetch(
+ `${import.meta.env.VITE_API_URL || 'http://localhost:3000'}/api/files/upload`,
+ {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: formData
+ }
+ );
+
+ const data = await response.json();
+
+ if (data.success) {
+ // Send message with file attachment
+ onSend(`📎 ${file.name}`, [data.file]);
+ }
+ } catch (error) {
+ console.error('File upload failed:', error);
+ alert('Failed to upload file');
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/astro-site/src/react-app/components/Chat/MessageList.css b/astro-site/src/react-app/components/Chat/MessageList.css
new file mode 100644
index 0000000..b236599
--- /dev/null
+++ b/astro-site/src/react-app/components/Chat/MessageList.css
@@ -0,0 +1,310 @@
+/* Message List Container - Dark Gaming Theme */
+.message-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1.5rem;
+ display: flex;
+ flex-direction: column-reverse;
+ gap: 1rem;
+ background: rgba(0, 0, 0, 0.5);
+}
+
+.message-list.empty {
+ justify-content: center;
+ align-items: center;
+}
+
+.no-messages {
+ text-align: center;
+ color: #9ca3af;
+}
+
+.no-messages p {
+ margin: 0.5rem 0;
+}
+
+.no-messages .hint {
+ font-size: 0.875rem;
+ color: #d1d5db;
+}
+
+/* Message Timestamp Divider */
+.message-timestamp-divider {
+ text-align: center;
+ margin: 1rem 0;
+ position: relative;
+}
+
+.message-timestamp-divider::before {
+ content: '';
+ position: absolute;
+ top: 50%;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: rgba(0, 217, 255, 0.1);
+ z-index: 0;
+}
+
+.message-timestamp-divider span,
+.message-timestamp-divider::after {
+ display: inline-block;
+ padding: 0.25rem 1rem;
+ background: rgba(0, 0, 0, 0.8);
+ color: #71717a;
+ font-size: 0.75rem;
+ border-radius: 12px;
+ position: relative;
+ z-index: 1;
+ border: 1px solid rgba(0, 217, 255, 0.1);
+}
+
+/* Message */
+.message {
+ display: flex;
+ gap: 0.75rem;
+ align-items: flex-start;
+}
+
+.message.own {
+ flex-direction: row-reverse;
+}
+
+.message.other {
+ flex-direction: row;
+}
+
+/* Message Avatar */
+.message-avatar {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.message-avatar img {
+ width: 100%;
+ height: 100%;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.avatar-placeholder {
+ width: 32px;
+ height: 32px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #000000;
+ font-weight: 700;
+ font-size: 0.875rem;
+}
+
+/* Message Content */
+.message-content-wrapper {
+ max-width: 70%;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.message.own .message-content-wrapper {
+ align-items: flex-end;
+}
+
+.message.other .message-content-wrapper {
+ align-items: flex-start;
+}
+
+.message-sender {
+ font-size: 0.75rem;
+ font-weight: 500;
+ color: #a1a1aa;
+ padding: 0 0.75rem;
+}
+
+.verified-badge {
+ color: #00d9ff;
+ margin-left: 0.25rem;
+}
+
+/* Message Bubble */
+.message-bubble {
+ padding: 0.75rem 1rem;
+ border-radius: 1rem;
+ position: relative;
+ word-wrap: break-word;
+}
+
+.message.own .message-bubble {
+ background: linear-gradient(135deg, #00d9ff 0%, #00ff88 100%);
+ color: #000000;
+ font-weight: 500;
+ border-bottom-right-radius: 0.25rem;
+ box-shadow: 0 4px 12px rgba(0, 217, 255, 0.3);
+}
+
+.message.other .message-bubble {
+ background: rgba(10, 10, 15, 0.8);
+ backdrop-filter: blur(20px);
+ color: #ffffff;
+ border: 1px solid rgba(0, 217, 255, 0.2);
+ border-bottom-left-radius: 0.25rem;
+}
+
+.message-reply-reference {
+ font-size: 0.75rem;
+ opacity: 0.7;
+ margin-bottom: 0.5rem;
+ padding-left: 0.5rem;
+ border-left: 2px solid currentColor;
+}
+
+.message-text {
+ font-size: 0.9375rem;
+ line-height: 1.5;
+ white-space: pre-wrap;
+}
+
+.message-attachments {
+ margin-top: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.attachment {
+ padding: 0.5rem;
+ background: rgba(0, 0, 0, 0.05);
+ border-radius: 0.5rem;
+ font-size: 0.875rem;
+}
+
+.message.own .attachment {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+/* Message Footer */
+.message-footer {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-top: 0.25rem;
+ font-size: 0.6875rem;
+ opacity: 0.7;
+}
+
+.message-time {
+ font-weight: 400;
+}
+
+.edited-indicator {
+ font-style: italic;
+}
+
+.sending-indicator {
+ font-style: italic;
+ animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 0.5;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
+/* Message Reactions */
+.message-reactions {
+ display: flex;
+ gap: 0.25rem;
+ margin-top: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.reaction {
+ padding: 0.25rem 0.5rem;
+ background: rgba(0, 0, 0, 0.05);
+ border-radius: 12px;
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.message.own .reaction {
+ background: rgba(255, 255, 255, 0.2);
+}
+
+.reaction:hover {
+ background: rgba(0, 0, 0, 0.1);
+}
+
+.message.own .reaction:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+/* Typing Indicator */
+.typing-indicator {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+}
+
+.typing-dots {
+ display: flex;
+ gap: 0.25rem;
+}
+
+.typing-dots span {
+ width: 8px;
+ height: 8px;
+ background: #9ca3af;
+ border-radius: 50%;
+ animation: typing 1.4s ease-in-out infinite;
+}
+
+.typing-dots span:nth-child(2) {
+ animation-delay: 0.2s;
+}
+
+.typing-dots span:nth-child(3) {
+ animation-delay: 0.4s;
+}
+
+@keyframes typing {
+ 0%, 60%, 100% {
+ transform: translateY(0);
+ }
+ 30% {
+ transform: translateY(-10px);
+ }
+}
+
+.typing-text {
+ font-size: 0.875rem;
+ color: #6b7280;
+ font-style: italic;
+}
+
+/* Scrollbar */
+.message-list::-webkit-scrollbar {
+ width: 6px;
+}
+
+.message-list::-webkit-scrollbar-track {
+ background: #f3f4f6;
+}
+
+.message-list::-webkit-scrollbar-thumb {
+ background: #d1d5db;
+ border-radius: 3px;
+}
+
+.message-list::-webkit-scrollbar-thumb:hover {
+ background: #9ca3af;
+}
diff --git a/astro-site/src/react-app/components/Chat/MessageList.jsx b/astro-site/src/react-app/components/Chat/MessageList.jsx
new file mode 100644
index 0000000..974dd5a
--- /dev/null
+++ b/astro-site/src/react-app/components/Chat/MessageList.jsx
@@ -0,0 +1,139 @@
+/**
+ * MessageList Component
+ * Displays messages in a conversation
+ */
+
+import React, { useEffect, useRef } from 'react';
+import './MessageList.css';
+
+export default function MessageList({ messages, typingUsers }) {
+ const messagesEndRef = useRef(null);
+
+ // Auto-scroll to bottom when new messages arrive
+ useEffect(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
+ }, [messages]);
+
+ const formatTime = (timestamp) => {
+ const date = new Date(timestamp);
+ return date.toLocaleTimeString('en-US', {
+ hour: 'numeric',
+ minute: '2-digit',
+ hour12: true
+ });
+ };
+
+ const getCurrentUserId = () => {
+ // In a real app, get this from auth context
+ return localStorage.getItem('userId');
+ };
+
+ const isOwnMessage = (message) => {
+ return message.senderId === getCurrentUserId();
+ };
+
+ if (messages.length === 0 && typingUsers.length === 0) {
+ return (
+
+
+
No messages yet
+
Send a message to start the conversation
+
+
+ );
+ }
+
+ return (
+
+ {messages.map((message, index) => {
+ const showAvatar = index === messages.length - 1 ||
+ messages[index + 1]?.senderId !== message.senderId;
+
+ const showTimestamp = index === 0 ||
+ new Date(message.createdAt) - new Date(messages[index - 1].createdAt) > 300000; // 5 mins
+
+ return (
+
+ {showTimestamp && (
+
+ {new Date(message.createdAt).toLocaleDateString()}
+
+ )}
+
+
+ {!isOwnMessage(message) && showAvatar && (
+
+ {message.senderAvatar ? (
+
+ ) : (
+
+ {message.senderUsername?.[0]?.toUpperCase()}
+
+ )}
+
+ )}
+
+
+ {!isOwnMessage(message) && (
+
+ {message.senderDomain || message.senderUsername}
+ {message.senderDomain && ✓ }
+
+ )}
+
+
+ {message.replyToId && (
+
+ Replying to a message
+
+ )}
+
+
{message.content}
+
+ {message.metadata?.attachments && message.metadata.attachments.length > 0 && (
+
+ {message.metadata.attachments.map((attachment, i) => (
+
+ 📎 {attachment.filename}
+
+ ))}
+
+ )}
+
+
+ {formatTime(message.createdAt)}
+ {message.editedAt && edited }
+ {message._sending && sending... }
+
+
+ {message.reactions && message.reactions.length > 0 && (
+
+ {message.reactions.map((reaction, i) => (
+
+ {reaction.emoji} {reaction.users.length > 1 && reaction.users.length}
+
+ ))}
+
+ )}
+
+
+
+
+ );
+ })}
+
+ {typingUsers.length > 0 && (
+
+
+
+
+
+
+
Someone is typing...
+
+ )}
+
+
+
+ );
+}
diff --git a/astro-site/src/react-app/components/DomainVerification.css b/astro-site/src/react-app/components/DomainVerification.css
new file mode 100644
index 0000000..2511788
--- /dev/null
+++ b/astro-site/src/react-app/components/DomainVerification.css
@@ -0,0 +1,316 @@
+.domain-verification {
+ max-width: 600px;
+ margin: 0 auto;
+ padding: 24px;
+ background: #ffffff;
+ border-radius: 12px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.domain-verification h3 {
+ margin: 0 0 8px 0;
+ color: #1a1a1a;
+ font-size: 24px;
+ font-weight: 600;
+}
+
+.domain-verification .description {
+ margin: 0 0 24px 0;
+ color: #666;
+ font-size: 14px;
+}
+
+/* Error message */
+.error-message {
+ padding: 12px;
+ margin-bottom: 16px;
+ background: #fee;
+ border: 1px solid #fcc;
+ border-radius: 6px;
+ color: #c33;
+}
+
+/* Input section */
+.input-section {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.form-group label {
+ font-weight: 500;
+ color: #333;
+ font-size: 14px;
+}
+
+.domain-input,
+.wallet-input {
+ padding: 12px;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ font-size: 16px;
+ transition: border-color 0.2s;
+}
+
+.domain-input:focus,
+.wallet-input:focus {
+ outline: none;
+ border-color: #4f46e5;
+}
+
+.domain-input:disabled,
+.wallet-input:disabled {
+ background: #f5f5f5;
+ cursor: not-allowed;
+}
+
+.help-text {
+ font-size: 12px;
+ color: #666;
+ margin-top: -4px;
+}
+
+/* Buttons */
+.primary-button,
+.secondary-button {
+ padding: 12px 24px;
+ border: none;
+ border-radius: 8px;
+ font-size: 16px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.primary-button {
+ background: #4f46e5;
+ color: white;
+}
+
+.primary-button:hover:not(:disabled) {
+ background: #4338ca;
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
+}
+
+.primary-button:disabled {
+ background: #9ca3af;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.secondary-button {
+ background: #f3f4f6;
+ color: #374151;
+}
+
+.secondary-button:hover:not(:disabled) {
+ background: #e5e7eb;
+}
+
+.cancel-button {
+ margin-top: 12px;
+}
+
+/* Instructions section */
+.instructions-section {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.instructions-section h4 {
+ margin: 0;
+ color: #1a1a1a;
+ font-size: 18px;
+}
+
+/* DNS record display */
+.dns-record {
+ background: #f9fafb;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+
+.record-field {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.record-field strong {
+ font-size: 12px;
+ text-transform: uppercase;
+ color: #6b7280;
+ letter-spacing: 0.5px;
+}
+
+.record-field span {
+ font-size: 14px;
+ color: #1f2937;
+}
+
+.value-container {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.value-container code {
+ flex: 1;
+ padding: 8px;
+ background: #fff;
+ border: 1px solid #e5e7eb;
+ border-radius: 4px;
+ font-size: 13px;
+ word-break: break-all;
+ font-family: 'Courier New', monospace;
+}
+
+.copy-button {
+ padding: 8px 12px;
+ background: #fff;
+ border: 1px solid #e5e7eb;
+ border-radius: 4px;
+ font-size: 12px;
+ cursor: pointer;
+ white-space: nowrap;
+ transition: all 0.2s;
+}
+
+.copy-button:hover {
+ background: #f3f4f6;
+ border-color: #d1d5db;
+}
+
+/* Help section */
+.help-section {
+ background: #eff6ff;
+ border: 1px solid #bfdbfe;
+ border-radius: 8px;
+ padding: 16px;
+}
+
+.help-section p {
+ margin: 0 0 8px 0;
+ color: #1e40af;
+ font-size: 14px;
+}
+
+.help-section ol {
+ margin: 8px 0;
+ padding-left: 24px;
+ color: #1e3a8a;
+}
+
+.help-section li {
+ margin: 4px 0;
+ font-size: 14px;
+}
+
+.expires-note {
+ margin-top: 12px;
+ padding-top: 12px;
+ border-top: 1px solid #bfdbfe;
+ font-size: 13px;
+ color: #1e40af;
+}
+
+/* Status message */
+.status-message {
+ padding: 12px;
+ border-radius: 6px;
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.status-message.success {
+ background: #d1fae5;
+ border: 1px solid #6ee7b7;
+ color: #065f46;
+}
+
+.status-message.error {
+ background: #fee2e2;
+ border: 1px solid #fca5a5;
+ color: #991b1b;
+}
+
+/* Blockchain verification */
+.blockchain-verification {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.blockchain-verification p {
+ margin: 0;
+ color: #374151;
+}
+
+/* Verified container */
+.verified-container {
+ text-align: center;
+}
+
+.verified-container h3 {
+ color: #059669;
+ font-size: 28px;
+}
+
+.verified-domain-display {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ margin: 20px 0;
+ padding: 16px;
+ background: #d1fae5;
+ border-radius: 8px;
+}
+
+.verified-domain-display strong {
+ font-size: 20px;
+ color: #065f46;
+}
+
+.verification-type {
+ padding: 4px 8px;
+ background: #059669;
+ color: white;
+ border-radius: 4px;
+ font-size: 12px;
+ text-transform: uppercase;
+}
+
+.verified-date {
+ color: #6b7280;
+ font-size: 14px;
+ margin-bottom: 20px;
+}
+
+/* Responsive */
+@media (max-width: 640px) {
+ .domain-verification {
+ padding: 16px;
+ }
+
+ .value-container {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .copy-button {
+ width: 100%;
+ }
+}
diff --git a/astro-site/src/react-app/components/DomainVerification.jsx b/astro-site/src/react-app/components/DomainVerification.jsx
new file mode 100644
index 0000000..5cacf7a
--- /dev/null
+++ b/astro-site/src/react-app/components/DomainVerification.jsx
@@ -0,0 +1,313 @@
+import React, { useState, useEffect } from 'react';
+import './DomainVerification.css';
+
+/**
+ * Domain verification UI component
+ * Allows users to verify domain ownership via DNS TXT records or blockchain
+ */
+export default function DomainVerification({ apiBaseUrl = 'https://api.aethex.cloud/api/passport/domain' }) {
+ const [domain, setDomain] = useState('');
+ const [walletAddress, setWalletAddress] = useState('');
+ const [verificationInstructions, setVerificationInstructions] = useState(null);
+ const [verificationStatus, setVerificationStatus] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [currentStatus, setCurrentStatus] = useState(null);
+
+ // Load current verification status on mount
+ useEffect(() => {
+ loadCurrentStatus();
+ }, []);
+
+ /**
+ * Load current verification status
+ */
+ async function loadCurrentStatus() {
+ try {
+ const token = localStorage.getItem('authToken');
+ const response = await fetch(`${apiBaseUrl}/status`, {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ setCurrentStatus(data);
+ }
+ } catch (err) {
+ console.error('Failed to load status:', err);
+ }
+ }
+
+ /**
+ * Request verification token from backend
+ */
+ async function requestVerification() {
+ if (!domain) {
+ setError('Please enter a domain');
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ const token = localStorage.getItem('authToken');
+ const response = await fetch(`${apiBaseUrl}/request-verification`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ },
+ body: JSON.stringify({ domain })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ setVerificationInstructions(data.verification);
+ } else {
+ setError(data.error || 'Failed to request verification');
+ }
+ } catch (err) {
+ setError('Network error. Please try again.');
+ console.error('Failed to request verification:', err);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ /**
+ * Verify domain ownership by checking DNS or blockchain
+ */
+ async function verifyDomain() {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const token = localStorage.getItem('authToken');
+ const body = { domain };
+
+ // Add wallet address if verifying .aethex domain
+ if (domain.endsWith('.aethex')) {
+ body.walletAddress = walletAddress;
+ }
+
+ const response = await fetch(`${apiBaseUrl}/verify`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${token}`
+ },
+ body: JSON.stringify(body)
+ });
+
+ const data = await response.json();
+ setVerificationStatus(data);
+
+ if (data.verified) {
+ // Refresh status and reload after short delay
+ setTimeout(() => {
+ loadCurrentStatus();
+ window.location.reload();
+ }, 1500);
+ } else {
+ setError(data.error || 'Verification failed');
+ }
+ } catch (err) {
+ setError('Network error. Please try again.');
+ console.error('Verification failed:', err);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ /**
+ * Copy text to clipboard
+ */
+ function copyToClipboard(text) {
+ navigator.clipboard.writeText(text);
+ // You could add a toast notification here
+ alert('Copied to clipboard!');
+ }
+
+ /**
+ * Reset form
+ */
+ function resetForm() {
+ setVerificationInstructions(null);
+ setVerificationStatus(null);
+ setDomain('');
+ setWalletAddress('');
+ setError(null);
+ }
+
+ // Show current verified domain if exists
+ if (currentStatus?.hasVerifiedDomain) {
+ return (
+
+
✓ Domain Verified
+
+ {currentStatus.domain}
+
+ {currentStatus.verificationType === 'blockchain' ? 'Blockchain' : 'DNS'}
+
+
+
+ Verified on {new Date(currentStatus.verifiedAt).toLocaleDateString()}
+
+
setCurrentStatus(null)}
+ >
+ Verify Another Domain
+
+
+ );
+ }
+
+ return (
+
+
Verify Your Domain
+
+ Prove ownership of a domain to display it on your profile
+
+
+ {error && (
+
+ ⚠️ {error}
+
+ )}
+
+ {!verificationInstructions ? (
+
+
+ Domain Name
+ setDomain(e.target.value.toLowerCase().trim())}
+ disabled={loading}
+ className="domain-input"
+ />
+
+ Enter a traditional domain (e.g., dev.aethex.dev) or a .aethex blockchain domain
+
+
+
+
+ {loading ? 'Generating...' : 'Request Verification'}
+
+
+ ) : (
+
+
+ {domain.endsWith('.aethex')
+ ? 'Connect Your Wallet'
+ : `Add this DNS record to ${verificationInstructions.domain}:`
+ }
+
+
+ {domain.endsWith('.aethex') ? (
+ // Blockchain verification
+
+
Connect the wallet that owns {domain}
+
+ Wallet Address
+ setWalletAddress(e.target.value.trim())}
+ disabled={loading}
+ className="wallet-input"
+ />
+
+
+ {loading ? 'Verifying...' : 'Verify Ownership'}
+
+
+ ) : (
+ // DNS verification
+
+
+
+ Type:
+ {verificationInstructions.recordType}
+
+
+ Name:
+ {verificationInstructions.recordName}
+
+
+
Value:
+
+ {verificationInstructions.recordValue}
+ copyToClipboard(verificationInstructions.recordValue)}
+ className="copy-button"
+ title="Copy to clipboard"
+ >
+ 📋 Copy
+
+
+
+
+
+
+
How to add DNS records:
+
+ Go to your domain's DNS settings (Google Domains, Cloudflare, etc.)
+ Add a new TXT record with the values above
+ Wait 5-10 minutes for DNS to propagate
+ Click "Verify Domain" below
+
+
+ ⏱️ This verification expires on {new Date(verificationInstructions.expiresAt).toLocaleDateString()}
+
+
+
+
+ {loading ? 'Verifying...' : 'Verify Domain'}
+
+
+ {verificationStatus && (
+
+ {verificationStatus.verified ? (
+ ✓ Domain verified successfully!
+ ) : (
+ ✗ {verificationStatus.error}
+ )}
+
+ )}
+
+ )}
+
+
+ Cancel
+
+
+ )}
+
+ );
+}
diff --git a/astro-site/src/react-app/components/GameForgeChat/ChannelList.css b/astro-site/src/react-app/components/GameForgeChat/ChannelList.css
new file mode 100644
index 0000000..041bc96
--- /dev/null
+++ b/astro-site/src/react-app/components/GameForgeChat/ChannelList.css
@@ -0,0 +1,84 @@
+.channel-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 8px 0;
+}
+
+.channel-group {
+ margin-bottom: 16px;
+}
+
+.channel-group-header {
+ padding: 8px 16px;
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ color: var(--text-tertiary, #999);
+ letter-spacing: 0.5px;
+}
+
+.channel-item {
+ display: flex;
+ align-items: center;
+ padding: 8px 16px;
+ cursor: pointer;
+ transition: background 0.2s;
+ gap: 12px;
+}
+
+.channel-item:hover {
+ background: var(--hover-bg, #f5f5f5);
+}
+
+.channel-item.active {
+ background: var(--active-bg, #e3f2fd);
+ border-left: 3px solid var(--primary-color, #2196F3);
+ padding-left: 13px;
+}
+
+.channel-item.unread {
+ font-weight: 600;
+}
+
+.channel-icon {
+ font-size: 18px;
+ flex-shrink: 0;
+}
+
+.channel-name {
+ flex: 1;
+ font-size: 14px;
+ color: var(--text-primary, #333);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.unread-badge {
+ background: var(--error-color, #f44336);
+ color: white;
+ font-size: 11px;
+ font-weight: 600;
+ padding: 2px 6px;
+ border-radius: 10px;
+ min-width: 18px;
+ text-align: center;
+}
+
+/* Scrollbar styling */
+.channel-list::-webkit-scrollbar {
+ width: 6px;
+}
+
+.channel-list::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.channel-list::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb, #ccc);
+ border-radius: 3px;
+}
+
+.channel-list::-webkit-scrollbar-thumb:hover {
+ background: var(--scrollbar-thumb-hover, #999);
+}
diff --git a/astro-site/src/react-app/components/GameForgeChat/ChannelList.jsx b/astro-site/src/react-app/components/GameForgeChat/ChannelList.jsx
new file mode 100644
index 0000000..b4863aa
--- /dev/null
+++ b/astro-site/src/react-app/components/GameForgeChat/ChannelList.jsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import './ChannelList.css';
+
+export default function ChannelList({ channels, activeChannel, onSelectChannel }) {
+ // Group channels by type
+ const defaultChannels = channels.filter(c =>
+ ['general', 'announcements', 'dev', 'art', 'design', 'testing'].includes(c.name)
+ );
+ const customChannels = channels.filter(c =>
+ !['general', 'announcements', 'dev', 'art', 'design', 'testing'].includes(c.name)
+ );
+
+ const renderChannel = (channel) => {
+ const isActive = activeChannel?.id === channel.id;
+ const hasUnread = channel.unreadCount > 0;
+
+ // Channel icons
+ const icons = {
+ 'general': '💬',
+ 'announcements': '📢',
+ 'dev': '💻',
+ 'art': '🎨',
+ 'design': '✏️',
+ 'testing': '🧪',
+ 'playtesting': '🎮'
+ };
+
+ const icon = icons[channel.name] || '#';
+
+ return (
+ onSelectChannel(channel)}
+ >
+ {icon}
+ {channel.name}
+ {hasUnread && (
+ {channel.unreadCount}
+ )}
+
+ );
+ };
+
+ return (
+
+ {defaultChannels.length > 0 && (
+
+
Channels
+ {defaultChannels.map(renderChannel)}
+
+ )}
+
+ {customChannels.length > 0 && (
+
+
Custom Channels
+ {customChannels.map(renderChannel)}
+
+ )}
+
+ );
+}
diff --git a/astro-site/src/react-app/components/GameForgeChat/ChannelView.css b/astro-site/src/react-app/components/GameForgeChat/ChannelView.css
new file mode 100644
index 0000000..94c5a3a
--- /dev/null
+++ b/astro-site/src/react-app/components/GameForgeChat/ChannelView.css
@@ -0,0 +1,46 @@
+.channel-view {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: var(--bg-primary, #ffffff);
+}
+
+.channel-view.loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: var(--text-secondary, #666);
+}
+
+.channel-header {
+ padding: 16px 20px;
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+ background: var(--bg-secondary, #fafafa);
+}
+
+.channel-header h3 {
+ margin: 0 0 4px 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-primary, #333);
+}
+
+.channel-description {
+ margin: 0;
+ font-size: 13px;
+ color: var(--text-secondary, #666);
+}
+
+.restricted-badge {
+ display: inline-block;
+ margin-top: 8px;
+ padding: 4px 8px;
+ background: var(--warning-bg, #fff3cd);
+ color: var(--warning-text, #856404);
+ border: 1px solid var(--warning-border, #ffeeba);
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+/* Reuse MessageList and MessageInput from Chat component */
diff --git a/astro-site/src/react-app/components/GameForgeChat/ChannelView.jsx b/astro-site/src/react-app/components/GameForgeChat/ChannelView.jsx
new file mode 100644
index 0000000..70819e8
--- /dev/null
+++ b/astro-site/src/react-app/components/GameForgeChat/ChannelView.jsx
@@ -0,0 +1,172 @@
+import React, { useState, useEffect } from 'react';
+import { useSocket } from '../../contexts/SocketContext';
+import MessageList from '../Chat/MessageList';
+import MessageInput from '../Chat/MessageInput';
+import { encryptMessage, decryptMessage } from '../../utils/crypto';
+import { useAuth } from '../../contexts/AuthContext';
+import './ChannelView.css';
+
+export default function ChannelView({ channel, projectId }) {
+ const { socket } = useSocket();
+ const { user } = useAuth();
+ const [messages, setMessages] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ if (channel) {
+ loadMessages();
+ }
+ }, [channel]);
+
+ // Socket listeners
+ useEffect(() => {
+ if (!socket || !channel) return;
+
+ const handleNewMessage = async (data) => {
+ if (data.conversationId === channel.id) {
+ const message = data.message;
+
+ // Decrypt if not system message
+ if (message.contentType !== 'system') {
+ try {
+ const decrypted = await decryptMessage(
+ JSON.parse(message.content),
+ user.password
+ );
+ message.content = decrypted;
+ } catch (error) {
+ console.error('Failed to decrypt message:', error);
+ message.content = '[Failed to decrypt]';
+ }
+ }
+
+ setMessages(prev => [message, ...prev]);
+ }
+ };
+
+ socket.on('message:new', handleNewMessage);
+
+ return () => {
+ socket.off('message:new', handleNewMessage);
+ };
+ }, [socket, channel]);
+
+ const loadMessages = async () => {
+ try {
+ setLoading(true);
+
+ const response = await fetch(`/api/conversations/${channel.id}/messages`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ // Decrypt messages
+ const decryptedMessages = await Promise.all(
+ data.messages.map(async (msg) => {
+ // System messages are not encrypted
+ if (msg.contentType === 'system') {
+ return msg;
+ }
+
+ try {
+ const decrypted = await decryptMessage(
+ JSON.parse(msg.content),
+ user.password
+ );
+ return {
+ ...msg,
+ content: decrypted
+ };
+ } catch (error) {
+ console.error('Failed to decrypt message:', error);
+ return {
+ ...msg,
+ content: '[Failed to decrypt]'
+ };
+ }
+ })
+ );
+
+ setMessages(decryptedMessages);
+ }
+
+ } catch (error) {
+ console.error('Failed to load messages:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const sendMessage = async (content) => {
+ if (!content.trim()) return;
+
+ try {
+ // Get recipient public keys (all channel participants)
+ const participantsResponse = await fetch(`/api/conversations/${channel.id}`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ const participantsData = await participantsResponse.json();
+
+ if (!participantsData.success) {
+ throw new Error('Failed to get participants');
+ }
+
+ const recipientKeys = participantsData.conversation.participants
+ .map(p => p.publicKey)
+ .filter(Boolean);
+
+ // Encrypt message
+ const encrypted = await encryptMessage(content, recipientKeys);
+
+ // Send via WebSocket
+ socket.emit('message:send', {
+ conversationId: channel.id,
+ content: JSON.stringify(encrypted),
+ contentType: 'text',
+ clientId: `temp-${Date.now()}`
+ });
+
+ } catch (error) {
+ console.error('Failed to send message:', error);
+ }
+ };
+
+ if (loading) {
+ return Loading messages...
;
+ }
+
+ return (
+
+
+
#{channel.name}
+
{channel.description}
+ {channel.permissions && !channel.permissions.includes('all') && (
+
+ Restricted to: {channel.permissions.join(', ')}
+
+ )}
+
+
+
+
+
{}}
+ onStopTyping={() => {}}
+ placeholder={`Message #${channel.name}`}
+ />
+
+ );
+}
diff --git a/astro-site/src/react-app/components/GameForgeChat/GameForgeChat.css b/astro-site/src/react-app/components/GameForgeChat/GameForgeChat.css
new file mode 100644
index 0000000..6caecb2
--- /dev/null
+++ b/astro-site/src/react-app/components/GameForgeChat/GameForgeChat.css
@@ -0,0 +1,99 @@
+.gameforge-chat {
+ display: flex;
+ height: 100%;
+ background: var(--bg-primary, #f5f5f5);
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.gameforge-chat.embedded {
+ border-radius: 0;
+ box-shadow: none;
+}
+
+.gameforge-chat-sidebar {
+ width: 260px;
+ background: var(--bg-secondary, #ffffff);
+ border-right: 1px solid var(--border-color, #e0e0e0);
+ display: flex;
+ flex-direction: column;
+}
+
+.gameforge-chat-main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-primary, #f5f5f5);
+}
+
+.project-header {
+ padding: 16px;
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+ background: var(--bg-accent, #fafafa);
+}
+
+.project-header h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-primary, #333);
+}
+
+.loading,
+.error {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: var(--text-secondary, #666);
+}
+
+.error {
+ flex-direction: column;
+ gap: 16px;
+}
+
+.error button {
+ padding: 8px 16px;
+ background: var(--primary-color, #4CAF50);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background 0.2s;
+}
+
+.error button:hover {
+ background: var(--primary-hover, #45a049);
+}
+
+.no-channel-selected {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: var(--text-secondary, #999);
+ font-size: 14px;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+ .gameforge-chat-sidebar {
+ width: 200px;
+ }
+}
+
+@media (max-width: 480px) {
+ .gameforge-chat {
+ flex-direction: column;
+ }
+
+ .gameforge-chat-sidebar {
+ width: 100%;
+ max-height: 200px;
+ border-right: none;
+ border-bottom: 1px solid var(--border-color, #e0e0e0);
+ }
+}
diff --git a/astro-site/src/react-app/components/GameForgeChat/index.jsx b/astro-site/src/react-app/components/GameForgeChat/index.jsx
new file mode 100644
index 0000000..5e9035e
--- /dev/null
+++ b/astro-site/src/react-app/components/GameForgeChat/index.jsx
@@ -0,0 +1,139 @@
+import React, { useState, useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { useSocket } from '../../contexts/SocketContext';
+import { useAuth } from '../../contexts/AuthContext';
+import ChannelList from './ChannelList';
+import ChannelView from './ChannelView';
+import './GameForgeChat.css';
+
+/**
+ * Embedded chat component for GameForge projects
+ * Can be embedded in GameForge UI via iframe or direct integration
+ */
+export default function GameForgeChat({ projectId: propProjectId, embedded = false }) {
+ const { projectId: paramProjectId } = useParams();
+ const projectId = propProjectId || paramProjectId;
+
+ const { socket } = useSocket();
+ const { user } = useAuth();
+
+ const [channels, setChannels] = useState([]);
+ const [activeChannel, setActiveChannel] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ // Load channels
+ useEffect(() => {
+ if (projectId) {
+ loadChannels();
+ }
+ }, [projectId]);
+
+ // Socket listeners for system notifications
+ useEffect(() => {
+ if (!socket) return;
+
+ socket.on('gameforge:notification', handleNotification);
+
+ return () => {
+ socket.off('gameforge:notification', handleNotification);
+ };
+ }, [socket]);
+
+ const loadChannels = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const response = await fetch(`/api/gameforge/projects/${projectId}/channels`, {
+ headers: {
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ setChannels(data.channels);
+
+ // Auto-select general channel
+ const generalChannel = data.channels.find(c => c.name === 'general');
+ if (generalChannel) {
+ setActiveChannel(generalChannel);
+ }
+ } else {
+ setError(data.error);
+ }
+
+ } catch (err) {
+ console.error('Failed to load channels:', err);
+ setError('Failed to load project channels');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleNotification = (notification) => {
+ // Update channel with new system message
+ const { channelId, message } = notification;
+
+ setChannels(prev => prev.map(channel => {
+ if (channel.id === channelId) {
+ return {
+ ...channel,
+ lastMessage: message,
+ unreadCount: activeChannel?.id === channelId ? 0 : channel.unreadCount + 1
+ };
+ }
+ return channel;
+ }));
+ };
+
+ if (loading) {
+ return (
+
+
Loading project chat...
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
Project Channels
+
+
+
+
+
+
+ {activeChannel ? (
+
+ ) : (
+
+
Select a channel to start chatting
+
+ )}
+
+
+ );
+}
diff --git a/astro-site/src/react-app/components/Overlay/Overlay.css b/astro-site/src/react-app/components/Overlay/Overlay.css
new file mode 100644
index 0000000..87dea93
--- /dev/null
+++ b/astro-site/src/react-app/components/Overlay/Overlay.css
@@ -0,0 +1,257 @@
+.in-game-overlay {
+ position: fixed;
+ width: 320px;
+ height: 480px;
+ background: rgba(20, 20, 30, 0.95);
+ backdrop-filter: blur(10px);
+ border-radius: 12px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
+ display: flex;
+ flex-direction: column;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ color: #fff;
+ overflow: hidden;
+ z-index: 999999;
+}
+
+.overlay-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px;
+ background: rgba(30, 30, 40, 0.8);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.overlay-tabs {
+ display: flex;
+ gap: 8px;
+}
+
+.overlay-tabs button {
+ background: transparent;
+ border: none;
+ color: #aaa;
+ padding: 8px 12px;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: all 0.2s;
+ position: relative;
+}
+
+.overlay-tabs button:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: #fff;
+}
+
+.overlay-tabs button.active {
+ background: rgba(88, 101, 242, 0.3);
+ color: #5865f2;
+}
+
+.overlay-tabs .badge {
+ position: absolute;
+ top: 4px;
+ right: 4px;
+ background: #ed4245;
+ color: #fff;
+ border-radius: 10px;
+ padding: 2px 6px;
+ font-size: 10px;
+ font-weight: bold;
+}
+
+.btn-minimize {
+ background: transparent;
+ border: none;
+ color: #aaa;
+ font-size: 18px;
+ cursor: pointer;
+ padding: 4px 12px;
+ border-radius: 4px;
+ transition: all 0.2s;
+}
+
+.btn-minimize:hover {
+ background: rgba(255, 255, 255, 0.1);
+ color: #fff;
+}
+
+.overlay-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 12px;
+}
+
+.overlay-content::-webkit-scrollbar {
+ width: 6px;
+}
+
+.overlay-content::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+.overlay-content::-webkit-scrollbar-thumb {
+ background: rgba(255, 255, 255, 0.2);
+ border-radius: 3px;
+}
+
+.friends-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.friend-item {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 8px;
+ transition: all 0.2s;
+ cursor: pointer;
+}
+
+.friend-item:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.friend-item img {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+
+.friend-info {
+ flex: 1;
+}
+
+.friend-name {
+ font-size: 14px;
+ font-weight: 600;
+ color: #fff;
+ margin-bottom: 2px;
+}
+
+.friend-game {
+ font-size: 12px;
+ color: #aaa;
+}
+
+.status-indicator {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ margin-left: auto;
+}
+
+.status-indicator.online {
+ background: #23a55a;
+ box-shadow: 0 0 8px rgba(35, 165, 90, 0.6);
+}
+
+.status-indicator.away {
+ background: #f0b232;
+}
+
+.status-indicator.offline {
+ background: #80848e;
+}
+
+.messages-preview {
+ color: #aaa;
+ text-align: center;
+ padding: 40px 20px;
+ font-size: 14px;
+}
+
+/* Minimized overlay */
+.overlay-minimized {
+ position: fixed;
+ width: 60px;
+ height: 60px;
+ background: rgba(88, 101, 242, 0.9);
+ backdrop-filter: blur(10px);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
+ cursor: pointer;
+ transition: all 0.2s;
+ z-index: 999999;
+}
+
+.overlay-minimized:hover {
+ transform: scale(1.1);
+ background: rgba(88, 101, 242, 1);
+}
+
+.minimized-icon {
+ font-size: 28px;
+}
+
+.minimized-badge {
+ position: absolute;
+ top: -4px;
+ right: -4px;
+ background: #ed4245;
+ color: #fff;
+ border-radius: 12px;
+ padding: 3px 7px;
+ font-size: 11px;
+ font-weight: bold;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
+}
+
+/* In-game notification */
+.aethex-notification {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ width: 320px;
+ background: rgba(20, 20, 30, 0.95);
+ backdrop-filter: blur(10px);
+ border-radius: 8px;
+ padding: 16px;
+ display: flex;
+ gap: 12px;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
+ animation: slideIn 0.3s ease-out;
+ z-index: 1000000;
+}
+
+@keyframes slideIn {
+ from {
+ transform: translateX(400px);
+ opacity: 0;
+ }
+ to {
+ transform: translateX(0);
+ opacity: 1;
+ }
+}
+
+.notif-icon {
+ font-size: 24px;
+ flex-shrink: 0;
+}
+
+.notif-content {
+ flex: 1;
+}
+
+.notif-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #fff;
+ margin-bottom: 4px;
+}
+
+.notif-body {
+ font-size: 13px;
+ color: #aaa;
+}
diff --git a/astro-site/src/react-app/components/Overlay/index.jsx b/astro-site/src/react-app/components/Overlay/index.jsx
new file mode 100644
index 0000000..6dd76bf
--- /dev/null
+++ b/astro-site/src/react-app/components/Overlay/index.jsx
@@ -0,0 +1,270 @@
+import React, { useState, useEffect } from 'react';
+import './Overlay.css';
+
+/**
+ * In-game overlay component
+ * Runs in iframe embedded in games via Nexus Engine
+ */
+export default function InGameOverlay() {
+ const [minimized, setMinimized] = useState(false);
+ const [activeTab, setActiveTab] = useState('friends');
+ const [friends, setFriends] = useState([]);
+ const [unreadMessages, setUnreadMessages] = useState(0);
+ const [inCall, setInCall] = useState(false);
+ const [socket, setSocket] = useState(null);
+
+ useEffect(() => {
+ initializeOverlay();
+ loadFriends();
+ setupWebSocket();
+
+ // Listen for messages from game
+ window.addEventListener('message', handleGameMessage);
+
+ return () => {
+ window.removeEventListener('message', handleGameMessage);
+ if (socket) {
+ socket.close();
+ }
+ };
+ }, []);
+
+ const initializeOverlay = () => {
+ // Get session ID from URL params
+ const params = new URLSearchParams(window.location.search);
+ const sessionId = params.get('session');
+
+ if (sessionId) {
+ localStorage.setItem('overlay_session', sessionId);
+ }
+ };
+
+ const setupWebSocket = () => {
+ const token = localStorage.getItem('token');
+ if (!token) return;
+
+ const ws = new WebSocket(`ws://localhost:5000?token=${token}`);
+
+ ws.onopen = () => {
+ console.log('[Overlay] WebSocket connected');
+ };
+
+ ws.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+ handleSocketMessage(data);
+ };
+
+ ws.onerror = (error) => {
+ console.error('[Overlay] WebSocket error:', error);
+ };
+
+ ws.onclose = () => {
+ console.log('[Overlay] WebSocket disconnected');
+ // Reconnect after 3 seconds
+ setTimeout(setupWebSocket, 3000);
+ };
+
+ setSocket(ws);
+ };
+
+ const loadFriends = async () => {
+ try {
+ const token = localStorage.getItem('token');
+ if (!token) return;
+
+ const response = await fetch('/api/friends', {
+ headers: {
+ 'Authorization': `Bearer ${token}`
+ }
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ setFriends(data.friends);
+ }
+ } catch (error) {
+ console.error('[Overlay] Failed to load friends:', error);
+ }
+ };
+
+ const handleSocketMessage = (data) => {
+ switch (data.type) {
+ case 'presence:updated':
+ handlePresenceUpdate(data);
+ break;
+ case 'message:new':
+ handleNewMessage(data);
+ break;
+ case 'friend:request':
+ showNotification({
+ icon: '👋',
+ title: 'Friend Request',
+ body: `${data.username} wants to be friends`
+ });
+ break;
+ case 'friend:accepted':
+ showNotification({
+ icon: '✅',
+ title: 'Friend Request Accepted',
+ body: `${data.username} accepted your friend request`
+ });
+ loadFriends(); // Refresh friends list
+ break;
+ }
+ };
+
+ const handlePresenceUpdate = (data) => {
+ setFriends(prev => prev.map(friend =>
+ friend.userId === data.userId
+ ? {
+ ...friend,
+ status: data.status,
+ lastSeen: data.lastSeenAt,
+ currentGame: data.currentGame
+ }
+ : friend
+ ));
+ };
+
+ const handleNewMessage = (data) => {
+ setUnreadMessages(prev => prev + 1);
+
+ // Show notification
+ showNotification({
+ icon: '💬',
+ title: 'New Message',
+ body: `${data.senderDisplayName}: ${data.content.substring(0, 50)}${data.content.length > 50 ? '...' : ''}`
+ });
+ };
+
+ const showNotification = (notification) => {
+ // Notify parent window (game)
+ if (window.parent !== window) {
+ window.parent.postMessage({
+ type: 'notification',
+ data: notification
+ }, '*');
+ }
+
+ // Also show in overlay if not minimized
+ if (!minimized) {
+ // Could add in-overlay toast here
+ }
+ };
+
+ const handleGameMessage = (event) => {
+ // Handle messages from game
+ if (event.data.type === 'auto_mute') {
+ if (event.data.mute && inCall) {
+ console.log('[Overlay] Auto-muting for in-game match');
+ // Auto-mute logic would trigger call service here
+ } else {
+ console.log('[Overlay] Auto-unmuting');
+ // Auto-unmute logic
+ }
+ }
+ };
+
+ const toggleMinimize = () => {
+ setMinimized(!minimized);
+
+ // Notify parent
+ if (window.parent !== window) {
+ window.parent.postMessage({
+ type: 'minimize',
+ minimized: !minimized
+ }, '*');
+ }
+ };
+
+ const handleFriendClick = (friend) => {
+ // Open quick actions menu for friend
+ console.log('[Overlay] Friend clicked:', friend.username);
+ // Could show: Send message, Join game, Voice call, etc.
+ };
+
+ if (minimized) {
+ return (
+
+
💬
+ {unreadMessages > 0 && (
+
{unreadMessages}
+ )}
+
+ );
+ }
+
+ return (
+
+
+
+ setActiveTab('friends')}
+ >
+ Friends ({friends.filter(f => f.status === 'online').length})
+
+ setActiveTab('messages')}
+ >
+ Messages
+ {unreadMessages > 0 && (
+ {unreadMessages}
+ )}
+
+
+
+ —
+
+
+
+
+ {activeTab === 'friends' && (
+
+ {friends.length === 0 ? (
+
+ ) : (
+ friends.map(friend => (
+
handleFriendClick(friend)}
+ >
+
+
+
{friend.username}
+ {friend.currentGame && (
+
+ 🎮 {friend.currentGame.gameName}
+
+ )}
+ {!friend.currentGame && friend.status === 'online' && (
+
Online
+ )}
+
+
+
+ ))
+ )}
+
+ )}
+
+ {activeTab === 'messages' && (
+
+
Recent messages appear here
+
+ Click a friend to start chatting
+
+
+ )}
+
+
+ );
+}
diff --git a/astro-site/src/react-app/components/Premium/UpgradeFlow.css b/astro-site/src/react-app/components/Premium/UpgradeFlow.css
new file mode 100644
index 0000000..d73dca4
--- /dev/null
+++ b/astro-site/src/react-app/components/Premium/UpgradeFlow.css
@@ -0,0 +1,217 @@
+.upgrade-flow {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 40px 20px;
+}
+
+.upgrade-flow h1 {
+ text-align: center;
+ font-size: 36px;
+ margin-bottom: 40px;
+ color: #fff;
+}
+
+.tier-selection {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 24px;
+ margin-bottom: 40px;
+}
+
+.tier-card {
+ background: rgba(30, 30, 40, 0.8);
+ border: 2px solid rgba(255, 255, 255, 0.1);
+ border-radius: 12px;
+ padding: 32px;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.tier-card:hover {
+ border-color: rgba(88, 101, 242, 0.5);
+ transform: translateY(-4px);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
+}
+
+.tier-card.selected {
+ border-color: #5865f2;
+ background: rgba(88, 101, 242, 0.1);
+}
+
+.tier-card h3 {
+ font-size: 24px;
+ margin-bottom: 16px;
+ color: #fff;
+}
+
+.tier-card .price {
+ font-size: 32px;
+ font-weight: bold;
+ color: #5865f2;
+ margin-bottom: 24px;
+}
+
+.tier-card ul {
+ list-style: none;
+ padding: 0;
+}
+
+.tier-card li {
+ padding: 8px 0;
+ color: #aaa;
+ font-size: 14px;
+}
+
+.domain-selection {
+ background: rgba(30, 30, 40, 0.8);
+ border-radius: 12px;
+ padding: 32px;
+ margin-bottom: 32px;
+}
+
+.domain-selection h3 {
+ margin-bottom: 24px;
+ color: #fff;
+}
+
+.domain-input-group {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+.domain-input {
+ flex: 1;
+ padding: 12px 16px;
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ color: #fff;
+ font-size: 16px;
+}
+
+.domain-input:focus {
+ outline: none;
+ border-color: #5865f2;
+}
+
+.domain-suffix {
+ color: #aaa;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.domain-input-group button {
+ padding: 12px 24px;
+ background: #5865f2;
+ border: none;
+ border-radius: 8px;
+ color: #fff;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.domain-input-group button:hover {
+ background: #4752c4;
+}
+
+.domain-input-group button:disabled {
+ background: #666;
+ cursor: not-allowed;
+}
+
+.domain-status {
+ padding: 16px;
+ border-radius: 8px;
+ margin-top: 16px;
+}
+
+.domain-status.available {
+ background: rgba(35, 165, 90, 0.1);
+ border: 1px solid rgba(35, 165, 90, 0.3);
+ color: #23a55a;
+}
+
+.domain-status.unavailable {
+ background: rgba(237, 66, 69, 0.1);
+ border: 1px solid rgba(237, 66, 69, 0.3);
+ color: #ed4245;
+}
+
+.domain-status ul {
+ list-style: none;
+ padding: 0;
+ margin-top: 12px;
+}
+
+.domain-status li {
+ padding: 8px 12px;
+ margin: 4px 0;
+ background: rgba(255, 255, 255, 0.05);
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.domain-status li:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.checkout-form {
+ background: rgba(30, 30, 40, 0.8);
+ border-radius: 12px;
+ padding: 32px;
+}
+
+.card-element-wrapper {
+ padding: 16px;
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ margin-bottom: 24px;
+}
+
+.error-message {
+ color: #ed4245;
+ padding: 12px;
+ background: rgba(237, 66, 69, 0.1);
+ border-radius: 8px;
+ margin-bottom: 16px;
+ font-size: 14px;
+}
+
+.btn-submit {
+ width: 100%;
+ padding: 16px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border: none;
+ border-radius: 8px;
+ color: #fff;
+ font-size: 16px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s;
+}
+
+.btn-submit:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4);
+}
+
+.btn-submit:disabled {
+ background: #666;
+ cursor: not-allowed;
+ transform: none;
+}
+
+@media (max-width: 768px) {
+ .tier-selection {
+ grid-template-columns: 1fr;
+ }
+
+ .upgrade-flow h1 {
+ font-size: 28px;
+ }
+}
diff --git a/astro-site/src/react-app/components/Premium/index.jsx b/astro-site/src/react-app/components/Premium/index.jsx
new file mode 100644
index 0000000..9318445
--- /dev/null
+++ b/astro-site/src/react-app/components/Premium/index.jsx
@@ -0,0 +1,307 @@
+import React, { useState } from 'react';
+import { loadStripe } from '@stripe/stripe-js';
+import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
+import './UpgradeFlow.css';
+
+const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || 'pk_test_51QTaIiRu6l8tVuJxtest_placeholder');
+
+/**
+ * Checkout form component
+ */
+function CheckoutForm({ tier, domain, onSuccess }) {
+ const stripe = useStripe();
+ const elements = useElements();
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!stripe || !elements) return;
+
+ setLoading(true);
+ setError(null);
+
+ try {
+ // Create payment method
+ const { error: pmError, paymentMethod } = await stripe.createPaymentMethod({
+ type: 'card',
+ card: elements.getElement(CardElement)
+ });
+
+ if (pmError) {
+ throw new Error(pmError.message);
+ }
+
+ // Subscribe or register domain
+ const endpoint = domain
+ ? '/api/premium/domains/register'
+ : '/api/premium/subscribe';
+
+ const body = domain ? {
+ domain: domain,
+ walletAddress: window.ethereum?.selectedAddress || '0x0000000000000000000000000000000000000000',
+ paymentMethodId: paymentMethod.id
+ } : {
+ tier: tier,
+ paymentMethodId: paymentMethod.id,
+ billingPeriod: 'yearly'
+ };
+
+ const response = await fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify(body)
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ onSuccess(data);
+ } else {
+ throw new Error(data.error || 'Subscription failed');
+ }
+
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getAmount = () => {
+ if (domain) return '$100/year';
+ if (tier === 'premium') return '$100/year';
+ if (tier === 'enterprise') return '$500/month';
+ return '$0';
+ };
+
+ return (
+
+
+
+
+
+ {error && (
+ {error}
+ )}
+
+
+ {loading ? 'Processing...' : `Subscribe - ${getAmount()}`}
+
+
+
+ By subscribing, you agree to our Terms of Service and Privacy Policy
+
+
+ );
+}
+
+/**
+ * Main upgrade flow component
+ */
+export default function UpgradeFlow({ currentTier = 'free' }) {
+ const [selectedTier, setSelectedTier] = useState('premium');
+ const [domainName, setDomainName] = useState('');
+ const [domainAvailable, setDomainAvailable] = useState(null);
+ const [checkingDomain, setCheckingDomain] = useState(false);
+
+ const checkDomain = async () => {
+ if (!domainName) {
+ setError('Please enter a domain name');
+ return;
+ }
+
+ setCheckingDomain(true);
+ setDomainAvailable(null);
+
+ try {
+ const response = await fetch('/api/premium/domains/check-availability', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${localStorage.getItem('token')}`
+ },
+ body: JSON.stringify({
+ domain: `${domainName}.aethex`
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.success) {
+ setDomainAvailable(data);
+ } else {
+ throw new Error(data.error);
+ }
+
+ } catch (error) {
+ console.error('Failed to check domain:', error);
+ setDomainAvailable({
+ available: false,
+ domain: `${domainName}.aethex`,
+ error: error.message
+ });
+ } finally {
+ setCheckingDomain(false);
+ }
+ };
+
+ const handleSuccess = (data) => {
+ alert('Subscription successful! Welcome to premium!');
+ // Redirect to dashboard or show success modal
+ window.location.href = '/dashboard';
+ };
+
+ return (
+
+
Upgrade to Premium
+
+
+
setSelectedTier('premium')}
+ >
+
Premium
+
$100/year
+
+ ✓ Custom .aethex domain
+ ✓ Blockchain NFT ownership
+ ✓ Unlimited friends
+ ✓ HD voice/video calls (1080p)
+ ✓ 10 GB storage
+ ✓ Custom branding
+ ✓ Analytics dashboard
+ ✓ Priority support
+ ✓ Ad-free experience
+
+
+
+
setSelectedTier('enterprise')}
+ >
+
Enterprise
+
$500+/month
+
+ ✓ Everything in Premium
+ ✓ White-label platform
+ ✓ Custom domain (chat.yoursite.com)
+ ✓ Unlimited team members
+ ✓ Dedicated infrastructure
+ ✓ 4K video quality
+ ✓ SLA guarantees (99.9% uptime)
+ ✓ Dedicated account manager
+ ✓ Custom integrations
+
+
+
+
+ {selectedTier === 'premium' && (
+
+
Choose Your .aethex Domain
+
+ Your premium blockchain domain with NFT ownership proof
+
+
+
+ setDomainName(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
+ placeholder="yourname"
+ className="domain-input"
+ maxLength={50}
+ />
+ .aethex
+
+ {checkingDomain ? 'Checking...' : 'Check'}
+
+
+
+ {domainAvailable && (
+
+ {domainAvailable.available ? (
+ <>
+
✓ {domainAvailable.domain} is available!
+
+ Price: ${domainAvailable.price}/year
+
+ >
+ ) : (
+
+
✗ {domainAvailable.domain} is taken
+ {domainAvailable.error && (
+
{domainAvailable.error}
+ )}
+ {domainAvailable.suggestedAlternatives && domainAvailable.suggestedAlternatives.length > 0 && (
+ <>
+
Try these alternatives:
+
+ {domainAvailable.suggestedAlternatives.map(alt => (
+ setDomainName(alt.replace('.aethex', ''))}
+ >
+ {alt}
+
+ ))}
+
+ >
+ )}
+
+ )}
+
+ )}
+
+ )}
+
+ {(selectedTier === 'enterprise' || (selectedTier === 'premium' && domainAvailable?.available)) && (
+
+
+
+ )}
+
+ {selectedTier === 'enterprise' && !domainAvailable && (
+
+ )}
+
+ );
+}
diff --git a/astro-site/src/react-app/components/VerifiedDomainBadge.css b/astro-site/src/react-app/components/VerifiedDomainBadge.css
new file mode 100644
index 0000000..e6495aa
--- /dev/null
+++ b/astro-site/src/react-app/components/VerifiedDomainBadge.css
@@ -0,0 +1,85 @@
+.verified-domain-badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 16px;
+ background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
+ border: 2px solid #6ee7b7;
+ border-radius: 24px;
+ font-size: 14px;
+ font-weight: 500;
+ color: #065f46;
+ box-shadow: 0 2px 4px rgba(16, 185, 129, 0.2);
+ transition: all 0.2s;
+}
+
+.verified-domain-badge:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(16, 185, 129, 0.3);
+}
+
+.badge-content {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.domain-text {
+ font-family: 'Courier New', monospace;
+ font-weight: 600;
+ color: #047857;
+}
+
+.checkmark {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ background: #10b981;
+ color: white;
+ border-radius: 50%;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.blockchain-indicator {
+ font-size: 16px;
+ opacity: 0.8;
+}
+
+.verified-info {
+ font-size: 11px;
+ color: #047857;
+ opacity: 0.8;
+ margin-top: 4px;
+}
+
+/* Compact variant */
+.verified-domain-badge.compact {
+ padding: 4px 12px;
+ font-size: 12px;
+}
+
+.verified-domain-badge.compact .checkmark {
+ width: 16px;
+ height: 16px;
+ font-size: 10px;
+}
+
+/* Dark mode support */
+@media (prefers-color-scheme: dark) {
+ .verified-domain-badge {
+ background: linear-gradient(135deg, #064e3b 0%, #065f46 100%);
+ border-color: #047857;
+ color: #d1fae5;
+ }
+
+ .domain-text {
+ color: #a7f3d0;
+ }
+
+ .verified-info {
+ color: #a7f3d0;
+ }
+}
diff --git a/astro-site/src/react-app/components/VerifiedDomainBadge.jsx b/astro-site/src/react-app/components/VerifiedDomainBadge.jsx
new file mode 100644
index 0000000..28d1c32
--- /dev/null
+++ b/astro-site/src/react-app/components/VerifiedDomainBadge.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import './VerifiedDomainBadge.css';
+
+/**
+ * Displays verified domain badge on user profile
+ * @param {Object} props
+ * @param {string} props.verifiedDomain - The verified domain name
+ * @param {string} props.verificationType - Type of verification (dns or blockchain)
+ * @param {Date} props.verifiedAt - When the domain was verified
+ */
+export default function VerifiedDomainBadge({
+ verifiedDomain,
+ verificationType = 'dns',
+ verifiedAt
+}) {
+ if (!verifiedDomain) return null;
+
+ return (
+
+
+ {verifiedDomain}
+
+ ✓
+
+
+ {verificationType === 'blockchain' && (
+
+ ⛓️
+
+ )}
+ {verifiedAt && (
+
+ Verified {new Date(verifiedAt).toLocaleDateString()}
+
+ )}
+
+ );
+}
diff --git a/astro-site/src/react-app/contexts/AuthContext.jsx b/astro-site/src/react-app/contexts/AuthContext.jsx
new file mode 100644
index 0000000..8757c84
--- /dev/null
+++ b/astro-site/src/react-app/contexts/AuthContext.jsx
@@ -0,0 +1,51 @@
+
+import React, { createContext, useContext, useState, useEffect } from 'react';
+import { createClient } from '@supabase/supabase-js';
+
+const supabase = createClient(
+ import.meta.env.PUBLIC_SUPABASE_URL,
+ import.meta.env.PUBLIC_SUPABASE_ANON_KEY
+);
+
+const AuthContext = createContext();
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}
+
+export function AuthProvider({ children }) {
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const getSession = async () => {
+ setLoading(true);
+ const { data: { session } } = await supabase.auth.getSession();
+ if (session?.user) {
+ setUser(session.user);
+ } else {
+ setUser(null);
+ }
+ setLoading(false);
+ };
+ getSession();
+ const { data: listener } = supabase.auth.onAuthStateChange((_event, session) => {
+ setUser(session?.user || null);
+ });
+ return () => {
+ listener?.subscription.unsubscribe();
+ };
+ }, []);
+
+ const value = { user, loading, supabase };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/astro-site/src/react-app/contexts/SocketContext.jsx b/astro-site/src/react-app/contexts/SocketContext.jsx
new file mode 100644
index 0000000..28c4563
--- /dev/null
+++ b/astro-site/src/react-app/contexts/SocketContext.jsx
@@ -0,0 +1,74 @@
+/**
+ * Socket Context
+ * Provides Socket.io connection to all components
+ */
+
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { io } from 'socket.io-client';
+
+const SocketContext = createContext(null);
+
+export function SocketProvider({ children }) {
+ const [socket, setSocket] = useState(null);
+ const [connected, setConnected] = useState(false);
+
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+
+ if (!token) {
+ console.log('No auth token, skipping socket connection');
+ return;
+ }
+
+ // Connect to Socket.io server
+ const socketInstance = io(import.meta.env.VITE_API_URL || 'http://localhost:3000', {
+ auth: {
+ token: token
+ },
+ reconnection: true,
+ reconnectionDelay: 1000,
+ reconnectionAttempts: 5
+ });
+
+ socketInstance.on('connect', () => {
+ console.log('✓ Connected to Socket.io server');
+ setConnected(true);
+ });
+
+ socketInstance.on('disconnect', () => {
+ console.log('✗ Disconnected from Socket.io server');
+ setConnected(false);
+ });
+
+ socketInstance.on('connect_error', (error) => {
+ console.error('Socket connection error:', error.message);
+ });
+
+ socketInstance.on('error', (error) => {
+ console.error('Socket error:', error);
+ });
+
+ setSocket(socketInstance);
+
+ // Cleanup on unmount
+ return () => {
+ if (socketInstance) {
+ socketInstance.disconnect();
+ }
+ };
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useSocket() {
+ const context = useContext(SocketContext);
+ if (context === undefined) {
+ throw new Error('useSocket must be used within SocketProvider');
+ }
+ return context;
+}
diff --git a/astro-site/src/react-app/index.css b/astro-site/src/react-app/index.css
new file mode 100644
index 0000000..7073852
--- /dev/null
+++ b/astro-site/src/react-app/index.css
@@ -0,0 +1,43 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ background: #000000;
+ color: #e4e4e7;
+ line-height: 1.5;
+}
+
+code {
+ font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
+}
+
+::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+
+::-webkit-scrollbar-track {
+ background: #18181b;
+}
+
+::-webkit-scrollbar-thumb {
+ background: #3f3f46;
+ border-radius: 5px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: #52525b;
+}
+
+::selection {
+ background: rgba(139, 92, 246, 0.3);
+ color: #e4e4e7;
+}
diff --git a/astro-site/src/react-app/index.html b/astro-site/src/react-app/index.html
new file mode 100644
index 0000000..211c1c4
--- /dev/null
+++ b/astro-site/src/react-app/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ AeThex Passport - Domain Verification
+
+
+
+
+
+
diff --git a/astro-site/src/react-app/main.jsx b/astro-site/src/react-app/main.jsx
new file mode 100644
index 0000000..8f4caa5
--- /dev/null
+++ b/astro-site/src/react-app/main.jsx
@@ -0,0 +1,11 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import AppWrapper from './App';
+import './index.css';
+import './Demo.css';
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+
+);
diff --git a/astro-site/src/react-app/mockup/ChannelSidebar.jsx b/astro-site/src/react-app/mockup/ChannelSidebar.jsx
new file mode 100644
index 0000000..a676452
--- /dev/null
+++ b/astro-site/src/react-app/mockup/ChannelSidebar.jsx
@@ -0,0 +1,65 @@
+import React from "react";
+
+export default function ChannelSidebar() {
+ return (
+
+ {/* Server Header */}
+
+ AeThex Foundation
+ Official
+
+ {/* Channel List */}
+
+
Announcements
+
+ 📢
+ updates
+ 3
+
+
+ 📜
+ changelog
+
+
Development
+
+ #
+ general
+
+
+ #
+ api-discussion
+
+
+ #
+ passport-development
+
+
Support
+
+ ❓
+ help
+
+
+ 🐛
+ bug-reports
+
+
Voice Channels
+
+ 🔊
+ Nexus Lounge
+ 3
+
+
+ {/* User Presence */}
+
+
A
+
+
Anderson
+
+
+ Building AeThex
+
+
+
+
+ );
+}
diff --git a/astro-site/src/react-app/mockup/ChatArea.jsx b/astro-site/src/react-app/mockup/ChatArea.jsx
new file mode 100644
index 0000000..e39cd3e
--- /dev/null
+++ b/astro-site/src/react-app/mockup/ChatArea.jsx
@@ -0,0 +1,41 @@
+import React from "react";
+import Message from "./Message";
+import MessageInput from "./MessageInput";
+
+const messages = [
+ { type: "system", label: "FOUNDATION", text: "Foundation authentication services upgraded to v2.1.0. Enhanced security protocols now active across all AeThex infrastructure.", className: "foundation" },
+ { type: "user", author: "Trevor", badge: "Foundation", time: "10:34 AM", text: "Just pushed the authentication updates. All services should automatically migrate to the new protocols within 24 hours.", avatar: "T", avatarBg: "from-red-600 to-red-800" },
+ { type: "user", author: "Marcus", time: "10:41 AM", text: "Excellent work! I've been testing the new Passport integration and it's incredibly smooth. The Trinity color-coding in the UI makes it really clear which division is handling what.", avatar: "M", avatarBg: "from-blue-600 to-blue-900" },
+ { type: "system", label: "LABS", text: "Nexus Engine v2.0-beta now available for testing. New cross-platform sync reduces latency by 40%. Join #labs-testing to participate.", className: "labs" },
+ { type: "user", author: "Sarah", badge: "Labs", time: "11:15 AM", text: "The Nexus v2 parallel compilation is insane. Cut my build time from 3 minutes to under 2. Still some edge cases with complex state synchronization but wow... ⚠️", avatar: "S", avatarBg: "from-orange-400 to-orange-700" },
+ { type: "user", author: "Anderson", badge: "Founder", time: "11:47 AM", text: "Love seeing the Trinity infrastructure working in harmony. Foundation keeping everything secure, Labs pushing the boundaries, Corporation delivering production-ready tools. This is exactly the vision.", avatar: "A", avatarBg: "from-red-600 via-blue-600 to-orange-400" },
+ { type: "user", author: "DevUser_2847", time: "12:03 PM", text: "Quick question - when using AeThex Studio, does the Terminal automatically connect to all three Trinity divisions, or do I need to configure that?", avatar: "D", avatarBg: "bg-[#1a1a1a]" },
+ { type: "system", label: "CORPORATION", text: "AeThex Studio Pro users: New Railway deployment templates available. Optimized configurations for Foundation APIs, Corporation services, and Labs experiments.", className: "corporation" },
+];
+
+export default function ChatArea() {
+ return (
+
+ {/* Chat Header */}
+
+
# general
+
+ 🔔
+ 📌
+ 👥 128
+ 🔍
+
+
+ {/* Messages */}
+
+ {messages.map((msg, i) => (
+
+ ))}
+
+ {/* Message Input */}
+
+
+
+
+ );
+}
diff --git a/astro-site/src/react-app/mockup/MainLayout.jsx b/astro-site/src/react-app/mockup/MainLayout.jsx
new file mode 100644
index 0000000..1956766
--- /dev/null
+++ b/astro-site/src/react-app/mockup/MainLayout.jsx
@@ -0,0 +1,17 @@
+import React from "react";
+import ServerList from "./ServerList";
+import ChannelSidebar from "./ChannelSidebar";
+import ChatArea from "./ChatArea";
+import MemberSidebar from "./MemberSidebar";
+import "./global.css";
+
+export default function MainLayout() {
+ return (
+
+
+
+
+
+
+ );
+}
diff --git a/astro-site/src/react-app/mockup/MemberSidebar.jsx b/astro-site/src/react-app/mockup/MemberSidebar.jsx
new file mode 100644
index 0000000..80b4b24
--- /dev/null
+++ b/astro-site/src/react-app/mockup/MemberSidebar.jsx
@@ -0,0 +1,43 @@
+import React from "react";
+
+const members = [
+ { section: "Foundation Team — 8", users: [
+ { name: "Anderson", avatar: "A", status: "online", avatarBg: "from-red-600 to-red-800" },
+ { name: "Trevor", avatar: "T", status: "online", avatarBg: "from-red-600 to-red-800" },
+ ]},
+ { section: "Labs Team — 12", users: [
+ { name: "Sarah", avatar: "S", status: "labs", avatarBg: "from-orange-400 to-orange-700", activity: "Testing v2.0" },
+ ]},
+ { section: "Developers — 47", users: [
+ { name: "Marcus", avatar: "M", status: "in-game", avatarBg: "bg-[#1a1a1a]", activity: "Building" },
+ { name: "DevUser_2847", avatar: "D", status: "online", avatarBg: "bg-[#1a1a1a]" },
+ ]},
+ { section: "Community — 61", users: [
+ { name: "JohnDev", avatar: "J", status: "offline", avatarBg: "bg-[#1a1a1a]" },
+ ]},
+];
+
+export default function MemberSidebar() {
+ return (
+
+
Members — 128
+
+ {members.map((section, i) => (
+
+
{section.section}
+ {section.users.map((user, j) => (
+
+
+
{user.name}
+ {user.activity &&
{user.activity}
}
+
+ ))}
+
+ ))}
+
+
+ );
+}
diff --git a/astro-site/src/react-app/mockup/Message.jsx b/astro-site/src/react-app/mockup/Message.jsx
new file mode 100644
index 0000000..a868461
--- /dev/null
+++ b/astro-site/src/react-app/mockup/Message.jsx
@@ -0,0 +1,27 @@
+import React from "react";
+
+export default function Message(props) {
+ if (props.type === "system") {
+ return (
+
+
[{props.label}] System Announcement
+
{props.text}
+
+ );
+ }
+ return (
+
+
{props.avatar}
+
+
+ {props.author}
+ {props.badge && (
+ {props.badge}
+ )}
+ {props.time}
+
+
{props.text}
+
+
+ );
+}
diff --git a/astro-site/src/react-app/mockup/MessageInput.jsx b/astro-site/src/react-app/mockup/MessageInput.jsx
new file mode 100644
index 0000000..4e7721a
--- /dev/null
+++ b/astro-site/src/react-app/mockup/MessageInput.jsx
@@ -0,0 +1,16 @@
+import React from "react";
+
+export default function MessageInput() {
+ return (
+
+ +
+
+ 🎤
+
+ );
+}
diff --git a/astro-site/src/react-app/mockup/ServerList.jsx b/astro-site/src/react-app/mockup/ServerList.jsx
new file mode 100644
index 0000000..a7b5d59
--- /dev/null
+++ b/astro-site/src/react-app/mockup/ServerList.jsx
@@ -0,0 +1,30 @@
+import React from "react";
+
+const servers = [
+ { id: "foundation", label: "F", active: true, className: "foundation" },
+ { id: "corporation", label: "C", active: false, className: "corporation" },
+ { id: "labs", label: "L", active: false, className: "labs" },
+ { id: "divider" },
+ { id: "community1", label: "AG", active: false, className: "community" },
+ { id: "community2", label: "RD", active: false, className: "community" },
+ { id: "add", label: "+", active: false, className: "community" },
+];
+
+export default function ServerList() {
+ return (
+
+ {servers.map((srv, i) =>
+ srv.id === "divider" ? (
+
+ ) : (
+
+ {srv.label}
+
+ )
+ )}
+
+ );
+}
diff --git a/astro-site/src/react-app/mockup/global.css b/astro-site/src/react-app/mockup/global.css
new file mode 100644
index 0000000..b514874
--- /dev/null
+++ b/astro-site/src/react-app/mockup/global.css
@@ -0,0 +1,57 @@
+@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&display=swap');
+
+html, body, #root {
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ font-family: 'Roboto Mono', monospace;
+ background: #0a0a0a;
+ color: #e0e0e0;
+}
+
+body::before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: repeating-linear-gradient(
+ 0deg,
+ rgba(0, 0, 0, 0.15),
+ rgba(0, 0, 0, 0.15) 1px,
+ transparent 1px,
+ transparent 2px
+ );
+ pointer-events: none;
+ z-index: 1000;
+}
+
+.connect-container {
+ height: 100vh;
+ display: flex;
+}
+
+.server-icon, .user-avatar, .member-avatar-small {
+ background: rgba(26,26,26,0.85);
+ backdrop-filter: blur(6px);
+}
+
+.channel-item.active, .channel-item:hover, .member-item:hover {
+ background: rgba(26,26,26,0.85);
+ backdrop-filter: blur(4px);
+}
+
+.message-input, .message-input-container {
+ background: rgba(15,15,15,0.95);
+ backdrop-filter: blur(4px);
+}
+
+::-webkit-scrollbar {
+ width: 8px;
+ background: #111;
+}
+::-webkit-scrollbar-thumb {
+ background: #222;
+ border-radius: 4px;
+}
diff --git a/astro-site/src/react-app/mockup/index.html b/astro-site/src/react-app/mockup/index.html
new file mode 100644
index 0000000..25131a0
--- /dev/null
+++ b/astro-site/src/react-app/mockup/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ AeThex Connect Mockup Web
+
+
+
+
+
+
+
diff --git a/astro-site/src/react-app/mockup/index.jsx b/astro-site/src/react-app/mockup/index.jsx
new file mode 100644
index 0000000..926f79f
--- /dev/null
+++ b/astro-site/src/react-app/mockup/index.jsx
@@ -0,0 +1,13 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import MainLayout from "./MainLayout";
+import "./global.css";
+
+const root = document.getElementById("root");
+if (root) {
+ createRoot(root).render(
+
+
+
+ );
+}
diff --git a/astro-site/src/react-app/package-lock.json b/astro-site/src/react-app/package-lock.json
new file mode 100644
index 0000000..f434479
--- /dev/null
+++ b/astro-site/src/react-app/package-lock.json
@@ -0,0 +1,2175 @@
+{
+ "name": "aethex-passport-frontend",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "aethex-passport-frontend",
+ "version": "1.0.0",
+ "dependencies": {
+ "@stripe/react-stripe-js": "^5.4.1",
+ "@stripe/stripe-js": "^8.6.1",
+ "axios": "^1.13.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.30.3",
+ "socket.io-client": "^4.8.3"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@vitejs/plugin-react": "^4.2.1",
+ "vite": "^5.0.8"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
+ "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
+ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
+ "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.5",
+ "@babel/types": "^7.28.5",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
+ "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.5"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
+ "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.5",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.5",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.5",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
+ "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz",
+ "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz",
+ "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz",
+ "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz",
+ "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz",
+ "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz",
+ "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz",
+ "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz",
+ "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz",
+ "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz",
+ "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz",
+ "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz",
+ "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz",
+ "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz",
+ "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz",
+ "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz",
+ "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz",
+ "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz",
+ "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz",
+ "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz",
+ "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz",
+ "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz",
+ "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz",
+ "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "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/@stripe/react-stripe-js": {
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-5.4.1.tgz",
+ "integrity": "sha512-ipeYcAHa4EPmjwfv0lFE+YDVkOQ0TMKkFWamW+BqmnSkEln/hO8rmxGPPWcd9WjqABx6Ro8Xg4pAS7evCcR9cw==",
+ "license": "MIT",
+ "dependencies": {
+ "prop-types": "^15.7.2"
+ },
+ "peerDependencies": {
+ "@stripe/stripe-js": ">=8.0.0 <9.0.0",
+ "react": ">=16.8.0 <20.0.0",
+ "react-dom": ">=16.8.0 <20.0.0"
+ }
+ },
+ "node_modules/@stripe/stripe-js": {
+ "version": "8.6.1",
+ "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.6.1.tgz",
+ "integrity": "sha512-UJ05U2062XDgydbUcETH1AoRQLNhigQ2KmDn1BG8sC3xfzu6JKg95Qt6YozdzFpxl1Npii/02m2LEWFt1RYjVA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12.16"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.27",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz",
+ "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
+ "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.9.14",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz",
+ "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001763",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz",
+ "integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.267",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
+ "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/engine.io-client": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
+ "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.18.3",
+ "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/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.55.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz",
+ "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.55.1",
+ "@rollup/rollup-android-arm64": "4.55.1",
+ "@rollup/rollup-darwin-arm64": "4.55.1",
+ "@rollup/rollup-darwin-x64": "4.55.1",
+ "@rollup/rollup-freebsd-arm64": "4.55.1",
+ "@rollup/rollup-freebsd-x64": "4.55.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.55.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.55.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.55.1",
+ "@rollup/rollup-linux-arm64-musl": "4.55.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.55.1",
+ "@rollup/rollup-linux-loong64-musl": "4.55.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.55.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.55.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.55.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.55.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-gnu": "4.55.1",
+ "@rollup/rollup-linux-x64-musl": "4.55.1",
+ "@rollup/rollup-openbsd-x64": "4.55.1",
+ "@rollup/rollup-openharmony-arm64": "4.55.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.55.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.55.1",
+ "@rollup/rollup-win32-x64-gnu": "4.55.1",
+ "@rollup/rollup-win32-x64-msvc": "4.55.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "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.5",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
+ "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "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/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/astro-site/src/react-app/package.json b/astro-site/src/react-app/package.json
new file mode 100644
index 0000000..57dfcee
--- /dev/null
+++ b/astro-site/src/react-app/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "aethex-passport-frontend",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@stripe/react-stripe-js": "^5.4.1",
+ "@stripe/stripe-js": "^8.6.1",
+ "axios": "^1.13.2",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.30.3",
+ "socket.io-client": "^4.8.3"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@vitejs/plugin-react": "^4.2.1",
+ "vite": "^5.0.8"
+ }
+}
diff --git a/astro-site/src/react-app/utils/crypto.js b/astro-site/src/react-app/utils/crypto.js
new file mode 100644
index 0000000..bc63d23
--- /dev/null
+++ b/astro-site/src/react-app/utils/crypto.js
@@ -0,0 +1,316 @@
+/**
+ * End-to-End Encryption Utilities
+ * Client-side encryption using Web Crypto API
+ * - RSA-OAEP for key exchange
+ * - AES-256-GCM for message content
+ */
+
+/**
+ * Generate RSA key pair for user
+ * @returns {Promise<{publicKey: string, privateKey: string}>}
+ */
+export async function generateKeyPair() {
+ const keyPair = await window.crypto.subtle.generateKey(
+ {
+ name: 'RSA-OAEP',
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1]),
+ hash: 'SHA-256'
+ },
+ true, // extractable
+ ['encrypt', 'decrypt']
+ );
+
+ // Export keys
+ const publicKey = await window.crypto.subtle.exportKey('spki', keyPair.publicKey);
+ const privateKey = await window.crypto.subtle.exportKey('pkcs8', keyPair.privateKey);
+
+ return {
+ publicKey: arrayBufferToBase64(publicKey),
+ privateKey: arrayBufferToBase64(privateKey)
+ };
+}
+
+/**
+ * Store private key encrypted with user's password
+ * @param {string} privateKey - Base64 encoded private key
+ * @param {string} password - User's password
+ */
+export async function storePrivateKey(privateKey, password) {
+ // Derive encryption key from password
+ const passwordKey = await deriveKeyFromPassword(password);
+
+ // Encrypt private key
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
+ const encrypted = await window.crypto.subtle.encrypt(
+ {
+ name: 'AES-GCM',
+ iv: iv
+ },
+ passwordKey,
+ base64ToArrayBuffer(privateKey)
+ );
+
+ // Store in localStorage
+ localStorage.setItem('encryptedPrivateKey', arrayBufferToBase64(encrypted));
+ localStorage.setItem('privateKeyIV', arrayBufferToBase64(iv));
+}
+
+/**
+ * Retrieve and decrypt private key
+ * @param {string} password - User's password
+ * @returns {Promise} Decrypted private key
+ */
+export async function getPrivateKey(password) {
+ const encryptedKey = localStorage.getItem('encryptedPrivateKey');
+ const iv = localStorage.getItem('privateKeyIV');
+
+ if (!encryptedKey || !iv) {
+ throw new Error('No stored private key found');
+ }
+
+ // Derive decryption key from password
+ const passwordKey = await deriveKeyFromPassword(password);
+
+ // Decrypt private key
+ const decrypted = await window.crypto.subtle.decrypt(
+ {
+ name: 'AES-GCM',
+ iv: base64ToArrayBuffer(iv)
+ },
+ passwordKey,
+ base64ToArrayBuffer(encryptedKey)
+ );
+
+ // Import as CryptoKey
+ return await window.crypto.subtle.importKey(
+ 'pkcs8',
+ decrypted,
+ {
+ name: 'RSA-OAEP',
+ hash: 'SHA-256'
+ },
+ true,
+ ['decrypt']
+ );
+}
+
+/**
+ * Derive encryption key from password using PBKDF2
+ * @param {string} password - User's password
+ * @returns {Promise} Derived key
+ */
+async function deriveKeyFromPassword(password) {
+ const encoder = new TextEncoder();
+ const passwordBuffer = encoder.encode(password);
+
+ // Import password as key material
+ const passwordKey = await window.crypto.subtle.importKey(
+ 'raw',
+ passwordBuffer,
+ 'PBKDF2',
+ false,
+ ['deriveKey']
+ );
+
+ // Get or generate salt
+ let salt = localStorage.getItem('keySalt');
+ if (!salt) {
+ const saltBuffer = window.crypto.getRandomValues(new Uint8Array(16));
+ salt = arrayBufferToBase64(saltBuffer);
+ localStorage.setItem('keySalt', salt);
+ }
+
+ // Derive AES key
+ return await window.crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ salt: base64ToArrayBuffer(salt),
+ iterations: 100000,
+ hash: 'SHA-256'
+ },
+ passwordKey,
+ {
+ name: 'AES-GCM',
+ length: 256
+ },
+ false,
+ ['encrypt', 'decrypt']
+ );
+}
+
+/**
+ * Encrypt message content
+ * @param {string} message - Plain text message
+ * @param {string[]} recipientPublicKeys - Array of recipient public keys (base64)
+ * @returns {Promise} Encrypted message bundle
+ */
+export async function encryptMessage(message, recipientPublicKeys) {
+ // Generate random AES key for this message
+ const messageKey = await window.crypto.subtle.generateKey(
+ {
+ name: 'AES-GCM',
+ length: 256
+ },
+ true,
+ ['encrypt', 'decrypt']
+ );
+
+ // Encrypt message with AES key
+ const iv = window.crypto.getRandomValues(new Uint8Array(12));
+ const encoder = new TextEncoder();
+ const messageBuffer = encoder.encode(message);
+
+ const encryptedMessage = await window.crypto.subtle.encrypt(
+ {
+ name: 'AES-GCM',
+ iv: iv
+ },
+ messageKey,
+ messageBuffer
+ );
+
+ // Export AES key
+ const exportedMessageKey = await window.crypto.subtle.exportKey('raw', messageKey);
+
+ // Encrypt AES key for each recipient with their RSA public key
+ const encryptedKeys = {};
+
+ for (const recipientKeyB64 of recipientPublicKeys) {
+ try {
+ // Import recipient's public key
+ const recipientKey = await window.crypto.subtle.importKey(
+ 'spki',
+ base64ToArrayBuffer(recipientKeyB64),
+ {
+ name: 'RSA-OAEP',
+ hash: 'SHA-256'
+ },
+ true,
+ ['encrypt']
+ );
+
+ // Encrypt message key with recipient's public key
+ const encryptedKey = await window.crypto.subtle.encrypt(
+ {
+ name: 'RSA-OAEP'
+ },
+ recipientKey,
+ exportedMessageKey
+ );
+
+ encryptedKeys[recipientKeyB64] = arrayBufferToBase64(encryptedKey);
+ } catch (error) {
+ console.error('Failed to encrypt for recipient:', error);
+ }
+ }
+
+ return {
+ ciphertext: arrayBufferToBase64(encryptedMessage),
+ iv: arrayBufferToBase64(iv),
+ encryptedKeys: encryptedKeys // Map of publicKey -> encrypted AES key
+ };
+}
+
+/**
+ * Decrypt message content
+ * @param {Object} encryptedBundle - Encrypted message bundle
+ * @param {string} userPassword - User's password (to decrypt private key)
+ * @param {string} userPublicKey - User's public key (to find correct encrypted key)
+ * @returns {Promise} Decrypted message
+ */
+export async function decryptMessage(encryptedBundle, userPassword, userPublicKey) {
+ // Get user's private key
+ const privateKey = await getPrivateKey(userPassword);
+
+ // Find the encrypted key for this user
+ const encryptedKeyB64 = encryptedBundle.encryptedKeys[userPublicKey];
+
+ if (!encryptedKeyB64) {
+ throw new Error('No encrypted key found for this user');
+ }
+
+ // Decrypt the AES key with user's private key
+ const decryptedKeyBuffer = await window.crypto.subtle.decrypt(
+ {
+ name: 'RSA-OAEP'
+ },
+ privateKey,
+ base64ToArrayBuffer(encryptedKeyB64)
+ );
+
+ // Import AES key
+ const messageKey = await window.crypto.subtle.importKey(
+ 'raw',
+ decryptedKeyBuffer,
+ {
+ name: 'AES-GCM',
+ length: 256
+ },
+ false,
+ ['decrypt']
+ );
+
+ // Decrypt message
+ const decryptedBuffer = await window.crypto.subtle.decrypt(
+ {
+ name: 'AES-GCM',
+ iv: base64ToArrayBuffer(encryptedBundle.iv)
+ },
+ messageKey,
+ base64ToArrayBuffer(encryptedBundle.ciphertext)
+ );
+
+ // Convert to text
+ const decoder = new TextDecoder();
+ return decoder.decode(decryptedBuffer);
+}
+
+/**
+ * Convert ArrayBuffer to Base64 string
+ * @param {ArrayBuffer} buffer
+ * @returns {string}
+ */
+function arrayBufferToBase64(buffer) {
+ const bytes = new Uint8Array(buffer);
+ let binary = '';
+ for (let i = 0; i < bytes.byteLength; i++) {
+ binary += String.fromCharCode(bytes[i]);
+ }
+ return window.btoa(binary);
+}
+
+/**
+ * Convert Base64 string to ArrayBuffer
+ * @param {string} base64
+ * @returns {ArrayBuffer}
+ */
+function base64ToArrayBuffer(base64) {
+ const binary = window.atob(base64);
+ const bytes = new Uint8Array(binary.length);
+ for (let i = 0; i < binary.length; i++) {
+ bytes[i] = binary.charCodeAt(i);
+ }
+ return bytes.buffer;
+}
+
+/**
+ * Check if encryption keys exist
+ * @returns {boolean}
+ */
+export function hasEncryptionKeys() {
+ return !!(
+ localStorage.getItem('encryptedPrivateKey') &&
+ localStorage.getItem('privateKeyIV') &&
+ localStorage.getItem('keySalt')
+ );
+}
+
+/**
+ * Clear all stored encryption keys
+ */
+export function clearEncryptionKeys() {
+ localStorage.removeItem('encryptedPrivateKey');
+ localStorage.removeItem('privateKeyIV');
+ localStorage.removeItem('keySalt');
+}
diff --git a/astro-site/src/react-app/utils/socket.js b/astro-site/src/react-app/utils/socket.js
new file mode 100644
index 0000000..188de1e
--- /dev/null
+++ b/astro-site/src/react-app/utils/socket.js
@@ -0,0 +1,39 @@
+// socket.js
+// Example Socket.io client for AeThex Connect (Supabase JWT auth)
+
+import { io } from 'socket.io-client';
+import { createClient } from '@supabase/supabase-js';
+
+// Initialize Supabase client
+const supabase = createClient(
+ import.meta.env.PUBLIC_SUPABASE_URL,
+ import.meta.env.PUBLIC_SUPABASE_ANON_KEY
+);
+
+export async function connectSocket() {
+ // Get current session (JWT)
+ const { data: { session } } = await supabase.auth.getSession();
+ if (!session) throw new Error('Not authenticated');
+
+ // Connect to signaling server
+ const socket = io(import.meta.env.PUBLIC_SIGNALING_SERVER_URL, {
+ auth: { token: session.access_token }
+ });
+
+ // Example: handle connection
+ socket.on('connect', () => {
+ console.log('Connected to signaling server');
+ });
+
+ // Example: join a voice room
+ function joinVoice(roomId) {
+ socket.emit('voice:join', roomId);
+ }
+
+ // Example: send/receive offers, answers, ICE
+ socket.on('voice:offer', (data) => { /* handle offer */ });
+ socket.on('voice:answer', (data) => { /* handle answer */ });
+ socket.on('voice:ice-candidate', (data) => { /* handle candidate */ });
+
+ return { socket, joinVoice };
+}
diff --git a/astro-site/src/react-app/utils/webrtc.js b/astro-site/src/react-app/utils/webrtc.js
new file mode 100644
index 0000000..769d6b7
--- /dev/null
+++ b/astro-site/src/react-app/utils/webrtc.js
@@ -0,0 +1,532 @@
+/**
+ * WebRTC Manager
+ * Handles all WebRTC peer connection logic for voice and video calls
+ */
+
+class WebRTCManager {
+ constructor(socket) {
+ this.socket = socket;
+ this.peerConnections = new Map(); // Map of userId -> RTCPeerConnection
+ this.localStream = null;
+ this.screenStream = null;
+ this.remoteStreams = new Map(); // Map of userId -> MediaStream
+ this.currentCallId = null;
+ this.isInitiator = false;
+
+ // WebRTC configuration with STUN/TURN servers
+ this.configuration = {
+ iceServers: [
+ { urls: 'stun:stun.l.google.com:19302' },
+ { urls: 'stun:stun1.l.google.com:19302' }
+ ]
+ };
+
+ // Media constraints
+ this.audioConstraints = {
+ echoCancellation: true,
+ noiseSuppression: true,
+ autoGainControl: true
+ };
+
+ this.videoConstraints = {
+ width: { ideal: 1280 },
+ height: { ideal: 720 },
+ frameRate: { ideal: 30 }
+ };
+
+ // Event handlers
+ this.onRemoteStream = null;
+ this.onRemoteStreamRemoved = null;
+ this.onConnectionStateChange = null;
+ this.onIceConnectionStateChange = null;
+
+ this.setupSocketListeners();
+ }
+
+ /**
+ * Setup Socket.io listeners for WebRTC signaling
+ */
+ setupSocketListeners() {
+ this.socket.on('call:offer', async (data) => {
+ await this.handleOffer(data);
+ });
+
+ this.socket.on('call:answer', async (data) => {
+ await this.handleAnswer(data);
+ });
+
+ this.socket.on('call:ice-candidate', async (data) => {
+ await this.handleIceCandidate(data);
+ });
+
+ this.socket.on('call:ended', () => {
+ this.cleanup();
+ });
+
+ this.socket.on('call:participant-joined', async (data) => {
+ console.log('Participant joined:', data);
+ // For group calls, establish connection with new participant
+ if (this.isInitiator) {
+ await this.initiateCallToUser(data.userId);
+ }
+ });
+
+ this.socket.on('call:participant-left', (data) => {
+ console.log('Participant left:', data);
+ this.removePeerConnection(data.userId);
+ });
+
+ this.socket.on('call:media-state-changed', (data) => {
+ console.log('Media state changed:', data);
+ // Update UI to reflect remote user's media state
+ if (this.onMediaStateChanged) {
+ this.onMediaStateChanged(data);
+ }
+ });
+ }
+
+ /**
+ * Set TURN server credentials
+ */
+ async setTurnCredentials(turnCredentials) {
+ if (turnCredentials && turnCredentials.urls) {
+ const turnServer = {
+ urls: turnCredentials.urls,
+ username: turnCredentials.username,
+ credential: turnCredentials.credential
+ };
+
+ this.configuration.iceServers.push(turnServer);
+ console.log('TURN server configured');
+ }
+ }
+
+ /**
+ * Initialize local media stream (audio and/or video)
+ */
+ async initializeLocalStream(audioEnabled = true, videoEnabled = true) {
+ try {
+ const constraints = {
+ audio: audioEnabled ? this.audioConstraints : false,
+ video: videoEnabled ? this.videoConstraints : false
+ };
+
+ this.localStream = await navigator.mediaDevices.getUserMedia(constraints);
+ console.log('Local stream initialized:', {
+ audio: audioEnabled,
+ video: videoEnabled,
+ tracks: this.localStream.getTracks().length
+ });
+
+ return this.localStream;
+ } catch (error) {
+ console.error('Error accessing media devices:', error);
+ throw new Error(`Failed to access camera/microphone: ${error.message}`);
+ }
+ }
+
+ /**
+ * Create a peer connection for a user
+ */
+ createPeerConnection(userId) {
+ if (this.peerConnections.has(userId)) {
+ return this.peerConnections.get(userId);
+ }
+
+ const peerConnection = new RTCPeerConnection(this.configuration);
+
+ // Add local stream tracks to peer connection
+ if (this.localStream) {
+ this.localStream.getTracks().forEach(track => {
+ peerConnection.addTrack(track, this.localStream);
+ });
+ }
+
+ // Handle incoming remote stream
+ peerConnection.ontrack = (event) => {
+ console.log('Received remote track from', userId, event.track.kind);
+
+ const [remoteStream] = event.streams;
+ this.remoteStreams.set(userId, remoteStream);
+
+ if (this.onRemoteStream) {
+ this.onRemoteStream(userId, remoteStream);
+ }
+ };
+
+ // Handle ICE candidates
+ peerConnection.onicecandidate = (event) => {
+ if (event.candidate) {
+ console.log('Sending ICE candidate to', userId);
+ this.socket.emit('call:ice-candidate', {
+ callId: this.currentCallId,
+ targetUserId: userId,
+ candidate: event.candidate
+ });
+ }
+ };
+
+ // Handle connection state changes
+ peerConnection.onconnectionstatechange = () => {
+ console.log(`Connection state with ${userId}:`, peerConnection.connectionState);
+
+ if (this.onConnectionStateChange) {
+ this.onConnectionStateChange(userId, peerConnection.connectionState);
+ }
+
+ // Cleanup if connection fails or closes
+ if (peerConnection.connectionState === 'failed' ||
+ peerConnection.connectionState === 'closed') {
+ this.removePeerConnection(userId);
+ }
+ };
+
+ // Handle ICE connection state changes
+ peerConnection.oniceconnectionstatechange = () => {
+ console.log(`ICE connection state with ${userId}:`, peerConnection.iceConnectionState);
+
+ if (this.onIceConnectionStateChange) {
+ this.onIceConnectionStateChange(userId, peerConnection.iceConnectionState);
+ }
+ };
+
+ this.peerConnections.set(userId, peerConnection);
+ return peerConnection;
+ }
+
+ /**
+ * Initiate a call to a user (create offer)
+ */
+ async initiateCallToUser(userId) {
+ try {
+ const peerConnection = this.createPeerConnection(userId);
+
+ // Create offer
+ const offer = await peerConnection.createOffer({
+ offerToReceiveAudio: true,
+ offerToReceiveVideo: true
+ });
+
+ await peerConnection.setLocalDescription(offer);
+
+ // Send offer through signaling server
+ this.socket.emit('call:offer', {
+ callId: this.currentCallId,
+ targetUserId: userId,
+ offer: offer
+ });
+
+ console.log('Call offer sent to', userId);
+ } catch (error) {
+ console.error('Error initiating call:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Handle incoming call offer
+ */
+ async handleOffer(data) {
+ const { callId, fromUserId, offer } = data;
+
+ try {
+ console.log('Received call offer from', fromUserId);
+
+ this.currentCallId = callId;
+ const peerConnection = this.createPeerConnection(fromUserId);
+
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
+
+ // Create answer
+ const answer = await peerConnection.createAnswer();
+ await peerConnection.setLocalDescription(answer);
+
+ // Send answer back
+ this.socket.emit('call:answer', {
+ callId: callId,
+ targetUserId: fromUserId,
+ answer: answer
+ });
+
+ console.log('Call answer sent to', fromUserId);
+ } catch (error) {
+ console.error('Error handling offer:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Handle incoming call answer
+ */
+ async handleAnswer(data) {
+ const { fromUserId, answer } = data;
+
+ try {
+ console.log('Received call answer from', fromUserId);
+
+ const peerConnection = this.peerConnections.get(fromUserId);
+ if (!peerConnection) {
+ throw new Error(`No peer connection found for user ${fromUserId}`);
+ }
+
+ await peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
+ console.log('Remote description set for', fromUserId);
+ } catch (error) {
+ console.error('Error handling answer:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Handle incoming ICE candidate
+ */
+ async handleIceCandidate(data) {
+ const { fromUserId, candidate } = data;
+
+ try {
+ const peerConnection = this.peerConnections.get(fromUserId);
+ if (!peerConnection) {
+ console.warn(`No peer connection found for user ${fromUserId}`);
+ return;
+ }
+
+ await peerConnection.addIceCandidate(new RTCIceCandidate(candidate));
+ console.log('ICE candidate added for', fromUserId);
+ } catch (error) {
+ console.error('Error adding ICE candidate:', error);
+ }
+ }
+
+ /**
+ * Remove peer connection for a user
+ */
+ removePeerConnection(userId) {
+ const peerConnection = this.peerConnections.get(userId);
+ if (peerConnection) {
+ peerConnection.close();
+ this.peerConnections.delete(userId);
+ }
+
+ const remoteStream = this.remoteStreams.get(userId);
+ if (remoteStream) {
+ remoteStream.getTracks().forEach(track => track.stop());
+ this.remoteStreams.delete(userId);
+
+ if (this.onRemoteStreamRemoved) {
+ this.onRemoteStreamRemoved(userId);
+ }
+ }
+
+ console.log('Peer connection removed for', userId);
+ }
+
+ /**
+ * Toggle audio track enabled/disabled
+ */
+ toggleAudio(enabled) {
+ if (this.localStream) {
+ const audioTrack = this.localStream.getAudioTracks()[0];
+ if (audioTrack) {
+ audioTrack.enabled = enabled;
+ console.log('Audio', enabled ? 'enabled' : 'disabled');
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Toggle video track enabled/disabled
+ */
+ toggleVideo(enabled) {
+ if (this.localStream) {
+ const videoTrack = this.localStream.getVideoTracks()[0];
+ if (videoTrack) {
+ videoTrack.enabled = enabled;
+ console.log('Video', enabled ? 'enabled' : 'disabled');
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Start screen sharing
+ */
+ async startScreenShare() {
+ try {
+ this.screenStream = await navigator.mediaDevices.getDisplayMedia({
+ video: {
+ cursor: 'always'
+ },
+ audio: false
+ });
+
+ const screenTrack = this.screenStream.getVideoTracks()[0];
+
+ // Replace video track in all peer connections
+ this.peerConnections.forEach((peerConnection) => {
+ const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
+ if (sender) {
+ sender.replaceTrack(screenTrack);
+ }
+ });
+
+ // Handle screen share stop
+ screenTrack.onended = () => {
+ this.stopScreenShare();
+ };
+
+ console.log('Screen sharing started');
+ return this.screenStream;
+ } catch (error) {
+ console.error('Error starting screen share:', error);
+ throw error;
+ }
+ }
+
+ /**
+ * Stop screen sharing and restore camera
+ */
+ stopScreenShare() {
+ if (this.screenStream) {
+ this.screenStream.getTracks().forEach(track => track.stop());
+ this.screenStream = null;
+
+ // Restore camera track
+ if (this.localStream) {
+ const videoTrack = this.localStream.getVideoTracks()[0];
+ if (videoTrack) {
+ this.peerConnections.forEach((peerConnection) => {
+ const sender = peerConnection.getSenders().find(s => s.track?.kind === 'video');
+ if (sender) {
+ sender.replaceTrack(videoTrack);
+ }
+ });
+ }
+ }
+
+ console.log('Screen sharing stopped');
+ }
+ }
+
+ /**
+ * Get connection statistics
+ */
+ async getConnectionStats(userId) {
+ const peerConnection = this.peerConnections.get(userId);
+ if (!peerConnection) {
+ return null;
+ }
+
+ const stats = await peerConnection.getStats();
+ const result = {
+ audio: {},
+ video: {},
+ connection: {}
+ };
+
+ stats.forEach(report => {
+ if (report.type === 'inbound-rtp') {
+ if (report.kind === 'audio') {
+ result.audio.bytesReceived = report.bytesReceived;
+ result.audio.packetsLost = report.packetsLost;
+ result.audio.jitter = report.jitter;
+ } else if (report.kind === 'video') {
+ result.video.bytesReceived = report.bytesReceived;
+ result.video.packetsLost = report.packetsLost;
+ result.video.framesDecoded = report.framesDecoded;
+ result.video.frameWidth = report.frameWidth;
+ result.video.frameHeight = report.frameHeight;
+ }
+ } else if (report.type === 'candidate-pair' && report.state === 'succeeded') {
+ result.connection.roundTripTime = report.currentRoundTripTime;
+ result.connection.availableOutgoingBitrate = report.availableOutgoingBitrate;
+ }
+ });
+
+ return result;
+ }
+
+ /**
+ * Cleanup all connections and streams
+ */
+ cleanup() {
+ console.log('Cleaning up WebRTC resources');
+
+ // Stop screen share if active
+ this.stopScreenShare();
+
+ // Close all peer connections
+ this.peerConnections.forEach((peerConnection, userId) => {
+ this.removePeerConnection(userId);
+ });
+
+ // Stop local stream
+ if (this.localStream) {
+ this.localStream.getTracks().forEach(track => track.stop());
+ this.localStream = null;
+ }
+
+ // Clear remote streams
+ this.remoteStreams.forEach((stream) => {
+ stream.getTracks().forEach(track => track.stop());
+ });
+ this.remoteStreams.clear();
+
+ this.currentCallId = null;
+ this.isInitiator = false;
+ }
+
+ /**
+ * Get local stream
+ */
+ getLocalStream() {
+ return this.localStream;
+ }
+
+ /**
+ * Get remote stream for a user
+ */
+ getRemoteStream(userId) {
+ return this.remoteStreams.get(userId);
+ }
+
+ /**
+ * Get all remote streams
+ */
+ getAllRemoteStreams() {
+ return Array.from(this.remoteStreams.entries());
+ }
+
+ /**
+ * Check if audio is enabled
+ */
+ isAudioEnabled() {
+ if (this.localStream) {
+ const audioTrack = this.localStream.getAudioTracks()[0];
+ return audioTrack ? audioTrack.enabled : false;
+ }
+ return false;
+ }
+
+ /**
+ * Check if video is enabled
+ */
+ isVideoEnabled() {
+ if (this.localStream) {
+ const videoTrack = this.localStream.getVideoTracks()[0];
+ return videoTrack ? videoTrack.enabled : false;
+ }
+ return false;
+ }
+
+ /**
+ * Check if screen sharing is active
+ */
+ isScreenSharing() {
+ return this.screenStream !== null;
+ }
+}
+
+export default WebRTCManager;
diff --git a/astro-site/src/react-app/vite.config.js b/astro-site/src/react-app/vite.config.js
new file mode 100644
index 0000000..f974b48
--- /dev/null
+++ b/astro-site/src/react-app/vite.config.js
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3000',
+ changeOrigin: true
+ }
+ }
+ }
+});
diff --git a/src/backend/server.js b/src/backend/server.js
index bd360f6..349bcdc 100644
--- a/src/backend/server.js
+++ b/src/backend/server.js
@@ -15,7 +15,7 @@ const path = require('path');
const app = express();
const httpServer = http.createServer(app);
-const PORT = process.env.PORT || 3000;
+const PORT = 3000;
// Trust proxy for Codespaces/containers
app.set('trust proxy', 1);
diff --git a/src/backend/signaling-server/signaling-server.js b/src/backend/signaling-server/signaling-server.js
index 2994740..2cfc511 100644
--- a/src/backend/signaling-server/signaling-server.js
+++ b/src/backend/signaling-server/signaling-server.js
@@ -8,7 +8,7 @@ require('dotenv').config();
const { createClient } = require('@supabase/supabase-js');
-const PORT = process.env.PORT || 4000;
+const PORT = 3000;
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;
diff --git a/src/frontend/App.jsx b/src/frontend/App.jsx
index 1a84876..366d5e5 100644
--- a/src/frontend/App.jsx
+++ b/src/frontend/App.jsx
@@ -1,4 +1,5 @@
-import React, { useState, useEffect } from 'react';
+import React from 'react';
+import { AuthProvider, useAuth } from './contexts/AuthContext';
import DomainVerification from './components/DomainVerification';
import VerifiedDomainBadge from './components/VerifiedDomainBadge';
import './App.css';
@@ -8,35 +9,25 @@ import './App.css';
* Demo of domain verification feature
*/
function App() {
- const [user, setUser] = useState(null);
- const [showVerification, setShowVerification] = useState(false);
+ const { user, loading } = useAuth();
+ const [showVerification, setShowVerification] = React.useState(false);
- // Mock user data - in production, fetch from API
- useEffect(() => {
- // Simulate fetching user data
- const mockUser = {
- id: 'user-123',
- name: 'Demo User',
- email: 'demo@aethex.dev',
- verifiedDomain: null, // Will be populated after verification
- domainVerifiedAt: null
- };
- setUser(mockUser);
- }, []);
+ if (loading) {
+ return Loading...
;
+ }
return (
AeThex Passport
- Domain Verification Demo
+ Domain Verification
{user && (
-
{user.name}
-
{user.email}
+
{user.email}
{user.verifiedDomain && (
+
+
+ );
+}
diff --git a/src/frontend/components/GameForgeChat/ChannelView.jsx b/src/frontend/components/GameForgeChat/ChannelView.jsx
index 7031598..70819e8 100644
--- a/src/frontend/components/GameForgeChat/ChannelView.jsx
+++ b/src/frontend/components/GameForgeChat/ChannelView.jsx
@@ -31,8 +31,7 @@ export default function ChannelView({ channel, projectId }) {
try {
const decrypted = await decryptMessage(
JSON.parse(message.content),
- user.password,
- user.publicKey
+ user.password
);
message.content = decrypted;
} catch (error) {
@@ -76,10 +75,8 @@ export default function ChannelView({ channel, projectId }) {
try {
const decrypted = await decryptMessage(
JSON.parse(msg.content),
- user.password,
- user.publicKey
+ user.password
);
-
return {
...msg,
content: decrypted
diff --git a/src/frontend/contexts/AuthContext.jsx b/src/frontend/contexts/AuthContext.jsx
index 123ac04..720d8f9 100644
--- a/src/frontend/contexts/AuthContext.jsx
+++ b/src/frontend/contexts/AuthContext.jsx
@@ -1,4 +1,10 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
+import { createClient } from '@supabase/supabase-js';
+
+const supabase = createClient(
+ import.meta.env.PUBLIC_SUPABASE_URL,
+ import.meta.env.PUBLIC_SUPABASE_ANON_KEY
+);
const AuthContext = createContext();
@@ -15,47 +21,37 @@ export function AuthProvider({ children }) {
const [loading, setLoading] = useState(true);
useEffect(() => {
- // Initialize with demo user for development
- const demoUser = {
- id: 'demo-user-123',
- name: 'Demo User',
- email: 'demo@aethex.dev',
- verifiedDomain: 'demo.aethex',
- domainVerifiedAt: new Date().toISOString(),
- isPremium: false,
- avatar: null
+ const getSession = async () => {
+ setLoading(true);
+ const { data: { session } } = await supabase.auth.getSession();
+ if (session?.user) {
+ setUser(session.user);
+ } else {
+ setUser(null);
+ }
+ setLoading(false);
+ };
+ getSession();
+ const { data: listener } = supabase.auth.onAuthStateChange((_event, session) => {
+ setUser(session?.user || null);
+ });
+ return () => {
+ listener?.subscription.unsubscribe();
};
-
- setUser(demoUser);
- setLoading(false);
}, []);
const login = async (email, password) => {
- // Mock login - in production, call actual API
- try {
- const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000';
- const response = await fetch(`${apiUrl}/api/auth/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ email, password })
- });
-
- if (response.ok) {
- const data = await response.json();
- setUser(data.user);
- localStorage.setItem('token', data.token);
- return { success: true };
- }
- return { success: false, error: 'Login failed' };
- } catch (error) {
- console.error('Login error:', error);
+ const { data, error } = await supabase.auth.signInWithPassword({ email, password });
+ if (error) {
return { success: false, error: error.message };
}
+ setUser(data.user);
+ return { success: true };
};
- const logout = () => {
+ const logout = async () => {
+ await supabase.auth.signOut();
setUser(null);
- localStorage.removeItem('token');
};
const updateUser = (updates) => {
diff --git a/src/frontend/main.jsx b/src/frontend/main.jsx
index 4d827d4..8f4caa5 100644
--- a/src/frontend/main.jsx
+++ b/src/frontend/main.jsx
@@ -1,11 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
-import Demo from './Demo';
+import AppWrapper from './App';
import './index.css';
import './Demo.css';
ReactDOM.createRoot(document.getElementById('root')).render(
-
+
);
diff --git a/src/frontend/vite.config.js b/src/frontend/vite.config.js
index ad4533d..f974b48 100644
--- a/src/frontend/vite.config.js
+++ b/src/frontend/vite.config.js
@@ -5,7 +5,7 @@ import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
- port: 5173,
+ port: 3000,
proxy: {
'/api': {
target: 'http://localhost:3000',
diff --git a/supabase/config.toml b/supabase/config.toml
index f5f54cd..344e5ee 100644
--- a/supabase/config.toml
+++ b/supabase/config.toml
@@ -7,7 +7,7 @@ project_id = "AeThex-Connect"
[api]
enabled = true
# Port to use for the API URL.
-port = 54321
+port = 3000
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"]
diff --git a/supabase/migrations/20260110120000_messaging_system.sql b/supabase/migrations/20260110120000_messaging_system.sql
index 1345845..0dfb704 100644
--- a/supabase/migrations/20260110120000_messaging_system.sql
+++ b/supabase/migrations/20260110120000_messaging_system.sql
@@ -5,33 +5,49 @@
-- CONVERSATIONS
-- ============================================================================
+-- Ensure all required columns exist for index creation
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS type VARCHAR(20);
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS created_by VARCHAR;
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS gameforge_project_id VARCHAR;
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS is_archived BOOLEAN DEFAULT false;
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW();
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW();
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS title VARCHAR(200);
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS description TEXT;
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS avatar_url VARCHAR(500);
+
CREATE TABLE IF NOT EXISTS conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(20) NOT NULL CHECK (type IN ('direct', 'group', 'channel')),
title VARCHAR(200),
description TEXT,
avatar_url VARCHAR(500),
- created_by UUID REFERENCES users(id) ON DELETE SET NULL,
- gameforge_project_id UUID, -- For GameForge integration (future)
+ created_by VARCHAR REFERENCES users(id) ON DELETE SET NULL,
+ gameforge_project_id VARCHAR, -- For GameForge integration (future)
is_archived BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
+-- Create index on type column
CREATE INDEX idx_conversations_type ON conversations(type);
+-- Create index on creator column
CREATE INDEX idx_conversations_creator ON conversations(created_by);
+-- Create index on project column
CREATE INDEX idx_conversations_project ON conversations(gameforge_project_id);
+-- Create index on updated_at column
CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC);
-- ============================================================================
-- CONVERSATION PARTICIPANTS
-- ============================================================================
+-- Update conversation_participants to match actual types and remove reference to identities
CREATE TABLE IF NOT EXISTS conversation_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- identity_id UUID REFERENCES identities(id) ON DELETE SET NULL,
+ user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ identity_id VARCHAR,
role VARCHAR(20) DEFAULT 'member' CHECK (role IN ('admin', 'moderator', 'member')),
joined_at TIMESTAMP DEFAULT NOW(),
last_read_at TIMESTAMP,
@@ -39,8 +55,11 @@ CREATE TABLE IF NOT EXISTS conversation_participants (
UNIQUE(conversation_id, user_id)
);
+-- Create index on conversation column
CREATE INDEX idx_participants_conversation ON conversation_participants(conversation_id);
+-- Create index on user column
CREATE INDEX idx_participants_user ON conversation_participants(user_id);
+-- Create index on identity column
CREATE INDEX idx_participants_identity ON conversation_participants(identity_id);
-- ============================================================================
@@ -61,25 +80,35 @@ CREATE TABLE IF NOT EXISTS messages (
created_at TIMESTAMP DEFAULT NOW()
);
+-- Ensure reply_to_id column exists for index creation
+ALTER TABLE messages ADD COLUMN IF NOT EXISTS reply_to_id UUID;
+
+-- Create index on conversation and created_at columns
CREATE INDEX idx_messages_conversation ON messages(conversation_id, created_at DESC);
+-- Create index on sender column
CREATE INDEX idx_messages_sender ON messages(sender_id);
+-- Create index on reply_to column
CREATE INDEX idx_messages_reply_to ON messages(reply_to_id);
+-- Create index on created_at column
CREATE INDEX idx_messages_created ON messages(created_at DESC);
-- ============================================================================
-- MESSAGE REACTIONS
-- ============================================================================
+-- Update message_reactions to match actual types
CREATE TABLE IF NOT EXISTS message_reactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
emoji VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(message_id, user_id, emoji)
);
+-- Create index on message column
CREATE INDEX idx_reactions_message ON message_reactions(message_id);
+-- Create index on user column
CREATE INDEX idx_reactions_user ON message_reactions(user_id);
-- ============================================================================
@@ -101,19 +130,29 @@ CREATE TABLE IF NOT EXISTS files (
expires_at TIMESTAMP -- For temporary files
);
+-- Ensure uploader_id column exists for index creation
+ALTER TABLE files ADD COLUMN IF NOT EXISTS uploader_id VARCHAR;
+
+-- Ensure conversation_id column exists for index creation
+ALTER TABLE files ADD COLUMN IF NOT EXISTS conversation_id UUID;
+
+-- Create index on uploader column
CREATE INDEX idx_files_uploader ON files(uploader_id);
+-- Create index on conversation column
CREATE INDEX idx_files_conversation ON files(conversation_id);
+-- Create index on created_at column
CREATE INDEX idx_files_created ON files(created_at DESC);
-- ============================================================================
-- CALLS
-- ============================================================================
+-- Update calls to match actual types
CREATE TABLE IF NOT EXISTS calls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('voice', 'video')),
- initiator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ initiator_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(20) DEFAULT 'ringing' CHECK (status IN ('ringing', 'active', 'ended', 'missed', 'declined')),
started_at TIMESTAMP,
ended_at TIMESTAMP,
@@ -121,26 +160,33 @@ CREATE TABLE IF NOT EXISTS calls (
created_at TIMESTAMP DEFAULT NOW()
);
+-- Create index on conversation column
CREATE INDEX idx_calls_conversation ON calls(conversation_id);
+-- Create index on initiator column
CREATE INDEX idx_calls_initiator ON calls(initiator_id);
+-- Create index on status column
CREATE INDEX idx_calls_status ON calls(status);
+-- Create index on created_at column
CREATE INDEX idx_calls_created ON calls(created_at DESC);
-- ============================================================================
-- CALL PARTICIPANTS
-- ============================================================================
+-- Update call_participants to match actual types
CREATE TABLE IF NOT EXISTS call_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
call_id UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE,
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
joined_at TIMESTAMP,
left_at TIMESTAMP,
media_state JSONB DEFAULT '{"audio": true, "video": false, "screen_share": false}'::jsonb,
UNIQUE(call_id, user_id)
);
+-- Create index on call column
CREATE INDEX idx_call_participants_call ON call_participants(call_id);
+-- Create index on user column
CREATE INDEX idx_call_participants_user ON call_participants(user_id);
-- ============================================================================
diff --git a/supabase/migrations/20260110130000_gameforge_integration.sql b/supabase/migrations/20260110130000_gameforge_integration.sql
index c0e7d59..78f8bc3 100644
--- a/supabase/migrations/20260110130000_gameforge_integration.sql
+++ b/supabase/migrations/20260110130000_gameforge_integration.sql
@@ -36,6 +36,10 @@ CREATE TABLE IF NOT EXISTS audit_logs (
created_at TIMESTAMP DEFAULT NOW()
);
+-- Ensure resource_type column exists for index creation
+ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS resource_type VARCHAR;
+ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS resource_id VARCHAR;
+
-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_gameforge_integrations_project_id ON gameforge_integrations(project_id);
CREATE INDEX IF NOT EXISTS idx_conversations_gameforge_project ON conversations(gameforge_project_id) WHERE gameforge_project_id IS NOT NULL;
diff --git a/supabase/migrations/20260110140000_voice_video_calls.sql b/supabase/migrations/20260110140000_voice_video_calls.sql
index 87194e6..b12bf54 100644
--- a/supabase/migrations/20260110140000_voice_video_calls.sql
+++ b/supabase/migrations/20260110140000_voice_video_calls.sql
@@ -19,7 +19,7 @@ ADD COLUMN IF NOT EXISTS connection_quality VARCHAR(20) DEFAULT 'good'; -- excel
-- Create turn_credentials table for temporary TURN server credentials
CREATE TABLE IF NOT EXISTS turn_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
username VARCHAR(100) NOT NULL,
credential VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
diff --git a/supabase/migrations/20260110150000_nexus_cross_platform.sql b/supabase/migrations/20260110150000_nexus_cross_platform.sql
index e878a3f..3200911 100644
--- a/supabase/migrations/20260110150000_nexus_cross_platform.sql
+++ b/supabase/migrations/20260110150000_nexus_cross_platform.sql
@@ -1,6 +1,11 @@
-- Migration 005: Nexus Cross-Platform Integration
-- Adds friend system, game sessions, lobbies, and enhanced Nexus features
+-- Ensure nexus_integrations table exists before ALTER TABLE
+CREATE TABLE IF NOT EXISTS nexus_integrations (
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
+);
+
-- Extend nexus_integrations table with session and overlay config
ALTER TABLE nexus_integrations
ADD COLUMN IF NOT EXISTS current_game_session_id UUID,
@@ -12,8 +17,8 @@ ADD COLUMN IF NOT EXISTS overlay_position VARCHAR(20) DEFAULT 'top-right';
-- Friend requests table
CREATE TABLE IF NOT EXISTS friend_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- from_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- to_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ from_user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ to_user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(20) DEFAULT 'pending', -- pending, accepted, rejected
created_at TIMESTAMP DEFAULT NOW(),
responded_at TIMESTAMP,
@@ -26,8 +31,8 @@ CREATE INDEX IF NOT EXISTS idx_friend_requests_from ON friend_requests(from_user
-- Friendships table
CREATE TABLE IF NOT EXISTS friendships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user1_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
- user2_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ user1_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ user2_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(),
CHECK (user1_id < user2_id), -- Prevent duplicates
UNIQUE(user1_id, user2_id)
@@ -50,6 +55,15 @@ CREATE TABLE IF NOT EXISTS game_sessions (
metadata JSONB -- {mapName, gameMode, score, etc.}
);
+-- Ensure started_at column exists for index creation
+ALTER TABLE game_sessions ADD COLUMN IF NOT EXISTS started_at TIMESTAMP;
+
+-- Ensure session_state column exists for index creation
+ALTER TABLE game_sessions ADD COLUMN IF NOT EXISTS session_state VARCHAR;
+
+-- Ensure nexus_player_id column exists for index creation
+ALTER TABLE game_sessions ADD COLUMN IF NOT EXISTS nexus_player_id VARCHAR;
+
CREATE INDEX IF NOT EXISTS idx_game_sessions_user ON game_sessions(user_id, started_at DESC);
CREATE INDEX IF NOT EXISTS idx_game_sessions_active ON game_sessions(user_id, session_state);
CREATE INDEX IF NOT EXISTS idx_game_sessions_nexus_player ON game_sessions(nexus_player_id);
@@ -59,7 +73,7 @@ CREATE TABLE IF NOT EXISTS game_lobbies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
game_id VARCHAR(100) NOT NULL,
lobby_code VARCHAR(50) UNIQUE,
- host_user_id UUID NOT NULL REFERENCES users(id),
+ host_user_id VARCHAR NOT NULL REFERENCES users(id),
conversation_id UUID REFERENCES conversations(id), -- Auto-created chat
max_players INTEGER DEFAULT 8,
is_public BOOLEAN DEFAULT false,
@@ -77,7 +91,7 @@ CREATE INDEX IF NOT EXISTS idx_game_lobbies_code ON game_lobbies(lobby_code);
CREATE TABLE IF NOT EXISTS game_lobby_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lobby_id UUID NOT NULL REFERENCES game_lobbies(id) ON DELETE CASCADE,
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
team_id VARCHAR(20), -- For team-based games
ready BOOLEAN DEFAULT false,
joined_at TIMESTAMP DEFAULT NOW(),
diff --git a/supabase/migrations/20260110160000_premium_monetization.sql b/supabase/migrations/20260110160000_premium_monetization.sql
index 81e6160..5830821 100644
--- a/supabase/migrations/20260110160000_premium_monetization.sql
+++ b/supabase/migrations/20260110160000_premium_monetization.sql
@@ -8,7 +8,7 @@ ADD COLUMN IF NOT EXISTS premium_tier VARCHAR(20) DEFAULT 'free'; -- free, premi
-- Premium subscriptions table
CREATE TABLE IF NOT EXISTS premium_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
tier VARCHAR(20) NOT NULL, -- free, premium, enterprise
status VARCHAR(20) DEFAULT 'active', -- active, cancelled, expired, suspended
stripe_subscription_id VARCHAR(100),
@@ -29,7 +29,7 @@ CREATE INDEX IF NOT EXISTS idx_premium_subscriptions_status ON premium_subscript
CREATE TABLE IF NOT EXISTS blockchain_domains (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain VARCHAR(100) NOT NULL UNIQUE, -- e.g., "anderson.aethex"
- owner_user_id UUID NOT NULL REFERENCES users(id),
+ owner_user_id VARCHAR NOT NULL REFERENCES users(id),
nft_token_id VARCHAR(100), -- Token ID from Freename contract
wallet_address VARCHAR(100), -- Owner's wallet address
verified BOOLEAN DEFAULT false,
@@ -51,8 +51,8 @@ CREATE INDEX IF NOT EXISTS idx_blockchain_domains_domain ON blockchain_domains(d
CREATE TABLE IF NOT EXISTS domain_transfers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain_id UUID NOT NULL REFERENCES blockchain_domains(id),
- from_user_id UUID REFERENCES users(id),
- to_user_id UUID REFERENCES users(id),
+ from_user_id VARCHAR REFERENCES users(id),
+ to_user_id VARCHAR REFERENCES users(id),
transfer_type VARCHAR(20), -- sale, gift, transfer
price_usd DECIMAL(10, 2),
transaction_hash VARCHAR(100), -- Blockchain tx hash
@@ -68,7 +68,7 @@ CREATE INDEX IF NOT EXISTS idx_domain_transfers_status ON domain_transfers(statu
CREATE TABLE IF NOT EXISTS enterprise_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_name VARCHAR(200) NOT NULL,
- owner_user_id UUID NOT NULL REFERENCES users(id),
+ owner_user_id VARCHAR NOT NULL REFERENCES users(id),
custom_domain VARCHAR(200), -- e.g., chat.yourgame.com
custom_domain_verified BOOLEAN DEFAULT false,
dns_txt_record VARCHAR(100), -- For domain verification
@@ -90,7 +90,7 @@ CREATE INDEX IF NOT EXISTS idx_enterprise_accounts_subscription ON enterprise_ac
CREATE TABLE IF NOT EXISTS enterprise_team_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
enterprise_id UUID NOT NULL REFERENCES enterprise_accounts(id) ON DELETE CASCADE,
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role VARCHAR(20) DEFAULT 'member', -- admin, member
joined_at TIMESTAMP DEFAULT NOW(),
UNIQUE(enterprise_id, user_id)
@@ -102,7 +102,7 @@ CREATE INDEX IF NOT EXISTS idx_enterprise_team_members_user ON enterprise_team_m
-- Usage analytics table
CREATE TABLE IF NOT EXISTS usage_analytics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
date DATE NOT NULL,
messages_sent INTEGER DEFAULT 0,
messages_received INTEGER DEFAULT 0,
@@ -145,7 +145,7 @@ ON CONFLICT (tier) DO NOTHING;
-- Payment transactions table (for audit trail)
CREATE TABLE IF NOT EXISTS payment_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id),
+ user_id VARCHAR NOT NULL REFERENCES users(id),
transaction_type VARCHAR(50), -- subscription, domain_purchase, domain_sale, etc.
amount_usd DECIMAL(10, 2) NOT NULL,
currency VARCHAR(3) DEFAULT 'usd',
diff --git a/supabase/migrations/20260119100000_fix_conversations_type.sql b/supabase/migrations/20260119100000_fix_conversations_type.sql
new file mode 100644
index 0000000..37bba1e
--- /dev/null
+++ b/supabase/migrations/20260119100000_fix_conversations_type.sql
@@ -0,0 +1,4 @@
+-- Fix for missing 'type' column in conversations table
+ALTER TABLE conversations ADD COLUMN IF NOT EXISTS type VARCHAR(20);
+-- Now create the index
+CREATE INDEX IF NOT EXISTS idx_conversations_type ON conversations(type);