new file: astro-site/src/components/auth/SupabaseLogin.jsx

new file:   astro-site/src/components/auth/SupabaseLogin.jsx
This commit is contained in:
Anderson 2026-02-03 09:05:15 +00:00 committed by GitHub
parent 48f095c8ad
commit de54903c15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 9940 additions and 111 deletions

View file

@ -202,7 +202,7 @@ Get temporary TURN server credentials.
```json ```json
{ {
"credentials": { "credentials": {
"urls": ["turn:turn.example.com:3478"], "urls": ["turn:turn.example.com:3000"],
"username": "1736517600:username", "username": "1736517600:username",
"credential": "hmac-sha1-hash" "credential": "hmac-sha1-hash"
}, },
@ -427,7 +427,7 @@ Edit `/etc/turnserver.conf`:
```conf ```conf
# Listening port # Listening port
listening-port=3478 listening-port=3000
tls-listening-port=5349 tls-listening-port=5349
# External IP (replace with your server IP) # External IP (replace with your server IP)
@ -467,7 +467,7 @@ Add to `.env`:
```env ```env
# TURN Server Configuration # TURN Server Configuration
TURN_SERVER_HOST=turn.yourdomain.com TURN_SERVER_HOST=turn.yourdomain.com
TURN_SERVER_PORT=3478 TURN_SERVER_PORT=3000
TURN_SECRET=your-turn-secret-key TURN_SECRET=your-turn-secret-key
TURN_TTL=86400 TURN_TTL=86400
``` ```
@ -476,8 +476,8 @@ TURN_TTL=86400
```bash ```bash
# Allow TURN ports # Allow TURN ports
sudo ufw allow 3478/tcp sudo ufw allow 3000/tcp
sudo ufw allow 3478/udp sudo ufw allow 3000/udp
sudo ufw allow 5349/tcp sudo ufw allow 5349/tcp
sudo ufw allow 5349/udp 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: 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 2. Generate TURN credentials using the HMAC method
3. Click "Gather candidates" 3. Click "Gather candidates"
4. Verify `relay` candidates appear 4. Verify `relay` candidates appear

View file

@ -97,7 +97,7 @@ Add to `.env`:
```env ```env
# TURN Server Configuration # TURN Server Configuration
TURN_SERVER_HOST=turn.example.com TURN_SERVER_HOST=turn.example.com
TURN_SERVER_PORT=3478 TURN_SERVER_PORT=3000
TURN_SECRET=your-turn-secret-key TURN_SECRET=your-turn-secret-key
TURN_TTL=86400 TURN_TTL=86400
``` ```

View file

@ -106,12 +106,12 @@ packages/ui/styles/tokens.ts # Complete redesign with dark theme
http://localhost:3000 http://localhost:3000
http://localhost:3000/health http://localhost:3000/health
# Port 4321 - Astro Landing Site # Port 3000 - Astro Landing Site
http://localhost:4321 http://localhost:3000
cd astro-site && npm run dev cd astro-site && npm run dev
# Port 5173 - React Frontend (Vite) # Port 3000 - React Frontend (Vite)
http://localhost:5173 http://localhost:3000
cd src/frontend && npm run dev cd src/frontend && npm run dev
``` ```
@ -327,7 +327,7 @@ cd src/frontend && npm run dev # React (Terminal 3)
### 2. Check Status ### 2. Check Status
- Visit http://localhost:5173 (React app) - Visit http://localhost:5173 (React app)
- Visit http://localhost:4321 (Astro landing) - Visit http://localhost:3000 (Astro landing)
- Check git status: `git status` - Check git status: `git status`
### 3. Continue Development ### 3. Continue Development

View file

@ -3,7 +3,7 @@
"version": "1.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "astro dev --port 4321", "dev": "astro dev --port 3000",
"build": "astro build", "build": "astro build",
"preview": "astro preview" "preview": "astro preview"
}, },

View file

@ -0,0 +1,7 @@
import React from "react";
import Demo from "../react-app/Demo";
export default function ReactAppIsland() {
return <Demo />;
}

View file

@ -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 (
<div className="login-bg min-h-screen flex flex-col items-center justify-center" style={{background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)"}}>
<div className="account-linker bg-[#181818cc] p-8 rounded-2xl max-w-md w-full mx-auto shadow-2xl border border-[#23234a] flex flex-col items-center animate-fade-in">
<img src="/favicon.svg" alt="AeThex Logo" className="w-16 h-16 mb-4 drop-shadow-lg" />
<h1 className="text-3xl font-extrabold mb-2 text-white tracking-tight text-center">AeThex Connect</h1>
<h2 className="text-lg font-semibold mb-6 text-blue-300 text-center">Sign in to your account</h2>
{error && <div className="mb-2 text-red-400 text-center w-full">{error}</div>}
{success && <div className="mb-2 text-green-400 text-center w-full">Login successful!</div>}
<form onSubmit={handleLogin} className="flex flex-col gap-4 w-full">
<input
type="email"
placeholder="Email"
className="px-4 py-3 rounded-lg bg-[#23234a] text-white border border-[#2d2d5a] focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
value={email}
onChange={e => setEmail(e.target.value)}
autoFocus
/>
<input
type="password"
placeholder="Password"
className="px-4 py-3 rounded-lg bg-[#23234a] text-white border border-[#2d2d5a] focus:outline-none focus:ring-2 focus:ring-blue-500 transition"
value={password}
onChange={e => setPassword(e.target.value)}
/>
<button type="submit" className="bg-gradient-to-r from-blue-600 to-purple-600 text-white rounded-lg py-3 font-bold mt-2 shadow-lg hover:from-blue-700 hover:to-purple-700 transition" disabled={loading}>
{loading ? "Signing in..." : "Sign In"}
</button>
</form>
<div className="mt-6 text-center text-gray-400 text-xs w-full">
<span>By continuing, you agree to the <a href="/terms" className="underline hover:text-blue-300">Terms of Service</a> and <a href="/privacy" className="underline hover:text-blue-300">Privacy Policy</a>.</span>
</div>
</div>
<style>{`
.animate-fade-in { animation: fadeIn 0.8s cubic-bezier(.4,0,.2,1) both; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: none; } }
`}</style>
</div>
);
}

View file

@ -1,7 +1,2 @@
import React from "react";
import MainLayout from "../components/mockup/MainLayout";
import "../components/mockup/global.css";
export default function MockupPage() { // Removed: This page is deprecated. Use /app for the full platform UI.
return <MainLayout />;
}

View file

@ -0,0 +1,6 @@
---
import ReactAppIsland from '../components/ReactAppIsland.jsx';
---
<ReactAppIsland client:load />

View file

@ -9,6 +9,8 @@ import Layout from '../layouts/Layout.astro';
<h1 class="hero-title">AeThex Connect</h1> <h1 class="hero-title">AeThex Connect</h1>
<p class="hero-subtitle">Next-generation voice & chat for gamers.<br />Own your identity. Connect everywhere.</p> <p class="hero-subtitle">Next-generation voice & chat for gamers.<br />Own your identity. Connect everywhere.</p>
<a href="/login" class="hero-btn">Get Started</a> <a href="/login" class="hero-btn">Get Started</a>
<a href="/app" class="hero-btn" style="margin-left: 1em; background: #00d9ff; color: #000;">Open AeThex Connect Platform</a>
<a href="/mockup" class="hero-btn" style="margin-left: 1em; background: #222; color: #00d9ff;">Legacy Mockup</a>
</div> </div>
</section> </section>
<section class="landing-features"> <section class="landing-features">

View file

@ -1,9 +1,9 @@
--- ---
import Layout from '../layouts/Layout.astro'; import Layout from '../layouts/Layout.astro';
import LoginIsland from '../components/auth/LoginIsland.jsx'; import SupabaseLogin from '../components/auth/SupabaseLogin.jsx';
--- ---
<Layout title="AeThex Connect Login"> <Layout title="AeThex Connect Login">
<LoginIsland client:load /> <SupabaseLogin client:load />
</Layout> </Layout>

View file

@ -0,0 +1,14 @@
<!-- AeThex Connect Mockup merged as Astro page -->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AeThex Connect - Metaverse Communication</title>
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<style>
/* All CSS from mockup HTML here */
</style>
</head>
<div class="connect-container">
<!-- ...existing mockup HTML content... -->
</div>

View file

@ -1,7 +1,2 @@
import React from "react";
import MainLayout from "../components/mockup/MainLayout";
import "../components/mockup/global.css";
export default function MockupPage() { // Removed: This page is deprecated. Use /app for the full platform UI.
return <MainLayout />;
}

View file

@ -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;
}
}

View file

@ -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 <div className="loading-screen">Loading...</div>;
}
return (
<div className="app">
<header className="app-header">
<h1>AeThex Passport</h1>
<p>Domain Verification</p>
</header>
<main className="app-main">
{user && (
<div className="user-profile">
<div className="profile-header">
<h2>{user.email}</h2>
{user.verifiedDomain && (
<VerifiedDomainBadge
verifiedDomain={user.verifiedDomain}
verifiedAt={user.domainVerifiedAt}
/>
)}
</div>
<div className="profile-section">
<button
onClick={() => setShowVerification(!showVerification)}
className="toggle-button"
>
{showVerification ? 'Hide' : 'Show'} Domain Verification
</button>
{showVerification && (
<div className="verification-container">
<DomainVerification />
</div>
)}
</div>
</div>
)}
<div className="info-section">
<h3>About Domain Verification</h3>
<p>
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.
</p>
<ul>
<li> Verify traditional domains via DNS TXT records</li>
<li> Verify .aethex domains via blockchain</li>
<li> Display verified domain on your profile</li>
<li> Prevent domain impersonation</li>
</ul>
</div>
</main>
<footer className="app-footer">
<p>&copy; 2026 AeThex Corporation. All rights reserved.</p>
</footer>
</div>
);
}
export default function AppWrapper() {
return (
<AuthProvider>
<App />
</AuthProvider>
);
}

View file

@ -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;
}
}

View file

@ -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 (
<div className="loading-screen">
<div className="loading-spinner">{String.fromCodePoint(0x1F680)}</div>
<p>Loading AeThex Connect...</p>
</div>
);
}
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 (
<SocketProvider>
<div className="demo-app">
<header className="demo-header">
<div className="header-content">
<div className="logo-section">
<h1>🚀 AeThex Connect</h1>
<p className="tagline">Next-Gen Communication for Gamers</p>
</div>
<div className="user-section">
<div className="user-info">
<span className="user-name">{user.name}</span>
<span className="user-email">{user.email}</span>
</div>
{user.verifiedDomain && (
<VerifiedDomainBadge
verifiedDomain={user.verifiedDomain}
verifiedAt={user.domainVerifiedAt}
/>
)}
</div>
</div>
</header>
<nav className="demo-nav">
{tabs.map(tab => (
<button
key={tab.id}
className={`nav-tab ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
<span className="tab-label">{tab.label}</span>
{tab.phase && <span className="tab-phase">{tab.phase}</span>}
</button>
))}
</nav>
<main className="demo-main">
{activeTab === 'overview' && (
<div className="overview-section">
<h2>Welcome to AeThex Connect</h2>
<p className="intro">
A comprehensive communication platform built specifically for gamers and game developers.
Explore each feature using the tabs above.
</p>
<div className="feature-grid">
<div className="feature-card">
<div className="feature-icon">🌐</div>
<h3>Domain Verification</h3>
<span className="badge phase-1">Phase 1</span>
<p>Verify ownership of traditional domains (DNS) or blockchain .aethex domains</p>
<ul>
<li>DNS TXT record verification</li>
<li>Blockchain domain integration</li>
<li>Verified profile badges</li>
</ul>
</div>
<div className="feature-card">
<div className="feature-icon">💬</div>
<h3>Real-time Messaging</h3>
<span className="badge phase-2">Phase 2</span>
<p>Instant, encrypted messaging with WebSocket connections</p>
<ul>
<li>Private conversations</li>
<li>Message history</li>
<li>Read receipts</li>
<li>Typing indicators</li>
</ul>
</div>
<div className="feature-card">
<div className="feature-icon">🎮</div>
<h3>GameForge Integration</h3>
<span className="badge phase-3">Phase 3</span>
<p>Built-in chat for game development teams</p>
<ul>
<li>Project channels</li>
<li>Team collaboration</li>
<li>Build notifications</li>
<li>Asset sharing</li>
</ul>
</div>
<div className="feature-card">
<div className="feature-icon">📞</div>
<h3>Voice & Video Calls</h3>
<span className="badge phase-4">Phase 4</span>
<p>High-quality WebRTC calls with screen sharing</p>
<ul>
<li>1-on-1 voice calls</li>
<li>Video conferencing</li>
<li>Screen sharing</li>
<li>Call recording</li>
</ul>
</div>
<div className="feature-card">
<div className="feature-icon">🔗</div>
<h3>Nexus Engine</h3>
<span className="badge phase-5">Phase 5</span>
<p>Cross-game identity and social features</p>
<ul>
<li>Unified player profiles</li>
<li>Friend system</li>
<li>Game lobbies</li>
<li>Rich presence</li>
</ul>
</div>
<div className="feature-card">
<div className="feature-icon"></div>
<h3>Premium Subscriptions</h3>
<span className="badge phase-6">Phase 6</span>
<p>Monetization with blockchain domains</p>
<ul>
<li>.aethex domain marketplace</li>
<li>Premium tiers ($10/mo)</li>
<li>Enterprise plans</li>
<li>Stripe integration</li>
</ul>
</div>
</div>
<div className="status-section">
<h3>🚀 Phase 7: Full Platform (In Progress)</h3>
<p>Transform AeThex Connect into cross-platform apps:</p>
<div className="platform-badges">
<span className="platform-badge">🌐 Progressive Web App</span>
<span className="platform-badge">📱 iOS & Android</span>
<span className="platform-badge">💻 Windows, macOS, Linux</span>
</div>
<p className="timeline">Expected completion: May 2026 (5 months)</p>
</div>
<div className="quick-stats">
<div className="stat">
<div className="stat-value">6</div>
<div className="stat-label">Phases Complete</div>
</div>
<div className="stat">
<div className="stat-value">1</div>
<div className="stat-label">Phase In Progress</div>
</div>
<div className="stat">
<div className="stat-value">3</div>
<div className="stat-label">Platforms</div>
</div>
<div className="stat">
<div className="stat-value">95%</div>
<div className="stat-label">Code Sharing</div>
</div>
</div>
</div>
)}
{activeTab === 'domain' && (
<div className="feature-section">
<div className="section-header">
<h2>🌐 Domain Verification</h2>
<span className="badge phase-1">Phase 1</span>
</div>
<p className="section-description">
Prove ownership of your domain to display it on your profile and prevent impersonation.
Supports both traditional domains (via DNS) and blockchain .aethex domains.
</p>
<DomainVerification />
</div>
)}
{activeTab === 'messaging' && (
<div className="feature-section">
<div className="section-header">
<h2>💬 Real-time Messaging</h2>
<span className="badge phase-2">Phase 2</span>
</div>
<p className="section-description">
Private encrypted conversations with real-time delivery. Messages sync across all devices.
</p>
<Chat />
</div>
)}
{activeTab === 'gameforge' && (
<div className="feature-section">
<div className="section-header">
<h2>🎮 GameForge Integration</h2>
<span className="badge phase-3">Phase 3</span>
</div>
<p className="section-description">
Collaborate with your game development team. Channels auto-provision with your GameForge projects.
</p>
<GameForgeChat />
</div>
)}
{activeTab === 'calls' && (
<div className="feature-section">
<div className="section-header">
<h2>📞 Voice & Video Calls</h2>
<span className="badge phase-4">Phase 4</span>
</div>
<p className="section-description">
Crystal-clear WebRTC calls with screen sharing. Perfect for co-op gaming or team standups.
</p>
<Call />
</div>
)}
{activeTab === 'premium' && (
<div className="feature-section">
<div className="section-header">
<h2> Premium Subscriptions</h2>
<span className="badge phase-6">Phase 6</span>
</div>
<p className="section-description">
Upgrade to unlock blockchain .aethex domains, increased storage, and advanced features.
</p>
<UpgradeFlow />
</div>
)}
</main>
<footer className="demo-footer">
<div className="footer-content">
<div className="footer-section">
<h4>AeThex Connect</h4>
<p>Next-generation communication platform</p>
</div>
<div className="footer-section">
<h4>Technology</h4>
<ul>
<li>React 18 + Vite</li>
<li>WebSocket (Socket.io)</li>
<li>WebRTC</li>
<li>Stripe</li>
</ul>
</div>
<div className="footer-section">
<h4>Phases</h4>
<ul>
<li> Phase 1-6 Complete</li>
<li>🔄 Phase 7 In Progress</li>
</ul>
</div>
<div className="footer-section">
<h4>Links</h4>
<ul>
<li><a href="#" onClick={(e) => { e.preventDefault(); setActiveTab('overview'); }}>Overview</a></li>
<li><a href="http://localhost:3000/health" target="_blank" rel="noopener noreferrer">API Health</a></li>
<li><a href="https://github.com/AeThex-Corporation/AeThex-Connect" target="_blank" rel="noopener noreferrer">GitHub</a></li>
</ul>
</div>
</div>
<div className="footer-bottom">
<p>&copy; 2026 AeThex Corporation. All rights reserved.</p>
</div>
</footer>
</div>
</SocketProvider>
);
}
function Demo() {
return (
<AuthProvider>
<DemoContent />
</AuthProvider>
);
}
export default Demo;

View file

@ -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;
}

View file

@ -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 (
<div className="call-controls">
<button className="control-btn accept-btn" onClick={answerCall}>
<span className="icon">📞</span>
Answer
</button>
<button className="control-btn reject-btn" onClick={rejectCall}>
<span className="icon">📵</span>
Reject
</button>
</div>
);
}
if (callStatus === 'connected' || callStatus === 'ringing') {
return (
<div className="call-controls">
<button
className={`control-btn ${isAudioEnabled ? 'active' : 'inactive'}`}
onClick={toggleAudio}
>
<span className="icon">{isAudioEnabled ? '🎤' : '🔇'}</span>
</button>
<button
className={`control-btn ${isVideoEnabled ? 'active' : 'inactive'}`}
onClick={toggleVideo}
>
<span className="icon">{isVideoEnabled ? '📹' : '🚫'}</span>
</button>
<button
className={`control-btn ${isScreenSharing ? 'active' : ''}`}
onClick={toggleScreenShare}
>
<span className="icon">🖥</span>
</button>
<button className="control-btn end-btn" onClick={endCall}>
<span className="icon">📵</span>
End
</button>
</div>
);
}
return null;
};
/**
* Render connection quality indicator
*/
const renderQualityIndicator = () => {
if (callStatus !== 'connected') return null;
const colors = {
good: '#4CAF50',
fair: '#FFC107',
poor: '#F44336'
};
return (
<div className="quality-indicator" style={{ backgroundColor: colors[connectionQuality] }}>
{connectionQuality}
</div>
);
};
return (
<div className="call-container">
{error && (
<div className="call-error">
{error}
<button onClick={() => setError(null)}>×</button>
</div>
)}
<div className="call-header">
<div className="call-status">
{callStatus === 'ringing' && 'Calling...'}
{callStatus === 'connected' && `Call Duration: ${formatDuration(callDuration)}`}
{callStatus === 'ended' && 'Call Ended'}
</div>
{renderQualityIndicator()}
</div>
<div className="video-container">
{/* Remote videos */}
<div className="remote-videos">
{remoteParticipants.map(participant => (
<div key={participant.userId} className="remote-video-wrapper">
<video
ref={el => {
if (el) remoteVideosRef.current.set(participant.userId, el);
}}
autoPlay
playsInline
className="remote-video"
/>
<div className="participant-name">{participant.userName || participant.userIdentifier}</div>
</div>
))}
</div>
{/* Local video */}
{(callStatus === 'ringing' || callStatus === 'connected') && (
<div className="local-video-wrapper">
<video
ref={localVideoRef}
autoPlay
playsInline
muted
className="local-video"
/>
<div className="local-label">You</div>
</div>
)}
</div>
{renderControls()}
{callStatus === 'idle' && (
<div className="call-actions">
<button className="start-call-btn audio" onClick={() => initiateCall('audio')}>
🎤 Start Audio Call
</button>
<button className="start-call-btn video" onClick={() => initiateCall('video')}>
📹 Start Video Call
</button>
</div>
)}
</div>
);
};
export default Call;

View file

@ -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%;
}
}

View file

@ -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 (
<div className="chat-loading">
<div className="spinner"></div>
<p>Loading conversations...</p>
</div>
);
}
if (error && conversations.length === 0) {
return (
<div className="chat-error">
<p> {error}</p>
<button onClick={loadConversations}>Retry</button>
</div>
);
}
return (
<div className="chat-container">
<div className="chat-status">
{connected ? (
<span className="status-indicator online"> Connected</span>
) : (
<span className="status-indicator offline"> Disconnected</span>
)}
</div>
<ConversationList
conversations={conversations}
activeConversation={activeConversation}
onSelectConversation={selectConversation}
/>
<div className="chat-main">
{activeConversation ? (
<>
<div className="chat-header">
<div className="conversation-info">
<div className="conversation-avatar">
{activeConversation.title?.[0] || '?'}
</div>
<div>
<h3>{activeConversation.title || 'Direct Message'}</h3>
<p className="participant-info">
{activeConversation.otherParticipants?.length || 0} participants
</p>
</div>
</div>
</div>
<MessageList
messages={messages}
typingUsers={Array.from(typingUsers)}
/>
<MessageInput
onSend={sendMessage}
onTyping={startTyping}
onStopTyping={stopTyping}
/>
</>
) : (
<div className="no-conversation-selected">
<p>Select a conversation to start messaging</p>
<p className="hint">or create a new conversation</p>
</div>
)}
</div>
</div>
);
}

View file

@ -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;
}

View file

@ -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 (
<div className="conversation-list">
<div className="conversation-list-header">
<h2>Messages</h2>
<button className="btn-new-conversation" title="New Conversation">
+
</button>
</div>
<div className="conversation-list-items">
{conversations.length === 0 ? (
<div className="no-conversations">
<p>No conversations yet</p>
<p className="hint">Start a new conversation to get started</p>
</div>
) : (
conversations.map(conv => (
<div
key={conv.id}
className={`conversation-item ${activeConversation?.id === conv.id ? 'active' : ''}`}
onClick={() => onSelectConversation(conv)}
>
<div className="conversation-avatar-container">
{getConversationAvatar(conv) ? (
<img
src={getConversationAvatar(conv)}
alt="Avatar"
className="conversation-avatar-img"
/>
) : (
<div className="conversation-avatar-placeholder">
{getConversationTitle(conv)[0]?.toUpperCase()}
</div>
)}
{conv.otherParticipants?.[0]?.status === 'online' && (
<span className="online-indicator"></span>
)}
</div>
<div className="conversation-details">
<div className="conversation-header-row">
<h3 className="conversation-title">{getConversationTitle(conv)}</h3>
<span className="conversation-time">
{formatTime(conv.updatedAt)}
</span>
</div>
<div className="conversation-last-message">
<p className="last-message-text">
{conv.lastMessage?.content || 'No messages yet'}
</p>
{conv.unreadCount > 0 && (
<span className="unread-badge">{conv.unreadCount}</span>
)}
</div>
</div>
</div>
))
)}
</div>
</div>
);
}

View file

@ -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;
}
}

View file

@ -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 (
<form className="message-input" onSubmit={handleSubmit}>
<button
type="button"
className="btn-attach"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
title="Attach file"
>
{uploading ? '⏳' : '📎'}
</button>
<input
type="file"
ref={fileInputRef}
onChange={handleFileUpload}
style={{ display: 'none' }}
/>
<textarea
value={message}
onChange={handleChange}
onKeyDown={handleKeyPress}
placeholder="Type a message..."
rows={1}
disabled={uploading}
className="message-textarea"
/>
<button
type="button"
className="btn-emoji"
title="Add emoji"
>
😊
</button>
<button
type="submit"
className="btn-send"
disabled={!message.trim() || uploading}
>
Send
</button>
</form>
);
}

View file

@ -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;
}

View file

@ -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 (
<div className="message-list empty">
<div className="no-messages">
<p>No messages yet</p>
<p className="hint">Send a message to start the conversation</p>
</div>
</div>
);
}
return (
<div className="message-list">
{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 (
<div key={message.id}>
{showTimestamp && (
<div className="message-timestamp-divider">
{new Date(message.createdAt).toLocaleDateString()}
</div>
)}
<div className={`message ${isOwnMessage(message) ? 'own' : 'other'}`}>
{!isOwnMessage(message) && showAvatar && (
<div className="message-avatar">
{message.senderAvatar ? (
<img src={message.senderAvatar} alt={message.senderUsername} />
) : (
<div className="avatar-placeholder">
{message.senderUsername?.[0]?.toUpperCase()}
</div>
)}
</div>
)}
<div className="message-content-wrapper">
{!isOwnMessage(message) && (
<div className="message-sender">
{message.senderDomain || message.senderUsername}
{message.senderDomain && <span className="verified-badge"></span>}
</div>
)}
<div className="message-bubble">
{message.replyToId && (
<div className="message-reply-reference">
Replying to a message
</div>
)}
<div className="message-text">{message.content}</div>
{message.metadata?.attachments && message.metadata.attachments.length > 0 && (
<div className="message-attachments">
{message.metadata.attachments.map((attachment, i) => (
<div key={i} className="attachment">
📎 {attachment.filename}
</div>
))}
</div>
)}
<div className="message-footer">
<span className="message-time">{formatTime(message.createdAt)}</span>
{message.editedAt && <span className="edited-indicator">edited</span>}
{message._sending && <span className="sending-indicator">sending...</span>}
</div>
{message.reactions && message.reactions.length > 0 && (
<div className="message-reactions">
{message.reactions.map((reaction, i) => (
<span key={i} className="reaction" title={`${reaction.users.length} reaction(s)`}>
{reaction.emoji} {reaction.users.length > 1 && reaction.users.length}
</span>
))}
</div>
)}
</div>
</div>
</div>
</div>
);
})}
{typingUsers.length > 0 && (
<div className="typing-indicator">
<div className="typing-dots">
<span></span>
<span></span>
<span></span>
</div>
<span className="typing-text">Someone is typing...</span>
</div>
)}
<div ref={messagesEndRef} />
</div>
);
}

View file

@ -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%;
}
}

View file

@ -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 (
<div className="domain-verification verified-container">
<h3> Domain Verified</h3>
<div className="verified-domain-display">
<strong>{currentStatus.domain}</strong>
<span className="verification-type">
{currentStatus.verificationType === 'blockchain' ? 'Blockchain' : 'DNS'}
</span>
</div>
<p className="verified-date">
Verified on {new Date(currentStatus.verifiedAt).toLocaleDateString()}
</p>
<button
className="secondary-button"
onClick={() => setCurrentStatus(null)}
>
Verify Another Domain
</button>
</div>
);
}
return (
<div className="domain-verification">
<h3>Verify Your Domain</h3>
<p className="description">
Prove ownership of a domain to display it on your profile
</p>
{error && (
<div className="error-message">
<span> {error}</span>
</div>
)}
{!verificationInstructions ? (
<div className="input-section">
<div className="form-group">
<label htmlFor="domain">Domain Name</label>
<input
id="domain"
type="text"
placeholder="yourdomain.com or anderson.aethex"
value={domain}
onChange={(e) => setDomain(e.target.value.toLowerCase().trim())}
disabled={loading}
className="domain-input"
/>
<small className="help-text">
Enter a traditional domain (e.g., dev.aethex.dev) or a .aethex blockchain domain
</small>
</div>
<button
onClick={requestVerification}
disabled={!domain || loading}
className="primary-button"
>
{loading ? 'Generating...' : 'Request Verification'}
</button>
</div>
) : (
<div className="instructions-section">
<h4>
{domain.endsWith('.aethex')
? 'Connect Your Wallet'
: `Add this DNS record to ${verificationInstructions.domain}:`
}
</h4>
{domain.endsWith('.aethex') ? (
// Blockchain verification
<div className="blockchain-verification">
<p>Connect the wallet that owns <strong>{domain}</strong></p>
<div className="form-group">
<label htmlFor="wallet">Wallet Address</label>
<input
id="wallet"
type="text"
placeholder="0x..."
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value.trim())}
disabled={loading}
className="wallet-input"
/>
</div>
<button
onClick={verifyDomain}
disabled={!walletAddress || loading}
className="primary-button verify-button"
>
{loading ? 'Verifying...' : 'Verify Ownership'}
</button>
</div>
) : (
// DNS verification
<div className="dns-verification">
<div className="dns-record">
<div className="record-field">
<strong>Type:</strong>
<span>{verificationInstructions.recordType}</span>
</div>
<div className="record-field">
<strong>Name:</strong>
<span>{verificationInstructions.recordName}</span>
</div>
<div className="record-field">
<strong>Value:</strong>
<div className="value-container">
<code>{verificationInstructions.recordValue}</code>
<button
onClick={() => copyToClipboard(verificationInstructions.recordValue)}
className="copy-button"
title="Copy to clipboard"
>
📋 Copy
</button>
</div>
</div>
</div>
<div className="help-section">
<p><strong>How to add DNS records:</strong></p>
<ol>
<li>Go to your domain's DNS settings (Google Domains, Cloudflare, etc.)</li>
<li>Add a new TXT record with the values above</li>
<li>Wait 5-10 minutes for DNS to propagate</li>
<li>Click "Verify Domain" below</li>
</ol>
<p className="expires-note">
This verification expires on {new Date(verificationInstructions.expiresAt).toLocaleDateString()}
</p>
</div>
<button
onClick={verifyDomain}
disabled={loading}
className="primary-button verify-button"
>
{loading ? 'Verifying...' : 'Verify Domain'}
</button>
{verificationStatus && (
<div className={`status-message ${verificationStatus.verified ? 'success' : 'error'}`}>
{verificationStatus.verified ? (
<span> Domain verified successfully!</span>
) : (
<span> {verificationStatus.error}</span>
)}
</div>
)}
</div>
)}
<button
onClick={resetForm}
className="secondary-button cancel-button"
disabled={loading}
>
Cancel
</button>
</div>
)}
</div>
);
}

View file

@ -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);
}

View file

@ -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 (
<div
key={channel.id}
className={`channel-item ${isActive ? 'active' : ''} ${hasUnread ? 'unread' : ''}`}
onClick={() => onSelectChannel(channel)}
>
<span className="channel-icon">{icon}</span>
<span className="channel-name">{channel.name}</span>
{hasUnread && (
<span className="unread-badge">{channel.unreadCount}</span>
)}
</div>
);
};
return (
<div className="channel-list">
{defaultChannels.length > 0 && (
<div className="channel-group">
<div className="channel-group-header">Channels</div>
{defaultChannels.map(renderChannel)}
</div>
)}
{customChannels.length > 0 && (
<div className="channel-group">
<div className="channel-group-header">Custom Channels</div>
{customChannels.map(renderChannel)}
</div>
)}
</div>
);
}

View file

@ -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 */

View file

@ -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 <div className="channel-view loading">Loading messages...</div>;
}
return (
<div className="channel-view">
<div className="channel-header">
<h3>#{channel.name}</h3>
<p className="channel-description">{channel.description}</p>
{channel.permissions && !channel.permissions.includes('all') && (
<span className="restricted-badge">
Restricted to: {channel.permissions.join(', ')}
</span>
)}
</div>
<MessageList
messages={messages}
currentUserId={user.id}
typingUsers={new Set()}
showSystemMessages={true}
/>
<MessageInput
onSend={sendMessage}
onTyping={() => {}}
onStopTyping={() => {}}
placeholder={`Message #${channel.name}`}
/>
</div>
);
}

View file

@ -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);
}
}

View file

@ -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 (
<div className={`gameforge-chat ${embedded ? 'embedded' : ''}`}>
<div className="loading">Loading project chat...</div>
</div>
);
}
if (error) {
return (
<div className={`gameforge-chat ${embedded ? 'embedded' : ''}`}>
<div className="error">
<p>{error}</p>
<button onClick={loadChannels}>Retry</button>
</div>
</div>
);
}
return (
<div className={`gameforge-chat ${embedded ? 'embedded' : ''}`}>
<div className="gameforge-chat-sidebar">
<div className="project-header">
<h3>Project Channels</h3>
</div>
<ChannelList
channels={channels}
activeChannel={activeChannel}
onSelectChannel={setActiveChannel}
/>
</div>
<div className="gameforge-chat-main">
{activeChannel ? (
<ChannelView
channel={activeChannel}
projectId={projectId}
/>
) : (
<div className="no-channel-selected">
<p>Select a channel to start chatting</p>
</div>
)}
</div>
</div>
);
}

View file

@ -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;
}

View file

@ -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 (
<div className="overlay-minimized" onClick={toggleMinimize}>
<div className="minimized-icon">💬</div>
{unreadMessages > 0 && (
<div className="minimized-badge">{unreadMessages}</div>
)}
</div>
);
}
return (
<div className="in-game-overlay">
<div className="overlay-header">
<div className="overlay-tabs">
<button
className={activeTab === 'friends' ? 'active' : ''}
onClick={() => setActiveTab('friends')}
>
Friends ({friends.filter(f => f.status === 'online').length})
</button>
<button
className={activeTab === 'messages' ? 'active' : ''}
onClick={() => setActiveTab('messages')}
>
Messages
{unreadMessages > 0 && (
<span className="badge">{unreadMessages}</span>
)}
</button>
</div>
<button className="btn-minimize" onClick={toggleMinimize}>
</button>
</div>
<div className="overlay-content">
{activeTab === 'friends' && (
<div className="friends-list">
{friends.length === 0 ? (
<div className="messages-preview">
<p>No friends yet</p>
</div>
) : (
friends.map(friend => (
<div
key={friend.userId}
className="friend-item"
onClick={() => handleFriendClick(friend)}
>
<img
src={friend.avatar || 'https://via.placeholder.com/40'}
alt={friend.username}
/>
<div className="friend-info">
<div className="friend-name">{friend.username}</div>
{friend.currentGame && (
<div className="friend-game">
🎮 {friend.currentGame.gameName}
</div>
)}
{!friend.currentGame && friend.status === 'online' && (
<div className="friend-game">Online</div>
)}
</div>
<div className={`status-indicator ${friend.status || 'offline'}`} />
</div>
))
)}
</div>
)}
{activeTab === 'messages' && (
<div className="messages-preview">
<p>Recent messages appear here</p>
<p style={{ fontSize: '12px', marginTop: '8px', color: '#666' }}>
Click a friend to start chatting
</p>
</div>
)}
</div>
</div>
);
}

View file

@ -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;
}
}

View file

@ -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 (
<form onSubmit={handleSubmit} className="checkout-form">
<div className="card-element-wrapper">
<CardElement
options={{
style: {
base: {
fontSize: '16px',
color: '#fff',
'::placeholder': {
color: '#9ca3af'
}
},
invalid: {
color: '#ed4245'
}
}
}}
/>
</div>
{error && (
<div className="error-message">{error}</div>
)}
<button
type="submit"
disabled={!stripe || loading}
className="btn-submit"
>
{loading ? 'Processing...' : `Subscribe - ${getAmount()}`}
</button>
<p style={{ textAlign: 'center', marginTop: '16px', fontSize: '12px', color: '#aaa' }}>
By subscribing, you agree to our Terms of Service and Privacy Policy
</p>
</form>
);
}
/**
* 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 (
<div className="upgrade-flow">
<h1>Upgrade to Premium</h1>
<div className="tier-selection">
<div
className={`tier-card ${selectedTier === 'premium' ? 'selected' : ''}`}
onClick={() => setSelectedTier('premium')}
>
<h3>Premium</h3>
<div className="price">$100/year</div>
<ul>
<li> Custom .aethex domain</li>
<li> Blockchain NFT ownership</li>
<li> Unlimited friends</li>
<li> HD voice/video calls (1080p)</li>
<li> 10 GB storage</li>
<li> Custom branding</li>
<li> Analytics dashboard</li>
<li> Priority support</li>
<li> Ad-free experience</li>
</ul>
</div>
<div
className={`tier-card ${selectedTier === 'enterprise' ? 'selected' : ''}`}
onClick={() => setSelectedTier('enterprise')}
>
<h3>Enterprise</h3>
<div className="price">$500+/month</div>
<ul>
<li> Everything in Premium</li>
<li> White-label platform</li>
<li> Custom domain (chat.yoursite.com)</li>
<li> Unlimited team members</li>
<li> Dedicated infrastructure</li>
<li> 4K video quality</li>
<li> SLA guarantees (99.9% uptime)</li>
<li> Dedicated account manager</li>
<li> Custom integrations</li>
</ul>
</div>
</div>
{selectedTier === 'premium' && (
<div className="domain-selection">
<h3>Choose Your .aethex Domain</h3>
<p style={{ color: '#aaa', marginBottom: '16px' }}>
Your premium blockchain domain with NFT ownership proof
</p>
<div className="domain-input-group">
<input
type="text"
value={domainName}
onChange={(e) => setDomainName(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))}
placeholder="yourname"
className="domain-input"
maxLength={50}
/>
<span className="domain-suffix">.aethex</span>
<button
type="button"
onClick={checkDomain}
disabled={checkingDomain || !domainName}
>
{checkingDomain ? 'Checking...' : 'Check'}
</button>
</div>
{domainAvailable && (
<div className={`domain-status ${domainAvailable.available ? 'available' : 'unavailable'}`}>
{domainAvailable.available ? (
<>
<p><strong> {domainAvailable.domain} is available!</strong></p>
<p style={{ fontSize: '14px', marginTop: '8px', opacity: 0.8 }}>
Price: ${domainAvailable.price}/year
</p>
</>
) : (
<div>
<p><strong> {domainAvailable.domain} is taken</strong></p>
{domainAvailable.error && (
<p style={{ fontSize: '14px', marginTop: '4px' }}>{domainAvailable.error}</p>
)}
{domainAvailable.suggestedAlternatives && domainAvailable.suggestedAlternatives.length > 0 && (
<>
<p style={{ marginTop: '12px' }}>Try these alternatives:</p>
<ul>
{domainAvailable.suggestedAlternatives.map(alt => (
<li
key={alt}
onClick={() => setDomainName(alt.replace('.aethex', ''))}
>
{alt}
</li>
))}
</ul>
</>
)}
</div>
)}
</div>
)}
</div>
)}
{(selectedTier === 'enterprise' || (selectedTier === 'premium' && domainAvailable?.available)) && (
<Elements stripe={stripePromise}>
<CheckoutForm
tier={selectedTier}
domain={domainAvailable?.available ? `${domainName}.aethex` : null}
onSuccess={handleSuccess}
/>
</Elements>
)}
{selectedTier === 'enterprise' && !domainAvailable && (
<div style={{ textAlign: 'center', padding: '40px', color: '#aaa' }}>
<p>For Enterprise plans, please contact our sales team:</p>
<p style={{ marginTop: '16px' }}>
<a href="mailto:enterprise@aethex.app" style={{ color: '#5865f2' }}>
enterprise@aethex.app
</a>
</p>
</div>
)}
</div>
);
}

View file

@ -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;
}
}

View file

@ -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 (
<div className="verified-domain-badge">
<div className="badge-content">
<span className="domain-text">{verifiedDomain}</span>
<span
className="checkmark"
title={`Verified via ${verificationType === 'blockchain' ? 'blockchain' : 'DNS'}`}
>
</span>
</div>
{verificationType === 'blockchain' && (
<span className="blockchain-indicator" title="Verified via blockchain">
</span>
)}
{verifiedAt && (
<div className="verified-info">
Verified {new Date(verifiedAt).toLocaleDateString()}
</div>
)}
</div>
);
}

View file

@ -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 (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}

View file

@ -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 (
<SocketContext.Provider value={{ socket, connected }}>
{children}
</SocketContext.Provider>
);
}
export function useSocket() {
const context = useContext(SocketContext);
if (context === undefined) {
throw new Error('useSocket must be used within SocketProvider');
}
return context;
}

View file

@ -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;
}

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AeThex Passport - Domain Verification</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>

View file

@ -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(
<React.StrictMode>
<AppWrapper />
</React.StrictMode>
);

View file

@ -0,0 +1,65 @@
import React from "react";
export default function ChannelSidebar() {
return (
<div className="channel-sidebar w-72 bg-[#0f0f0f] border-r border-[#1a1a1a] flex flex-col">
{/* Server Header */}
<div className="server-header p-4 border-b border-[#1a1a1a] font-bold text-base flex items-center justify-between">
<span>AeThex Foundation</span>
<span className="server-badge foundation text-xs px-2 py-1 rounded bg-red-900/20 text-red-500 border border-red-500 uppercase tracking-wider">Official</span>
</div>
{/* Channel List */}
<div className="channel-list flex-1 overflow-y-auto py-2">
<div className="channel-category px-4 pt-4 pb-2 text-xs uppercase tracking-widest text-gray-500 font-bold">Announcements</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">📢</span>
<span className="channel-name flex-1">updates</span>
<span className="channel-badge text-xs bg-red-600 text-white px-2 rounded-full">3</span>
</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">📜</span>
<span className="channel-name flex-1">changelog</span>
</div>
<div className="channel-category px-4 pt-4 pb-2 text-xs uppercase tracking-widest text-gray-500 font-bold">Development</div>
<div className="channel-item active flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm bg-[#1a1a1a]">
<span className="channel-icon">#</span>
<span className="channel-name flex-1">general</span>
</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">#</span>
<span className="channel-name flex-1">api-discussion</span>
</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">#</span>
<span className="channel-name flex-1">passport-development</span>
</div>
<div className="channel-category px-4 pt-4 pb-2 text-xs uppercase tracking-widest text-gray-500 font-bold">Support</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon"></span>
<span className="channel-name flex-1">help</span>
</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">🐛</span>
<span className="channel-name flex-1">bug-reports</span>
</div>
<div className="channel-category px-4 pt-4 pb-2 text-xs uppercase tracking-widest text-gray-500 font-bold">Voice Channels</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">🔊</span>
<span className="channel-name flex-1">Nexus Lounge</span>
<span className="text-gray-500 text-xs">3</span>
</div>
</div>
{/* User Presence */}
<div className="user-presence p-3 border-t border-[#1a1a1a] flex items-center gap-3 text-sm">
<div className="user-avatar w-10 h-10 rounded-full flex items-center justify-center font-bold bg-gradient-to-tr from-red-600 via-blue-600 to-orange-400">A</div>
<div className="user-info flex-1">
<div className="user-name font-bold mb-0.5">Anderson</div>
<div className="user-status flex items-center gap-1 text-xs text-gray-500">
<span className="status-dot w-2 h-2 rounded-full bg-green-400 shadow-green-400/50 shadow" />
<span>Building AeThex</span>
</div>
</div>
</div>
</div>
);
}

View file

@ -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 (
<div className="chat-area flex flex-col flex-1 bg-[#0a0a0a]">
{/* Chat Header */}
<div className="chat-header px-5 py-4 border-b border-[#1a1a1a] flex items-center gap-3">
<span className="channel-name-header flex-1 font-bold text-base"># general</span>
<div className="chat-tools flex gap-4 text-sm text-gray-500">
<span className="chat-tool cursor-pointer hover:text-blue-500">🔔</span>
<span className="chat-tool cursor-pointer hover:text-blue-500">📌</span>
<span className="chat-tool cursor-pointer hover:text-blue-500">👥 128</span>
<span className="chat-tool cursor-pointer hover:text-blue-500">🔍</span>
</div>
</div>
{/* Messages */}
<div className="chat-messages flex-1 overflow-y-auto px-5 py-5">
{messages.map((msg, i) => (
<Message key={i} {...msg} />
))}
</div>
{/* Message Input */}
<div className="message-input-container px-5 py-5 border-t border-[#1a1a1a]">
<MessageInput />
</div>
</div>
);
}

View file

@ -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 (
<div className="connect-container flex h-screen">
<ServerList />
<ChannelSidebar />
<ChatArea />
<MemberSidebar />
</div>
);
}

View file

@ -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 (
<div className="member-sidebar w-72 bg-[#0f0f0f] border-l border-[#1a1a1a] flex flex-col">
<div className="member-header p-4 border-b border-[#1a1a1a] text-xs uppercase tracking-widest text-gray-500">Members 128</div>
<div className="member-list flex-1 overflow-y-auto py-3">
{members.map((section, i) => (
<div key={i} className="member-section mb-4">
<div className="member-section-title px-4 py-2 text-xs uppercase tracking-wider text-gray-500 font-bold">{section.section}</div>
{section.users.map((user, j) => (
<div key={j} className="member-item flex items-center gap-3 px-4 py-1.5 cursor-pointer hover:bg-[#1a1a1a]">
<div className={`member-avatar-small w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm relative bg-gradient-to-tr ${user.avatarBg}`}>
{user.avatar}
<div className={`online-indicator absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-[#0f0f0f] ${user.status === "online" ? "bg-green-400" : user.status === "in-game" ? "bg-blue-500" : user.status === "labs" ? "bg-orange-400" : "bg-gray-700"}`}></div>
</div>
<div className="member-name flex-1 text-sm">{user.name}</div>
{user.activity && <div className="member-activity text-xs text-gray-500">{user.activity}</div>}
</div>
))}
</div>
))}
</div>
</div>
);
}

View file

@ -0,0 +1,27 @@
import React from "react";
export default function Message(props) {
if (props.type === "system") {
return (
<div className={`message-system ${props.className} bg-[#0f0f0f] border-l-4 pl-4 pr-4 py-3 mb-4 text-sm`}>
<div className={`system-label ${props.className} text-xs uppercase tracking-wider font-bold mb-1`}>[{props.label}] System Announcement</div>
<div>{props.text}</div>
</div>
);
}
return (
<div className="message flex gap-4 mb-5 p-3 rounded transition hover:bg-[#0f0f0f]">
<div className={`message-avatar w-10 h-10 rounded-full flex items-center justify-center font-bold text-base flex-shrink-0 bg-gradient-to-tr ${props.avatarBg}`}>{props.avatar}</div>
<div className="message-content flex-1">
<div className="message-header flex items-baseline gap-3 mb-1">
<span className="message-author font-bold">{props.author}</span>
{props.badge && (
<span className={`message-badge ${props.className} text-xs px-2 py-1 rounded uppercase tracking-wider font-bold`}>{props.badge}</span>
)}
<span className="message-time text-xs text-gray-500">{props.time}</span>
</div>
<div className="message-text leading-relaxed text-gray-300">{props.text}</div>
</div>
</div>
);
}

View file

@ -0,0 +1,16 @@
import React from "react";
export default function MessageInput() {
return (
<div className="flex items-center gap-2">
<button className="attachButton w-10 h-10 flex items-center justify-center rounded bg-[#1a1a1a] text-xl text-gray-400 mr-2">+</button>
<input
type="text"
className="message-input flex-1 bg-[#0f0f0f] border border-[#1a1a1a] rounded-lg px-4 py-3 text-gray-200 text-sm focus:outline-none focus:border-blue-500 placeholder:text-gray-500"
placeholder="Message #general (Foundation infrastructure channel)"
maxLength={2000}
/>
<button className="sendButton w-10 h-10 flex items-center justify-center rounded bg-blue-600 text-xl text-white ml-2">🎤</button>
</div>
);
}

View file

@ -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 (
<div className="server-list flex flex-col items-center py-3 gap-3 w-20 bg-[#0d0d0d] border-r border-[#1a1a1a]">
{servers.map((srv, i) =>
srv.id === "divider" ? (
<div key={i} className="server-divider w-10 h-0.5 bg-[#1a1a1a] my-1" />
) : (
<div
key={srv.id}
className={`server-icon ${srv.className} ${srv.active ? "active" : ""} w-14 h-14 rounded-xl flex items-center justify-center font-bold text-lg cursor-pointer transition-all relative`}
>
{srv.label}
</div>
)
)}
</div>
);
}

View file

@ -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;
}

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AeThex Connect Mockup Web</title>
<link rel="stylesheet" href="./global.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.jsx"></script>
</body>
</html>

View file

@ -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(
<React.StrictMode>
<MainLayout />
</React.StrictMode>
);
}

2175
astro-site/src/react-app/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -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"
}
}

316
astro-site/src/react-app/utils/crypto.js vendored Normal file
View file

@ -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<CryptoKey>} 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<CryptoKey>} 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<Object>} 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<string>} 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');
}

View file

@ -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 };
}

532
astro-site/src/react-app/utils/webrtc.js vendored Normal file
View file

@ -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;

View file

@ -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
}
}
}
});

View file

@ -15,7 +15,7 @@ const path = require('path');
const app = express(); const app = express();
const httpServer = http.createServer(app); const httpServer = http.createServer(app);
const PORT = process.env.PORT || 3000; const PORT = 3000;
// Trust proxy for Codespaces/containers // Trust proxy for Codespaces/containers
app.set('trust proxy', 1); app.set('trust proxy', 1);

View file

@ -8,7 +8,7 @@ require('dotenv').config();
const { createClient } = require('@supabase/supabase-js'); const { createClient } = require('@supabase/supabase-js');
const PORT = process.env.PORT || 4000; const PORT = 3000;
const SUPABASE_URL = process.env.SUPABASE_URL; const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY; const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;

View file

@ -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 DomainVerification from './components/DomainVerification';
import VerifiedDomainBadge from './components/VerifiedDomainBadge'; import VerifiedDomainBadge from './components/VerifiedDomainBadge';
import './App.css'; import './App.css';
@ -8,35 +9,25 @@ import './App.css';
* Demo of domain verification feature * Demo of domain verification feature
*/ */
function App() { function App() {
const [user, setUser] = useState(null); const { user, loading } = useAuth();
const [showVerification, setShowVerification] = useState(false); const [showVerification, setShowVerification] = React.useState(false);
// Mock user data - in production, fetch from API if (loading) {
useEffect(() => { return <div className="loading-screen">Loading...</div>;
// 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);
}, []);
return ( return (
<div className="app"> <div className="app">
<header className="app-header"> <header className="app-header">
<h1>AeThex Passport</h1> <h1>AeThex Passport</h1>
<p>Domain Verification Demo</p> <p>Domain Verification</p>
</header> </header>
<main className="app-main"> <main className="app-main">
{user && ( {user && (
<div className="user-profile"> <div className="user-profile">
<div className="profile-header"> <div className="profile-header">
<h2>{user.name}</h2> <h2>{user.email}</h2>
<p>{user.email}</p>
{user.verifiedDomain && ( {user.verifiedDomain && (
<VerifiedDomainBadge <VerifiedDomainBadge
verifiedDomain={user.verifiedDomain} verifiedDomain={user.verifiedDomain}
@ -84,4 +75,10 @@ function App() {
); );
} }
export default App; export default function AppWrapper() {
return (
<AuthProvider>
<App />
</AuthProvider>
);
}

View file

@ -31,8 +31,7 @@ export default function ChannelView({ channel, projectId }) {
try { try {
const decrypted = await decryptMessage( const decrypted = await decryptMessage(
JSON.parse(message.content), JSON.parse(message.content),
user.password, user.password
user.publicKey
); );
message.content = decrypted; message.content = decrypted;
} catch (error) { } catch (error) {
@ -76,10 +75,8 @@ export default function ChannelView({ channel, projectId }) {
try { try {
const decrypted = await decryptMessage( const decrypted = await decryptMessage(
JSON.parse(msg.content), JSON.parse(msg.content),
user.password, user.password
user.publicKey
); );
return { return {
...msg, ...msg,
content: decrypted content: decrypted

View file

@ -1,4 +1,10 @@
import React, { createContext, useContext, useState, useEffect } from 'react'; 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(); const AuthContext = createContext();
@ -15,47 +21,37 @@ export function AuthProvider({ children }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { useEffect(() => {
// Initialize with demo user for development const getSession = async () => {
const demoUser = { setLoading(true);
id: 'demo-user-123', const { data: { session } } = await supabase.auth.getSession();
name: 'Demo User', if (session?.user) {
email: 'demo@aethex.dev', setUser(session.user);
verifiedDomain: 'demo.aethex', } else {
domainVerifiedAt: new Date().toISOString(), setUser(null);
isPremium: false, }
avatar: null
};
setUser(demoUser);
setLoading(false); setLoading(false);
};
getSession();
const { data: listener } = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user || null);
});
return () => {
listener?.subscription.unsubscribe();
};
}, []); }, []);
const login = async (email, password) => { const login = async (email, password) => {
// Mock login - in production, call actual API const { data, error } = await supabase.auth.signInWithPassword({ email, password });
try { if (error) {
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);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
setUser(data.user);
return { success: true };
}; };
const logout = () => { const logout = async () => {
await supabase.auth.signOut();
setUser(null); setUser(null);
localStorage.removeItem('token');
}; };
const updateUser = (updates) => { const updateUser = (updates) => {

View file

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import Demo from './Demo'; import AppWrapper from './App';
import './index.css'; import './index.css';
import './Demo.css'; import './Demo.css';
ReactDOM.createRoot(document.getElementById('root')).render( ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode> <React.StrictMode>
<Demo /> <AppWrapper />
</React.StrictMode> </React.StrictMode>
); );

View file

@ -5,7 +5,7 @@ import react from '@vitejs/plugin-react';
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
port: 5173, port: 3000,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: 'http://localhost:3000',

View file

@ -7,7 +7,7 @@ project_id = "AeThex-Connect"
[api] [api]
enabled = true enabled = true
# Port to use for the API URL. # 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 # 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. # endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"] schemas = ["public", "graphql_public"]

View file

@ -5,33 +5,49 @@
-- CONVERSATIONS -- 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 ( CREATE TABLE IF NOT EXISTS conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(20) NOT NULL CHECK (type IN ('direct', 'group', 'channel')), type VARCHAR(20) NOT NULL CHECK (type IN ('direct', 'group', 'channel')),
title VARCHAR(200), title VARCHAR(200),
description TEXT, description TEXT,
avatar_url VARCHAR(500), avatar_url VARCHAR(500),
created_by UUID REFERENCES users(id) ON DELETE SET NULL, created_by VARCHAR REFERENCES users(id) ON DELETE SET NULL,
gameforge_project_id UUID, -- For GameForge integration (future) gameforge_project_id VARCHAR, -- For GameForge integration (future)
is_archived BOOLEAN DEFAULT false, is_archived BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW() updated_at TIMESTAMP DEFAULT NOW()
); );
-- Create index on type column
CREATE INDEX idx_conversations_type ON conversations(type); CREATE INDEX idx_conversations_type ON conversations(type);
-- Create index on creator column
CREATE INDEX idx_conversations_creator ON conversations(created_by); 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 idx_conversations_project ON conversations(gameforge_project_id);
-- Create index on updated_at column
CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC); CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC);
-- ============================================================================ -- ============================================================================
-- CONVERSATION PARTICIPANTS -- CONVERSATION PARTICIPANTS
-- ============================================================================ -- ============================================================================
-- Update conversation_participants to match actual types and remove reference to identities
CREATE TABLE IF NOT EXISTS conversation_participants ( CREATE TABLE IF NOT EXISTS conversation_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE, conversation_id UUID NOT NULL REFERENCES conversations(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,
identity_id UUID REFERENCES identities(id) ON DELETE SET NULL, identity_id VARCHAR,
role VARCHAR(20) DEFAULT 'member' CHECK (role IN ('admin', 'moderator', 'member')), role VARCHAR(20) DEFAULT 'member' CHECK (role IN ('admin', 'moderator', 'member')),
joined_at TIMESTAMP DEFAULT NOW(), joined_at TIMESTAMP DEFAULT NOW(),
last_read_at TIMESTAMP, last_read_at TIMESTAMP,
@ -39,8 +55,11 @@ CREATE TABLE IF NOT EXISTS conversation_participants (
UNIQUE(conversation_id, user_id) UNIQUE(conversation_id, user_id)
); );
-- Create index on conversation column
CREATE INDEX idx_participants_conversation ON conversation_participants(conversation_id); 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 idx_participants_user ON conversation_participants(user_id);
-- Create index on identity column
CREATE INDEX idx_participants_identity ON conversation_participants(identity_id); 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() 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 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 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 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); CREATE INDEX idx_messages_created ON messages(created_at DESC);
-- ============================================================================ -- ============================================================================
-- MESSAGE REACTIONS -- MESSAGE REACTIONS
-- ============================================================================ -- ============================================================================
-- Update message_reactions to match actual types
CREATE TABLE IF NOT EXISTS message_reactions ( CREATE TABLE IF NOT EXISTS message_reactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE, 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, emoji VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(message_id, user_id, emoji) UNIQUE(message_id, user_id, emoji)
); );
-- Create index on message column
CREATE INDEX idx_reactions_message ON message_reactions(message_id); 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); 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 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 idx_files_uploader ON files(uploader_id);
-- Create index on conversation column
CREATE INDEX idx_files_conversation ON files(conversation_id); 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); CREATE INDEX idx_files_created ON files(created_at DESC);
-- ============================================================================ -- ============================================================================
-- CALLS -- CALLS
-- ============================================================================ -- ============================================================================
-- Update calls to match actual types
CREATE TABLE IF NOT EXISTS calls ( CREATE TABLE IF NOT EXISTS calls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL, conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('voice', 'video')), 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')), status VARCHAR(20) DEFAULT 'ringing' CHECK (status IN ('ringing', 'active', 'ended', 'missed', 'declined')),
started_at TIMESTAMP, started_at TIMESTAMP,
ended_at TIMESTAMP, ended_at TIMESTAMP,
@ -121,26 +160,33 @@ CREATE TABLE IF NOT EXISTS calls (
created_at TIMESTAMP DEFAULT NOW() created_at TIMESTAMP DEFAULT NOW()
); );
-- Create index on conversation column
CREATE INDEX idx_calls_conversation ON calls(conversation_id); 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 idx_calls_initiator ON calls(initiator_id);
-- Create index on status column
CREATE INDEX idx_calls_status ON calls(status); CREATE INDEX idx_calls_status ON calls(status);
-- Create index on created_at column
CREATE INDEX idx_calls_created ON calls(created_at DESC); CREATE INDEX idx_calls_created ON calls(created_at DESC);
-- ============================================================================ -- ============================================================================
-- CALL PARTICIPANTS -- CALL PARTICIPANTS
-- ============================================================================ -- ============================================================================
-- Update call_participants to match actual types
CREATE TABLE IF NOT EXISTS call_participants ( CREATE TABLE IF NOT EXISTS call_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
call_id UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE, 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, joined_at TIMESTAMP,
left_at TIMESTAMP, left_at TIMESTAMP,
media_state JSONB DEFAULT '{"audio": true, "video": false, "screen_share": false}'::jsonb, media_state JSONB DEFAULT '{"audio": true, "video": false, "screen_share": false}'::jsonb,
UNIQUE(call_id, user_id) UNIQUE(call_id, user_id)
); );
-- Create index on call column
CREATE INDEX idx_call_participants_call ON call_participants(call_id); 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); CREATE INDEX idx_call_participants_user ON call_participants(user_id);
-- ============================================================================ -- ============================================================================

View file

@ -36,6 +36,10 @@ CREATE TABLE IF NOT EXISTS audit_logs (
created_at TIMESTAMP DEFAULT NOW() 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 -- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_gameforge_integrations_project_id ON gameforge_integrations(project_id); 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; CREATE INDEX IF NOT EXISTS idx_conversations_gameforge_project ON conversations(gameforge_project_id) WHERE gameforge_project_id IS NOT NULL;

View file

@ -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 turn_credentials table for temporary TURN server credentials
CREATE TABLE IF NOT EXISTS turn_credentials ( CREATE TABLE IF NOT EXISTS turn_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 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, username VARCHAR(100) NOT NULL,
credential VARCHAR(100) NOT NULL, credential VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(),

View file

@ -1,6 +1,11 @@
-- Migration 005: Nexus Cross-Platform Integration -- Migration 005: Nexus Cross-Platform Integration
-- Adds friend system, game sessions, lobbies, and enhanced Nexus features -- 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 -- Extend nexus_integrations table with session and overlay config
ALTER TABLE nexus_integrations ALTER TABLE nexus_integrations
ADD COLUMN IF NOT EXISTS current_game_session_id UUID, 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 -- Friend requests table
CREATE TABLE IF NOT EXISTS friend_requests ( CREATE TABLE IF NOT EXISTS friend_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_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 UUID 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 status VARCHAR(20) DEFAULT 'pending', -- pending, accepted, rejected
created_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(),
responded_at TIMESTAMP, responded_at TIMESTAMP,
@ -26,8 +31,8 @@ CREATE INDEX IF NOT EXISTS idx_friend_requests_from ON friend_requests(from_user
-- Friendships table -- Friendships table
CREATE TABLE IF NOT EXISTS friendships ( CREATE TABLE IF NOT EXISTS friendships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user1_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user1_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user2_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, user2_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(), created_at TIMESTAMP DEFAULT NOW(),
CHECK (user1_id < user2_id), -- Prevent duplicates CHECK (user1_id < user2_id), -- Prevent duplicates
UNIQUE(user1_id, user2_id) UNIQUE(user1_id, user2_id)
@ -50,6 +55,15 @@ CREATE TABLE IF NOT EXISTS game_sessions (
metadata JSONB -- {mapName, gameMode, score, etc.} 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_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_active ON game_sessions(user_id, session_state);
CREATE INDEX IF NOT EXISTS idx_game_sessions_nexus_player ON game_sessions(nexus_player_id); 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(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
game_id VARCHAR(100) NOT NULL, game_id VARCHAR(100) NOT NULL,
lobby_code VARCHAR(50) UNIQUE, 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 conversation_id UUID REFERENCES conversations(id), -- Auto-created chat
max_players INTEGER DEFAULT 8, max_players INTEGER DEFAULT 8,
is_public BOOLEAN DEFAULT false, 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 ( CREATE TABLE IF NOT EXISTS game_lobby_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lobby_id UUID NOT NULL REFERENCES game_lobbies(id) ON DELETE CASCADE, 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 team_id VARCHAR(20), -- For team-based games
ready BOOLEAN DEFAULT false, ready BOOLEAN DEFAULT false,
joined_at TIMESTAMP DEFAULT NOW(), joined_at TIMESTAMP DEFAULT NOW(),

View file

@ -8,7 +8,7 @@ ADD COLUMN IF NOT EXISTS premium_tier VARCHAR(20) DEFAULT 'free'; -- free, premi
-- Premium subscriptions table -- Premium subscriptions table
CREATE TABLE IF NOT EXISTS premium_subscriptions ( CREATE TABLE IF NOT EXISTS premium_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 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 tier VARCHAR(20) NOT NULL, -- free, premium, enterprise
status VARCHAR(20) DEFAULT 'active', -- active, cancelled, expired, suspended status VARCHAR(20) DEFAULT 'active', -- active, cancelled, expired, suspended
stripe_subscription_id VARCHAR(100), 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 ( CREATE TABLE IF NOT EXISTS blockchain_domains (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain VARCHAR(100) NOT NULL UNIQUE, -- e.g., "anderson.aethex" 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 nft_token_id VARCHAR(100), -- Token ID from Freename contract
wallet_address VARCHAR(100), -- Owner's wallet address wallet_address VARCHAR(100), -- Owner's wallet address
verified BOOLEAN DEFAULT false, 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 ( CREATE TABLE IF NOT EXISTS domain_transfers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
domain_id UUID NOT NULL REFERENCES blockchain_domains(id), domain_id UUID NOT NULL REFERENCES blockchain_domains(id),
from_user_id UUID REFERENCES users(id), from_user_id VARCHAR REFERENCES users(id),
to_user_id UUID REFERENCES users(id), to_user_id VARCHAR REFERENCES users(id),
transfer_type VARCHAR(20), -- sale, gift, transfer transfer_type VARCHAR(20), -- sale, gift, transfer
price_usd DECIMAL(10, 2), price_usd DECIMAL(10, 2),
transaction_hash VARCHAR(100), -- Blockchain tx hash 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 ( CREATE TABLE IF NOT EXISTS enterprise_accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_name VARCHAR(200) NOT NULL, 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 VARCHAR(200), -- e.g., chat.yourgame.com
custom_domain_verified BOOLEAN DEFAULT false, custom_domain_verified BOOLEAN DEFAULT false,
dns_txt_record VARCHAR(100), -- For domain verification 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 ( CREATE TABLE IF NOT EXISTS enterprise_team_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
enterprise_id UUID NOT NULL REFERENCES enterprise_accounts(id) ON DELETE CASCADE, 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 role VARCHAR(20) DEFAULT 'member', -- admin, member
joined_at TIMESTAMP DEFAULT NOW(), joined_at TIMESTAMP DEFAULT NOW(),
UNIQUE(enterprise_id, user_id) 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 -- Usage analytics table
CREATE TABLE IF NOT EXISTS usage_analytics ( CREATE TABLE IF NOT EXISTS usage_analytics (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 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, date DATE NOT NULL,
messages_sent INTEGER DEFAULT 0, messages_sent INTEGER DEFAULT 0,
messages_received INTEGER DEFAULT 0, messages_received INTEGER DEFAULT 0,
@ -145,7 +145,7 @@ ON CONFLICT (tier) DO NOTHING;
-- Payment transactions table (for audit trail) -- Payment transactions table (for audit trail)
CREATE TABLE IF NOT EXISTS payment_transactions ( CREATE TABLE IF NOT EXISTS payment_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), 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. transaction_type VARCHAR(50), -- subscription, domain_purchase, domain_sale, etc.
amount_usd DECIMAL(10, 2) NOT NULL, amount_usd DECIMAL(10, 2) NOT NULL,
currency VARCHAR(3) DEFAULT 'usd', currency VARCHAR(3) DEFAULT 'usd',

View file

@ -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);