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
{
"credentials": {
"urls": ["turn:turn.example.com:3478"],
"urls": ["turn:turn.example.com:3000"],
"username": "1736517600:username",
"credential": "hmac-sha1-hash"
},
@ -427,7 +427,7 @@ Edit `/etc/turnserver.conf`:
```conf
# Listening port
listening-port=3478
listening-port=3000
tls-listening-port=5349
# External IP (replace with your server IP)
@ -467,7 +467,7 @@ Add to `.env`:
```env
# TURN Server Configuration
TURN_SERVER_HOST=turn.yourdomain.com
TURN_SERVER_PORT=3478
TURN_SERVER_PORT=3000
TURN_SECRET=your-turn-secret-key
TURN_TTL=86400
```
@ -476,8 +476,8 @@ TURN_TTL=86400
```bash
# Allow TURN ports
sudo ufw allow 3478/tcp
sudo ufw allow 3478/udp
sudo ufw allow 3000/tcp
sudo ufw allow 3000/udp
sudo ufw allow 5349/tcp
sudo ufw allow 5349/udp
@ -496,7 +496,7 @@ sudo systemctl status coturn
Use the [Trickle ICE](https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/) test page:
1. Add your TURN server URL: `turn:YOUR_SERVER_IP:3478`
1. Add your TURN server URL: `turn:YOUR_SERVER_IP:3000`
2. Generate TURN credentials using the HMAC method
3. Click "Gather candidates"
4. Verify `relay` candidates appear

View file

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

View file

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

View file

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

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() {
return <MainLayout />;
}
// Removed: This page is deprecated. Use /app for the full platform UI.

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>
<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="/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>
</section>
<section class="landing-features">

View file

@ -1,9 +1,9 @@
---
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">
<LoginIsland client:load />
<SupabaseLogin client:load />
</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() {
return <MainLayout />;
}
// Removed: This page is deprecated. Use /app for the full platform UI.

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 httpServer = http.createServer(app);
const PORT = process.env.PORT || 3000;
const PORT = 3000;
// Trust proxy for Codespaces/containers
app.set('trust proxy', 1);

View file

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

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 VerifiedDomainBadge from './components/VerifiedDomainBadge';
import './App.css';
@ -8,35 +9,25 @@ import './App.css';
* Demo of domain verification feature
*/
function App() {
const [user, setUser] = useState(null);
const [showVerification, setShowVerification] = useState(false);
const { user, loading } = useAuth();
const [showVerification, setShowVerification] = React.useState(false);
// Mock user data - in production, fetch from API
useEffect(() => {
// Simulate fetching user data
const mockUser = {
id: 'user-123',
name: 'Demo User',
email: 'demo@aethex.dev',
verifiedDomain: null, // Will be populated after verification
domainVerifiedAt: null
};
setUser(mockUser);
}, []);
if (loading) {
return <div className="loading-screen">Loading...</div>;
}
return (
<div className="app">
<header className="app-header">
<h1>AeThex Passport</h1>
<p>Domain Verification Demo</p>
<p>Domain Verification</p>
</header>
<main className="app-main">
{user && (
<div className="user-profile">
<div className="profile-header">
<h2>{user.name}</h2>
<p>{user.email}</p>
<h2>{user.email}</h2>
{user.verifiedDomain && (
<VerifiedDomainBadge
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 {
const decrypted = await decryptMessage(
JSON.parse(message.content),
user.password,
user.publicKey
user.password
);
message.content = decrypted;
} catch (error) {
@ -76,10 +75,8 @@ export default function ChannelView({ channel, projectId }) {
try {
const decrypted = await decryptMessage(
JSON.parse(msg.content),
user.password,
user.publicKey
user.password
);
return {
...msg,
content: decrypted

View file

@ -1,4 +1,10 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
);
const AuthContext = createContext();
@ -15,47 +21,37 @@ export function AuthProvider({ children }) {
const [loading, setLoading] = useState(true);
useEffect(() => {
// Initialize with demo user for development
const demoUser = {
id: 'demo-user-123',
name: 'Demo User',
email: 'demo@aethex.dev',
verifiedDomain: 'demo.aethex',
domainVerifiedAt: new Date().toISOString(),
isPremium: false,
avatar: null
};
setUser(demoUser);
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 login = async (email, password) => {
// Mock login - in production, call actual API
try {
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000';
const response = await fetch(`${apiUrl}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (response.ok) {
const data = await response.json();
setUser(data.user);
localStorage.setItem('token', data.token);
return { success: true };
}
return { success: false, error: 'Login failed' };
} catch (error) {
console.error('Login error:', error);
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (error) {
return { success: false, error: error.message };
}
setUser(data.user);
return { success: true };
};
const logout = () => {
const logout = async () => {
await supabase.auth.signOut();
setUser(null);
localStorage.removeItem('token');
};
const updateUser = (updates) => {

View file

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

View file

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

View file

@ -7,7 +7,7 @@ project_id = "AeThex-Connect"
[api]
enabled = true
# Port to use for the API URL.
port = 54321
port = 3000
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
# endpoints. `public` and `graphql_public` schemas are included by default.
schemas = ["public", "graphql_public"]

View file

@ -5,33 +5,49 @@
-- CONVERSATIONS
-- ============================================================================
-- Ensure all required columns exist for index creation
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS type VARCHAR(20);
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS created_by VARCHAR;
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS gameforge_project_id VARCHAR;
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS is_archived BOOLEAN DEFAULT false;
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW();
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW();
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS title VARCHAR(200);
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS description TEXT;
ALTER TABLE conversations ADD COLUMN IF NOT EXISTS avatar_url VARCHAR(500);
CREATE TABLE IF NOT EXISTS conversations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(20) NOT NULL CHECK (type IN ('direct', 'group', 'channel')),
title VARCHAR(200),
description TEXT,
avatar_url VARCHAR(500),
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
gameforge_project_id UUID, -- For GameForge integration (future)
created_by VARCHAR REFERENCES users(id) ON DELETE SET NULL,
gameforge_project_id VARCHAR, -- For GameForge integration (future)
is_archived BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Create index on type column
CREATE INDEX idx_conversations_type ON conversations(type);
-- Create index on creator column
CREATE INDEX idx_conversations_creator ON conversations(created_by);
-- Create index on project column
CREATE INDEX idx_conversations_project ON conversations(gameforge_project_id);
-- Create index on updated_at column
CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC);
-- ============================================================================
-- CONVERSATION PARTICIPANTS
-- ============================================================================
-- Update conversation_participants to match actual types and remove reference to identities
CREATE TABLE IF NOT EXISTS conversation_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID NOT NULL REFERENCES conversations(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
identity_id UUID REFERENCES identities(id) ON DELETE SET NULL,
user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
identity_id VARCHAR,
role VARCHAR(20) DEFAULT 'member' CHECK (role IN ('admin', 'moderator', 'member')),
joined_at TIMESTAMP DEFAULT NOW(),
last_read_at TIMESTAMP,
@ -39,8 +55,11 @@ CREATE TABLE IF NOT EXISTS conversation_participants (
UNIQUE(conversation_id, user_id)
);
-- Create index on conversation column
CREATE INDEX idx_participants_conversation ON conversation_participants(conversation_id);
-- Create index on user column
CREATE INDEX idx_participants_user ON conversation_participants(user_id);
-- Create index on identity column
CREATE INDEX idx_participants_identity ON conversation_participants(identity_id);
-- ============================================================================
@ -61,25 +80,35 @@ CREATE TABLE IF NOT EXISTS messages (
created_at TIMESTAMP DEFAULT NOW()
);
-- Ensure reply_to_id column exists for index creation
ALTER TABLE messages ADD COLUMN IF NOT EXISTS reply_to_id UUID;
-- Create index on conversation and created_at columns
CREATE INDEX idx_messages_conversation ON messages(conversation_id, created_at DESC);
-- Create index on sender column
CREATE INDEX idx_messages_sender ON messages(sender_id);
-- Create index on reply_to column
CREATE INDEX idx_messages_reply_to ON messages(reply_to_id);
-- Create index on created_at column
CREATE INDEX idx_messages_created ON messages(created_at DESC);
-- ============================================================================
-- MESSAGE REACTIONS
-- ============================================================================
-- Update message_reactions to match actual types
CREATE TABLE IF NOT EXISTS message_reactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
message_id UUID NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
emoji VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(message_id, user_id, emoji)
);
-- Create index on message column
CREATE INDEX idx_reactions_message ON message_reactions(message_id);
-- Create index on user column
CREATE INDEX idx_reactions_user ON message_reactions(user_id);
-- ============================================================================
@ -101,19 +130,29 @@ CREATE TABLE IF NOT EXISTS files (
expires_at TIMESTAMP -- For temporary files
);
-- Ensure uploader_id column exists for index creation
ALTER TABLE files ADD COLUMN IF NOT EXISTS uploader_id VARCHAR;
-- Ensure conversation_id column exists for index creation
ALTER TABLE files ADD COLUMN IF NOT EXISTS conversation_id UUID;
-- Create index on uploader column
CREATE INDEX idx_files_uploader ON files(uploader_id);
-- Create index on conversation column
CREATE INDEX idx_files_conversation ON files(conversation_id);
-- Create index on created_at column
CREATE INDEX idx_files_created ON files(created_at DESC);
-- ============================================================================
-- CALLS
-- ============================================================================
-- Update calls to match actual types
CREATE TABLE IF NOT EXISTS calls (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
conversation_id UUID REFERENCES conversations(id) ON DELETE SET NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('voice', 'video')),
initiator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
initiator_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(20) DEFAULT 'ringing' CHECK (status IN ('ringing', 'active', 'ended', 'missed', 'declined')),
started_at TIMESTAMP,
ended_at TIMESTAMP,
@ -121,26 +160,33 @@ CREATE TABLE IF NOT EXISTS calls (
created_at TIMESTAMP DEFAULT NOW()
);
-- Create index on conversation column
CREATE INDEX idx_calls_conversation ON calls(conversation_id);
-- Create index on initiator column
CREATE INDEX idx_calls_initiator ON calls(initiator_id);
-- Create index on status column
CREATE INDEX idx_calls_status ON calls(status);
-- Create index on created_at column
CREATE INDEX idx_calls_created ON calls(created_at DESC);
-- ============================================================================
-- CALL PARTICIPANTS
-- ============================================================================
-- Update call_participants to match actual types
CREATE TABLE IF NOT EXISTS call_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
call_id UUID NOT NULL REFERENCES calls(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
joined_at TIMESTAMP,
left_at TIMESTAMP,
media_state JSONB DEFAULT '{"audio": true, "video": false, "screen_share": false}'::jsonb,
UNIQUE(call_id, user_id)
);
-- Create index on call column
CREATE INDEX idx_call_participants_call ON call_participants(call_id);
-- Create index on user column
CREATE INDEX idx_call_participants_user ON call_participants(user_id);
-- ============================================================================

View file

@ -36,6 +36,10 @@ CREATE TABLE IF NOT EXISTS audit_logs (
created_at TIMESTAMP DEFAULT NOW()
);
-- Ensure resource_type column exists for index creation
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS resource_type VARCHAR;
ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS resource_id VARCHAR;
-- Indexes for performance
CREATE INDEX IF NOT EXISTS idx_gameforge_integrations_project_id ON gameforge_integrations(project_id);
CREATE INDEX IF NOT EXISTS idx_conversations_gameforge_project ON conversations(gameforge_project_id) WHERE gameforge_project_id IS NOT NULL;

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 TABLE IF NOT EXISTS turn_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
username VARCHAR(100) NOT NULL,
credential VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),

View file

@ -1,6 +1,11 @@
-- Migration 005: Nexus Cross-Platform Integration
-- Adds friend system, game sessions, lobbies, and enhanced Nexus features
-- Ensure nexus_integrations table exists before ALTER TABLE
CREATE TABLE IF NOT EXISTS nexus_integrations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid()
);
-- Extend nexus_integrations table with session and overlay config
ALTER TABLE nexus_integrations
ADD COLUMN IF NOT EXISTS current_game_session_id UUID,
@ -12,8 +17,8 @@ ADD COLUMN IF NOT EXISTS overlay_position VARCHAR(20) DEFAULT 'top-right';
-- Friend requests table
CREATE TABLE IF NOT EXISTS friend_requests (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
from_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
to_user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
from_user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
to_user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status VARCHAR(20) DEFAULT 'pending', -- pending, accepted, rejected
created_at TIMESTAMP DEFAULT NOW(),
responded_at TIMESTAMP,
@ -26,8 +31,8 @@ CREATE INDEX IF NOT EXISTS idx_friend_requests_from ON friend_requests(from_user
-- Friendships table
CREATE TABLE IF NOT EXISTS friendships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user1_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user2_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user1_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user2_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT NOW(),
CHECK (user1_id < user2_id), -- Prevent duplicates
UNIQUE(user1_id, user2_id)
@ -50,6 +55,15 @@ CREATE TABLE IF NOT EXISTS game_sessions (
metadata JSONB -- {mapName, gameMode, score, etc.}
);
-- Ensure started_at column exists for index creation
ALTER TABLE game_sessions ADD COLUMN IF NOT EXISTS started_at TIMESTAMP;
-- Ensure session_state column exists for index creation
ALTER TABLE game_sessions ADD COLUMN IF NOT EXISTS session_state VARCHAR;
-- Ensure nexus_player_id column exists for index creation
ALTER TABLE game_sessions ADD COLUMN IF NOT EXISTS nexus_player_id VARCHAR;
CREATE INDEX IF NOT EXISTS idx_game_sessions_user ON game_sessions(user_id, started_at DESC);
CREATE INDEX IF NOT EXISTS idx_game_sessions_active ON game_sessions(user_id, session_state);
CREATE INDEX IF NOT EXISTS idx_game_sessions_nexus_player ON game_sessions(nexus_player_id);
@ -59,7 +73,7 @@ CREATE TABLE IF NOT EXISTS game_lobbies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
game_id VARCHAR(100) NOT NULL,
lobby_code VARCHAR(50) UNIQUE,
host_user_id UUID NOT NULL REFERENCES users(id),
host_user_id VARCHAR NOT NULL REFERENCES users(id),
conversation_id UUID REFERENCES conversations(id), -- Auto-created chat
max_players INTEGER DEFAULT 8,
is_public BOOLEAN DEFAULT false,
@ -77,7 +91,7 @@ CREATE INDEX IF NOT EXISTS idx_game_lobbies_code ON game_lobbies(lobby_code);
CREATE TABLE IF NOT EXISTS game_lobby_participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lobby_id UUID NOT NULL REFERENCES game_lobbies(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user_id VARCHAR NOT NULL REFERENCES users(id) ON DELETE CASCADE,
team_id VARCHAR(20), -- For team-based games
ready BOOLEAN DEFAULT false,
joined_at TIMESTAMP DEFAULT NOW(),

View file

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

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