From de54903c15deb3ac38b2cbb3f56f3b841c6da601 Mon Sep 17 00:00:00 2001 From: MrPiglr Date: Tue, 3 Feb 2026 09:05:15 +0000 Subject: [PATCH] new file: astro-site/src/components/auth/SupabaseLogin.jsx new file: astro-site/src/components/auth/SupabaseLogin.jsx --- PHASE4-CALLS.md | 12 +- PHASE4-QUICK-START.md | 2 +- SAVE-POINT-2026-01-12.md | 10 +- astro-site/package.json | 2 +- astro-site/src/components/ReactAppIsland.jsx | 7 + .../src/components/auth/SupabaseLogin.jsx | 70 + astro-site/src/pages/_mockup.jsx | 7 +- astro-site/src/pages/app.astro | 6 + astro-site/src/pages/index.astro | 2 + astro-site/src/pages/login.astro | 4 +- astro-site/src/pages/mockup.astro | 14 + astro-site/src/pages/mockup.jsx | 7 +- astro-site/src/react-app/App.css | 189 ++ astro-site/src/react-app/App.jsx | 84 + astro-site/src/react-app/Demo.css | 579 +++++ astro-site/src/react-app/Demo.jsx | 311 +++ .../src/react-app/components/Call/Call.css | 345 +++ .../src/react-app/components/Call/index.jsx | 556 +++++ .../src/react-app/components/Chat/Chat.css | 156 ++ .../src/react-app/components/Chat/Chat.jsx | 425 ++++ .../components/Chat/ConversationList.css | 201 ++ .../components/Chat/ConversationList.jsx | 110 + .../components/Chat/MessageInput.css | 117 + .../components/Chat/MessageInput.jsx | 134 + .../react-app/components/Chat/MessageList.css | 310 +++ .../react-app/components/Chat/MessageList.jsx | 139 ++ .../components/DomainVerification.css | 316 +++ .../components/DomainVerification.jsx | 313 +++ .../components/GameForgeChat/ChannelList.css | 84 + .../components/GameForgeChat/ChannelList.jsx | 62 + .../components/GameForgeChat/ChannelView.css | 46 + .../components/GameForgeChat/ChannelView.jsx | 172 ++ .../GameForgeChat/GameForgeChat.css | 99 + .../components/GameForgeChat/index.jsx | 139 ++ .../react-app/components/Overlay/Overlay.css | 257 ++ .../react-app/components/Overlay/index.jsx | 270 ++ .../components/Premium/UpgradeFlow.css | 217 ++ .../react-app/components/Premium/index.jsx | 307 +++ .../components/VerifiedDomainBadge.css | 85 + .../components/VerifiedDomainBadge.jsx | 41 + .../src/react-app/contexts/AuthContext.jsx | 51 + .../src/react-app/contexts/SocketContext.jsx | 74 + astro-site/src/react-app/index.css | 43 + astro-site/src/react-app/index.html | 13 + astro-site/src/react-app/main.jsx | 11 + .../src/react-app/mockup/ChannelSidebar.jsx | 65 + astro-site/src/react-app/mockup/ChatArea.jsx | 41 + .../src/react-app/mockup/MainLayout.jsx | 17 + .../src/react-app/mockup/MemberSidebar.jsx | 43 + astro-site/src/react-app/mockup/Message.jsx | 27 + .../src/react-app/mockup/MessageInput.jsx | 16 + .../src/react-app/mockup/ServerList.jsx | 30 + astro-site/src/react-app/mockup/global.css | 57 + astro-site/src/react-app/mockup/index.html | 13 + astro-site/src/react-app/mockup/index.jsx | 13 + astro-site/src/react-app/package-lock.json | 2175 +++++++++++++++++ astro-site/src/react-app/package.json | 26 + astro-site/src/react-app/utils/crypto.js | 316 +++ astro-site/src/react-app/utils/socket.js | 39 + astro-site/src/react-app/utils/webrtc.js | 532 ++++ astro-site/src/react-app/vite.config.js | 16 + src/backend/server.js | 2 +- .../signaling-server/signaling-server.js | 2 +- src/frontend/App.jsx | 35 +- .../components/GameForgeChat/ChannelView.jsx | 7 +- src/frontend/contexts/AuthContext.jsx | 60 +- src/frontend/main.jsx | 4 +- src/frontend/vite.config.js | 2 +- supabase/config.toml | 2 +- .../20260110120000_messaging_system.sql | 60 +- .../20260110130000_gameforge_integration.sql | 4 + .../20260110140000_voice_video_calls.sql | 2 +- .../20260110150000_nexus_cross_platform.sql | 26 +- .../20260110160000_premium_monetization.sql | 16 +- .../20260119100000_fix_conversations_type.sql | 4 + 75 files changed, 9940 insertions(+), 111 deletions(-) create mode 100644 astro-site/src/components/ReactAppIsland.jsx create mode 100644 astro-site/src/components/auth/SupabaseLogin.jsx create mode 100644 astro-site/src/pages/app.astro create mode 100644 astro-site/src/pages/mockup.astro create mode 100644 astro-site/src/react-app/App.css create mode 100644 astro-site/src/react-app/App.jsx create mode 100644 astro-site/src/react-app/Demo.css create mode 100644 astro-site/src/react-app/Demo.jsx create mode 100644 astro-site/src/react-app/components/Call/Call.css create mode 100644 astro-site/src/react-app/components/Call/index.jsx create mode 100644 astro-site/src/react-app/components/Chat/Chat.css create mode 100644 astro-site/src/react-app/components/Chat/Chat.jsx create mode 100644 astro-site/src/react-app/components/Chat/ConversationList.css create mode 100644 astro-site/src/react-app/components/Chat/ConversationList.jsx create mode 100644 astro-site/src/react-app/components/Chat/MessageInput.css create mode 100644 astro-site/src/react-app/components/Chat/MessageInput.jsx create mode 100644 astro-site/src/react-app/components/Chat/MessageList.css create mode 100644 astro-site/src/react-app/components/Chat/MessageList.jsx create mode 100644 astro-site/src/react-app/components/DomainVerification.css create mode 100644 astro-site/src/react-app/components/DomainVerification.jsx create mode 100644 astro-site/src/react-app/components/GameForgeChat/ChannelList.css create mode 100644 astro-site/src/react-app/components/GameForgeChat/ChannelList.jsx create mode 100644 astro-site/src/react-app/components/GameForgeChat/ChannelView.css create mode 100644 astro-site/src/react-app/components/GameForgeChat/ChannelView.jsx create mode 100644 astro-site/src/react-app/components/GameForgeChat/GameForgeChat.css create mode 100644 astro-site/src/react-app/components/GameForgeChat/index.jsx create mode 100644 astro-site/src/react-app/components/Overlay/Overlay.css create mode 100644 astro-site/src/react-app/components/Overlay/index.jsx create mode 100644 astro-site/src/react-app/components/Premium/UpgradeFlow.css create mode 100644 astro-site/src/react-app/components/Premium/index.jsx create mode 100644 astro-site/src/react-app/components/VerifiedDomainBadge.css create mode 100644 astro-site/src/react-app/components/VerifiedDomainBadge.jsx create mode 100644 astro-site/src/react-app/contexts/AuthContext.jsx create mode 100644 astro-site/src/react-app/contexts/SocketContext.jsx create mode 100644 astro-site/src/react-app/index.css create mode 100644 astro-site/src/react-app/index.html create mode 100644 astro-site/src/react-app/main.jsx create mode 100644 astro-site/src/react-app/mockup/ChannelSidebar.jsx create mode 100644 astro-site/src/react-app/mockup/ChatArea.jsx create mode 100644 astro-site/src/react-app/mockup/MainLayout.jsx create mode 100644 astro-site/src/react-app/mockup/MemberSidebar.jsx create mode 100644 astro-site/src/react-app/mockup/Message.jsx create mode 100644 astro-site/src/react-app/mockup/MessageInput.jsx create mode 100644 astro-site/src/react-app/mockup/ServerList.jsx create mode 100644 astro-site/src/react-app/mockup/global.css create mode 100644 astro-site/src/react-app/mockup/index.html create mode 100644 astro-site/src/react-app/mockup/index.jsx create mode 100644 astro-site/src/react-app/package-lock.json create mode 100644 astro-site/src/react-app/package.json create mode 100644 astro-site/src/react-app/utils/crypto.js create mode 100644 astro-site/src/react-app/utils/socket.js create mode 100644 astro-site/src/react-app/utils/webrtc.js create mode 100644 astro-site/src/react-app/vite.config.js create mode 100644 supabase/migrations/20260119100000_fix_conversations_type.sql 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 Logo +

AeThex Connect

+

Sign in to your account

+ {error &&
{error}
} + {success &&
Login successful!
} +
+ setEmail(e.target.value)} + autoFocus + /> + setPassword(e.target.value)} + /> + +
+
+ By continuing, you agree to the Terms of Service and Privacy Policy. +
+
+ +
+ ); +} 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 && ( + + )} +
+ +
+ + + {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
  • +
+
+
+ +
+

© 2026 AeThex Corporation. All rights reserved.

+
+
+ ); +} + +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 && ( + + )} +
+
+
+ + + +
+ {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)

+
+ +
+
+
6
+
Phases Complete
+
+
+
1
+
Phase In Progress
+
+
+
3
+
Platforms
+
+
+
95%
+
Code Sharing
+
+
+
+ )} + + {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 ( +
+ + +
+ ); + } + + if (callStatus === 'connected' || callStatus === 'ringing') { + return ( +
+ + + + + + + +
+ ); + } + + 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} + +
+ )} + +
+
+ {callStatus === 'ringing' && 'Calling...'} + {callStatus === 'connected' && `Call Duration: ${formatDuration(callDuration)}`} + {callStatus === 'ended' && 'Call Ended'} +
+ {renderQualityIndicator()} +
+ +
+ {/* Remote videos */} +
+ {remoteParticipants.map(participant => ( +
+
+ ))} +
+ + {/* Local video */} + {(callStatus === 'ringing' || callStatus === 'connected') && ( +
+
+ )} +
+ + {renderControls()} + + {callStatus === 'idle' && ( +
+ + +
+ )} +
+ ); +}; + +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 ( +
+

⚠️ {error}

+ +
+ ); + } + + 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) ? ( + Avatar + ) : ( +
+ {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 ( +
+ + + + +