deleted: astro-site/src/pages/mockup.jsx

This commit is contained in:
Anderson 2026-02-07 01:28:39 +00:00 committed by GitHub
parent 7f4107c13f
commit 770d0e38ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
186 changed files with 1910 additions and 35847 deletions

View file

@ -1,5 +1,5 @@
{
"_variables": {
"lastUpdateCheck": 1768189288502
"lastUpdateCheck": 1770417750117
}
}

View file

@ -5,4 +5,8 @@ import react from '@astrojs/react';
export default defineConfig({
integrations: [tailwind(), react()],
site: 'https://aethex-connect.com',
server: {
port: 4321,
host: 'localhost'
}
});

View file

@ -0,0 +1,275 @@
/**
* AeThex Provider
* Main context provider for AeThex Connect - handles auth, chat, and real-time features
*/
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { io } from 'socket.io-client';
const API_URL = import.meta.env?.VITE_API_URL || 'http://localhost:3000';
const API_BASE = `${API_URL}/api`;
const AeThexContext = createContext(null);
export function useAeThex() {
return useContext(AeThexContext);
}
export function AeThexProvider({ children }) {
// Auth state
const [user, setUser] = useState(null);
const [token, setTokenState] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Socket state
const [socket, setSocket] = useState(null);
const [connected, setConnected] = useState(false);
// Chat state
const [servers, setServers] = useState([]);
const [channels, setChannels] = useState([]);
const [messages, setMessages] = useState([]);
const [currentServer, setCurrentServer] = useState(null);
const [currentChannel, setCurrentChannel] = useState(null);
const [onlineUsers, setOnlineUsers] = useState([]);
// Token management
const setToken = useCallback((newToken) => {
setTokenState(newToken);
if (newToken) {
localStorage.setItem('aethex_token', newToken);
} else {
localStorage.removeItem('aethex_token');
}
}, []);
// API request helper
const apiRequest = useCallback(async (endpoint, options = {}) => {
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
const currentToken = token || localStorage.getItem('aethex_token');
if (currentToken) {
headers['Authorization'] = `Bearer ${currentToken}`;
}
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Request failed');
}
return data;
}, [token]);
// Auth functions
const login = useCallback(async (email, password) => {
setError(null);
setLoading(true);
try {
const response = await apiRequest('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
if (response.success) {
setToken(response.data.token);
setUser(response.data.user);
return { success: true };
}
throw new Error(response.error || 'Login failed');
} catch (err) {
setError(err.message);
return { success: false, error: err.message };
} finally {
setLoading(false);
}
}, [apiRequest, setToken]);
const register = useCallback(async (email, password, username, displayName) => {
setError(null);
setLoading(true);
try {
const response = await apiRequest('/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, username, displayName }),
});
if (response.success) {
setToken(response.data.token);
setUser(response.data.user);
return { success: true };
}
throw new Error(response.error || 'Registration failed');
} catch (err) {
setError(err.message);
return { success: false, error: err.message };
} finally {
setLoading(false);
}
}, [apiRequest, setToken]);
const demoLogin = useCallback(async () => {
setError(null);
setLoading(true);
try {
const response = await apiRequest('/auth/demo', {
method: 'POST',
});
if (response.success) {
setToken(response.data.token);
setUser(response.data.user);
return { success: true };
}
throw new Error(response.error || 'Demo login failed');
} catch (err) {
setError(err.message);
return { success: false, error: err.message };
} finally {
setLoading(false);
}
}, [apiRequest, setToken]);
const logout = useCallback(() => {
setToken(null);
setUser(null);
if (socket) {
socket.disconnect();
setSocket(null);
}
setConnected(false);
}, [socket, setToken]);
// Socket connection
const connectSocket = useCallback((authToken) => {
if (socket) {
socket.disconnect();
}
const newSocket = io(API_URL, {
auth: { token: authToken },
reconnection: true,
reconnectionDelay: 1000,
reconnectionAttempts: 10,
transports: ['websocket', 'polling']
});
newSocket.on('connect', () => {
console.log('✓ Connected to AeThex Connect');
setConnected(true);
});
newSocket.on('disconnect', () => {
console.log('✗ Disconnected from AeThex Connect');
setConnected(false);
});
newSocket.on('message:new', (message) => {
setMessages(prev => [...prev, message]);
});
newSocket.on('presence:online', (users) => {
setOnlineUsers(users);
});
setSocket(newSocket);
return newSocket;
}, [socket]);
// Chat functions
const sendMessage = useCallback((content) => {
if (!socket || !connected || !currentChannel) return;
socket.emit('channel:message', {
channelId: currentChannel.id,
content
});
}, [socket, connected, currentChannel]);
const joinChannel = useCallback((channelId) => {
if (!socket || !connected) return;
socket.emit('channel:join', { channelId });
}, [socket, connected]);
// Check auth on mount
useEffect(() => {
const checkAuth = async () => {
const storedToken = localStorage.getItem('aethex_token');
if (storedToken) {
setTokenState(storedToken);
try {
const response = await fetch(`${API_BASE}/auth/me`, {
headers: { 'Authorization': `Bearer ${storedToken}` }
});
const data = await response.json();
if (data.success) {
setUser(data.data);
connectSocket(storedToken);
} else {
localStorage.removeItem('aethex_token');
}
} catch (err) {
console.error('Auth check failed:', err);
localStorage.removeItem('aethex_token');
}
}
setLoading(false);
};
checkAuth();
return () => {
if (socket) {
socket.disconnect();
}
};
}, []);
// Connect socket when user logs in
useEffect(() => {
if (user && token && !socket) {
connectSocket(token);
}
}, [user, token]);
const value = {
// Auth
user,
loading,
error,
isAuthenticated: !!user,
login,
register,
demoLogin,
logout,
// Socket
socket,
connected,
// Chat
servers,
channels,
messages,
currentServer,
currentChannel,
onlineUsers,
setCurrentServer,
setCurrentChannel,
setMessages,
sendMessage,
joinChannel,
// API helper
apiRequest
};
return (
<AeThexContext.Provider value={value}>
{children}
</AeThexContext.Provider>
);
}

View file

@ -1,18 +1,13 @@
import React, { useState } from "react";
import { useMatrix } from "../matrix/MatrixProvider.jsx";
import { useAeThex } from "../aethex/AeThexProvider.jsx";
/**
* UI for linking AeThex and Matrix accounts.
* 1. User logs in with AeThex credentials (simulate for now)
* 2. User logs in with Matrix credentials
* 3. Store mapping in localStorage (or call backend in real app)
* UI for AeThex account login/registration
*/
export default function AccountLinker({ onLinked }) {
const matrixCtx = useMatrix();
if (!matrixCtx) {
const aethex = useAeThex();
if (!aethex) {
return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-[#1a1a2e] via-[#23234a] to-[#0f3460]">
<div className="relative flex flex-col items-center">
@ -24,7 +19,7 @@ export default function AccountLinker({ onLinked }) {
</div>
<div className="toast bg-[#23234a] text-white px-6 py-4 rounded-xl shadow-lg border border-pink-400 animate-fade-in">
<span className="font-bold text-pink-400">Hang tight!</span> <br />
<span className="text-sm text-blue-200">Were prepping your login experience</span>
<span className="text-sm text-blue-200">We're prepping your login experience</span>
</div>
</div>
<style>{`
@ -42,61 +37,60 @@ export default function AccountLinker({ onLinked }) {
</div>
);
}
const { login } = matrixCtx;
const [aethexUser, setAethexUser] = useState("");
const [aethexPass, setAethexPass] = useState("");
const [matrixUser, setMatrixUser] = useState("");
const [matrixPass, setMatrixPass] = useState("");
const [step, setStep] = useState(1);
const [error, setError] = useState(null);
const { login, register, demoLogin, loading, error, isAuthenticated, user } = aethex;
const [mode, setMode] = useState('login');
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [username, setUsername] = useState("");
const [displayName, setDisplayName] = useState("");
const [formError, setFormError] = useState(null);
// Simulate AeThex auth (replace with real API call)
const handleAethexLogin = (e) => {
// Already authenticated
if (isAuthenticated && user) {
if (onLinked) onLinked({ user });
return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-[#1a1a2e] via-[#23234a] to-[#0f3460]">
<div className="text-white text-lg">Welcome, {user.displayName || user.username}!</div>
</div>
);
}
const handleSubmit = async (e) => {
e.preventDefault();
if (aethexUser && aethexPass) {
setStep(2);
setError(null);
} else {
setError("Enter AeThex username and password.");
setFormError(null);
try {
let result;
if (mode === 'login') {
result = await login(email, password);
} else {
result = await register(email, password, username, displayName);
}
if (result.success && onLinked) {
onLinked({ user: result.user });
} else if (!result.success) {
setFormError(result.error || 'Authentication failed');
}
} catch (err) {
setFormError(err.message || 'An error occurred');
}
};
// Simulate Matrix auth (let MatrixProvider handle real login)
const handleMatrixLogin = (e) => {
e.preventDefault();
if (matrixUser && matrixPass) {
// Store mapping (simulate)
localStorage.setItem("aethex-matrix-link", JSON.stringify({ aethexUser, matrixUser }));
setError(null);
if (onLinked) onLinked({ aethexUser, matrixUser, matrixPass });
} else {
setError("Enter Matrix username and password.");
}
};
// Demo login handler
const handleDemoLogin = async () => {
setAethexUser('demo');
setAethexPass('demo123');
setStep(2);
setTimeout(async () => {
setMatrixUser('@mrpiglr:matrix.org');
setMatrixPass('Max!FTW2023!');
setTimeout(async () => {
localStorage.setItem("aethex-matrix-link", JSON.stringify({ aethexUser: 'demo', matrixUser: '@mrpiglr:matrix.org' }));
setError(null);
if (login) {
try {
await login('@mrpiglr:matrix.org', 'Max!FTW2023!');
} catch (e) {
setError(e.message || 'Login failed.');
return;
}
}
if (onLinked) onLinked({ aethexUser: 'demo', matrixUser: '@mrpiglr:matrix.org', matrixPass: 'Max!FTW2023!' });
// window.location.href = '/';
}, 500);
}, 500);
setFormError(null);
try {
const result = await demoLogin();
if (result.success && onLinked) {
onLinked({ user: result.user });
} else if (!result.success) {
setFormError(result.error || 'Demo login failed');
}
} catch (err) {
setFormError(err.message || 'Demo login failed');
}
};
return (
@ -104,54 +98,94 @@ export default function AccountLinker({ onLinked }) {
<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>
<button onClick={handleDemoLogin} className="mb-4 w-full bg-gradient-to-r from-yellow-400 to-pink-500 text-white rounded-lg py-2 font-bold shadow hover:from-yellow-500 hover:to-pink-600 transition">Demo Login</button>
{error && <div className="mb-2 text-red-400 text-center w-full">{error}</div>}
{/* Show MatrixProvider error if present */}
{matrixCtx && matrixCtx.error && (
<div className="mb-2 text-red-400 text-center w-full">Matrix error: {matrixCtx.error}</div>
<h2 className="text-lg font-semibold mb-6 text-blue-300 text-center">
{mode === 'login' ? 'Sign in to your account' : 'Create your account'}
</h2>
<button
onClick={handleDemoLogin}
disabled={loading}
className="mb-4 w-full bg-gradient-to-r from-yellow-400 to-pink-500 text-white rounded-lg py-2 font-bold shadow hover:from-yellow-500 hover:to-pink-600 transition disabled:opacity-50"
>
{loading ? 'Loading...' : '🚀 Try Demo'}
</button>
<div className="flex items-center w-full mb-4">
<div className="flex-1 border-t border-gray-600"></div>
<span className="px-3 text-gray-400 text-sm">or</span>
<div className="flex-1 border-t border-gray-600"></div>
</div>
{(formError || error) && (
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400 text-center w-full text-sm">
{formError || error}
</div>
)}
{step === 1 && (
<form onSubmit={handleAethexLogin} className="flex flex-col gap-4 w-full">
<input
type="text"
placeholder="AeThex Username"
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={aethexUser}
onChange={e => setAethexUser(e.target.value)}
autoFocus
/>
<input
type="password"
placeholder="AeThex 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={aethexPass}
onChange={e => setAethexPass(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">Continue to Matrix</button>
</form>
)}
{step === 2 && (
<form onSubmit={handleMatrixLogin} className="flex flex-col gap-4 w-full">
<input
type="text"
placeholder="Matrix Username (@user:matrix.org)"
className="px-4 py-3 rounded-lg bg-[#23234a] text-white border border-[#2d2d5a] focus:outline-none focus:ring-2 focus:ring-green-500 transition"
value={matrixUser}
onChange={e => setMatrixUser(e.target.value)}
autoFocus
/>
<input
type="password"
placeholder="Matrix Password"
className="px-4 py-3 rounded-lg bg-[#23234a] text-white border border-[#2d2d5a] focus:outline-none focus:ring-2 focus:ring-green-500 transition"
value={matrixPass}
onChange={e => setMatrixPass(e.target.value)}
/>
<button type="submit" className="bg-gradient-to-r from-green-500 to-blue-500 text-white rounded-lg py-3 font-bold mt-2 shadow-lg hover:from-green-600 hover:to-blue-600 transition">Link Accounts & Login</button>
</form>
)}
<div className="mt-6 text-center text-gray-400 text-xs w-full">
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full">
{mode === 'register' && (
<>
<input
type="text"
placeholder="Username"
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={username}
onChange={e => setUsername(e.target.value)}
/>
<input
type="text"
placeholder="Display Name (optional)"
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={displayName}
onChange={e => setDisplayName(e.target.value)}
/>
</>
)}
<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)}
required
/>
<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)}
required
minLength={8}
/>
<button
type="submit"
disabled={loading}
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:opacity-50"
>
{loading ? 'Please wait...' : (mode === 'login' ? 'Sign In' : 'Create Account')}
</button>
</form>
<div className="mt-6 text-center text-gray-400 text-sm">
{mode === 'login' ? (
<span>
Don't have an account?{' '}
<button onClick={() => setMode('register')} className="text-blue-400 hover:text-blue-300 underline">
Sign up
</button>
</span>
) : (
<span>
Already have an account?{' '}
<button onClick={() => setMode('login')} className="text-blue-400 hover:text-blue-300 underline">
Sign in
</button>
</span>
)}
</div>
<div className="mt-4 text-center text-gray-500 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>

View file

@ -0,0 +1,215 @@
/**
* LoginForm Component
* Handles user authentication for AeThex Connect
*/
import React, { useState } from "react";
import { useAeThex } from "../aethex/AeThexProvider.jsx";
import { PRESET_IMAGES, getAvatarImage } from "../../utils/unsplash.js";
export default function LoginForm() {
const context = useAeThex();
const [mode, setMode] = useState('login'); // 'login' or 'register'
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [username, setUsername] = useState('');
const [displayName, setDisplayName] = useState('');
const [formError, setFormError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
// Loading state while checking auth
if (!context) {
return (
<div
className="flex items-center justify-center min-h-screen"
style={{
backgroundImage: `linear-gradient(rgba(26, 26, 46, 0.85), rgba(15, 52, 96, 0.9)), url(${PRESET_IMAGES.loginBackground})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
<div className="relative flex flex-col items-center">
<div className="w-16 h-16 mb-6">
<svg className="animate-spin" viewBox="0 0 50 50">
<circle className="opacity-20" cx="25" cy="25" r="20" stroke="#fff" strokeWidth="5" fill="none" />
<circle cx="25" cy="25" r="20" stroke="#ff6bcb" strokeWidth="5" fill="none" strokeDasharray="100" strokeDashoffset="60" />
</svg>
</div>
<div className="text-white text-lg">Loading...</div>
</div>
</div>
);
}
const { login, register, demoLogin, loading, error, isAuthenticated, user } = context;
// If already authenticated, redirect to app
if (isAuthenticated && user) {
window.location.href = '/app';
return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-[#1a1a2e] via-[#23234a] to-[#0f3460]">
<div className="text-white text-lg">Welcome, {user.displayName || user.username}! Redirecting...</div>
</div>
);
}
const handleSubmit = async (e) => {
e.preventDefault();
setFormError(null);
setIsSubmitting(true);
try {
let result;
if (mode === 'login') {
result = await login(email, password);
} else {
result = await register(email, password, username, displayName);
}
if (result.success) {
window.location.href = '/app';
} else {
setFormError(result.error || 'Authentication failed');
}
} catch (err) {
setFormError(err.message || 'An error occurred');
} finally {
setIsSubmitting(false);
}
};
const handleDemoLogin = async () => {
setFormError(null);
setIsSubmitting(true);
try {
const result = await demoLogin();
if (result.success) {
window.location.href = '/app';
} else {
setFormError(result.error || 'Demo login failed');
}
} catch (err) {
setFormError(err.message || 'Demo login failed');
} finally {
setIsSubmitting(false);
}
};
return (
<div
className="login-bg min-h-screen flex flex-col items-center justify-center"
style={{
backgroundImage: `linear-gradient(135deg, rgba(26, 26, 46, 0.88) 0%, rgba(22, 33, 62, 0.9) 50%, rgba(15, 52, 96, 0.85) 100%), url(${PRESET_IMAGES.loginBackground})`,
backgroundSize: 'cover',
backgroundPosition: 'center'
}}
>
<div className="bg-[#181818cc] backdrop-blur-sm 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">
{mode === 'login' ? 'Sign in to your account' : 'Create your account'}
</h2>
{/* Demo Login Button */}
<button
onClick={handleDemoLogin}
disabled={isSubmitting}
className="mb-4 w-full bg-gradient-to-r from-yellow-400 to-pink-500 text-white rounded-lg py-2 font-bold shadow hover:from-yellow-500 hover:to-pink-600 transition disabled:opacity-50"
>
{isSubmitting ? 'Loading...' : '🚀 Try Demo'}
</button>
<div className="flex items-center w-full mb-4">
<div className="flex-1 border-t border-gray-600"></div>
<span className="px-3 text-gray-400 text-sm">or</span>
<div className="flex-1 border-t border-gray-600"></div>
</div>
{/* Error Display */}
{(formError || error) && (
<div className="mb-4 p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400 text-center w-full text-sm">
{formError || error}
</div>
)}
{/* Login/Register Form */}
<form onSubmit={handleSubmit} className="flex flex-col gap-4 w-full">
{mode === 'register' && (
<>
<input
type="text"
placeholder="Username"
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={username}
onChange={e => setUsername(e.target.value)}
/>
<input
type="text"
placeholder="Display Name (optional)"
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={displayName}
onChange={e => setDisplayName(e.target.value)}
/>
</>
)}
<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)}
required
/>
<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)}
required
minLength={8}
/>
<button
type="submit"
disabled={isSubmitting}
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:opacity-50"
>
{isSubmitting ? 'Please wait...' : (mode === 'login' ? 'Sign In' : 'Create Account')}
</button>
</form>
{/* Toggle Mode */}
<div className="mt-6 text-center text-gray-400 text-sm">
{mode === 'login' ? (
<span>
Don't have an account?{' '}
<button onClick={() => setMode('register')} className="text-blue-400 hover:text-blue-300 underline">
Sign up
</button>
</span>
) : (
<span>
Already have an account?{' '}
<button onClick={() => setMode('login')} className="text-blue-400 hover:text-blue-300 underline">
Sign in
</button>
</span>
)}
</div>
<div className="mt-4 text-center text-gray-500 text-xs">
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>.
</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,11 +1,11 @@
import React from "react";
import { MatrixProvider } from "../matrix/MatrixProvider.jsx";
import AccountLinker from "./AccountLinker.jsx";
import { AeThexProvider } from "../aethex/AeThexProvider.jsx";
import LoginForm from "./LoginForm.jsx";
export default function LoginIsland() {
return (
<MatrixProvider>
<AccountLinker onLinked={() => {}} />
</MatrixProvider>
<AeThexProvider>
<LoginForm />
</AeThexProvider>
);
}

View file

@ -1,70 +0,0 @@
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,30 +1,28 @@
import React, { useEffect } from "react";
import Message from "./Message";
import MessageInput from "./MessageInput";
import { useMatrix } from "../matrix/MatrixProvider.jsx";
import { useAeThex } from "../aethex/AeThexProvider.jsx";
import DemoLoginButton from "./DemoLoginButton.jsx";
// Default room to join (replace with your Matrix room ID)
const DEFAULT_ROOM_ID = "!foundation:matrix.org";
// Default channel to join
const DEFAULT_CHANNEL_ID = "general";
export default function ChatArea() {
const { messages, joinRoom, currentRoomId, user, login, loading } = useMatrix();
const { messages, joinChannel, currentChannelId, user, demoLogin, loading, isAuthenticated } = useAeThex();
// Join the default room on login
// Join the default channel on login
useEffect(() => {
if (user && !currentRoomId) {
joinRoom(DEFAULT_ROOM_ID);
if (isAuthenticated && user && !currentChannelId) {
joinChannel(DEFAULT_CHANNEL_ID);
}
}, [user, currentRoomId, joinRoom]);
}, [isAuthenticated, user, currentChannelId, joinChannel]);
// Demo login handler
const handleDemoLogin = () => {
// Use a public Matrix test account or a known demo account
// You can change these credentials as needed
login("@mrpiglr:matrix.org", "Max!FTW2023!", "https://matrix.org");
const handleDemoLogin = async () => {
await demoLogin();
};
if (!user) {
if (!isAuthenticated || !user) {
return (
<div className="chat-area flex flex-col flex-1 bg-[#0a0a0a] items-center justify-center">
<DemoLoginButton onDemoLogin={handleDemoLogin} />
@ -49,7 +47,7 @@ export default function ChatArea() {
<div className="chat-messages flex-1 overflow-y-auto px-5 py-5">
{messages && messages.length > 0 ? (
messages.map((msg, i) => (
<Message key={i} {...matrixToMessageProps(msg)} />
<Message key={msg.id || i} {...messageToProps(msg)} />
))
) : (
<div className="text-gray-500 text-center">No messages yet.</div>
@ -65,19 +63,15 @@ export default function ChatArea() {
}
// Helper to convert Matrix event to Message props
function matrixToMessageProps(event) {
if (!event) return {};
if (event.type === "m.room.message") {
return {
type: "user",
author: event.sender?.split(":")[0]?.replace("@", "") || "User",
text: event.content?.body || "",
time: new Date(event.origin_server_ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
avatar: event.sender?.charAt(1)?.toUpperCase() || "U",
avatarBg: "from-blue-600 to-blue-900",
};
}
// Add system message mapping if needed
return { type: "system", label: "MATRIX", text: JSON.stringify(event), className: "foundation" };
// Helper to convert AeThex message to Message props
function messageToProps(msg) {
if (!msg) return {};
return {
type: "user",
author: msg.sender?.display_name || msg.sender?.username || "User",
text: msg.content || "",
time: new Date(msg.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }),
avatar: (msg.sender?.display_name || msg.sender?.username || "U").charAt(0).toUpperCase(),
avatarBg: "from-blue-600 to-blue-900",
};
}

View file

@ -1,16 +1,16 @@
import React, { useState } from "react";
import { useMatrix } from "../matrix/MatrixProvider.jsx";
import { useAeThex } from "../aethex/AeThexProvider.jsx";
const DEFAULT_ROOM_ID = "!foundation:matrix.org";
const DEFAULT_CHANNEL_ID = "general";
export default function MessageInput() {
const [text, setText] = useState("");
const { sendMessage, user, currentRoomId } = useMatrix();
const { sendMessage, user, currentChannelId, isAuthenticated } = useAeThex();
const handleSend = async (e) => {
e.preventDefault();
if (!text.trim() || !user) return;
await sendMessage(currentRoomId || DEFAULT_ROOM_ID, text);
if (!text.trim() || !user || !isAuthenticated) return;
await sendMessage(currentChannelId || DEFAULT_CHANNEL_ID, text);
setText("");
};
@ -24,7 +24,7 @@ export default function MessageInput() {
maxLength={2000}
value={text}
onChange={e => setText(e.target.value)}
disabled={!user}
disabled={!user || !isAuthenticated}
/>
<button type="submit" className="sendButton w-10 h-10 flex items-center justify-center rounded bg-blue-600 text-xl text-white ml-2">3a4</button>
</form>

View file

@ -1,7 +1,7 @@
import React, { createContext, useContext, useRef, useState, useEffect, useCallback } from "react";
import Peer from "simple-peer";
import { useMatrix } from "../matrix/MatrixProvider.jsx";
import { useAeThex } from "../aethex/AeThexProvider.jsx";
const WebRTCContext = createContext(null);
@ -13,31 +13,32 @@ export function WebRTCProvider({ children }) {
const [peers, setPeers] = useState([]); // [{ peerId, peer, stream }]
const [localStream, setLocalStream] = useState(null);
const [joined, setJoined] = useState(false);
const [currentVoiceChannel, setCurrentVoiceChannel] = useState(null);
const peersRef = useRef({});
const matrix = useMatrix();
if (!matrix) {
// Optionally render a fallback or nothing if Matrix context is not available
const aethex = useAeThex();
if (!aethex) {
return null;
}
const { client, user, currentRoomId } = matrix;
const SIGNAL_EVENT = "org.aethex.voice.signal";
const VOICE_ROOM = currentRoomId || "!foundation:matrix.org";
const { socket, user, isAuthenticated } = aethex;
// Helper: Send signal via Matrix event
// Helper: Send signal via Socket.io
const sendSignal = useCallback((to, data) => {
if (!client || !VOICE_ROOM) return;
client.sendEvent(VOICE_ROOM, SIGNAL_EVENT, { to, from: user?.userId, data }, "");
}, [client, VOICE_ROOM, user]);
if (!socket || !currentVoiceChannel) return;
socket.emit('voice:signal', { to, channelId: currentVoiceChannel, data });
}, [socket, currentVoiceChannel]);
// Join a voice channel (start local audio, announce self)
const joinVoice = async () => {
if (localStream || !client || !user) return;
const joinVoice = async (channelId) => {
if (localStream || !socket || !user || !isAuthenticated) return;
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
setLocalStream(stream);
setCurrentVoiceChannel(channelId);
setJoined(true);
// Announce self to others
client.sendEvent(VOICE_ROOM, SIGNAL_EVENT, { join: true, from: user.userId }, "");
socket.emit('voice:join', { channelId });
} catch (err) {
alert("Could not access microphone: " + err.message);
}
@ -53,57 +54,78 @@ export function WebRTCProvider({ children }) {
peersRef.current = {};
setPeers([]);
setJoined(false);
if (client && user) {
client.sendEvent(VOICE_ROOM, SIGNAL_EVENT, { leave: true, from: user.userId }, "");
if (socket && currentVoiceChannel) {
socket.emit('voice:leave', { channelId: currentVoiceChannel });
}
setCurrentVoiceChannel(null);
};
// Handle incoming Matrix signal events
// Handle incoming Socket.io signal events
useEffect(() => {
if (!client || !user || !VOICE_ROOM) return;
const handler = (event) => {
if (event.getType() !== SIGNAL_EVENT) return;
const { from, to, data, join, leave } = event.getContent();
if (from === user.userId) return;
// Handle join: create peer if not exists
if (join) {
if (!peersRef.current[from]) {
const initiator = user.userId > from; // simple deterministic initiator
const peer = new Peer({ initiator, trickle: false, stream: localStream });
peer.on("signal", signal => sendSignal(from, signal));
peer.on("stream", remoteStream => {
setPeers(p => [...p, { peerId: from, peer, stream: remoteStream }]);
});
peer.on("close", () => {
setPeers(p => p.filter(x => x.peerId !== from));
delete peersRef.current[from];
});
peersRef.current[from] = { peer };
}
}
// Handle leave: remove peer
if (leave && peersRef.current[from]) {
peersRef.current[from].peer.destroy();
delete peersRef.current[from];
setPeers(p => p.filter(x => x.peerId !== from));
}
// Handle signal: pass to peer
if (data && peersRef.current[from]) {
peersRef.current[from].peer.signal(data);
if (!socket || !user || !isAuthenticated) return;
const handleUserJoined = ({ userId, channelId }) => {
if (userId === user.id || channelId !== currentVoiceChannel || !localStream) return;
if (!peersRef.current[userId]) {
const initiator = user.id > userId; // deterministic initiator
const peer = new Peer({ initiator, trickle: false, stream: localStream });
peer.on("signal", signal => sendSignal(userId, signal));
peer.on("stream", remoteStream => {
setPeers(p => [...p, { peerId: userId, peer, stream: remoteStream }]);
});
peer.on("close", () => {
setPeers(p => p.filter(x => x.peerId !== userId));
delete peersRef.current[userId];
});
peersRef.current[userId] = { peer };
}
};
client.on("event", handler);
return () => client.removeListener("event", handler);
}, [client, user, VOICE_ROOM, localStream, sendSignal]);
// Announce self to new joiners
useEffect(() => {
if (!joined || !client || !user) return;
client.sendEvent(VOICE_ROOM, SIGNAL_EVENT, { join: true, from: user.userId }, "");
}, [joined, client, user, VOICE_ROOM]);
const handleUserLeft = ({ userId }) => {
if (peersRef.current[userId]) {
peersRef.current[userId].peer.destroy();
delete peersRef.current[userId];
setPeers(p => p.filter(x => x.peerId !== userId));
}
};
const handleSignal = ({ from, data }) => {
if (peersRef.current[from]) {
peersRef.current[from].peer.signal(data);
} else if (localStream) {
// Create peer for late joiners
const peer = new Peer({ initiator: false, trickle: false, stream: localStream });
peer.on("signal", signal => sendSignal(from, signal));
peer.on("stream", remoteStream => {
setPeers(p => [...p, { peerId: from, peer, stream: remoteStream }]);
});
peer.on("close", () => {
setPeers(p => p.filter(x => x.peerId !== from));
delete peersRef.current[from];
});
peer.signal(data);
peersRef.current[from] = { peer };
}
};
socket.on('voice:user_joined', handleUserJoined);
socket.on('voice:user_left', handleUserLeft);
socket.on('voice:signal', handleSignal);
return () => {
socket.off('voice:user_joined', handleUserJoined);
socket.off('voice:user_left', handleUserLeft);
socket.off('voice:signal', handleSignal);
};
}, [socket, user, isAuthenticated, currentVoiceChannel, localStream, sendSignal]);
return (
<WebRTCContext.Provider value={{ peers, localStream, joined, joinVoice, leaveVoice }}>
<WebRTCContext.Provider value={{ peers, localStream, joined, joinVoice, leaveVoice, currentVoiceChannel }}>
{children}
</WebRTCContext.Provider>
);

View file

@ -1,2 +1,2 @@
// Removed: This page is deprecated. Use /app for the full platform UI.
// Removed: This page is deprecated. Use /app for the full platform UI.

View file

@ -0,0 +1,154 @@
---
/**
* Image Gallery Demo - Shows available Unsplash assets
*/
---
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AeThex Image Assets</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
color: white;
padding: 40px 20px;
}
h1 { text-align: center; margin-bottom: 40px; font-size: 2.5rem; }
h2 { margin: 30px 0 20px; color: #60a5fa; border-bottom: 1px solid #334155; padding-bottom: 10px; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; max-width: 1400px; margin: 0 auto; }
.card {
background: rgba(30, 41, 59, 0.8);
border-radius: 12px;
overflow: hidden;
border: 1px solid #334155;
}
.card img { width: 100%; height: 200px; object-fit: cover; }
.card-body { padding: 16px; }
.card-title { font-weight: 600; margin-bottom: 8px; }
.card-url { font-size: 12px; color: #94a3b8; word-break: break-all; }
.avatars { display: flex; gap: 16px; flex-wrap: wrap; justify-content: center; margin: 20px 0; }
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
border: 3px solid #60a5fa;
background: #1e293b;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: #60a5fa;
text-decoration: none;
}
.back-link:hover { text-decoration: underline; }
</style>
</head>
<body>
<a href="/" class="back-link">← Back to Home</a>
<h1>🎨 AeThex Image Assets</h1>
<p style="text-align: center; color: #94a3b8; margin-bottom: 40px;">
Free stock images from Unsplash + DiceBear avatars
</p>
<h2>📸 Preset Background Images</h2>
<div class="grid">
<div class="card">
<img src="https://images.unsplash.com/photo-1538481199705-c710c4e965fc?w=600&q=80" alt="Hero" loading="lazy" />
<div class="card-body">
<div class="card-title">Hero Background</div>
<div class="card-url">Gaming setup - perfect for landing pages</div>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=600&q=80" alt="Login" loading="lazy" />
<div class="card-body">
<div class="card-title">Login Background</div>
<div class="card-url">Retro gaming aesthetic</div>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1614850523459-c2f4c699c52e?w=600&q=80" alt="Chat" loading="lazy" />
<div class="card-body">
<div class="card-title">Chat Background</div>
<div class="card-url">Dark abstract pattern</div>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1542751371-adc38448a05e?w=600&q=80" alt="Server" loading="lazy" />
<div class="card-body">
<div class="card-title">Server Banner</div>
<div class="card-url">Esports/competitive gaming</div>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1511512578047-dfb367046420?w=600&q=80" alt="Profile" loading="lazy" />
<div class="card-body">
<div class="card-title">Profile Banner</div>
<div class="card-url">Gaming aesthetic</div>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1598488035139-bdbb2231ce04?w=600&q=80" alt="Voice" loading="lazy" />
<div class="card-body">
<div class="card-title">Voice Channel</div>
<div class="card-url">Audio waves visualization</div>
</div>
</div>
<div class="card">
<img src="https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=600&q=80" alt="Premium" loading="lazy" />
<div class="card-body">
<div class="card-title">Premium Banner</div>
<div class="card-url">Purple gradient - perfect for upgrades</div>
</div>
</div>
</div>
<h2>👤 DiceBear Avatars</h2>
<p style="color: #94a3b8; margin-bottom: 20px;">Auto-generated based on username seed</p>
<div class="avatars">
<img class="avatar" src="https://api.dicebear.com/7.x/adventurer/svg?seed=player1&size=80" alt="Avatar 1" />
<img class="avatar" src="https://api.dicebear.com/7.x/avataaars/svg?seed=gamer42&size=80" alt="Avatar 2" />
<img class="avatar" src="https://api.dicebear.com/7.x/bottts/svg?seed=techuser&size=80" alt="Avatar 3" />
<img class="avatar" src="https://api.dicebear.com/7.x/fun-emoji/svg?seed=happyface&size=80" alt="Avatar 4" />
<img class="avatar" src="https://api.dicebear.com/7.x/lorelei/svg?seed=mystical&size=80" alt="Avatar 5" />
<img class="avatar" src="https://api.dicebear.com/7.x/notionists/svg?seed=creative&size=80" alt="Avatar 6" />
<img class="avatar" src="https://api.dicebear.com/7.x/personas/svg?seed=unique&size=80" alt="Avatar 7" />
</div>
<h2>🎮 Dynamic Gaming Images</h2>
<p style="color: #94a3b8; margin-bottom: 20px;">Random images from Unsplash (refreshes on reload)</p>
<div class="grid">
<div class="card">
<img src="https://source.unsplash.com/600x400/?gaming,neon" alt="Random Gaming" loading="lazy" />
<div class="card-body">
<div class="card-title">Gaming + Neon</div>
<div class="card-url">source.unsplash.com/600x400/?gaming,neon</div>
</div>
</div>
<div class="card">
<img src="https://source.unsplash.com/600x400/?technology,dark" alt="Random Tech" loading="lazy" />
<div class="card-body">
<div class="card-title">Technology + Dark</div>
<div class="card-url">source.unsplash.com/600x400/?technology,dark</div>
</div>
</div>
<div class="card">
<img src="https://source.unsplash.com/600x400/?abstract,gradient" alt="Random Abstract" loading="lazy" />
<div class="card-body">
<div class="card-title">Abstract + Gradient</div>
<div class="card-url">source.unsplash.com/600x400/?abstract,gradient</div>
</div>
</div>
</div>
<div style="text-align: center; margin-top: 60px; color: #64748b;">
<p>All images from <a href="https://unsplash.com" style="color: #60a5fa;">Unsplash</a> (free for commercial use)</p>
<p>Avatars from <a href="https://dicebear.com" style="color: #60a5fa;">DiceBear</a> (free for commercial use)</p>
</div>
</body>
</html>

View file

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

View file

@ -1,2 +0,0 @@
// Removed: This page is deprecated. Use /app for the full platform UI.

View file

@ -15,10 +15,47 @@ import './App.css';
*/
function DemoContent() {
const [activeTab, setActiveTab] = useState('overview');
const { user, loading } = useAuth();
const { user, loading, isAuthenticated, demoLogin, error } = useAuth();
const [loginLoading, setLoginLoading] = useState(false);
// Handle demo login
const handleDemoLogin = async () => {
setLoginLoading(true);
await demoLogin();
setLoginLoading(false);
};
// Show login screen when not authenticated
if (!loading && !isAuthenticated) {
return (
<div className="loading-screen" style={{ flexDirection: 'column', gap: '20px' }}>
<div className="loading-spinner">{String.fromCodePoint(0x1F680)}</div>
<h2 style={{ color: '#fff', margin: 0 }}>AeThex Connect Demo</h2>
<p style={{ color: '#aaa', margin: 0 }}>Sign in to explore all features</p>
{error && <p style={{ color: '#ff6b6b' }}>{error}</p>}
<button
onClick={handleDemoLogin}
disabled={loginLoading}
style={{
background: 'linear-gradient(90deg, #f7b42c, #fc575e)',
border: 'none',
padding: '12px 32px',
borderRadius: '8px',
color: '#fff',
fontWeight: 'bold',
fontSize: '16px',
cursor: loginLoading ? 'wait' : 'pointer',
opacity: loginLoading ? 0.7 : 1,
}}
>
{loginLoading ? 'Loading...' : '🚀 Try Demo'}
</button>
</div>
);
}
// Show loading state while auth initializes
if (loading || !user) {
if (loading) {
return (
<div className="loading-screen">
<div className="loading-spinner">{String.fromCodePoint(0x1F680)}</div>
@ -47,7 +84,7 @@ function DemoContent() {
</div>
<div className="user-section">
<div className="user-info">
<span className="user-name">{user.name}</span>
<span className="user-name">{user.displayName || user.username}</span>
<span className="user-email">{user.email}</span>
</div>
{user.verifiedDomain && (

View file

@ -1,11 +1,8 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { createClient } from '@supabase/supabase-js';
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
const supabase = createClient(
import.meta.env.PUBLIC_SUPABASE_URL,
import.meta.env.PUBLIC_SUPABASE_ANON_KEY
);
const API_URL = import.meta.env?.VITE_API_URL || 'http://localhost:3000';
const API_BASE = `${API_URL}/api`;
const AuthContext = createContext();
@ -19,29 +16,164 @@ export function useAuth() {
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [token, setTokenState] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Token management
const setToken = useCallback((newToken) => {
setTokenState(newToken);
if (newToken) {
localStorage.setItem('aethex_token', newToken);
} else {
localStorage.removeItem('aethex_token');
}
}, []);
// API helper
const apiRequest = useCallback(async (endpoint, options = {}) => {
const headers = {
'Content-Type': 'application/json',
...options.headers,
};
const currentToken = token || localStorage.getItem('aethex_token');
if (currentToken) {
headers['Authorization'] = `Bearer ${currentToken}`;
}
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Request failed');
}
return data;
}, [token]);
// Check auth on mount
useEffect(() => {
const getSession = async () => {
const checkAuth = async () => {
setLoading(true);
const { data: { session } } = await supabase.auth.getSession();
if (session?.user) {
setUser(session.user);
} else {
setUser(null);
const storedToken = localStorage.getItem('aethex_token');
if (storedToken) {
setTokenState(storedToken);
try {
const response = await fetch(`${API_BASE}/auth/me`, {
headers: {
'Authorization': `Bearer ${storedToken}`,
},
});
const data = await response.json();
if (response.ok && data.success) {
setUser(data.user);
} else {
// Token invalid, clear it
localStorage.removeItem('aethex_token');
setTokenState(null);
setUser(null);
}
} catch (err) {
console.error('Auth check failed:', err);
setUser(null);
}
}
setLoading(false);
};
getSession();
const { data: listener } = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user || null);
});
return () => {
listener?.subscription.unsubscribe();
};
checkAuth();
}, []);
const value = { user, loading, supabase };
// Login
const login = useCallback(async (email, password) => {
setError(null);
setLoading(true);
try {
const response = await apiRequest('/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
if (response.success && response.data) {
setToken(response.data.token);
setUser(response.data.user);
return { success: true, user: response.data.user };
}
return { success: false, error: response.error };
} catch (err) {
setError(err.message);
return { success: false, error: err.message };
} finally {
setLoading(false);
}
}, [apiRequest, setToken]);
// Register
const register = useCallback(async (email, password, username, displayName) => {
setError(null);
setLoading(true);
try {
const response = await apiRequest('/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, username, displayName }),
});
if (response.success && response.data) {
setToken(response.data.token);
setUser(response.data.user);
return { success: true, user: response.data.user };
}
return { success: false, error: response.error };
} catch (err) {
setError(err.message);
return { success: false, error: err.message };
} finally {
setLoading(false);
}
}, [apiRequest, setToken]);
// Demo login
const demoLogin = useCallback(async () => {
setError(null);
setLoading(true);
try {
const response = await apiRequest('/auth/demo', {
method: 'POST',
});
if (response.success && response.data) {
setToken(response.data.token);
setUser(response.data.user);
return { success: true, user: response.data.user };
}
return { success: false, error: response.error };
} catch (err) {
setError(err.message);
return { success: false, error: err.message };
} finally {
setLoading(false);
}
}, [apiRequest, setToken]);
// Logout
const logout = useCallback(async () => {
try {
await apiRequest('/auth/logout', { method: 'POST' });
} catch (err) {
console.error('Logout API call failed:', err);
}
setToken(null);
setUser(null);
}, [apiRequest, setToken]);
const value = {
user,
token,
loading,
error,
isAuthenticated: !!user,
login,
register,
demoLogin,
logout,
};
return (
<AuthContext.Provider value={value}>

View file

@ -3,27 +3,31 @@
* Provides Socket.io connection to all components
*/
import React, { createContext, useContext, useEffect, useState } from 'react';
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
import { io } from 'socket.io-client';
import { useAuth } from './AuthContext';
const SocketContext = createContext(null);
export function SocketProvider({ children }) {
const [socket, setSocket] = useState(null);
const [connected, setConnected] = useState(false);
const [messages, setMessages] = useState([]);
const [typingUsers, setTypingUsers] = useState({});
const { token, isAuthenticated } = useAuth();
useEffect(() => {
const token = localStorage.getItem('token');
const authToken = token || localStorage.getItem('aethex_token');
if (!token) {
console.log('No auth token, skipping socket connection');
if (!authToken || !isAuthenticated) {
console.log('No auth, skipping socket connection');
return;
}
// Connect to Socket.io server
const socketInstance = io(import.meta.env.VITE_API_URL || 'http://localhost:3000', {
auth: {
token: token
token: authToken
},
reconnection: true,
reconnectionDelay: 1000,
@ -48,6 +52,28 @@ export function SocketProvider({ children }) {
console.error('Socket error:', error);
});
// Message events
socketInstance.on('message', (msg) => {
setMessages(prev => [...prev, msg]);
});
socketInstance.on('user_typing', ({ conversationId, userId, username }) => {
setTypingUsers(prev => ({
...prev,
[conversationId]: { ...prev[conversationId], [userId]: username }
}));
});
socketInstance.on('user_stopped_typing', ({ conversationId, userId }) => {
setTypingUsers(prev => {
const updated = { ...prev };
if (updated[conversationId]) {
delete updated[conversationId][userId];
}
return updated;
});
});
setSocket(socketInstance);
// Cleanup on unmount
@ -56,10 +82,39 @@ export function SocketProvider({ children }) {
socketInstance.disconnect();
}
};
}, []);
}, [token, isAuthenticated]);
// Send message
const sendMessage = useCallback((channelId, content) => {
if (socket && connected) {
socket.emit('message', { channelId, content });
}
}, [socket, connected]);
// Join channel
const joinChannel = useCallback((channelId) => {
if (socket && connected) {
socket.emit('join_conversation', { conversationId: channelId });
}
}, [socket, connected]);
// Send typing indicator
const sendTyping = useCallback((channelId, isTyping) => {
if (socket && connected) {
socket.emit(isTyping ? 'typing_start' : 'typing_stop', { conversationId: channelId });
}
}, [socket, connected]);
return (
<SocketContext.Provider value={{ socket, connected }}>
<SocketContext.Provider value={{
socket,
connected,
messages,
typingUsers,
sendMessage,
joinChannel,
sendTyping
}}>
{children}
</SocketContext.Provider>
);

View file

@ -1,23 +1,18 @@
// socket.js
// Example Socket.io client for AeThex Connect (Supabase JWT auth)
// Socket.io client for AeThex Connect
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
);
const API_URL = import.meta.env?.VITE_API_URL || 'http://localhost:3001';
export async function connectSocket() {
// Get current session (JWT)
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Not authenticated');
// Get AeThex token from localStorage
const token = localStorage.getItem('aethex_token');
if (!token) throw new Error('Not authenticated');
// Connect to signaling server
const socket = io(import.meta.env.PUBLIC_SIGNALING_SERVER_URL, {
auth: { token: session.access_token }
const socket = io(API_URL, {
auth: { token }
});
// Example: handle connection

View file

@ -0,0 +1,106 @@
/**
* Unsplash Image Utility
* Provides free stock images for AeThex Connect
* Uses Unsplash Source API (no key required for basic usage)
*/
// Curated gaming/tech collection IDs from Unsplash
const COLLECTIONS = {
gaming: '1424240', // Gaming collection
tech: '3582603', // Technology collection
neon: '1459961', // Neon/cyberpunk aesthetic
dark: '827743', // Dark moody shots
abstract: '1065976', // Abstract patterns
};
/**
* Get a random image URL from Unsplash
* @param {Object} options
* @param {number} options.width - Image width
* @param {number} options.height - Image height
* @param {string} options.query - Search query
* @param {string} options.collection - Collection ID
* @returns {string} Image URL
*/
export function getUnsplashImage({ width = 1920, height = 1080, query, collection } = {}) {
const base = 'https://source.unsplash.com';
if (collection && COLLECTIONS[collection]) {
return `${base}/collection/${COLLECTIONS[collection]}/${width}x${height}`;
}
if (query) {
return `${base}/${width}x${height}/?${encodeURIComponent(query)}`;
}
return `${base}/random/${width}x${height}`;
}
/**
* Get a gaming-themed background
*/
export function getGamingBackground(width = 1920, height = 1080) {
return getUnsplashImage({ width, height, query: 'gaming,neon,dark' });
}
/**
* Get a tech-themed background
*/
export function getTechBackground(width = 1920, height = 1080) {
return getUnsplashImage({ width, height, query: 'technology,dark,abstract' });
}
/**
* Get a random avatar placeholder
* Uses specific seeds for consistency
*/
export function getAvatarImage(seed, size = 200) {
// Use DiceBear for avatars (more reliable than Unsplash for small images)
const styles = ['adventurer', 'avataaars', 'bottts', 'fun-emoji', 'lorelei', 'notionists', 'personas'];
const style = styles[Math.abs(hashCode(seed)) % styles.length];
return `https://api.dicebear.com/7.x/${style}/svg?seed=${encodeURIComponent(seed)}&size=${size}`;
}
/**
* Get abstract pattern for cards/backgrounds
*/
export function getAbstractPattern(width = 800, height = 600) {
return getUnsplashImage({ width, height, query: 'abstract,gradient,dark' });
}
/**
* Prebuilt image URLs for specific use cases
*/
export const PRESET_IMAGES = {
heroBackground: 'https://images.unsplash.com/photo-1538481199705-c710c4e965fc?w=1920&q=80', // Gaming setup
loginBackground: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?w=1920&q=80', // Retro gaming
chatBackground: 'https://images.unsplash.com/photo-1614850523459-c2f4c699c52e?w=1920&q=80', // Dark abstract
serverBanner: 'https://images.unsplash.com/photo-1542751371-adc38448a05e?w=1200&q=80', // Esports
profileBanner: 'https://images.unsplash.com/photo-1511512578047-dfb367046420?w=1200&q=80', // Gaming aesthetic
defaultAvatar: 'https://images.unsplash.com/photo-1566577134770-3d85bb3a9cc4?w=400&q=80', // Abstract
voiceChannel: 'https://images.unsplash.com/photo-1598488035139-bdbb2231ce04?w=800&q=80', // Audio waves
premiumBanner: 'https://images.unsplash.com/photo-1557682250-33bd709cbe85?w=1200&q=80', // Purple gradient
};
/**
* Simple hash function for consistent results
*/
function hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return hash;
}
export default {
getUnsplashImage,
getGamingBackground,
getTechBackground,
getAvatarImage,
getAbstractPattern,
PRESET_IMAGES,
COLLECTIONS,
};

10
package-lock.json generated
View file

@ -16,6 +16,7 @@
"dependencies": {
"@supabase/supabase-js": "^2.90.1",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"ethers": "^6.10.0",
@ -5044,6 +5045,15 @@
"node": ">= 10.0.0"
}
},
"node_modules/bcryptjs": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
"license": "BSD-3-Clause",
"bin": {
"bcrypt": "bin/bcrypt"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",

View file

@ -47,6 +47,7 @@
"dependencies": {
"@supabase/supabase-js": "^2.90.1",
"bcrypt": "^5.1.1",
"bcryptjs": "^3.0.3",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"ethers": "^6.10.0",

View file

@ -1,153 +0,0 @@
# AeThex Connect - Web PWA
Progressive Web App for AeThex Connect built with React, TypeScript, and Vite.
## Features
- **Real-time Messaging** - End-to-end encrypted conversations
- **Voice & Video Calls** - WebRTC-powered calls with crystal-clear quality
- **Offline Support** - Service worker enables offline messaging and call history
- **Dark Gaming Theme** - Modern, dark UI optimized for long sessions
- **Progressive Enhancement** - Works on desktop and mobile with native app-like experience
- **Responsive Design** - Tailwind CSS for mobile-first design
## Project Structure
```
packages/web/src/
├── index.tsx # App entry point with PWA registration
├── App.tsx # Main router and layout
├── pages/
│ ├── LoginPage.tsx # Authentication
│ ├── HomePage.tsx # Dashboard
│ ├── ChatPage.tsx # Messaging interface
│ ├── CallsPage.tsx # Voice/video calls
│ └── SettingsPage.tsx # User preferences
├── layouts/
│ └── MainLayout.tsx # Sidebar + header layout
├── components/ # Reusable UI components
├── hooks/ # Custom React hooks
├── utils/
│ ├── serviceWorker.ts # PWA registration
│ └── webrtc.ts # WebRTC signaling
└── styles/
├── global.css # Tailwind + custom styles
└── app.css # Component styles
```
## Installation
```bash
npm install --workspace=@aethex/web
```
## Development
```bash
npm run dev -w @aethex/web
```
Open http://localhost:5173 in your browser.
## Building
```bash
npm run build -w @aethex/web
npm run preview -w @aethex/web
```
## PWA Features
### Service Worker
- **Cache First**: Static assets cached for instant loading
- **Network First**: API requests fetch fresh data with cache fallback
- **Background Sync**: Offline messages synced when connection restored
### Manifest
- **Installable**: Add to homescreen on mobile devices
- **Standalone**: Runs as full-screen app without browser UI
- **Icons**: Adaptive icons for modern devices
### Offline Support
- Browse offline messages and call history
- Compose messages while offline (synced automatically)
- Works without internet connection
## Redux State Management
- **Auth Slice**: User authentication, tokens, profile
- **Messaging Slice**: Conversations, messages, read receipts
- **Calls Slice**: Active calls, call history, voice state
## WebRTC Integration
- **Peer Connections**: Manage multiple simultaneous calls
- **Signaling**: Socket.IO-based call negotiation
- **Media Streams**: Audio/video track control
- **ICE Candidates**: Automatic NAT traversal
## Styling
Uses **Tailwind CSS** with custom configuration:
- Dark gaming theme (purple/pink accents)
- Responsive breakpoints
- Smooth animations and transitions
- Custom components (@layer directives)
## Environment Variables
Create `.env.local`:
```
VITE_API_URL=http://localhost:3000
VITE_SOCKET_URL=ws://localhost:3000
VITE_SUPABASE_URL=your_supabase_url
VITE_SUPABASE_KEY=your_supabase_key
```
## Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Mobile browsers with Service Worker support
## Performance Optimizations
- Code splitting with React Router
- Lazy component loading
- Image optimization
- CSS purging with Tailwind
- Service worker caching strategies
## Testing
```bash
npm run test -w @aethex/web
```
## Deployment
### Vercel
```bash
vercel deploy
```
### Netlify
```bash
netlify deploy --prod --dir dist
```
### Docker
```bash
docker build -t aethex-web .
docker run -p 3000:80 aethex-web
```
## Contributing
See main [CONTRIBUTING.md](../../CONTRIBUTING.md)
## License
MIT

View file

@ -1,42 +0,0 @@
{
"name": "@aethex/web",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint src --ext ts,tsx",
"clean": "rm -rf dist"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.0",
"@reduxjs/toolkit": "^2.0.1",
"react-redux": "^9.0.4",
"socket.io-client": "^4.6.0",
"workbox-precaching": "^7.0.0",
"workbox-routing": "^7.0.0",
"workbox-strategies": "^7.0.0",
"workbox-background-sync": "^7.0.0",
"workbox-expiration": "^7.0.0"
},
"devDependencies": {
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.0.8",
"vite-plugin-pwa": "^0.17.4",
"vitest": "^1.1.0",
"typescript": "^5.3.3",
"tailwindcss": "^3.3.7",
"postcss": "^8.4.33",
"autoprefixer": "^10.4.17",
"eslint": "^8.55.0",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0"
}
}

View file

@ -1,9 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
export default config;

View file

@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#a855f7" />
<meta name="description" content="AeThex Connect - Next-generation communication platform with blockchain identity verification" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="AeThex" />
<title>AeThex Connect</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="alternate icon" type="image/png" href="/favicon.png" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />
<!-- Preconnect to critical domains -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- PWA Support -->
<meta name="color-scheme" content="dark" />
<meta name="supported-color-schemes" content="dark" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View file

@ -1,124 +0,0 @@
{
"name": "AeThex Connect",
"short_name": "AeThex",
"description": "Next-generation communication platform with blockchain identity verification, real-time messaging, voice/video calls, and premium features",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#0a0a0f",
"theme_color": "#a855f7",
"categories": ["communication", "productivity", "social"],
"icons": [
{
"src": "/icon-72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "/icon-96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "/icon-128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "/icon-144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "/icon-152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icon-384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"screenshots": [
{
"src": "/screenshot-1.png",
"sizes": "1280x720",
"type": "image/png",
"label": "Main chat interface"
},
{
"src": "/screenshot-2.png",
"sizes": "1280x720",
"type": "image/png",
"label": "Voice channel"
}
],
"share_target": {
"action": "/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url",
"files": [
{
"name": "media",
"accept": ["image/*", "video/*", "audio/*"]
}
]
}
},
"shortcuts": [
{
"name": "New Message",
"short_name": "Message",
"description": "Start a new conversation",
"url": "/new-message",
"icons": [{ "src": "/icon-message.png", "sizes": "96x96" }]
},
{
"name": "Voice Channel",
"short_name": "Voice",
"description": "Join voice channel",
"url": "/voice",
"icons": [{ "src": "/icon-voice.png", "sizes": "96x96" }]
},
{
"name": "Friends",
"short_name": "Friends",
"description": "View friends list",
"url": "/friends",
"icons": [{ "src": "/icon-friends.png", "sizes": "96x96" }]
}
],
"protocol_handlers": [
{
"protocol": "web+aethex",
"url": "/handle?url=%s"
}
],
"file_handlers": [
{
"action": "/share",
"accept": {
"image/*": [".png", ".jpg", ".jpeg", ".gif", ".webp"],
"video/*": [".mp4", ".webm"],
"audio/*": [".mp3", ".wav", ".ogg"]
}
}
]
}

View file

@ -1,171 +0,0 @@
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { BackgroundSyncPlugin } from 'workbox-background-sync';
import { ExpirationPlugin } from 'workbox-expiration';
declare const self: ServiceWorkerGlobalScope;
// Precache all build assets
precacheAndRoute(self.__WB_MANIFEST);
// API requests - Network first, cache fallback
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 minutes
}),
],
})
);
// Images - Cache first
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images',
plugins: [
new ExpirationPlugin({
maxEntries: 100,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
}),
],
})
);
// Fonts - Cache first
registerRoute(
({ request }) => request.destination === 'font',
new CacheFirst({
cacheName: 'fonts',
plugins: [
new ExpirationPlugin({
maxEntries: 20,
maxAgeSeconds: 365 * 24 * 60 * 60, // 1 year
}),
],
})
);
// Background sync for failed POST requests
const bgSyncPlugin = new BackgroundSyncPlugin('message-queue', {
maxRetentionTime: 24 * 60, // Retry for 24 hours
});
registerRoute(
({ url }) => url.pathname.startsWith('/api/messages'),
new NetworkFirst({
plugins: [bgSyncPlugin],
}),
'POST'
);
// Push notifications
self.addEventListener('push', (event) => {
const data = event.data?.json() || {};
const options: NotificationOptions = {
body: data.body || 'You have a new message',
icon: data.icon || '/icon-192.png',
badge: '/badge-96.png',
tag: data.tag || 'notification',
data: data.data || {},
actions: data.actions || [
{ action: 'open', title: 'Open' },
{ action: 'dismiss', title: 'Dismiss' },
],
vibrate: [200, 100, 200],
requireInteraction: data.requireInteraction || false,
};
event.waitUntil(
self.registration.showNotification(data.title || 'AeThex Connect', options)
);
});
// Notification click handler
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open' || !event.action) {
const urlToOpen = event.notification.data?.url || '/';
event.waitUntil(
self.clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
// Check if there's already a window open
for (const client of clientList) {
if (client.url === urlToOpen && 'focus' in client) {
return client.focus();
}
}
// Open new window if none exists
if (self.clients.openWindow) {
return self.clients.openWindow(urlToOpen);
}
})
);
}
});
// Handle notification actions (reply, etc.)
self.addEventListener('notificationclick', (event) => {
if (event.action === 'reply' && event.reply) {
// Send reply via background sync
event.waitUntil(
fetch('/api/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
conversationId: event.notification.data.conversationId,
content: event.reply,
contentType: 'text',
}),
})
);
}
});
// Periodic background sync (for checking new messages when offline)
self.addEventListener('periodicsync', (event: any) => {
if (event.tag === 'check-messages') {
event.waitUntil(checkForNewMessages());
}
});
async function checkForNewMessages() {
try {
const response = await fetch('/api/messages/unread');
const data = await response.json();
if (data.count > 0) {
self.registration.showNotification('New Messages', {
body: `You have ${data.count} unread messages`,
icon: '/icon-192.png',
badge: '/badge-96.png',
tag: 'unread-messages',
});
}
} catch (error) {
console.error('Error checking messages:', error);
}
}
// Skip waiting and claim clients immediately
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim());
});
// Log service worker version
console.log('AeThex Connect Service Worker v1.0.0');

View file

@ -1,148 +0,0 @@
const CACHE_NAME = 'aethex-connect-v1';
const ASSETS_TO_CACHE = [
'/',
'/index.html',
'/manifest.json',
];
// Install event
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('Caching app shell');
return cache.addAll(ASSETS_TO_CACHE).catch(err => {
console.log('Cache addAll error:', err);
// Don't fail installation if some assets can't be cached
});
})
);
self.skipWaiting();
});
// Activate event
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
);
self.clients.claim();
});
// Fetch event - Network first, fallback to cache
self.addEventListener('fetch', (event) => {
const { request } = event;
// Skip non-GET requests
if (request.method !== 'GET') {
return;
}
// API requests - network first
if (request.url.includes('/api/')) {
event.respondWith(
fetch(request)
.then((response) => {
if (!response || response.status !== 200) {
return response;
}
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
return response;
})
.catch(() => {
return caches.match(request);
})
);
} else {
// Static assets - cache first
event.respondWith(
caches.match(request).then((response) => {
if (response) {
return response;
}
return fetch(request).then((response) => {
if (!response || response.status !== 200 || response.type === 'error') {
return response;
}
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseClone);
});
return response;
});
})
);
}
});
// Background sync for offline messages
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});
async function syncMessages() {
try {
const db = await openIndexedDB();
const messages = await getPendingMessages(db);
for (const message of messages) {
try {
await fetch('/api/messages', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(message),
});
await deletePendingMessage(db, message.id);
} catch (error) {
console.error('Failed to sync message:', error);
}
}
} catch (error) {
console.error('Sync error:', error);
}
}
function openIndexedDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('aethex-connect', 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
function getPendingMessages(db) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['pendingMessages'], 'readonly');
const store = transaction.objectStore('pendingMessages');
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
function deletePendingMessage(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['pendingMessages'], 'readwrite');
const store = transaction.objectStore('pendingMessages');
const request = store.delete(id);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}

View file

@ -1,55 +0,0 @@
import React, { useEffect } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { useAppSelector } from './store';
import MainLayout from './layouts/MainLayout';
import HomePage from './pages/HomePage';
import ChatPage from './pages/ChatPage';
import CallsPage from './pages/CallsPage';
import SettingsPage from './pages/SettingsPage';
import LoginPage from './pages/LoginPage';
import './styles/app.css';
export default function App() {
const { user, loading } = useAppSelector(state => state.auth);
useEffect(() => {
// Restore auth state on app load
const token = localStorage.getItem('authToken');
if (token && !user) {
// Token exists but user not loaded - this would be handled by Redux persist
console.log('Auth token found, waiting for hydration...');
}
}, []);
if (loading) {
return (
<div className="flex items-center justify-center h-screen bg-gray-900">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-purple-500"></div>
</div>
);
}
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/*"
element={
user ? (
<MainLayout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/chat/*" element={<ChatPage />} />
<Route path="/calls/*" element={<CallsPage />} />
<Route path="/settings/*" element={<SettingsPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</MainLayout>
) : (
<Navigate to="/login" replace />
)
}
/>
</Routes>
);
}

View file

@ -1,21 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import { store } from './store';
import './styles/global.css';
import { registerServiceWorker } from './utils/serviceWorker';
// Register service worker for PWA capabilities
registerServiceWorker();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>
);

View file

@ -1,72 +0,0 @@
import React from 'react';
interface MainLayoutProps {
children: React.ReactNode;
}
export default function MainLayout({ children }: MainLayoutProps) {
return (
<div className="flex h-screen bg-gray-900">
{/* Sidebar */}
<aside className="w-64 bg-gray-800 border-r border-gray-700 overflow-y-auto">
<nav className="p-4 space-y-2">
<a
href="/"
className="flex items-center px-4 py-2 rounded-lg text-gray-100 hover:bg-gray-700 transition"
>
<span className="text-xl mr-3">🏠</span>
<span>Home</span>
</a>
<a
href="/chat"
className="flex items-center px-4 py-2 rounded-lg text-gray-100 hover:bg-gray-700 transition"
>
<span className="text-xl mr-3">💬</span>
<span>Messages</span>
</a>
<a
href="/calls"
className="flex items-center px-4 py-2 rounded-lg text-gray-100 hover:bg-gray-700 transition"
>
<span className="text-xl mr-3">📞</span>
<span>Calls</span>
</a>
<a
href="/settings"
className="flex items-center px-4 py-2 rounded-lg text-gray-100 hover:bg-gray-700 transition"
>
<span className="text-xl mr-3"></span>
<span>Settings</span>
</a>
</nav>
</aside>
{/* Main Content */}
<main className="flex-1 flex flex-col overflow-hidden">
{/* Header */}
<header className="bg-gray-800 border-b border-gray-700 px-6 py-4">
<div className="flex justify-between items-center">
<h1 className="text-xl font-bold text-white">AeThex Connect</h1>
<div className="flex items-center space-x-4">
<button className="text-gray-400 hover:text-white">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
</svg>
</button>
<button className="text-gray-400 hover:text-white">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5.121 17.804A13.937 13.937 0 0112 16c2.5 0 4.847.655 6.879 1.804M15 10h.01M9 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
</div>
</header>
{/* Content Area */}
<div className="flex-1 overflow-auto bg-gray-900">
{children}
</div>
</main>
</div>
);
}

View file

@ -1,92 +0,0 @@
import React, { useState } from 'react';
import { useAppSelector } from '../store';
export default function CallsPage() {
const { activeCall, callHistory } = useAppSelector(state => state.calls);
const [selectedCallHistory, setSelectedCallHistory] = useState(0);
return (
<div className="p-8 max-w-6xl mx-auto">
{activeCall ? (
// Active Call View
<div className="bg-gray-800 rounded-lg p-8 text-center">
<div className="mb-8">
<div className="w-24 h-24 mx-auto mb-4 bg-gradient-to-br from-purple-400 to-pink-600 rounded-full flex items-center justify-center">
<span className="text-4xl">👤</span>
</div>
<h2 className="text-3xl font-bold text-white mb-2">{activeCall.participantName}</h2>
<p className="text-gray-400">Call in progress</p>
<p className="text-2xl font-mono text-purple-400 mt-4">{activeCall.duration}</p>
</div>
<div className="flex justify-center gap-4 mb-8">
<button
className={`w-16 h-16 rounded-full flex items-center justify-center transition ${
activeCall.isMuted ? 'bg-red-600 hover:bg-red-700' : 'bg-gray-700 hover:bg-gray-600'
}`}
>
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</button>
<button
className={`w-16 h-16 rounded-full flex items-center justify-center transition ${
activeCall.isCameraOn ? 'bg-gray-700 hover:bg-gray-600' : 'bg-red-600 hover:bg-red-700'
}`}
>
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
<button className="w-16 h-16 rounded-full bg-red-600 hover:bg-red-700 flex items-center justify-center transition">
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M16.5 1h-9C6.12 1 5 2.12 5 3.5v17C5 21.88 6.12 23 7.5 23h9c1.38 0 2.5-1.12 2.5-2.5v-17C19 2.12 17.88 1 16.5 1zm-4 21c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm4.5-4H7V4h9v14z" />
</svg>
</button>
</div>
</div>
) : (
// Call History
<div>
<h1 className="text-3xl font-bold text-white mb-8">Calls</h1>
{callHistory.length === 0 ? (
<div className="bg-gray-800 rounded-lg p-12 text-center">
<p className="text-gray-400 text-lg">No call history yet. Start a call to get begun!</p>
</div>
) : (
<div className="space-y-4">
{callHistory.map((call, index) => (
<div key={index} className="bg-gray-800 rounded-lg p-4 flex items-center justify-between hover:bg-gray-700 transition">
<div className="flex items-center gap-4 flex-1">
<div className="w-12 h-12 bg-gradient-to-br from-purple-400 to-pink-600 rounded-full flex items-center justify-center flex-shrink-0">
<span className="text-lg">👤</span>
</div>
<div className="flex-1 min-w-0">
<h3 className="text-white font-semibold">{call.participantName}</h3>
<p className="text-gray-400 text-sm">
{call.type === 'voice' ? '📞 Voice Call' : '📹 Video Call'} {call.duration}
</p>
</div>
</div>
<div className="flex items-center gap-4 flex-shrink-0">
<p className="text-gray-400 text-sm">
{new Date(call.timestamp).toLocaleDateString()}
</p>
<button className="text-purple-400 hover:text-purple-300 transition">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</button>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
);
}

View file

@ -1,127 +0,0 @@
import React, { useState, useEffect } from 'react';
import { useAppSelector } from '../store';
export default function ChatPage() {
const { conversations, messages } = useAppSelector(state => state.messaging);
const [selectedConversation, setSelectedConversation] = useState(conversations[0]?.id);
const [inputValue, setInputValue] = useState('');
const currentMessages = messages[selectedConversation] || [];
const handleSendMessage = (e: React.FormEvent) => {
e.preventDefault();
if (!inputValue.trim()) return;
console.log('Sending message:', inputValue);
setInputValue('');
};
return (
<div className="flex h-full bg-gray-900">
{/* Conversations List */}
<div className="w-64 bg-gray-800 border-r border-gray-700 flex flex-col">
<div className="p-4 border-b border-gray-700">
<h3 className="text-lg font-semibold text-white mb-4">Messages</h3>
<button className="w-full px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition">
+ New Chat
</button>
</div>
<div className="flex-1 overflow-y-auto">
{conversations.map((conversation) => (
<button
key={conversation.id}
onClick={() => setSelectedConversation(conversation.id)}
className={`w-full px-4 py-3 text-left border-l-4 transition ${
selectedConversation === conversation.id
? 'bg-gray-700 border-purple-500'
: 'border-transparent hover:bg-gray-700'
}`}
>
<div className="font-semibold text-white text-sm">{conversation.participantName}</div>
<div className="text-gray-400 text-xs truncate">{conversation.lastMessage}</div>
</button>
))}
</div>
</div>
{/* Chat Area */}
<div className="flex-1 flex flex-col">
{selectedConversation ? (
<>
{/* Chat Header */}
<div className="bg-gray-800 border-b border-gray-700 px-6 py-4 flex justify-between items-center">
<h2 className="text-xl font-semibold text-white">
{conversations.find(c => c.id === selectedConversation)?.participantName}
</h2>
<div className="flex gap-4">
<button className="text-gray-400 hover:text-white transition">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" />
</svg>
</button>
<button className="text-gray-400 hover:text-white transition">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</div>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{currentMessages.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-500">
<p>No messages yet. Start the conversation!</p>
</div>
) : (
currentMessages.map((msg) => (
<div key={msg.id} className={`flex ${msg.senderId === 'self' ? 'justify-end' : 'justify-start'}`}>
<div
className={`max-w-xs px-4 py-2 rounded-lg ${
msg.senderId === 'self'
? 'bg-purple-600 text-white'
: 'bg-gray-700 text-gray-100'
}`}
>
<p>{msg.content}</p>
<p className="text-xs opacity-70 mt-1">{new Date(msg.createdAt).toLocaleTimeString()}</p>
</div>
</div>
))
)}
</div>
{/* Input */}
<form onSubmit={handleSendMessage} className="bg-gray-800 border-t border-gray-700 p-4">
<div className="flex gap-4">
<button type="button" className="text-gray-400 hover:text-white transition">
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm3.5-9c.83 0 1.5-.67 1.5-1.5S16.33 8 15.5 8 14 8.67 14 9.5s.67 1.5 1.5 1.5zm-7 0c.83 0 1.5-.67 1.5-1.5S9.33 8 8.5 8 7 8.67 7 9.5 7.67 11 8.5 11zm3.5 6.5c2.33 0 4.31-1.46 5.11-3.5H6.89c.8 2.04 2.78 3.5 5.11 3.5z" />
</svg>
</button>
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Type a message..."
className="flex-1 px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500"
/>
<button
type="submit"
className="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition"
>
Send
</button>
</div>
</form>
</>
) : (
<div className="flex items-center justify-center h-full text-gray-500">
<p>Select a conversation to start messaging</p>
</div>
)}
</div>
</div>
);
}

View file

@ -1,65 +0,0 @@
import React from 'react';
export default function HomePage() {
return (
<div className="p-8">
<div className="max-w-4xl mx-auto">
<h1 className="text-4xl font-bold text-white mb-6">Welcome to AeThex Connect</h1>
<p className="text-gray-300 text-lg mb-8">
Your next-generation communication platform with blockchain identity verification,
real-time messaging, voice/video calls, and premium features.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Feature Cards */}
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700 hover:border-purple-500 transition">
<div className="text-3xl mb-4">💬</div>
<h3 className="text-xl font-semibold text-white mb-2">Instant Messaging</h3>
<p className="text-gray-400">
End-to-end encrypted messages with real-time synchronization across all devices.
</p>
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700 hover:border-purple-500 transition">
<div className="text-3xl mb-4">📞</div>
<h3 className="text-xl font-semibold text-white mb-2">Voice & Video</h3>
<p className="text-gray-400">
Crystal-clear voice calls and HD video conferencing with WebRTC technology.
</p>
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700 hover:border-purple-500 transition">
<div className="text-3xl mb-4">🎮</div>
<h3 className="text-xl font-semibold text-white mb-2">GameForge Integration</h3>
<p className="text-gray-400">
Connect with game communities and manage channels with GameForge.
</p>
</div>
<div className="bg-gray-800 rounded-lg p-6 border border-gray-700 hover:border-purple-500 transition">
<div className="text-3xl mb-4">🔐</div>
<h3 className="text-xl font-semibold text-white mb-2">Verified Identity</h3>
<p className="text-gray-400">
Blockchain-backed domain verification for authentic user identification.
</p>
</div>
</div>
<div className="mt-12 p-8 bg-gradient-to-r from-purple-900 to-pink-900 rounded-lg">
<h2 className="text-2xl font-bold text-white mb-4">Get Started Now</h2>
<p className="text-gray-200 mb-6">
Explore your messages, start a call, or customize your settings to unlock the full potential of AeThex Connect.
</p>
<div className="flex gap-4">
<button className="px-6 py-3 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition">
Start Messaging
</button>
<button className="px-6 py-3 bg-gray-700 hover:bg-gray-600 text-white font-semibold rounded-lg transition">
Schedule a Call
</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,114 +0,0 @@
import React, { useState } from 'react';
import { useAppDispatch, useAppSelector, loginAsync } from '../store';
export default function LoginPage() {
const dispatch = useAppDispatch();
const { loading, error } = useAppSelector(state => state.auth);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isSignUp, setIsSignUp] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (isSignUp) {
// Sign up logic would go here
console.log('Sign up:', email, password);
} else {
// Login
dispatch(loginAsync({ email, password }));
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-purple-900 to-gray-900 flex items-center justify-center p-4">
<div className="w-full max-w-md">
{/* Logo/Header */}
<div className="text-center mb-8">
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-400 to-pink-600 bg-clip-text text-transparent mb-2">
AeThex Connect
</h1>
<p className="text-gray-400">Next-generation communication platform</p>
</div>
{/* Form Card */}
<div className="bg-gray-800 border border-gray-700 rounded-lg p-8 shadow-2xl">
<h2 className="text-2xl font-bold text-white mb-6">
{isSignUp ? 'Create Account' : 'Welcome Back'}
</h2>
{error && (
<div className="mb-6 p-4 bg-red-900 border border-red-700 rounded text-red-200">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition"
placeholder="you@example.com"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition"
placeholder="••••••••"
required
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2 bg-gradient-to-r from-purple-600 to-pink-600 text-white font-semibold rounded-lg hover:from-purple-700 hover:to-pink-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
>
{loading ? (
<>
<span className="animate-spin rounded-full h-4 w-4 border-t-2 border-white mr-2"></span>
{isSignUp ? 'Creating Account...' : 'Signing In...'}
</>
) : isSignUp ? (
'Create Account'
) : (
'Sign In'
)}
</button>
</form>
<div className="mt-6">
<button
type="button"
onClick={() => {
setIsSignUp(!isSignUp);
}}
className="w-full text-center text-gray-400 hover:text-gray-300 transition"
>
{isSignUp ? 'Already have an account? Sign in' : "Don't have an account? Sign up"}
</button>
</div>
<div className="mt-6 p-4 bg-gray-700 rounded-lg">
<p className="text-xs text-gray-400 mb-2">Demo Credentials:</p>
<p className="text-xs text-gray-500">Email: demo@aethex.dev</p>
<p className="text-xs text-gray-500">Password: demo123</p>
</div>
</div>
{/* Footer */}
<p className="text-center text-gray-500 text-sm mt-8">
© 2026 AeThex Corporation. All rights reserved.
</p>
</div>
</div>
);
}

View file

@ -1,197 +0,0 @@
import React, { useState } from 'react';
import { useAppDispatch, useAppSelector, logoutAsync } from '../store';
export default function SettingsPage() {
const dispatch = useAppDispatch();
const { user } = useAppSelector(state => state.auth);
const [activeTab, setActiveTab] = useState('profile');
const handleLogout = () => {
dispatch(logoutAsync());
};
const tabs = [
{ id: 'profile', label: 'Profile', icon: '👤' },
{ id: 'privacy', label: 'Privacy & Security', icon: '🔒' },
{ id: 'notifications', label: 'Notifications', icon: '🔔' },
{ id: 'appearance', label: 'Appearance', icon: '🎨' },
{ id: 'about', label: 'About', icon: '' },
];
return (
<div className="p-8 max-w-4xl mx-auto">
<h1 className="text-3xl font-bold text-white mb-8">Settings</h1>
<div className="flex gap-8">
{/* Sidebar Navigation */}
<div className="w-48">
<nav className="space-y-2">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full text-left px-4 py-3 rounded-lg transition ${
activeTab === tab.id
? 'bg-purple-600 text-white'
: 'text-gray-400 hover:bg-gray-800 hover:text-white'
}`}
>
<span className="mr-2">{tab.icon}</span>
{tab.label}
</button>
))}
</nav>
</div>
{/* Content Area */}
<div className="flex-1">
{activeTab === 'profile' && (
<div className="space-y-6">
<div>
<h2 className="text-2xl font-bold text-white mb-4">Profile Settings</h2>
<div className="bg-gray-800 rounded-lg p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Display Name</label>
<input
type="text"
defaultValue={user?.email || ''}
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Email</label>
<input
type="email"
defaultValue={user?.email || ''}
disabled
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-gray-400 cursor-not-allowed"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">Bio</label>
<textarea
placeholder="Tell us about yourself..."
className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:border-purple-500"
rows={4}
/>
</div>
<button className="px-6 py-2 bg-purple-600 hover:bg-purple-700 text-white font-semibold rounded-lg transition">
Save Changes
</button>
</div>
</div>
</div>
)}
{activeTab === 'privacy' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-white mb-4">Privacy & Security</h2>
<div className="bg-gray-800 rounded-lg p-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-white font-semibold">Two-Factor Authentication</h3>
<p className="text-gray-400 text-sm">Add an extra layer of security to your account</p>
</div>
<button className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition">
Enable
</button>
</div>
<hr className="border-gray-700" />
<div className="flex items-center justify-between">
<div>
<h3 className="text-white font-semibold">E2E Encryption</h3>
<p className="text-gray-400 text-sm">Your messages are end-to-end encrypted</p>
</div>
<span className="text-green-400"> Enabled</span>
</div>
<hr className="border-gray-700" />
<div>
<button className="text-red-400 hover:text-red-300 transition font-semibold">
Change Password
</button>
</div>
</div>
</div>
)}
{activeTab === 'notifications' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-white mb-4">Notification Preferences</h2>
<div className="bg-gray-800 rounded-lg p-6 space-y-4">
{['Messages', 'Calls', 'Friend Requests', 'Community Updates'].map((item) => (
<div key={item} className="flex items-center justify-between">
<span className="text-gray-300">{item}</span>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" defaultChecked className="sr-only peer" />
<div className="w-11 h-6 bg-gray-700 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-purple-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-purple-600"></div>
</label>
</div>
))}
</div>
</div>
)}
{activeTab === 'appearance' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-white mb-4">Appearance</h2>
<div className="bg-gray-800 rounded-lg p-6 space-y-4">
<div>
<h3 className="text-white font-semibold mb-4">Theme</h3>
<div className="space-y-2">
{['Dark', 'Light', 'Auto'].map((theme) => (
<label key={theme} className="flex items-center gap-3 cursor-pointer">
<input
type="radio"
name="theme"
defaultChecked={theme === 'Dark'}
className="w-4 h-4"
/>
<span className="text-gray-300">{theme}</span>
</label>
))}
</div>
</div>
</div>
</div>
)}
{activeTab === 'about' && (
<div className="space-y-6">
<h2 className="text-2xl font-bold text-white mb-4">About AeThex Connect</h2>
<div className="bg-gray-800 rounded-lg p-6 space-y-4 text-gray-300">
<div>
<p className="font-semibold text-white mb-2">Version</p>
<p>1.0.0</p>
</div>
<div>
<p className="font-semibold text-white mb-2">Build Date</p>
<p>February 3, 2026</p>
</div>
<div>
<p className="font-semibold text-white mb-2">Features</p>
<ul className="space-y-1 text-sm">
<li> Real-time messaging with E2E encryption</li>
<li> WebRTC voice and video calls</li>
<li> Blockchain domain verification</li>
<li> GameForge community integration</li>
<li> Premium subscriptions</li>
</ul>
</div>
</div>
</div>
)}
{/* Logout Button at Bottom */}
<div className="mt-8 pt-8 border-t border-gray-700">
<button
onClick={handleLogout}
className="px-6 py-2 bg-red-600 hover:bg-red-700 text-white font-semibold rounded-lg transition"
>
Sign Out
</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,171 +0,0 @@
import { configureStore, createSlice, PayloadAction } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
// Auth Slice
interface User {
id: string;
email: string;
username?: string;
}
interface AuthState {
user: User | null;
loading: boolean;
error: string | null;
}
const initialAuthState: AuthState = {
user: null,
loading: false,
error: null,
};
const authSlice = createSlice({
name: 'auth',
initialState: initialAuthState,
reducers: {
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setUser: (state, action: PayloadAction<User | null>) => {
state.user = action.payload;
state.loading = false;
state.error = null;
},
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload;
state.loading = false;
},
logout: (state) => {
state.user = null;
state.loading = false;
state.error = null;
},
},
});
export const { setLoading, setUser, setError, logout } = authSlice.actions;
// Messaging Slice
interface Conversation {
id: string;
participantName: string;
lastMessage: string;
unreadCount: number;
}
interface Message {
id: string;
conversationId: string;
content: string;
senderId: string;
timestamp: string;
createdAt: string;
}
interface MessagingState {
conversations: Conversation[];
messages: Record<string, Message[]>;
}
const initialMessagingState: MessagingState = {
conversations: [],
messages: {},
};
const messagingSlice = createSlice({
name: 'messaging',
initialState: initialMessagingState,
reducers: {
setConversations: (state, action: PayloadAction<Conversation[]>) => {
state.conversations = action.payload;
},
addMessage: (state, action: PayloadAction<Message>) => {
const msg = action.payload;
if (!state.messages[msg.conversationId]) {
state.messages[msg.conversationId] = [];
}
state.messages[msg.conversationId].push(msg);
},
},
});
export const { setConversations, addMessage } = messagingSlice.actions;
// Calls Slice
interface Call {
id: string;
participantName: string;
type: 'audio' | 'video' | 'voice';
status: 'active' | 'ended' | 'missed';
duration?: string;
timestamp: string;
isMuted?: boolean;
isCameraOn?: boolean;
}
interface CallsState {
activeCall: Call | null;
callHistory: Call[];
}
const initialCallsState: CallsState = {
activeCall: null,
callHistory: [],
};
const callsSlice = createSlice({
name: 'calls',
initialState: initialCallsState,
reducers: {
setActiveCall: (state, action: PayloadAction<Call | null>) => {
state.activeCall = action.payload;
},
addCallToHistory: (state, action: PayloadAction<Call>) => {
state.callHistory.unshift(action.payload);
},
},
});
export const { setActiveCall, addCallToHistory } = callsSlice.actions;
// Async thunks
export const loginAsync = (credentials: { email: string; password: string }) => async (dispatch: AppDispatch) => {
dispatch(setLoading(true));
try {
// TODO: Integrate with Supabase auth
const mockUser: User = {
id: '1',
email: credentials.email,
username: credentials.email.split('@')[0],
};
dispatch(setUser(mockUser));
} catch (error) {
dispatch(setError((error as Error).message));
}
};
export const logoutAsync = () => async (dispatch: AppDispatch) => {
dispatch(setLoading(true));
try {
// TODO: Integrate with Supabase auth
dispatch(logout());
} catch (error) {
dispatch(setError((error as Error).message));
}
};
// Store
export const store = configureStore({
reducer: {
auth: authSlice.reducer,
messaging: messagingSlice.reducer,
calls: callsSlice.reducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View file

@ -1,43 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Smooth transitions */
a, button {
transition: all 0.2s ease;
}
/* Form elements */
input, textarea, select {
font-family: inherit;
}
/* Remove default button styling */
button {
border: none;
cursor: pointer;
background: none;
}
/* Links */
a {
text-decoration: none;
color: inherit;
}
/* Ensure images are responsive */
img {
max-width: 100%;
height: auto;
display: block;
}

View file

@ -1,96 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-bg-primary: #0a0a0f;
--color-bg-secondary: #1a1a2e;
--color-bg-tertiary: #2d2d44;
--color-border: #404060;
--color-text-primary: #ffffff;
--color-text-secondary: #a0a0b0;
--color-accent-primary: #a855f7;
--color-accent-secondary: #ec4899;
}
* {
@apply selection:bg-purple-600 selection:text-white;
}
html, body {
@apply bg-gray-900 text-white;
}
@layer components {
.btn-primary {
@apply px-4 py-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold rounded-lg transition disabled:opacity-50 disabled:cursor-not-allowed;
}
.btn-secondary {
@apply px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white font-semibold rounded-lg transition;
}
.input-field {
@apply px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:border-purple-500 focus:ring-1 focus:ring-purple-500 transition;
}
.card {
@apply bg-gray-800 border border-gray-700 rounded-lg p-6;
}
.card-hover {
@apply card hover:border-purple-500 transition;
}
}
/* Animations */
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
.float {
animation: float 3s ease-in-out infinite;
}
/* Loading Spinner */
.spinner {
border: 3px solid rgba(168, 85, 247, 0.1);
border-top: 3px solid #a855f7;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Scrollbar Styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #4b5563;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #5a6b7a;
}

View file

@ -1,26 +0,0 @@
export function registerServiceWorker() {
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/sw.js')
.then((registration) => {
console.log('ServiceWorker registered:', registration);
})
.catch((error) => {
console.log('ServiceWorker registration failed:', error);
});
});
}
}
export function requestNotificationPermission() {
if ('Notification' in window) {
if (Notification.permission === 'granted') {
return Promise.resolve();
}
if (Notification.permission !== 'denied') {
return Notification.requestPermission();
}
}
return Promise.reject('Notifications not supported');
}

View file

@ -1,116 +0,0 @@
import { Socket } from 'socket.io-client';
export class WebRTCService {
private socket: Socket | null = null;
private peerConnections: Map<string, RTCPeerConnection> = new Map();
constructor(socket: Socket) {
this.socket = socket;
this.setupSocketListeners();
}
private setupSocketListeners() {
if (!this.socket) return;
this.socket.on('offer', (data: any) => {
console.log('Received offer from:', data.from);
this.handleOffer(data);
});
this.socket.on('answer', (data: any) => {
console.log('Received answer from:', data.from);
this.handleAnswer(data);
});
this.socket.on('ice-candidate', (data: any) => {
console.log('Received ICE candidate from:', data.from);
this.handleIceCandidate(data);
});
}
async startCall(recipientId: string): Promise<void> {
try {
const peerConnection = this.createPeerConnection(recipientId);
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
stream.getTracks().forEach((track) => {
peerConnection.addTrack(track, stream);
});
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
this.socket?.emit('offer', {
to: recipientId,
offer: offer,
});
} catch (error) {
console.error('Error starting call:', error);
}
}
private createPeerConnection(peerId: string): RTCPeerConnection {
if (this.peerConnections.has(peerId)) {
return this.peerConnections.get(peerId)!;
}
const iceServers = [
{ urls: ['stun:stun.l.google.com:19302', 'stun:stun1.l.google.com:19302'] },
];
const peerConnection = new RTCPeerConnection({
iceServers,
});
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.socket?.emit('ice-candidate', {
to: peerId,
candidate: event.candidate,
});
}
};
peerConnection.ontrack = (event) => {
console.log('Received remote track:', event.track);
};
this.peerConnections.set(peerId, peerConnection);
return peerConnection;
}
private async handleOffer(data: any) {
const peerConnection = this.createPeerConnection(data.from);
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.offer));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
this.socket?.emit('answer', {
to: data.from,
answer: answer,
});
}
private async handleAnswer(data: any) {
const peerConnection = this.peerConnections.get(data.from);
if (peerConnection) {
await peerConnection.setRemoteDescription(new RTCSessionDescription(data.answer));
}
}
private async handleIceCandidate(data: any) {
const peerConnection = this.peerConnections.get(data.from);
if (peerConnection && data.candidate) {
await peerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
}
}
endCall(peerId: string): void {
const peerConnection = this.peerConnections.get(peerId);
if (peerConnection) {
peerConnection.close();
this.peerConnections.delete(peerId);
}
}
}

View file

@ -1,54 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./public/index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
gray: {
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#0f172a',
},
purple: {
50: '#f9f5ff',
100: '#f3e8ff',
200: '#e9d5ff',
300: '#d8b4fe',
400: '#c084fc',
500: '#a855f7',
600: '#9333ea',
700: '#7e22ce',
800: '#6b21a8',
900: '#581c87',
},
pink: {
600: '#ec4899',
700: '#be185d',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
},
keyframes: {
float: {
'0%, 100%': { transform: 'translateY(0px)' },
'50%': { transform: 'translateY(-10px)' },
},
},
animation: {
float: 'float 3s ease-in-out infinite',
},
},
},
plugins: [],
}

View file

@ -1,32 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -1,11 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"strict": true
},
"include": ["vite.config.ts"]
}

View file

@ -1,83 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'
import path from 'path'
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
clientsClaim: true,
skipWaiting: true,
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.aethex\.dev\/.*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
networkTimeoutSeconds: 3,
expiration: {
maxEntries: 50,
maxAgeSeconds: 7 * 24 * 60 * 60, // 1 week
},
},
},
],
},
manifest: {
name: 'AeThex Connect',
short_name: 'AeThex',
description: 'Next-generation communication platform',
theme_color: '#a855f7',
background_color: '#0a0a0f',
display: 'standalone',
scope: '/',
start_url: '/',
orientation: 'portrait-primary',
},
devOptions: {
enabled: true,
navigateFallback: 'index.html',
suppressWarnings: true,
},
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
build: {
target: 'es2020',
outDir: 'dist',
sourcemap: false,
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
},
},
rollupOptions: {
output: {
manualChunks: {
'vendor-core': ['react', 'react-dom', 'react-router-dom'],
'vendor-state': ['@reduxjs/toolkit', 'react-redux'],
'vendor-webrtc': ['socket.io-client'],
},
},
},
},
server: {
port: 5173,
proxy: {
'/api': {
target: process.env.VITE_API_URL || 'http://localhost:3000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
})

View file

@ -0,0 +1,362 @@
/**
* Authentication Routes
* Handles user registration, login, and session management
*/
const express = require('express');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const db = require('../database/db');
const { authenticateUser } = require('../middleware/auth');
const router = express.Router();
/**
* POST /api/auth/register
* Register a new user
*/
router.post('/register', async (req, res) => {
try {
const { email, password, username, displayName } = req.body;
// Validation
if (!email || !password) {
return res.status(400).json({
success: false,
error: 'Email and password are required'
});
}
if (password.length < 8) {
return res.status(400).json({
success: false,
error: 'Password must be at least 8 characters'
});
}
// Check if user already exists
const existingUser = await db.query(
'SELECT id FROM users WHERE email = $1',
[email.toLowerCase()]
);
if (existingUser.rows.length > 0) {
return res.status(409).json({
success: false,
error: 'An account with this email already exists'
});
}
// Check if username is taken (if provided)
if (username) {
const existingUsername = await db.query(
'SELECT id FROM users WHERE username = $1',
[username.toLowerCase()]
);
if (existingUsername.rows.length > 0) {
return res.status(409).json({
success: false,
error: 'This username is already taken'
});
}
}
// Hash password
const salt = await bcrypt.genSalt(12);
const passwordHash = await bcrypt.hash(password, salt);
// Create user
const result = await db.query(
`INSERT INTO users (email, password_hash, username, display_name, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW())
RETURNING id, email, username, display_name, created_at`,
[email.toLowerCase(), passwordHash, username?.toLowerCase() || null, displayName || username || email.split('@')[0]]
);
const user = result.rows[0];
// Generate JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({
success: true,
data: {
user: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.display_name
},
token
}
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({
success: false,
error: 'Failed to create account'
});
}
});
/**
* POST /api/auth/login
* Login with email and password
*/
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
// Validation
if (!email || !password) {
return res.status(400).json({
success: false,
error: 'Email and password are required'
});
}
// Find user
const result = await db.query(
`SELECT id, email, password_hash, username, display_name, verified_domain, avatar_url, is_premium
FROM users WHERE email = $1`,
[email.toLowerCase()]
);
if (result.rows.length === 0) {
return res.status(401).json({
success: false,
error: 'Invalid email or password'
});
}
const user = result.rows[0];
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password_hash);
if (!isValidPassword) {
return res.status(401).json({
success: false,
error: 'Invalid email or password'
});
}
// Generate JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
// Update last login
await db.query(
'UPDATE users SET last_login = NOW() WHERE id = $1',
[user.id]
);
res.json({
success: true,
data: {
user: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.display_name,
verifiedDomain: user.verified_domain,
avatarUrl: user.avatar_url,
isPremium: user.is_premium
},
token
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
success: false,
error: 'Login failed'
});
}
});
/**
* POST /api/auth/demo
* Demo login - creates or returns a demo user
*/
router.post('/demo', async (req, res) => {
try {
const demoEmail = 'demo@aethex.dev';
// Check if demo user exists
let result = await db.query(
`SELECT id, email, username, display_name, verified_domain, avatar_url, is_premium
FROM users WHERE email = $1`,
[demoEmail]
);
let user;
if (result.rows.length === 0) {
// Create demo user
const salt = await bcrypt.genSalt(12);
const passwordHash = await bcrypt.hash('demo123456', salt);
result = await db.query(
`INSERT INTO users (email, password_hash, username, display_name, verified_domain, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, NOW(), NOW())
RETURNING id, email, username, display_name, verified_domain, avatar_url, is_premium`,
[demoEmail, passwordHash, 'demo', 'Demo User', 'demo.aethex']
);
}
user = result.rows[0];
// Generate JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
success: true,
data: {
user: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.display_name,
verifiedDomain: user.verified_domain,
avatarUrl: user.avatar_url,
isPremium: user.is_premium
},
token
}
});
} catch (error) {
console.error('Demo login error:', error);
res.status(500).json({
success: false,
error: 'Demo login failed'
});
}
});
/**
* GET /api/auth/me
* Get current user info
*/
router.get('/me', authenticateUser, async (req, res) => {
try {
const result = await db.query(
`SELECT id, email, username, display_name, verified_domain, avatar_url, is_premium, created_at
FROM users WHERE id = $1`,
[req.user.id]
);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
error: 'User not found'
});
}
const user = result.rows[0];
res.json({
success: true,
data: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.display_name,
verifiedDomain: user.verified_domain,
avatarUrl: user.avatar_url,
isPremium: user.is_premium,
createdAt: user.created_at
}
});
} catch (error) {
console.error('Get user error:', error);
res.status(500).json({
success: false,
error: 'Failed to get user info'
});
}
});
/**
* PUT /api/auth/profile
* Update user profile
*/
router.put('/profile', authenticateUser, async (req, res) => {
try {
const { displayName, username, avatarUrl } = req.body;
const userId = req.user.id;
// Check if username is taken
if (username) {
const existingUsername = await db.query(
'SELECT id FROM users WHERE username = $1 AND id != $2',
[username.toLowerCase(), userId]
);
if (existingUsername.rows.length > 0) {
return res.status(409).json({
success: false,
error: 'This username is already taken'
});
}
}
const result = await db.query(
`UPDATE users
SET display_name = COALESCE($1, display_name),
username = COALESCE($2, username),
avatar_url = COALESCE($3, avatar_url),
updated_at = NOW()
WHERE id = $4
RETURNING id, email, username, display_name, verified_domain, avatar_url, is_premium`,
[displayName, username?.toLowerCase(), avatarUrl, userId]
);
const user = result.rows[0];
res.json({
success: true,
data: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.display_name,
verifiedDomain: user.verified_domain,
avatarUrl: user.avatar_url,
isPremium: user.is_premium
}
});
} catch (error) {
console.error('Update profile error:', error);
res.status(500).json({
success: false,
error: 'Failed to update profile'
});
}
});
/**
* POST /api/auth/logout
* Logout (client-side token removal, but we can track it)
*/
router.post('/logout', authenticateUser, (req, res) => {
// In a more complex system, you'd invalidate the token here
res.json({
success: true,
message: 'Logged out successfully'
});
});
module.exports = router;

View file

@ -5,6 +5,7 @@ const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
const authRoutes = require('./routes/authRoutes');
const domainRoutes = require('./routes/domainRoutes');
const messagingRoutes = require('./routes/messagingRoutes');
const gameforgeRoutes = require('./routes/gameforgeRoutes');
@ -52,6 +53,7 @@ const frontendPath = path.join(__dirname, '../frontend/dist');
app.use(express.static(frontendPath));
// API routes
app.use('/api/auth', authRoutes);
app.use('/api/passport/domain', domainRoutes);
app.use('/api/messaging', messagingRoutes);
app.use('/api/gameforge', gameforgeRoutes);

View file

@ -160,6 +160,34 @@ class SocketService {
}
});
// Voice channel events
socket.on('voice:join', async (data) => {
try {
await this.handleVoiceJoin(socket, data);
} catch (error) {
console.error('Error handling voice:join:', error);
socket.emit('error', { event: 'voice:join', message: error.message });
}
});
socket.on('voice:leave', async (data) => {
try {
await this.handleVoiceLeave(socket, data);
} catch (error) {
console.error('Error handling voice:leave:', error);
socket.emit('error', { event: 'voice:leave', message: error.message });
}
});
socket.on('voice:signal', async (data) => {
try {
await this.handleVoiceSignal(socket, data);
} catch (error) {
console.error('Error handling voice:signal:', error);
socket.emit('error', { event: 'voice:signal', message: error.message });
}
});
// Disconnect handler
socket.on('disconnect', async () => {
try {
@ -247,12 +275,82 @@ class SocketService {
});
}
/**
* Handle voice channel join
*/
handleVoiceJoin(socket, data) {
const { channelId } = data;
const voiceRoom = `voice:${channelId}`;
// Join the voice room
socket.join(voiceRoom);
// Store the voice channel on the socket
socket.voiceChannel = channelId;
console.log(`User ${socket.user.username} joined voice channel ${channelId}`);
// Notify others in the channel
socket.to(voiceRoom).emit('voice:user_joined', {
userId: socket.user.id,
username: socket.user.username,
channelId
});
}
/**
* Handle voice channel leave
*/
handleVoiceLeave(socket, data) {
const { channelId } = data;
const voiceRoom = `voice:${channelId}`;
// Leave the voice room
socket.leave(voiceRoom);
// Clear the voice channel from socket
socket.voiceChannel = null;
console.log(`User ${socket.user.username} left voice channel ${channelId}`);
// Notify others in the channel
socket.to(voiceRoom).emit('voice:user_left', {
userId: socket.user.id,
username: socket.user.username,
channelId
});
}
/**
* Handle voice signaling (WebRTC)
*/
handleVoiceSignal(socket, data) {
const { to, channelId, data: signalData } = data;
// Send signal to specific user
this.io.to(`user:${to}`).emit('voice:signal', {
from: socket.user.id,
channelId,
data: signalData
});
}
/**
* Handle disconnect
*/
handleDisconnect(socket, userId) {
console.log(`User disconnected: ${socket.user.username} (${userId})`);
// If user was in a voice channel, notify others
if (socket.voiceChannel) {
const voiceRoom = `voice:${socket.voiceChannel}`;
socket.to(voiceRoom).emit('voice:user_left', {
userId: socket.user.id,
username: socket.user.username,
channelId: socket.voiceChannel
});
}
// Remove socket from tracking
if (this.userSockets.has(userId)) {
this.userSockets.get(userId).delete(socket.id);

View file

@ -1,189 +0,0 @@
/* 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

@ -1,105 +0,0 @@
import React from 'react';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { MessagingProvider } from './contexts/MessagingContext';
import MainLayout from './mockup/MainLayout';
import LandingPage from './mockup/pages/LandingPage';
import LoginPage from './mockup/pages/LoginPage';
import RegisterPage from './mockup/pages/RegisterPage';
import AboutPage from './mockup/pages/AboutPage';
import FeaturesPage from './mockup/pages/FeaturesPage';
import './index.css';
import './mockup/mockup.css';
/**
* Protected route - requires authentication
*/
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="loading-screen">
<div className="loading-spinner"></div>
<span>Loading...</span>
</div>
);
}
return isAuthenticated ? children : <Navigate to="/login" replace />;
}
/**
* Public route - redirects to app if logged in
*/
function PublicRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="loading-screen">
<div className="loading-spinner"></div>
<span>Loading...</span>
</div>
);
}
return !isAuthenticated ? children : <Navigate to="/app" replace />;
}
/**
* Main app with routing
*/
function AppRoutes() {
const { login, register, logout } = useAuth();
return (
<Routes>
{/* Marketing pages - public */}
<Route path="/" element={<LandingPage />} />
<Route path="/features" element={<FeaturesPage />} />
<Route path="/about" element={<AboutPage />} />
{/* Auth pages */}
<Route path="/login" element={
<PublicRoute>
<LoginPage onLogin={login} />
</PublicRoute>
} />
<Route path="/register" element={
<PublicRoute>
<RegisterPage onRegister={register} />
</PublicRoute>
} />
{/* Connect App - protected */}
<Route path="/app" element={
<ProtectedRoute>
<MessagingProvider>
<MainLayout onLogout={logout} />
</MessagingProvider>
</ProtectedRoute>
} />
<Route path="/app/*" element={
<ProtectedRoute>
<MessagingProvider>
<MainLayout onLogout={logout} />
</MessagingProvider>
</ProtectedRoute>
} />
{/* Catch all - redirect to home */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}
export default function App() {
return (
<BrowserRouter>
<AuthProvider>
<AppRoutes />
</AuthProvider>
</BrowserRouter>
);
}

View file

@ -1,580 +0,0 @@
/* 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-content: center;
background: #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

@ -1,311 +0,0 @@
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">🚀</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

@ -1,345 +0,0 @@
.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

@ -1,556 +0,0 @@
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

@ -1,156 +0,0 @@
/* 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

@ -1,436 +0,0 @@
/**
* 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);
const activeConversationRef = 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();
// Only update messages if this conversation is still active
// This prevents race conditions when rapidly switching conversations
if (data.success && activeConversationRef.current === conversationId) {
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) => {
const conversationId = conversation.id;
// Update state and ref immediately to prevent race conditions
setActiveConversation(conversation);
activeConversationRef.current = conversationId;
// Clear current messages while loading
setMessages([]);
await loadMessages(conversationId);
};
// 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

@ -1,201 +0,0 @@
/* 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

@ -1,110 +0,0 @@
/**
* 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

@ -1,117 +0,0 @@
/* 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

@ -1,134 +0,0 @@
/**
* 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

@ -1,310 +0,0 @@
/* 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

@ -1,139 +0,0 @@
/**
* 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

@ -1,316 +0,0 @@
.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

@ -1,313 +0,0 @@
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

@ -1,84 +0,0 @@
.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

@ -1,62 +0,0 @@
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

@ -1,46 +0,0 @@
.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

@ -1,172 +0,0 @@
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

@ -1,99 +0,0 @@
.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

@ -1,139 +0,0 @@
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

@ -1,257 +0,0 @@
.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

@ -1,270 +0,0 @@
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

@ -1,217 +0,0 @@
.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

@ -1,307 +0,0 @@
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

@ -1,85 +0,0 @@
.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

@ -1,41 +0,0 @@
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

@ -1,93 +0,0 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
import.meta.env.VITE_SUPABASE_URL || 'http://127.0.0.1:3000',
import.meta.env.VITE_SUPABASE_ANON_KEY || 'sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH'
);
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 login = async (email, password) => {
const { data, error } = await supabase.auth.signInWithPassword({ email, password });
if (error) {
return { success: false, error: error.message };
}
setUser(data.user);
return { success: true };
};
const register = async (email, password, metadata = {}) => {
const { data, error } = await supabase.auth.signUp({
email,
password,
options: {
data: metadata
}
});
if (error) {
return { success: false, error: error.message };
}
setUser(data.user);
return { success: true };
};
const logout = async () => {
await supabase.auth.signOut();
setUser(null);
};
const updateUser = (updates) => {
setUser(prev => ({ ...prev, ...updates }));
};
const value = {
user,
loading,
login,
register,
logout,
updateUser,
isAuthenticated: !!user
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
}
export default AuthContext;

View file

@ -1,159 +0,0 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
import { messagingService, socket, connectSocket, disconnectSocket } from '../services/messaging';
import { useAuth } from './AuthContext';
const MessagingContext = createContext();
export function useMessaging() {
const context = useContext(MessagingContext);
if (!context) {
throw new Error('useMessaging must be used within a MessagingProvider');
}
return context;
}
export function MessagingProvider({ children }) {
const { user } = useAuth();
const [conversations, setConversations] = useState([]);
const [currentConversation, setCurrentConversation] = useState(null);
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Load conversations when user logs in
useEffect(() => {
if (user?.id) {
connectSocket(user.id);
loadConversations();
} else {
disconnectSocket();
setConversations([]);
setMessages([]);
}
return () => {
disconnectSocket();
};
}, [user?.id]);
// Listen for real-time messages
useEffect(() => {
const handleNewMessage = (message) => {
if (message.conversation_id === currentConversation?.id) {
setMessages(prev => [...prev, message]);
}
// Update conversation's last message
setConversations(prev =>
prev.map(conv =>
conv.id === message.conversation_id
? { ...conv, lastMessage: message, updated_at: message.created_at }
: conv
)
);
};
socket.on('message:new', handleNewMessage);
return () => {
socket.off('message:new', handleNewMessage);
};
}, [currentConversation?.id]);
// Subscribe to current conversation's messages
useEffect(() => {
if (!currentConversation?.id) return;
const unsubscribe = messagingService.subscribeToMessages(
currentConversation.id,
(newMessage) => {
setMessages(prev => {
// Avoid duplicates
if (prev.find(m => m.id === newMessage.id)) return prev;
return [...prev, newMessage];
});
}
);
return unsubscribe;
}, [currentConversation?.id]);
const loadConversations = useCallback(async () => {
if (!user?.id) return;
setLoading(true);
try {
const data = await messagingService.getConversations(user.id);
setConversations(data);
} catch (err) {
console.error('Failed to load conversations:', err);
setError(err.message);
} finally {
setLoading(false);
}
}, [user?.id]);
const selectConversation = useCallback(async (conversation) => {
setCurrentConversation(conversation);
setLoading(true);
try {
const msgs = await messagingService.getMessages(conversation.id);
setMessages(msgs);
} catch (err) {
console.error('Failed to load messages:', err);
setError(err.message);
} finally {
setLoading(false);
}
}, []);
const sendMessage = useCallback(async (content) => {
if (!currentConversation?.id || !user?.id || !content.trim()) return;
try {
const message = await messagingService.sendMessage(
currentConversation.id,
user.id,
content
);
// Optimistically add to local state
setMessages(prev => [...prev, message]);
} catch (err) {
console.error('Failed to send message:', err);
setError(err.message);
}
}, [currentConversation?.id, user?.id]);
const createConversation = useCallback(async (type, title, participantIds) => {
if (!user?.id) return;
try {
const conversation = await messagingService.createConversation(
type,
title,
[user.id, ...participantIds]
);
setConversations(prev => [conversation, ...prev]);
return conversation;
} catch (err) {
console.error('Failed to create conversation:', err);
setError(err.message);
}
}, [user?.id]);
const value = {
conversations,
currentConversation,
messages,
loading,
error,
selectConversation,
sendMessage,
createConversation,
refreshConversations: loadConversations,
};
return (
<MessagingContext.Provider value={value}>
{children}
</MessagingContext.Provider>
);
}

View file

@ -1,74 +0,0 @@
/**
* 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

@ -1,31 +0,0 @@
@import "tailwindcss";
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0a0a;
color: #e4e4e7;
line-height: 1.5;
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #111;
}
::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #444;
}

View file

@ -1,99 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Primary Meta Tags -->
<title>AeThex Connect - Secure, Open-Source Communication Platform</title>
<meta name="title" content="AeThex Connect - Secure, Open-Source Communication Platform" />
<meta name="description" content="Privacy-first communication platform with end-to-end encryption, crystal-clear voice & video calls, and seamless cross-platform sync. Open source and community-driven." />
<meta name="keywords" content="AeThex, Connect, Discord alternative, secure messaging, encrypted chat, voice calls, video calls, open source, privacy, communication platform, gaming, community" />
<meta name="author" content="AeThex Corporation" />
<meta name="robots" content="index, follow" />
<meta name="language" content="English" />
<meta name="revisit-after" content="7 days" />
<!-- Canonical URL -->
<link rel="canonical" href="https://aethex.online/" />
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="msapplication-TileColor" content="#0a0a0a" />
<meta name="theme-color" content="#0a0a0a" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://aethex.online/" />
<meta property="og:title" content="AeThex Connect - Secure, Open-Source Communication" />
<meta property="og:description" content="Privacy-first communication platform with end-to-end encryption, crystal-clear voice & video calls, and seamless cross-platform sync." />
<meta property="og:image" content="https://aethex.online/og-image.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="AeThex Connect - The Trinity of Communication" />
<meta property="og:site_name" content="AeThex Connect" />
<meta property="og:locale" content="en_US" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content="https://aethex.online/" />
<meta name="twitter:title" content="AeThex Connect - Secure, Open-Source Communication" />
<meta name="twitter:description" content="Privacy-first communication platform with end-to-end encryption, crystal-clear voice & video calls, and seamless cross-platform sync." />
<meta name="twitter:image" content="https://aethex.online/og-image.png" />
<meta name="twitter:image:alt" content="AeThex Connect - The Trinity of Communication" />
<meta name="twitter:site" content="@AeThexCorp" />
<meta name="twitter:creator" content="@AeThexCorp" />
<!-- Additional SEO -->
<meta name="application-name" content="AeThex Connect" />
<meta name="apple-mobile-web-app-title" content="AeThex Connect" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="format-detection" content="telephone=no" />
<!-- DNS Prefetch for performance -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
<!-- Preconnect for critical resources -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "AeThex Connect",
"applicationCategory": "CommunicationApplication",
"operatingSystem": "Web, Windows, macOS, Linux, iOS, Android",
"description": "Privacy-first communication platform with end-to-end encryption, voice & video calls, and cross-platform sync.",
"url": "https://aethex.online",
"author": {
"@type": "Organization",
"name": "AeThex Corporation",
"url": "https://aethex.online"
},
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9",
"ratingCount": "1250"
}
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.jsx"></script>
</body>
</html>

View file

@ -1,11 +0,0 @@
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

@ -1,186 +0,0 @@
import React from 'react';
export default function ActivityStatus({ activity, size = 'normal' }) {
if (!activity) return null;
const activityTypes = {
playing: { icon: '🎮', label: 'Playing' },
streaming: { icon: '📺', label: 'Streaming' },
listening: { icon: '🎧', label: 'Listening to' },
watching: { icon: '📺', label: 'Watching' },
competing: { icon: '🏆', label: 'Competing in' },
custom: { icon: null, label: null },
developing: { icon: '💻', label: 'Developing' },
building: { icon: '🔨', label: 'Building' },
};
const typeInfo = activityTypes[activity.type] || activityTypes.custom;
if (size === 'small') {
return (
<div className="activity-status-small">
{typeInfo.icon && <span className="activity-icon-small">{typeInfo.icon}</span>}
<span className="activity-text-small">
{typeInfo.label ? `${typeInfo.label} ${activity.name}` : activity.name}
</span>
</div>
);
}
if (size === 'mini') {
return (
<span className="activity-status-mini">
{activity.name}
</span>
);
}
return (
<div className="activity-status">
<div className="activity-header">
{typeInfo.icon && <span className="activity-type-icon">{typeInfo.icon}</span>}
<span className="activity-type-label">{typeInfo.label || 'Activity'}</span>
</div>
<div className="activity-content">
{activity.largeImage && (
<div className="activity-image">
<img src={activity.largeImage} alt="" />
{activity.smallImage && (
<img src={activity.smallImage} alt="" className="activity-image-small" />
)}
</div>
)}
<div className="activity-details">
<div className="activity-name">{activity.name}</div>
{activity.details && <div className="activity-detail">{activity.details}</div>}
{activity.state && <div className="activity-state">{activity.state}</div>}
{activity.startTime && (
<div className="activity-time">
<ActivityTimer startTime={activity.startTime} />
</div>
)}
</div>
</div>
{activity.buttons && activity.buttons.length > 0 && (
<div className="activity-buttons">
{activity.buttons.map((btn, idx) => (
<button key={idx} className="activity-btn">
{btn.label}
</button>
))}
</div>
)}
</div>
);
}
function ActivityTimer({ startTime }) {
const [elapsed, setElapsed] = React.useState('');
React.useEffect(() => {
const updateElapsed = () => {
const now = Date.now();
const diff = now - startTime;
const hours = Math.floor(diff / 3600000);
const minutes = Math.floor((diff % 3600000) / 60000);
const seconds = Math.floor((diff % 60000) / 1000);
if (hours > 0) {
setElapsed(`${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')} elapsed`);
} else {
setElapsed(`${minutes}:${seconds.toString().padStart(2, '0')} elapsed`);
}
};
updateElapsed();
const interval = setInterval(updateElapsed, 1000);
return () => clearInterval(interval);
}, [startTime]);
return <span>{elapsed}</span>;
}
// Rich Presence card for games
export function RichPresence({ game }) {
if (!game) return null;
return (
<div className="rich-presence">
<div className="rp-header">
<span className="rp-icon">🎮</span>
<span className="rp-label">Playing a game</span>
</div>
<div className="rp-content">
<div className="rp-image">
{game.icon ? (
<img src={game.icon} alt={game.name} />
) : (
<span className="rp-placeholder">🎮</span>
)}
</div>
<div className="rp-info">
<div className="rp-name">{game.name}</div>
{game.details && <div className="rp-details">{game.details}</div>}
{game.state && <div className="rp-state">{game.state}</div>}
<div className="rp-elapsed">
<ActivityTimer startTime={game.startTime || Date.now() - 3600000} />
</div>
</div>
</div>
</div>
);
}
// Spotify-style listening activity
export function SpotifyActivity({ track }) {
if (!track) return null;
return (
<div className="spotify-activity">
<div className="spotify-header">
<span className="spotify-icon">🎧</span>
<span className="spotify-label">Listening to Spotify</span>
</div>
<div className="spotify-content">
<div className="spotify-album">
{track.albumArt ? (
<img src={track.albumArt} alt={track.album} />
) : (
<span className="spotify-placeholder">🎵</span>
)}
</div>
<div className="spotify-info">
<div className="spotify-song">{track.name}</div>
<div className="spotify-artist">by {track.artist}</div>
<div className="spotify-album-name">on {track.album}</div>
</div>
</div>
<div className="spotify-progress">
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${(track.elapsed / track.duration) * 100}%` }}
/>
</div>
<div className="progress-times">
<span>{formatTime(track.elapsed)}</span>
<span>{formatTime(track.duration)}</span>
</div>
</div>
</div>
);
}
function formatTime(ms) {
const minutes = Math.floor(ms / 60000);
const seconds = Math.floor((ms % 60000) / 1000);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}

View file

@ -1,220 +0,0 @@
import React, { useState } from 'react';
const categories = [
{ id: 'all', label: 'All Apps', icon: '🔷' },
{ id: 'moderation', label: 'Moderation', icon: '🛡️' },
{ id: 'entertainment', label: 'Entertainment', icon: '🎮' },
{ id: 'social', label: 'Social', icon: '💬' },
{ id: 'utility', label: 'Utility', icon: '🔧' },
{ id: 'music', label: 'Music', icon: '🎵' },
];
const apps = [
{
id: 1,
name: 'MEE6',
icon: '🤖',
color: '#5865f2',
description: 'The most popular bot for moderation, leveling, and social commands.',
category: 'moderation',
verified: true,
servers: '19M+',
rating: 4.5,
features: ['Moderation', 'Leveling', 'Custom Commands', 'Music'],
},
{
id: 2,
name: 'Dyno',
icon: '⚡',
color: '#ffa500',
description: 'Fully customizable server moderation bot with a web dashboard.',
category: 'moderation',
verified: true,
servers: '4.7M+',
rating: 4.3,
features: ['Auto-mod', 'Dashboard', 'Logs', 'Announcements'],
},
{
id: 3,
name: 'Rythm',
icon: '🎵',
color: '#e91e63',
description: 'High quality music bot with support for multiple platforms.',
category: 'music',
verified: true,
servers: '6M+',
rating: 4.7,
features: ['Music', 'Playlists', 'Lyrics', 'Volume Control'],
},
{
id: 4,
name: 'Dank Memer',
icon: '🐸',
color: '#3ba55d',
description: 'Meme posting, currency system, and fun commands.',
category: 'entertainment',
verified: true,
servers: '8M+',
rating: 4.6,
features: ['Economy', 'Memes', 'Games', 'Gambling'],
},
{
id: 5,
name: 'Carl-bot',
icon: '🎭',
color: '#9b59b6',
description: 'Reaction roles, logging, automod, and utility commands.',
category: 'utility',
verified: true,
servers: '3.5M+',
rating: 4.4,
features: ['Reaction Roles', 'Logging', 'Tags', 'Embeds'],
},
{
id: 6,
name: 'Pokétwo',
icon: '🔴',
color: '#ff0000',
description: 'Catch, trade, and battle Pokémon in Discord.',
category: 'entertainment',
verified: true,
servers: '1.2M+',
rating: 4.5,
features: ['Pokémon', 'Trading', 'Battles', 'Events'],
},
{
id: 7,
name: 'Tickets',
icon: '🎫',
color: '#5865f2',
description: 'Simple and powerful ticket system for support.',
category: 'utility',
verified: false,
servers: '500K+',
rating: 4.2,
features: ['Tickets', 'Transcripts', 'Categories', 'Staff Roles'],
},
{
id: 8,
name: 'Arcane',
icon: '🔮',
color: '#eb459e',
description: 'XP leveling, role rewards, and leaderboards.',
category: 'social',
verified: false,
servers: '800K+',
rating: 4.1,
features: ['Leveling', 'Role Rewards', 'Leaderboard', 'Card Design'],
},
];
export default function AppDirectory({ onClose }) {
const [search, setSearch] = useState('');
const [activeCategory, setActiveCategory] = useState('all');
const [selectedApp, setSelectedApp] = useState(null);
const filteredApps = apps.filter(app => {
const matchesSearch = !search ||
app.name.toLowerCase().includes(search.toLowerCase()) ||
app.description.toLowerCase().includes(search.toLowerCase());
const matchesCategory = activeCategory === 'all' || app.category === activeCategory;
return matchesSearch && matchesCategory;
});
return (
<div className="app-directory">
<div className="app-dir-header">
<h2>🔷 App Directory</h2>
<button className="app-dir-close" onClick={onClose}></button>
</div>
<div className="app-dir-search">
<span className="search-icon">🔍</span>
<input
type="text"
placeholder="Search apps..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="app-dir-categories">
{categories.map(cat => (
<button
key={cat.id}
className={`app-cat-btn ${activeCategory === cat.id ? 'active' : ''}`}
onClick={() => setActiveCategory(cat.id)}
>
<span>{cat.icon}</span> {cat.label}
</button>
))}
</div>
<div className="app-dir-content">
{selectedApp ? (
<div className="app-detail">
<button className="back-btn" onClick={() => setSelectedApp(null)}>
Back
</button>
<div className="app-detail-header">
<div
className="app-icon-large"
style={{ background: selectedApp.color }}
>
{selectedApp.icon}
</div>
<div className="app-detail-info">
<div className="app-name-row">
<h2>{selectedApp.name}</h2>
{selectedApp.verified && <span className="verified-badge"> Verified</span>}
</div>
<p className="app-description">{selectedApp.description}</p>
<div className="app-stats">
<span>📊 {selectedApp.servers} servers</span>
<span> {selectedApp.rating}/5</span>
</div>
</div>
<button className="add-to-server-btn">Add to Server</button>
</div>
<div className="app-features">
<h3>Features</h3>
<div className="feature-tags">
{selectedApp.features.map((feature, idx) => (
<span key={idx} className="feature-tag">{feature}</span>
))}
</div>
</div>
</div>
) : (
<div className="app-grid">
{filteredApps.map(app => (
<div
key={app.id}
className="app-card"
onClick={() => setSelectedApp(app)}
>
<div
className="app-card-icon"
style={{ background: app.color }}
>
{app.icon}
</div>
<div className="app-card-info">
<div className="app-card-name">
{app.name}
{app.verified && <span className="verified-small"></span>}
</div>
<p className="app-card-desc">{app.description}</p>
<div className="app-card-stats">
<span>{app.servers} servers</span>
<span> {app.rating}</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View file

@ -1,170 +0,0 @@
import React, { useState } from 'react';
const mockLogs = [
{ id: 1, action: 'member_kick', user: { name: 'ModUser', avatar: 'M', color: '#5865f2' }, target: 'SpamBot#1234', time: '2 minutes ago', reason: 'Spam' },
{ id: 2, action: 'channel_create', user: { name: 'Admin', avatar: 'A', color: '#ff0000' }, target: '#announcements', time: '15 minutes ago' },
{ id: 3, action: 'role_update', user: { name: 'Admin', avatar: 'A', color: '#ff0000' }, target: 'Moderator', time: '1 hour ago', changes: 'Added kick permission' },
{ id: 4, action: 'member_ban', user: { name: 'ModUser', avatar: 'M', color: '#5865f2' }, target: 'ToxicUser#9999', time: '2 hours ago', reason: 'Harassment' },
{ id: 5, action: 'message_delete', user: { name: 'AutoMod', avatar: '🤖', color: '#3ba55d' }, target: '5 messages', time: '3 hours ago', reason: 'Spam filter' },
{ id: 6, action: 'invite_create', user: { name: 'Member', avatar: 'M', color: '#ffa500' }, target: 'discord.gg/abc123', time: '4 hours ago' },
{ id: 7, action: 'member_timeout', user: { name: 'ModUser', avatar: 'M', color: '#5865f2' }, target: 'RuleBreaker#0001', time: '5 hours ago', reason: 'Breaking rules', duration: '10 minutes' },
{ id: 8, action: 'server_update', user: { name: 'Owner', avatar: 'O', color: '#e91e63' }, target: 'Server name', time: '1 day ago', changes: 'Changed to AeThex Foundation' },
{ id: 9, action: 'emoji_create', user: { name: 'Admin', avatar: 'A', color: '#ff0000' }, target: ':custom_emoji:', time: '2 days ago' },
{ id: 10, action: 'webhook_create', user: { name: 'Developer', avatar: 'D', color: '#0066ff' }, target: 'GitHub Webhook', time: '3 days ago' },
];
const actionTypes = [
{ id: 'all', label: 'All Actions' },
{ id: 'member', label: 'Member Actions' },
{ id: 'channel', label: 'Channel Actions' },
{ id: 'role', label: 'Role Actions' },
{ id: 'message', label: 'Message Actions' },
{ id: 'server', label: 'Server Actions' },
{ id: 'invite', label: 'Invite Actions' },
];
const actionIcons = {
member_kick: '👢',
member_ban: '🔨',
member_timeout: '⏰',
member_join: '📥',
member_leave: '📤',
channel_create: '',
channel_delete: '🗑️',
channel_update: '📝',
role_create: '🎭',
role_update: '🎭',
role_delete: '🎭',
message_delete: '💬',
message_pin: '📌',
server_update: '⚙️',
invite_create: '🔗',
invite_delete: '🔗',
emoji_create: '😀',
emoji_delete: '😀',
webhook_create: '🔌',
webhook_delete: '🔌',
};
const actionLabels = {
member_kick: 'kicked',
member_ban: 'banned',
member_timeout: 'timed out',
member_join: 'joined',
member_leave: 'left',
channel_create: 'created channel',
channel_delete: 'deleted channel',
channel_update: 'updated channel',
role_create: 'created role',
role_update: 'updated role',
role_delete: 'deleted role',
message_delete: 'deleted',
message_pin: 'pinned',
server_update: 'updated',
invite_create: 'created invite',
invite_delete: 'deleted invite',
emoji_create: 'added emoji',
emoji_delete: 'removed emoji',
webhook_create: 'created webhook',
webhook_delete: 'deleted webhook',
};
export default function AuditLog({ onClose }) {
const [filter, setFilter] = useState('all');
const [searchUser, setSearchUser] = useState('');
const [expandedLog, setExpandedLog] = useState(null);
const filteredLogs = mockLogs.filter(log => {
if (filter !== 'all' && !log.action.startsWith(filter)) return false;
if (searchUser && !log.user.name.toLowerCase().includes(searchUser.toLowerCase())) return false;
return true;
});
return (
<div className="audit-log">
<div className="audit-header">
<h2>📋 Audit Log</h2>
<button className="audit-close" onClick={onClose}></button>
</div>
<div className="audit-filters">
<div className="audit-search">
<span>🔍</span>
<input
type="text"
placeholder="Filter by user..."
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
/>
</div>
<select
className="audit-type-filter"
value={filter}
onChange={(e) => setFilter(e.target.value)}
>
{actionTypes.map(type => (
<option key={type.id} value={type.id}>{type.label}</option>
))}
</select>
</div>
<div className="audit-list">
{filteredLogs.map(log => (
<div
key={log.id}
className={`audit-entry ${expandedLog === log.id ? 'expanded' : ''}`}
onClick={() => setExpandedLog(expandedLog === log.id ? null : log.id)}
>
<div className="audit-entry-main">
<span className="audit-icon">{actionIcons[log.action] || '📝'}</span>
<div
className="audit-user-avatar"
style={{ background: log.user.color }}
>
{log.user.avatar}
</div>
<div className="audit-info">
<span className="audit-user-name">{log.user.name}</span>
<span className="audit-action">{actionLabels[log.action] || log.action}</span>
<span className="audit-target">{log.target}</span>
</div>
<span className="audit-time">{log.time}</span>
</div>
{expandedLog === log.id && (
<div className="audit-details">
{log.reason && (
<div className="audit-detail-row">
<span className="detail-label">Reason:</span>
<span className="detail-value">{log.reason}</span>
</div>
)}
{log.duration && (
<div className="audit-detail-row">
<span className="detail-label">Duration:</span>
<span className="detail-value">{log.duration}</span>
</div>
)}
{log.changes && (
<div className="audit-detail-row">
<span className="detail-label">Changes:</span>
<span className="detail-value">{log.changes}</span>
</div>
)}
<div className="audit-detail-row">
<span className="detail-label">ID:</span>
<span className="detail-value audit-id">{log.id}</span>
</div>
</div>
)}
</div>
))}
</div>
<div className="audit-footer">
<span className="audit-count">{filteredLogs.length} entries</span>
<button className="audit-load-more">Load More</button>
</div>
</div>
);
}

View file

@ -1,259 +0,0 @@
import React, { useState } from 'react';
const autoModRules = [
{
id: 'profanity',
name: 'Block Profanity',
description: 'Automatically block common profane words',
icon: '🤬',
severity: 'high'
},
{
id: 'spam',
name: 'Block Spam',
description: 'Prevent repeated messages, excessive mentions, and invite links',
icon: '📧',
severity: 'medium'
},
{
id: 'links',
name: 'Block Links',
description: 'Block messages containing links (with allowlist)',
icon: '🔗',
severity: 'low'
},
{
id: 'mentions',
name: 'Limit Mentions',
description: 'Block messages with excessive @mentions',
icon: '@',
severity: 'medium'
},
{
id: 'caps',
name: 'Block Excessive Caps',
description: 'Block messages that are mostly uppercase',
icon: '🔠',
severity: 'low'
},
{
id: 'attachments',
name: 'Block Attachments',
description: 'Restrict file attachments in messages',
icon: '📎',
severity: 'low'
},
];
const defaultActions = [
{ id: 'delete', label: 'Delete Message', icon: '🗑️' },
{ id: 'alert', label: 'Send Alert to Channel', icon: '⚠️' },
{ id: 'timeout', label: 'Timeout Member', icon: '⏰' },
{ id: 'log', label: 'Log to Mod Channel', icon: '📝' },
];
export default function AutoModSettings({ onSave, onClose }) {
const [rules, setRules] = useState({
profanity: { enabled: true, action: 'delete' },
spam: { enabled: true, action: 'delete' },
links: { enabled: false, action: 'delete', allowlist: [] },
mentions: { enabled: true, action: 'alert', limit: 5 },
caps: { enabled: false, action: 'delete', threshold: 70 },
attachments: { enabled: false, action: 'delete' },
});
const [customWords, setCustomWords] = useState(['badword1', 'badword2']);
const [allowedLinks, setAllowedLinks] = useState(['youtube.com', 'twitter.com']);
const [logChannel, setLogChannel] = useState('mod-logs');
const [activeRule, setActiveRule] = useState(null);
const toggleRule = (ruleId) => {
setRules(prev => ({
...prev,
[ruleId]: { ...prev[ruleId], enabled: !prev[ruleId]?.enabled }
}));
};
const updateRuleAction = (ruleId, action) => {
setRules(prev => ({
...prev,
[ruleId]: { ...prev[ruleId], action }
}));
};
return (
<div className="automod-settings">
<div className="automod-header">
<h2>🤖 AutoMod</h2>
<p className="automod-subtitle">Automatically moderate your server with configurable rules</p>
<button className="automod-close" onClick={onClose}></button>
</div>
<div className="automod-content">
<section className="automod-section">
<h3>Moderation Rules</h3>
<div className="rules-list">
{autoModRules.map(rule => (
<div
key={rule.id}
className={`rule-card ${rules[rule.id]?.enabled ? 'enabled' : ''}`}
>
<div className="rule-main">
<div className="rule-toggle">
<input
type="checkbox"
checked={rules[rule.id]?.enabled || false}
onChange={() => toggleRule(rule.id)}
/>
</div>
<div className="rule-icon">{rule.icon}</div>
<div className="rule-info">
<h4>{rule.name}</h4>
<p>{rule.description}</p>
</div>
<div className={`severity-badge ${rule.severity}`}>
{rule.severity}
</div>
<button
className="configure-btn"
onClick={() => setActiveRule(activeRule === rule.id ? null : rule.id)}
>
{activeRule === rule.id ? '▼' : '▶'}
</button>
</div>
{activeRule === rule.id && (
<div className="rule-config">
<div className="config-field">
<label>Action when triggered</label>
<div className="action-options">
{defaultActions.map(action => (
<button
key={action.id}
className={`action-btn ${rules[rule.id]?.action === action.id ? 'active' : ''}`}
onClick={() => updateRuleAction(rule.id, action.id)}
>
<span className="action-icon">{action.icon}</span>
<span>{action.label}</span>
</button>
))}
</div>
</div>
{rule.id === 'mentions' && (
<div className="config-field">
<label>Maximum mentions per message</label>
<input
type="number"
min="1"
max="20"
value={rules.mentions?.limit || 5}
onChange={(e) => setRules(prev => ({
...prev,
mentions: { ...prev.mentions, limit: Number(e.target.value) }
}))}
className="number-input"
/>
</div>
)}
{rule.id === 'caps' && (
<div className="config-field">
<label>Caps threshold (%)</label>
<input
type="range"
min="50"
max="100"
value={rules.caps?.threshold || 70}
onChange={(e) => setRules(prev => ({
...prev,
caps: { ...prev.caps, threshold: Number(e.target.value) }
}))}
/>
<span>{rules.caps?.threshold || 70}%</span>
</div>
)}
{rule.id === 'links' && (
<div className="config-field">
<label>Allowed Domains</label>
<div className="tag-list">
{allowedLinks.map((link, idx) => (
<span key={idx} className="tag">
{link}
<button onClick={() => setAllowedLinks(prev => prev.filter((_, i) => i !== idx))}></button>
</span>
))}
</div>
<input
type="text"
placeholder="Add domain..."
onKeyPress={(e) => {
if (e.key === 'Enter' && e.target.value) {
setAllowedLinks(prev => [...prev, e.target.value]);
e.target.value = '';
}
}}
/>
</div>
)}
</div>
)}
</div>
))}
</div>
</section>
<section className="automod-section">
<h3>Custom Blocked Words</h3>
<p className="section-description">Add your own words to block in messages</p>
<div className="custom-words">
<div className="tag-list">
{customWords.map((word, idx) => (
<span key={idx} className="tag blocked">
{word}
<button onClick={() => setCustomWords(prev => prev.filter((_, i) => i !== idx))}></button>
</span>
))}
</div>
<input
type="text"
placeholder="Add word to block..."
onKeyPress={(e) => {
if (e.key === 'Enter' && e.target.value) {
setCustomWords(prev => [...prev, e.target.value.toLowerCase()]);
e.target.value = '';
}
}}
/>
</div>
</section>
<section className="automod-section">
<h3>Log Channel</h3>
<p className="section-description">Where to send AutoMod logs</p>
<select
value={logChannel}
onChange={(e) => setLogChannel(e.target.value)}
className="channel-select"
>
<option value="">None</option>
<option value="mod-logs"># mod-logs</option>
<option value="admin"># admin</option>
<option value="bot-logs"># bot-logs</option>
</select>
</section>
</div>
<div className="automod-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button
className="save-btn"
onClick={() => onSave?.({ rules, customWords, allowedLinks, logChannel })}
>
Save Changes
</button>
</div>
</div>
);
}

View file

@ -1,127 +0,0 @@
import React, { useState } from 'react';
const mockBans = [
{ id: 1, user: { name: 'SpamBot_123', tag: '#0000', avatar: 'S' }, reason: 'Spamming invite links', bannedBy: 'Trevor', bannedAt: '2025-12-15T14:30:00' },
{ id: 2, user: { name: 'ToxicUser', tag: '#6666', avatar: 'T' }, reason: 'Harassment and hate speech', bannedBy: 'Sarah', bannedAt: '2025-11-20T09:15:00' },
{ id: 3, user: { name: 'RaidLeader', tag: '#9999', avatar: 'R' }, reason: 'Organizing raid against server', bannedBy: 'Trevor', bannedAt: '2025-10-05T18:45:00' },
{ id: 4, user: { name: 'ScammerX', tag: '#1111', avatar: 'X' }, reason: 'Attempting to scam members', bannedBy: 'Sarah', bannedAt: '2025-09-12T11:00:00' },
{ id: 5, user: { name: 'BadActor', tag: '#4321', avatar: 'B' }, reason: 'No reason given', bannedBy: 'Trevor', bannedAt: '2025-08-01T16:20:00' },
];
export default function BanList({ onClose }) {
const [bans, setBans] = useState(mockBans);
const [searchQuery, setSearchQuery] = useState('');
const [selectedBan, setSelectedBan] = useState(null);
const [showRevokeConfirm, setShowRevokeConfirm] = useState(false);
const filteredBans = bans.filter(ban =>
ban.user.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
ban.reason.toLowerCase().includes(searchQuery.toLowerCase())
);
const formatDate = (dateStr) => {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
const handleRevoke = (banId) => {
setBans(prev => prev.filter(b => b.id !== banId));
setShowRevokeConfirm(false);
setSelectedBan(null);
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="ban-list-modal" onClick={(e) => e.stopPropagation()}>
<div className="ban-list-header">
<h2>🚫 Server Bans</h2>
<button className="ban-list-close" onClick={onClose}></button>
</div>
<div className="ban-list-search">
<input
type="text"
placeholder="Search bans..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="ban-list-content">
{filteredBans.length === 0 ? (
<div className="no-bans">
<span className="no-bans-icon"></span>
<p>No bans found</p>
<span className="no-bans-hint">This server has no banned users</span>
</div>
) : (
<>
<div className="ban-count">{filteredBans.length} banned user{filteredBans.length !== 1 ? 's' : ''}</div>
{filteredBans.map(ban => (
<div
key={ban.id}
className={`ban-item ${selectedBan === ban.id ? 'selected' : ''}`}
onClick={() => setSelectedBan(selectedBan === ban.id ? null : ban.id)}
>
<div className="ban-user-info">
<div className="ban-avatar">{ban.user.avatar}</div>
<div className="ban-user-details">
<div className="ban-username">
{ban.user.name}<span className="ban-tag">{ban.user.tag}</span>
</div>
<div className="ban-reason">{ban.reason}</div>
</div>
</div>
<div className="ban-meta">
<div className="ban-date">{formatDate(ban.bannedAt)}</div>
<div className="ban-by">by {ban.bannedBy}</div>
</div>
{selectedBan === ban.id && (
<div className="ban-actions">
<button
className="revoke-ban-btn"
onClick={(e) => {
e.stopPropagation();
setShowRevokeConfirm(true);
}}
>
Revoke Ban
</button>
</div>
)}
</div>
))}
</>
)}
</div>
{showRevokeConfirm && selectedBan && (
<div className="revoke-confirm-overlay" onClick={() => setShowRevokeConfirm(false)}>
<div className="revoke-confirm-modal" onClick={(e) => e.stopPropagation()}>
<h3>Revoke Ban</h3>
<p>
Are you sure you want to unban{' '}
<strong>{bans.find(b => b.id === selectedBan)?.user.name}</strong>?
</p>
<p className="revoke-warning">They will be able to rejoin the server with an invite.</p>
<div className="revoke-actions">
<button className="cancel-btn" onClick={() => setShowRevokeConfirm(false)}>
Cancel
</button>
<button className="confirm-revoke-btn" onClick={() => handleRevoke(selectedBan)}>
Revoke Ban
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}

View file

@ -1,155 +0,0 @@
import React, { useState } from 'react';
export default function CategoryEditor({ category, onClose, onSave, onDelete }) {
const categoryData = category || {
id: 'new',
name: '',
channels: [],
};
const [name, setName] = useState(categoryData.name);
const [isPrivate, setIsPrivate] = useState(false);
const [syncPermissions, setSyncPermissions] = useState(true);
const availableRoles = [
{ id: 'founder', name: 'Founder', color: '#ff0000' },
{ id: 'foundation', name: 'Foundation', color: '#ff0000' },
{ id: 'corporation', name: 'Corporation', color: '#0066ff' },
{ id: 'labs', name: 'Labs', color: '#ffa500' },
{ id: 'everyone', name: '@everyone', color: '#99aab5' },
];
const [allowedRoles, setAllowedRoles] = useState(['everyone']);
const toggleRole = (roleId) => {
setAllowedRoles(prev =>
prev.includes(roleId)
? prev.filter(id => id !== roleId)
: [...prev, roleId]
);
};
const handleSave = () => {
onSave?.({
id: categoryData.id,
name: name.toUpperCase(),
isPrivate,
allowedRoles,
syncPermissions,
});
};
const isNew = !category?.name;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="category-editor" onClick={(e) => e.stopPropagation()}>
<div className="category-editor-header">
<h2>{isNew ? 'Create Category' : 'Edit Category'}</h2>
<button className="category-close" onClick={onClose}></button>
</div>
<div className="category-editor-content">
<div className="setting-group">
<label>Category Name</label>
<input
type="text"
className="category-name-input"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="New Category"
maxLength={100}
/>
</div>
<div className="setting-group toggle-group">
<div className="toggle-info">
<label>Private Category</label>
<p className="setting-hint">Only selected roles can view this category</p>
</div>
<label className="toggle-switch">
<input
type="checkbox"
checked={isPrivate}
onChange={(e) => setIsPrivate(e.target.checked)}
/>
<span className="toggle-slider"></span>
</label>
</div>
{isPrivate && (
<div className="setting-group">
<label>Who can view this category?</label>
<div className="role-selector">
{availableRoles.map(role => (
<label
key={role.id}
className={`role-checkbox ${allowedRoles.includes(role.id) ? 'checked' : ''}`}
>
<input
type="checkbox"
checked={allowedRoles.includes(role.id)}
onChange={() => toggleRole(role.id)}
/>
<span className="role-color" style={{ background: role.color }}></span>
<span className="role-name">{role.name}</span>
</label>
))}
</div>
</div>
)}
{!isNew && categoryData.channels?.length > 0 && (
<div className="setting-group toggle-group">
<div className="toggle-info">
<label>Sync Permissions</label>
<p className="setting-hint">
Sync permissions with {categoryData.channels.length} channels in this category
</p>
</div>
<label className="toggle-switch">
<input
type="checkbox"
checked={syncPermissions}
onChange={(e) => setSyncPermissions(e.target.checked)}
/>
<span className="toggle-slider"></span>
</label>
</div>
)}
{!isNew && (
<div className="category-channels">
<label>Channels in this Category</label>
<div className="channel-list-preview">
{(categoryData.channels?.length > 0 ? categoryData.channels : [
{ id: 'general', name: 'general', type: 'text' },
{ id: 'api-discussion', name: 'api-discussion', type: 'text' },
]).map(ch => (
<div key={ch.id} className="channel-preview-item">
<span className="channel-icon">{ch.type === 'voice' ? '🔊' : '#'}</span>
<span>{ch.name}</span>
</div>
))}
</div>
</div>
)}
</div>
<div className="category-editor-footer">
{!isNew && (
<button className="delete-category-btn" onClick={onDelete}>
Delete Category
</button>
)}
<div className="footer-actions">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button className="save-btn" onClick={handleSave} disabled={!name.trim()}>
{isNew ? 'Create Category' : 'Save Changes'}
</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,182 +0,0 @@
import React, { useState } from 'react';
const permissionCategories = [
{
name: 'General Channel Permissions',
permissions: [
{ id: 'view_channel', label: 'View Channel', description: 'Allows members to view this channel' },
{ id: 'manage_channel', label: 'Manage Channel', description: 'Allows members to change name, description, and settings' },
{ id: 'manage_permissions', label: 'Manage Permissions', description: 'Allows members to change permission overrides' },
{ id: 'manage_webhooks', label: 'Manage Webhooks', description: 'Allows members to create, edit and delete webhooks' },
]
},
{
name: 'Text Channel Permissions',
permissions: [
{ id: 'send_messages', label: 'Send Messages', description: 'Allows members to send messages in this channel' },
{ id: 'send_messages_threads', label: 'Send Messages in Threads', description: 'Allows members to send messages in threads' },
{ id: 'create_public_threads', label: 'Create Public Threads', description: 'Allows members to create public threads' },
{ id: 'create_private_threads', label: 'Create Private Threads', description: 'Allows members to create private threads' },
{ id: 'embed_links', label: 'Embed Links', description: 'Links will show preview embeds' },
{ id: 'attach_files', label: 'Attach Files', description: 'Allows members to upload files and images' },
{ id: 'add_reactions', label: 'Add Reactions', description: 'Allows members to add reactions to messages' },
{ id: 'use_external_emoji', label: 'Use External Emoji', description: 'Allows members to use emoji from other servers' },
{ id: 'use_external_stickers', label: 'Use External Stickers', description: 'Allows members to use stickers from other servers' },
{ id: 'mention_everyone', label: 'Mention @everyone', description: 'Allows members to mention @everyone and @here' },
{ id: 'manage_messages', label: 'Manage Messages', description: 'Allows members to delete and pin messages' },
{ id: 'manage_threads', label: 'Manage Threads', description: 'Allows members to rename, delete, and archive threads' },
{ id: 'read_message_history', label: 'Read Message History', description: 'Allows reading of message history' },
{ id: 'send_tts', label: 'Send TTS Messages', description: 'Allows sending text-to-speech messages' },
{ id: 'use_application_commands', label: 'Use Application Commands', description: 'Allows using slash commands' },
]
},
{
name: 'Voice Channel Permissions',
permissions: [
{ id: 'connect', label: 'Connect', description: 'Allows members to join this voice channel' },
{ id: 'speak', label: 'Speak', description: 'Allows members to speak in this voice channel' },
{ id: 'video', label: 'Video', description: 'Allows members to share video or stream' },
{ id: 'mute_members', label: 'Mute Members', description: 'Allows muting other members' },
{ id: 'deafen_members', label: 'Deafen Members', description: 'Allows deafening other members' },
{ id: 'move_members', label: 'Move Members', description: 'Allows moving members between voice channels' },
{ id: 'use_vad', label: 'Use Voice Activity', description: 'Allows using voice activity detection instead of push-to-talk' },
{ id: 'priority_speaker', label: 'Priority Speaker', description: 'Allows using priority speaker mode' },
{ id: 'request_to_speak', label: 'Request to Speak', description: 'Allows requesting to speak in stage channels' },
]
}
];
export default function ChannelPermissions({ target, channelName, onClose, onSave }) {
const targetData = target || {
id: 'mod',
name: 'Moderator',
type: 'role',
color: '#5865f2',
};
const [permissions, setPermissions] = useState({});
const [searchQuery, setSearchQuery] = useState('');
const getPermState = (permId) => permissions[permId] || 'neutral';
const cyclePermission = (permId) => {
setPermissions(prev => {
const current = prev[permId] || 'neutral';
const next = current === 'neutral' ? 'allow' : current === 'allow' ? 'deny' : 'neutral';
return { ...prev, [permId]: next };
});
};
const setAllPermissions = (state) => {
const newPerms = {};
permissionCategories.forEach(cat => {
cat.permissions.forEach(perm => {
newPerms[perm.id] = state;
});
});
setPermissions(newPerms);
};
const handleSave = () => {
onSave?.(permissions);
};
const filteredCategories = permissionCategories.map(cat => ({
...cat,
permissions: cat.permissions.filter(p =>
p.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
p.description.toLowerCase().includes(searchQuery.toLowerCase())
)
})).filter(cat => cat.permissions.length > 0);
return (
<div className="modal-overlay" onClick={onClose}>
<div className="channel-permissions-modal" onClick={(e) => e.stopPropagation()}>
<div className="permissions-header">
<div className="permissions-title">
<h2>Edit Permissions</h2>
<span className="permissions-context">
{targetData.type === 'role' ? '@' : ''}
<span style={targetData.color ? { color: targetData.color } : {}}>
{targetData.name}
</span>
<span className="channel-ref">in #{channelName || 'general'}</span>
</span>
</div>
<button className="permissions-close" onClick={onClose}></button>
</div>
<div className="permissions-toolbar">
<input
type="text"
className="permissions-search"
placeholder="Search permissions..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<div className="bulk-actions">
<button onClick={() => setAllPermissions('allow')}>Allow All</button>
<button onClick={() => setAllPermissions('neutral')}>Clear All</button>
<button onClick={() => setAllPermissions('deny')}>Deny All</button>
</div>
</div>
<div className="permissions-legend">
<div className="legend-item">
<span className="legend-icon allow"></span>
<span>Allow</span>
</div>
<div className="legend-item">
<span className="legend-icon neutral">/</span>
<span>Inherit</span>
</div>
<div className="legend-item">
<span className="legend-icon deny"></span>
<span>Deny</span>
</div>
</div>
<div className="permissions-list">
{filteredCategories.map(category => (
<div key={category.name} className="permission-category">
<h3>{category.name}</h3>
{category.permissions.map(perm => (
<div key={perm.id} className="permission-row">
<div className="permission-info">
<span className="permission-label">{perm.label}</span>
<span className="permission-desc">{perm.description}</span>
</div>
<div className="permission-toggles">
<button
className={`perm-btn allow ${getPermState(perm.id) === 'allow' ? 'active' : ''}`}
onClick={() => setPermissions(p => ({ ...p, [perm.id]: 'allow' }))}
>
</button>
<button
className={`perm-btn neutral ${getPermState(perm.id) === 'neutral' ? 'active' : ''}`}
onClick={() => setPermissions(p => ({ ...p, [perm.id]: 'neutral' }))}
>
/
</button>
<button
className={`perm-btn deny ${getPermState(perm.id) === 'deny' ? 'active' : ''}`}
onClick={() => setPermissions(p => ({ ...p, [perm.id]: 'deny' }))}
>
</button>
</div>
</div>
))}
</div>
))}
</div>
<div className="permissions-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button className="save-btn" onClick={handleSave}>Save Permissions</button>
</div>
</div>
</div>
);
}

View file

@ -1,271 +0,0 @@
import React, { useState } from 'react';
export default function ChannelSettings({ channel, onClose, onSave }) {
const [activeTab, setActiveTab] = useState('overview');
const channelData = channel || {
id: 'general',
name: 'general',
type: 'text',
topic: 'Welcome to the general chat!',
slowMode: 0,
nsfw: false,
category: 'Development',
};
const [name, setName] = useState(channelData.name);
const [topic, setTopic] = useState(channelData.topic);
const [slowMode, setSlowMode] = useState(channelData.slowMode);
const [nsfw, setNsfw] = useState(channelData.nsfw);
const tabs = [
{ id: 'overview', label: 'Overview', icon: '📋' },
{ id: 'permissions', label: 'Permissions', icon: '🔒' },
{ id: 'invites', label: 'Invites', icon: '✉️' },
{ id: 'integrations', label: 'Integrations', icon: '🔗' },
];
const slowModeOptions = [
{ value: 0, label: 'Off' },
{ value: 5, label: '5s' },
{ value: 10, label: '10s' },
{ value: 15, label: '15s' },
{ value: 30, label: '30s' },
{ value: 60, label: '1m' },
{ value: 300, label: '5m' },
{ value: 600, label: '10m' },
{ value: 900, label: '15m' },
{ value: 1800, label: '30m' },
{ value: 3600, label: '1h' },
{ value: 7200, label: '2h' },
{ value: 21600, label: '6h' },
];
const permissionOverrides = [
{ id: 'founder', name: 'Founder', color: '#ff0000', type: 'role', allow: ['all'], deny: [] },
{ id: 'mod', name: 'Moderator', color: '#5865f2', type: 'role', allow: ['manage_messages'], deny: [] },
{ id: 'user123', name: 'SpecificUser', color: null, type: 'member', allow: [], deny: ['send_messages'] },
];
const handleSave = () => {
onSave?.({ name, topic, slowMode, nsfw });
onClose?.();
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="channel-settings-modal" onClick={(e) => e.stopPropagation()}>
<div className="settings-sidebar">
<div className="settings-channel-info">
<span className="channel-hash">#</span>
<span className="settings-channel-name">{name}</span>
</div>
<div className="settings-nav">
{tabs.map(tab => (
<div
key={tab.id}
className={`settings-nav-item ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
<span className="nav-icon">{tab.icon}</span>
<span>{tab.label}</span>
</div>
))}
<div className="settings-divider"></div>
<div className="settings-nav-item danger">
<span className="nav-icon">🗑</span>
<span>Delete Channel</span>
</div>
</div>
</div>
<div className="settings-content">
<div className="settings-header">
<h2>{tabs.find(t => t.id === activeTab)?.label}</h2>
<button className="settings-close" onClick={onClose}></button>
</div>
{activeTab === 'overview' && (
<div className="channel-overview">
<div className="setting-group">
<label>Channel Name</label>
<div className="channel-name-input">
<span className="input-prefix">#</span>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value.toLowerCase().replace(/\s+/g, '-'))}
/>
</div>
</div>
<div className="setting-group">
<label>Channel Topic</label>
<textarea
className="setting-textarea"
value={topic}
onChange={(e) => setTopic(e.target.value)}
placeholder="Let everyone know what this channel is about"
maxLength={1024}
/>
<span className="char-count">{topic.length}/1024</span>
</div>
<div className="setting-group">
<label>Slowmode</label>
<div className="slowmode-select">
<select value={slowMode} onChange={(e) => setSlowMode(parseInt(e.target.value))}>
{slowModeOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<p className="setting-hint">Members can only send one message per this interval</p>
</div>
<div className="setting-group toggle-group">
<div className="toggle-info">
<label>NSFW Channel</label>
<p className="setting-hint">Users must verify their age to view content</p>
</div>
<label className="toggle-switch">
<input type="checkbox" checked={nsfw} onChange={(e) => setNsfw(e.target.checked)} />
<span className="toggle-slider"></span>
</label>
</div>
<div className="setting-group">
<label>Category</label>
<select className="setting-select" defaultValue={channelData.category}>
<option>Announcements</option>
<option>Development</option>
<option>Support</option>
<option>Voice Channels</option>
<option>No Category</option>
</select>
</div>
</div>
)}
{activeTab === 'permissions' && (
<div className="channel-permissions">
<div className="permissions-info">
<p>Channel permissions override server-level permissions for specific roles or members.</p>
</div>
<div className="permissions-section">
<div className="section-header">
<h3>Roles/Members</h3>
<button className="add-override-btn">+ Add Role or Member</button>
</div>
<div className="overrides-list">
{permissionOverrides.map(override => (
<div key={override.id} className="override-item">
<div className="override-info">
{override.type === 'role' ? (
<span className="role-indicator" style={{ background: override.color }}>@</span>
) : (
<span className="member-indicator">👤</span>
)}
<span className="override-name" style={override.color ? { color: override.color } : {}}>
{override.name}
</span>
<span className="override-type">{override.type}</span>
</div>
<div className="override-actions">
<button className="edit-override">Edit</button>
<button className="remove-override"></button>
</div>
</div>
))}
</div>
</div>
<div className="permissions-section">
<h3>Advanced Permissions</h3>
<div className="advanced-perms-grid">
<div className="perm-category">
<h4>General Channel Permissions</h4>
<div className="perm-item">
<span>View Channel</span>
<div className="perm-toggles">
<button className="perm-btn allow"></button>
<button className="perm-btn neutral active">/</button>
<button className="perm-btn deny"></button>
</div>
</div>
<div className="perm-item">
<span>Manage Channel</span>
<div className="perm-toggles">
<button className="perm-btn allow"></button>
<button className="perm-btn neutral active">/</button>
<button className="perm-btn deny"></button>
</div>
</div>
</div>
<div className="perm-category">
<h4>Text Channel Permissions</h4>
<div className="perm-item">
<span>Send Messages</span>
<div className="perm-toggles">
<button className="perm-btn allow active"></button>
<button className="perm-btn neutral">/</button>
<button className="perm-btn deny"></button>
</div>
</div>
<div className="perm-item">
<span>Embed Links</span>
<div className="perm-toggles">
<button className="perm-btn allow active"></button>
<button className="perm-btn neutral">/</button>
<button className="perm-btn deny"></button>
</div>
</div>
<div className="perm-item">
<span>Attach Files</span>
<div className="perm-toggles">
<button className="perm-btn allow"></button>
<button className="perm-btn neutral active">/</button>
<button className="perm-btn deny"></button>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'invites' && (
<div className="channel-invites">
<p className="no-invites">No invites have been created for this channel yet.</p>
<button className="create-invite-btn">Create Invite</button>
</div>
)}
{activeTab === 'integrations' && (
<div className="channel-integrations">
<div className="integration-section">
<h3>Webhooks</h3>
<p className="integration-desc">Webhooks allow external services to send messages to this channel.</p>
<button className="create-webhook-btn">Create Webhook</button>
</div>
</div>
)}
<div className="settings-save-bar">
<span>Careful you have unsaved changes!</span>
<div className="save-actions">
<button className="reset-btn" onClick={onClose}>Reset</button>
<button className="save-btn" onClick={handleSave}>Save Changes</button>
</div>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,112 +0,0 @@
import React, { useState } from 'react';
import UserSettingsBar from './UserSettingsBar';
import VoiceChannel from './VoiceChannel';
const channels = [
{ category: 'Announcements', items: [
{ id: 'updates', icon: '📢', name: 'updates', badge: 3 },
{ id: 'changelog', icon: '📜', name: 'changelog' },
]},
{ category: 'Development', items: [
{ id: 'general', icon: '#', name: 'general' },
{ id: 'api-discussion', icon: '#', name: 'api-discussion' },
{ id: 'passport-development', icon: '#', name: 'passport-development' },
]},
{ category: 'Support', items: [
{ id: 'help', icon: '❓', name: 'help' },
{ id: 'bug-reports', icon: '🐛', name: 'bug-reports' },
]},
];
const voiceChannels = [
{ id: 'nexus-lounge', name: 'Nexus Lounge', users: [
{ id: 1, name: 'Trevor', avatar: 'T', color: '#ff0000', speaking: true, muted: false },
{ id: 2, name: 'Sarah', avatar: 'S', color: '#ffa500', speaking: false, muted: true },
{ id: 3, name: 'DevUser_2847', avatar: 'D', color: '#666', speaking: false, muted: false, streaming: true },
]},
{ id: 'collab-space', name: 'Collab Space', users: [] },
];
export default function ChannelSidebar({ onCreateChannel, onSettingsClick, onJoinVoice }) {
const [activeChannel, setActiveChannel] = useState('general');
const [expandedVoice, setExpandedVoice] = useState(['nexus-lounge']);
const [hoveredCategory, setHoveredCategory] = useState(null);
return (
<div className="channel-sidebar">
<div className="server-header">
<span>AeThex Foundation</span>
<span className="server-badge foundation">Official</span>
</div>
<div className="channel-list">
{channels.map((category) => (
<div key={category.category}>
<div
className="channel-category"
onMouseEnter={() => setHoveredCategory(category.category)}
onMouseLeave={() => setHoveredCategory(null)}
>
{category.category}
{hoveredCategory === category.category && onCreateChannel && (
<button
className="category-add-btn"
onClick={(e) => { e.stopPropagation(); onCreateChannel(); }}
title="Create Channel"
>
+
</button>
)}
</div>
{category.items.map((channel) => (
<div
key={channel.id}
className={`channel-item${activeChannel === channel.id ? ' active' : ''}`}
onClick={() => setActiveChannel(channel.id)}
>
<span className="channel-icon">{channel.icon}</span>
<span className="channel-name">{channel.name}</span>
{channel.badge && <span className="channel-badge">{channel.badge}</span>}
</div>
))}
</div>
))}
{/* Voice Channels */}
<div
className="channel-category"
onMouseEnter={() => setHoveredCategory('voice')}
onMouseLeave={() => setHoveredCategory(null)}
>
Voice Channels
{hoveredCategory === 'voice' && onCreateChannel && (
<button
className="category-add-btn"
onClick={(e) => { e.stopPropagation(); onCreateChannel(); }}
title="Create Channel"
>
+
</button>
)}
</div>
{voiceChannels.map((vc) => (
<VoiceChannel
key={vc.id}
channel={vc}
expanded={expandedVoice.includes(vc.id)}
onToggle={() => {
setExpandedVoice((prev) =>
prev.includes(vc.id)
? prev.filter((id) => id !== vc.id)
: [...prev, vc.id]
);
}}
onJoin={onJoinVoice}
/>
))}
</div>
<UserSettingsBar onSettingsClick={onSettingsClick} />
</div>
);
}

View file

@ -1,283 +0,0 @@
import React, { useState, useRef, useEffect } from 'react';
import MessageActions from './MessageActions';
import TypingIndicator from './TypingIndicator';
import EmojiPicker from './EmojiPicker';
const initialMessages = [
{
type: 'system',
division: 'foundation',
label: '[FOUNDATION] System Announcement',
text: 'Foundation authentication services upgraded to v2.1.0. Enhanced security protocols now active across all AeThex infrastructure.',
},
{
type: 'message',
id: 1,
author: 'Trevor',
badge: 'foundation',
badgeLabel: 'Foundation',
time: '10:34 AM',
avatar: { initial: 'T', gradient: 'linear-gradient(135deg, #ff0000, #cc0000)' },
text: 'Just pushed the authentication updates. All services should automatically migrate to the new protocols within 24 hours.',
reactions: [{ emoji: '🔥', count: 3, reacted: true }, { emoji: '👍', count: 2, reacted: false }],
},
{
type: 'message',
id: 2,
author: 'Marcus',
time: '10:41 AM',
avatar: { initial: 'M', gradient: 'linear-gradient(135deg, #0066ff, #003380)' },
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.",
},
{
type: 'system',
division: 'labs',
label: '[LABS] Experimental Feature Alert',
text: 'Nexus Engine v2.0-beta now available for testing. New cross-platform sync reduces latency by 40%. Join #labs-testing to participate.',
},
{
type: 'message',
id: 3,
author: 'Sarah',
badge: 'labs',
badgeLabel: 'Labs',
time: '11:15 AM',
avatar: { initial: 'S', gradient: 'linear-gradient(135deg, #ffa500, #ff8c00)' },
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...',
reactions: [{ emoji: '🚀', count: 5, reacted: true }],
},
{
type: 'message',
id: 4,
author: 'Anderson',
badge: 'foundation',
badgeLabel: 'Founder',
time: '11:47 AM',
avatar: { initial: 'A', gradient: 'linear-gradient(135deg, #ff0000, #0066ff, #ffa500)' },
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.',
reactions: [{ emoji: '❤️', count: 8, reacted: false }, { emoji: '🔥', count: 4, reacted: true }],
},
{
type: 'message',
id: 5,
author: 'DevUser_2847',
time: '12:03 PM',
avatar: { initial: 'D' },
text: 'Quick question - when using AeThex Studio, does the Terminal automatically connect to all three Trinity divisions, or do I need to configure that?',
},
{
type: 'system',
division: 'corporation',
label: '[CORPORATION] Service Update',
text: 'AeThex Studio Pro users: New Railway deployment templates available. Optimized configurations for Foundation APIs, Corporation services, and Labs experiments.',
},
];
export default function ChatArea({ onOpenSearch, onOpenThread, onPinnedClick, onNotificationsClick, onContextMenu }) {
const [messages, setMessages] = useState(initialMessages);
const [inputValue, setInputValue] = useState('');
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [hoveredMessage, setHoveredMessage] = useState(null);
const [typingUsers] = useState(['Sarah', 'Marcus']); // Simulated typing
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSend = () => {
if (!inputValue.trim()) return;
const newMessage = {
type: 'message',
id: Date.now(),
author: 'You',
time: new Date().toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }),
avatar: { initial: 'Y', gradient: 'linear-gradient(135deg, #0066ff, #003380)' },
text: inputValue,
};
setMessages([...messages, newMessage]);
setInputValue('');
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleSend();
}
};
const handleEmojiSelect = (emoji) => {
setInputValue((prev) => prev + emoji);
setShowEmojiPicker(false);
};
const handleReaction = (messageId, emoji) => {
setMessages((prev) =>
prev.map((msg) => {
if (msg.id !== messageId) return msg;
const reactions = msg.reactions || [];
const existing = reactions.find((r) => r.emoji === emoji);
if (existing) {
return {
...msg,
reactions: reactions.map((r) =>
r.emoji === emoji
? { ...r, count: r.reacted ? r.count - 1 : r.count + 1, reacted: !r.reacted }
: r
).filter((r) => r.count > 0),
};
}
return {
...msg,
reactions: [...reactions, { emoji, count: 1, reacted: true }],
};
})
);
};
return (
<div className="chat-area">
<div className="chat-header">
<span className="channel-name-header"># general</span>
<div className="chat-tools">
<span
className="chat-tool"
onClick={onNotificationsClick}
style={{ cursor: 'pointer' }}
title="Notifications"
>
🔔
</span>
<span
className="chat-tool"
onClick={onPinnedClick}
style={{ cursor: 'pointer' }}
title="Pinned Messages"
>
📌
</span>
<span className="chat-tool">👥 128</span>
<span className="chat-tool" onClick={onOpenSearch} style={{ cursor: 'pointer' }} title="Search">🔍</span>
</div>
</div>
<div className="chat-messages">
{messages.map((msg, idx) => {
if (msg.type === 'system') {
return (
<div key={`system-${idx}`} className={`message-system ${msg.division}`}>
<div className={`system-label ${msg.division}`}>{msg.label}</div>
<div>{msg.text}</div>
</div>
);
}
return (
<div
key={msg.id}
className="message"
onMouseEnter={() => setHoveredMessage(msg.id)}
onMouseLeave={() => setHoveredMessage(null)}
>
<div
className="message-avatar"
style={msg.avatar.gradient ? { background: msg.avatar.gradient } : undefined}
>
{msg.avatar.initial}
</div>
<div className="message-content">
<div className="message-header">
<span className="message-author">{msg.author}</span>
{msg.badge && (
<span className={`message-badge ${msg.badge}`}>{msg.badgeLabel}</span>
)}
<span className="message-time">{msg.time}</span>
</div>
<div className="message-text">{msg.text}</div>
{/* Reactions */}
{msg.reactions && msg.reactions.length > 0 && (
<div className="message-reactions" style={{ display: 'flex', gap: '6px', marginTop: '6px' }}>
{msg.reactions.map((r) => (
<button
key={r.emoji}
onClick={() => handleReaction(msg.id, r.emoji)}
style={{
display: 'flex',
alignItems: 'center',
gap: '4px',
padding: '2px 8px',
background: r.reacted ? 'rgba(88, 101, 242, 0.3)' : '#2f3136',
border: r.reacted ? '1px solid #5865f2' : '1px solid transparent',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.85em',
}}
>
<span>{r.emoji}</span>
<span style={{ color: r.reacted ? '#5865f2' : '#b9bbbe' }}>{r.count}</span>
</button>
))}
</div>
)}
</div>
{/* Message Actions on Hover */}
{hoveredMessage === msg.id && (
<MessageActions
onReact={(emoji) => handleReaction(msg.id, emoji)}
onReply={() => onOpenThread && onOpenThread(msg)}
onThread={() => onOpenThread && onOpenThread(msg)}
isOwn={msg.author === 'You'}
/>
)}
</div>
);
})}
<div ref={messagesEndRef} />
</div>
{/* Typing Indicator */}
{typingUsers.length > 0 && <TypingIndicator users={typingUsers} />}
<div className="message-input-container" style={{ position: 'relative' }}>
<button
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
style={{
position: 'absolute',
right: '12px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
cursor: 'pointer',
fontSize: '1.2em',
zIndex: 10,
}}
>
😊
</button>
<input
type="text"
className="message-input"
placeholder="Message #general (Foundation infrastructure channel)"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
style={{ paddingRight: '48px' }}
/>
{showEmojiPicker && (
<EmojiPicker
onSelect={handleEmojiSelect}
onClose={() => setShowEmojiPicker(false)}
/>
)}
</div>
</div>
);
}

View file

@ -1,133 +0,0 @@
import React, { useEffect, useRef } from 'react';
export default function ContextMenu({ x, y, type, data, onClose, onAction }) {
const menuRef = useRef(null);
useEffect(() => {
const handleClickOutside = (e) => {
if (menuRef.current && !menuRef.current.contains(e.target)) {
onClose?.();
}
};
const handleEscape = (e) => {
if (e.key === 'Escape') onClose?.();
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [onClose]);
const menuItems = {
message: [
{ id: 'add-reaction', label: 'Add Reaction', icon: '😀' },
{ divider: true },
{ id: 'reply', label: 'Reply', icon: '↩️' },
{ id: 'create-thread', label: 'Create Thread', icon: '🧵' },
{ divider: true },
{ id: 'edit', label: 'Edit Message', icon: '✏️', ownerOnly: true },
{ id: 'pin', label: 'Pin Message', icon: '📌' },
{ id: 'mark-unread', label: 'Mark Unread', icon: '📩' },
{ id: 'copy-text', label: 'Copy Text', icon: '📋' },
{ id: 'copy-link', label: 'Copy Message Link', icon: '🔗' },
{ divider: true },
{ id: 'speak', label: 'Speak Message', icon: '🔊' },
{ divider: true },
{ id: 'delete', label: 'Delete Message', icon: '🗑️', danger: true, ownerOnly: true },
{ id: 'report', label: 'Report Message', icon: '🚩', danger: true },
],
user: [
{ id: 'profile', label: 'Profile', icon: '👤' },
{ id: 'mention', label: 'Mention', icon: '@' },
{ id: 'message', label: 'Message', icon: '💬' },
{ divider: true },
{ id: 'call', label: 'Call', icon: '📞' },
{ id: 'video-call', label: 'Video Call', icon: '📹' },
{ divider: true },
{ id: 'add-note', label: 'Add Note', icon: '📝' },
{ id: 'add-friend', label: 'Add Friend', icon: '👋' },
{ divider: true },
{ id: 'invite', label: 'Invite to Server', icon: '', submenu: true },
{ divider: true },
{ id: 'mute', label: 'Mute', icon: '🔇' },
{ id: 'block', label: 'Block', icon: '🚫', danger: true },
{ id: 'report', label: 'Report', icon: '🚩', danger: true },
],
channel: [
{ id: 'mark-read', label: 'Mark As Read', icon: '✓' },
{ divider: true },
{ id: 'mute', label: 'Mute Channel', icon: '🔇', submenu: true },
{ id: 'notification-settings', label: 'Notification Settings', icon: '🔔' },
{ divider: true },
{ id: 'edit', label: 'Edit Channel', icon: '✏️' },
{ id: 'duplicate', label: 'Duplicate Channel', icon: '📋' },
{ id: 'create-invite', label: 'Create Invite', icon: '✉️' },
{ divider: true },
{ id: 'copy-link', label: 'Copy Channel Link', icon: '🔗' },
{ id: 'copy-id', label: 'Copy Channel ID', icon: '#️⃣' },
{ divider: true },
{ id: 'delete', label: 'Delete Channel', icon: '🗑️', danger: true },
],
server: [
{ id: 'mark-read', label: 'Mark As Read', icon: '✓' },
{ divider: true },
{ id: 'invite-people', label: 'Invite People', icon: '👋' },
{ divider: true },
{ id: 'mute', label: 'Mute Server', icon: '🔇', submenu: true },
{ id: 'notification-settings', label: 'Notification Settings', icon: '🔔' },
{ id: 'hide-muted', label: 'Hide Muted Channels', icon: '👁️' },
{ divider: true },
{ id: 'privacy-settings', label: 'Privacy Settings', icon: '🔒' },
{ id: 'edit-nickname', label: 'Edit Server Nickname', icon: '✏️' },
{ divider: true },
{ id: 'server-settings', label: 'Server Settings', icon: '⚙️' },
{ id: 'create-channel', label: 'Create Channel', icon: '' },
{ id: 'create-category', label: 'Create Category', icon: '📁' },
{ divider: true },
{ id: 'copy-id', label: 'Copy Server ID', icon: '#️⃣' },
{ divider: true },
{ id: 'leave', label: 'Leave Server', icon: '🚪', danger: true },
],
};
const items = menuItems[type] || [];
const isOwn = data?.isOwn;
// Adjust position to stay within viewport
const adjustedStyle = {
position: 'fixed',
top: y,
left: x,
zIndex: 10000,
};
return (
<div className="context-menu" ref={menuRef} style={adjustedStyle}>
{items.map((item, idx) => {
if (item.divider) {
return <div key={`divider-${idx}`} className="context-menu-divider" />;
}
if (item.ownerOnly && !isOwn) return null;
return (
<div
key={item.id}
className={`context-menu-item ${item.danger ? 'danger' : ''}`}
onClick={() => {
onAction?.(item.id, data);
onClose?.();
}}
>
<span className="context-menu-icon">{item.icon}</span>
<span className="context-menu-label">{item.label}</span>
{item.submenu && <span className="context-menu-arrow"></span>}
</div>
);
})}
</div>
);
}

View file

@ -1,115 +0,0 @@
import React, { useState } from 'react';
export default function CreateChannelModal({ category, onClose, onCreate }) {
const [channelType, setChannelType] = useState('text');
const [channelName, setChannelName] = useState('');
const [isPrivate, setIsPrivate] = useState(false);
const channelTypes = [
{ id: 'text', icon: '#', label: 'Text', description: 'Send messages, images, GIFs, emoji, opinions, and puns' },
{ id: 'voice', icon: '🔊', label: 'Voice', description: 'Hang out together with voice, video, and screen share' },
{ id: 'forum', icon: '💬', label: 'Forum', description: 'Create a space for organized discussions' },
{ id: 'announcement', icon: '📢', label: 'Announcement', description: 'Important updates followers can subscribe to' },
{ id: 'stage', icon: '🎭', label: 'Stage', description: 'Host events with moderated discussions' },
];
const handleCreate = () => {
if (!channelName.trim()) return;
onCreate?.({
type: channelType,
name: channelName.toLowerCase().replace(/\s+/g, '-'),
isPrivate,
category,
});
onClose?.();
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="create-channel-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Create Channel</h2>
{category && <span className="category-name">in {category}</span>}
<button className="modal-close" onClick={onClose}></button>
</div>
<div className="modal-content">
<div className="channel-type-section">
<label className="section-label">CHANNEL TYPE</label>
<div className="channel-types">
{channelTypes.map((type) => (
<label
key={type.id}
className={`channel-type-option ${channelType === type.id ? 'selected' : ''}`}
>
<input
type="radio"
name="channelType"
value={type.id}
checked={channelType === type.id}
onChange={() => setChannelType(type.id)}
/>
<span className="type-icon">{type.icon}</span>
<div className="type-info">
<span className="type-label">{type.label}</span>
<span className="type-description">{type.description}</span>
</div>
</label>
))}
</div>
</div>
<div className="channel-name-section">
<label className="section-label">CHANNEL NAME</label>
<div className="channel-name-input">
<span className="channel-prefix">
{channelType === 'text' ? '#' :
channelType === 'voice' ? '🔊' :
channelType === 'forum' ? '💬' :
channelType === 'announcement' ? '📢' : '🎭'}
</span>
<input
type="text"
placeholder="new-channel"
value={channelName}
onChange={(e) => setChannelName(e.target.value)}
maxLength={100}
/>
</div>
</div>
<div className="channel-privacy-section">
<label className="privacy-toggle">
<div className="privacy-info">
<span className="privacy-icon">🔒</span>
<div>
<span className="privacy-label">Private Channel</span>
<span className="privacy-description">
Only selected members and roles will be able to view this channel.
</span>
</div>
</div>
<input
type="checkbox"
checked={isPrivate}
onChange={(e) => setIsPrivate(e.target.checked)}
/>
</label>
</div>
</div>
<div className="modal-footer">
<button className="modal-cancel" onClick={onClose}>Cancel</button>
<button
className="modal-submit"
onClick={handleCreate}
disabled={!channelName.trim()}
>
Create Channel
</button>
</div>
</div>
</div>
);
}

View file

@ -1,242 +0,0 @@
import React, { useState } from 'react';
const friends = [
{ id: 1, name: 'Trevor', tag: 'Trevor#0042', avatar: 'T', gradient: 'linear-gradient(135deg, #ff0000, #cc0000)', status: 'online', activity: 'Working on AeThex' },
{ id: 2, name: 'Sarah', tag: 'Sarah#1337', avatar: 'S', gradient: 'linear-gradient(135deg, #ffa500, #ff8c00)', status: 'online', activity: 'Playing Valorant' },
{ id: 3, name: 'Marcus', tag: 'Marcus#2048', avatar: 'M', gradient: 'linear-gradient(135deg, #0066ff, #003380)', status: 'idle' },
{ id: 4, name: 'DevUser_2847', tag: 'DevUser_2847#2847', avatar: 'D', status: 'dnd', activity: 'Do Not Disturb' },
{ id: 5, name: 'JohnDev', tag: 'JohnDev#9999', avatar: 'J', status: 'offline' },
];
const dmConversations = [
{ id: 1, user: friends[0], lastMessage: 'The auth system is ready!', time: '2m', unread: 2 },
{ id: 2, user: friends[1], lastMessage: 'Check out the new Nexus build', time: '15m', unread: 0 },
{ id: 3, user: friends[2], lastMessage: 'Thanks for the help!', time: '1h', unread: 0 },
{ id: 4, user: { name: 'Group Chat', avatar: '👥', isGroup: true, members: ['Trevor', 'Sarah', 'Marcus'] }, lastMessage: 'Sarah: Let\'s sync up tomorrow', time: '3h', unread: 5 },
];
const pendingRequests = [
{ id: 1, name: 'NewDev123', tag: 'NewDev123#4567', avatar: 'N', type: 'incoming', mutualServers: 2 },
{ id: 2, name: 'CoolCoder', tag: 'CoolCoder#8899', avatar: 'C', type: 'outgoing' },
];
export default function DMsView({ onSelectDM, onOpenProfile }) {
const [activeTab, setActiveTab] = useState('online');
const [searchQuery, setSearchQuery] = useState('');
const tabs = [
{ id: 'online', label: 'Online' },
{ id: 'all', label: 'All' },
{ id: 'pending', label: 'Pending', count: pendingRequests.length },
{ id: 'blocked', label: 'Blocked' },
{ id: 'add', label: 'Add Friend', isAction: true },
];
const statusColors = {
online: '#3ba55d',
idle: '#faa61a',
dnd: '#ed4245',
offline: '#747f8d',
};
const filteredFriends = friends.filter((f) => {
if (activeTab === 'online') return f.status === 'online';
if (activeTab === 'all') return true;
return true;
}).filter((f) =>
f.name.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<div className="dms-view">
{/* DMs Sidebar */}
<div className="dms-sidebar">
<div className="dms-search">
<input
type="text"
placeholder="Find or start a conversation"
className="dms-search-input"
/>
</div>
<div className="dms-section">
<div className="dms-section-header">
<span>Friends</span>
</div>
<div
className="dms-nav-item"
onClick={() => setActiveTab('online')}
>
<span className="nav-icon">👥</span>
<span>Friends</span>
</div>
<div className="dms-nav-item">
<span className="nav-icon">🚀</span>
<span>Nitro</span>
</div>
<div className="dms-nav-item">
<span className="nav-icon">🛒</span>
<span>Shop</span>
</div>
</div>
<div className="dms-section">
<div className="dms-section-header">
<span>Direct Messages</span>
<button className="add-dm-btn">+</button>
</div>
{dmConversations.map((dm) => (
<div
key={dm.id}
className="dm-item"
onClick={() => onSelectDM?.(dm)}
>
<div className="dm-avatar" style={{ background: dm.user.gradient || '#36393f' }}>
{dm.user.avatar}
{!dm.user.isGroup && (
<div
className="dm-status"
style={{ background: statusColors[dm.user.status] || statusColors.offline }}
/>
)}
</div>
<div className="dm-info">
<div className="dm-name">
{dm.user.name}
{dm.user.isGroup && <span className="group-count">{dm.user.members?.length}</span>}
</div>
<div className="dm-preview">{dm.lastMessage}</div>
</div>
{dm.unread > 0 && (
<div className="dm-unread">{dm.unread}</div>
)}
<button className="dm-close" onClick={(e) => { e.stopPropagation(); }}></button>
</div>
))}
</div>
</div>
{/* Friends List Panel */}
<div className="friends-panel">
<div className="friends-header">
<div className="friends-title">
<span className="friends-icon">👥</span>
<span>Friends</span>
</div>
<div className="friends-tabs">
{tabs.map((tab) => (
<button
key={tab.id}
className={`friends-tab ${activeTab === tab.id ? 'active' : ''} ${tab.isAction ? 'action' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
{tab.count > 0 && <span className="tab-count">{tab.count}</span>}
</button>
))}
</div>
</div>
{activeTab === 'add' ? (
<div className="add-friend-panel">
<h2>Add Friend</h2>
<p>You can add friends with their AeThex username.</p>
<div className="add-friend-input">
<input
type="text"
placeholder="Enter a Username#0000"
className="friend-input"
/>
<button className="send-request-btn">Send Friend Request</button>
</div>
</div>
) : activeTab === 'pending' ? (
<div className="pending-requests">
<div className="friends-search">
<input
type="text"
placeholder="Search"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="friends-count">PENDING {pendingRequests.length}</div>
{pendingRequests.map((req) => (
<div key={req.id} className="friend-item">
<div className="friend-avatar">{req.avatar}</div>
<div className="friend-info">
<div className="friend-name">{req.name}</div>
<div className="friend-tag">
{req.type === 'incoming' ? 'Incoming Friend Request' : 'Outgoing Friend Request'}
</div>
</div>
<div className="friend-actions">
{req.type === 'incoming' && (
<>
<button className="friend-action accept"></button>
<button className="friend-action decline"></button>
</>
)}
{req.type === 'outgoing' && (
<button className="friend-action decline"></button>
)}
</div>
</div>
))}
</div>
) : (
<>
<div className="friends-search">
<input
type="text"
placeholder="Search"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="friends-count">
{activeTab === 'online' ? 'ONLINE' : 'ALL FRIENDS'} {filteredFriends.length}
</div>
<div className="friends-list">
{filteredFriends.map((friend) => (
<div
key={friend.id}
className="friend-item"
onClick={() => onOpenProfile?.(friend)}
>
<div className="friend-avatar" style={{ background: friend.gradient || '#36393f' }}>
{friend.avatar}
<div
className="friend-status"
style={{ background: statusColors[friend.status] }}
/>
</div>
<div className="friend-info">
<div className="friend-name">{friend.name}</div>
<div className="friend-activity">{friend.activity || friend.status}</div>
</div>
<div className="friend-actions">
<button className="friend-action" title="Message">💬</button>
<button className="friend-action" title="Voice Call">📞</button>
<button className="friend-action" title="Video Call">📹</button>
<button className="friend-action" title="More"></button>
</div>
</div>
))}
</div>
</>
)}
</div>
{/* Activity/Now Playing Panel */}
<div className="activity-panel">
<h3>Active Now</h3>
<div className="activity-placeholder">
<p>It's quiet for now...</p>
<p className="activity-hint">When a friend starts an activitylike playing a game or hanging out on voicewe'll show it here!</p>
</div>
</div>
</div>
);
}

View file

@ -1,68 +0,0 @@
import React, { useState } from 'react';
const emojiCategories = [
{ id: 'recent', icon: '🕐', name: 'Recent', emojis: ['👍', '❤️', '😂', '🔥', '✅', '👀', '🎉', '💯'] },
{ id: 'smileys', icon: '😀', name: 'Smileys', emojis: ['😀', '😃', '😄', '😁', '😅', '😂', '🤣', '😊', '😇', '🙂', '😉', '😌', '😍', '🥰', '😘', '😗', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '😮', '🤤', '😪', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🥵', '🥶', '😵', '🤯', '😎', '🥳', '😱', '😨', '😰', '😥', '😢', '😭', '😤', '😡', '🤬'] },
{ id: 'people', icon: '👋', name: 'People', emojis: ['👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃', '🧠', '🫀', '🫁', '🦷', '🦴', '👀', '👁️', '👅', '👄'] },
{ id: 'nature', icon: '🌿', name: 'Nature', emojis: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🌸', '🌺', '🌻', '🌹', '🌷', '🌲', '🌳', '🌴', '🌵', '🌾', '☀️', '🌙', '⭐', '🌈', '☁️', '⛈️', '❄️', '🔥', '💧'] },
{ id: 'food', icon: '🍔', name: 'Food', emojis: ['🍎', '🍊', '🍋', '🍌', '🍉', '🍇', '🍓', '🫐', '🍒', '🍑', '🥭', '🍍', '🥥', '🥝', '🍅', '🍆', '🥑', '🥦', '🌽', '🌶️', '🫑', '🥒', '🥬', '🧄', '🧅', '🥔', '🍠', '🥐', '🍞', '🥖', '🥨', '🧀', '🍖', '🍗', '🥩', '🥓', '🍔', '🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🫔', '🥙', '🧆', '🥚', '🍳', '🥘', '🍲', '🫕', '🥣', '🥗', '🍿', '🧈', '🧂', '🥫'] },
{ id: 'activities', icon: '⚽', name: 'Activities', emojis: ['⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱', '🪀', '🏓', '🏸', '🏒', '🏑', '🥍', '🏏', '🪃', '🥅', '⛳', '🪁', '🏹', '🎣', '🤿', '🥊', '🥋', '🎽', '🛹', '🛼', '🛷', '⛸️', '🥌', '🎿', '⛷️', '🏂', '🎮', '🕹️', '🎲', '🧩', '♟️', '🎯', '🎳', '🎭', '🎨', '🎬', '🎤', '🎧', '🎼', '🎹', '🥁', '🪘', '🎷', '🎺', '🪗', '🎸', '🪕', '🎻'] },
{ id: 'objects', icon: '💡', name: 'Objects', emojis: ['💻', '🖥️', '🖨️', '⌨️', '🖱️', '💽', '💾', '💿', '📀', '📱', '📲', '☎️', '📞', '📟', '📠', '🔋', '🔌', '💡', '🔦', '🕯️', '🧯', '🛢️', '💸', '💵', '💴', '💶', '💷', '🪙', '💰', '💳', '💎', '⚖️', '🪜', '🧰', '🪛', '🔧', '🔨', '⚒️', '🛠️', '⛏️', '🪚', '🔩', '⚙️', '🪤', '🧱', '⛓️', '🧲', '🔫', '💣', '🧨', '🪓', '🔪', '🗡️', '⚔️', '🛡️', '🚬', '⚰️', '🪦', '⚱️', '🏺'] },
{ id: 'symbols', icon: '❤️', name: 'Symbols', emojis: ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️', '✝️', '☪️', '🕉️', '☸️', '✡️', '🔯', '🕎', '☯️', '☦️', '🛐', '⛎', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '🆔', '⚛️', '🉑', '☢️', '☣️', '📴', '📳', '🈶', '🈚', '🈸', '🈺', '🈷️', '✴️', '🆚', '💮', '🉐', '㊙️', '㊗️'] },
{ id: 'custom', icon: '⭐', name: 'AeThex', emojis: ['🔴', '🔵', '🟠', '⚡', '🛡️', '🧪', '🏢', '🚀', '💻', '🔐', '🌐', '⚙️'] },
];
export default function EmojiPicker({ onSelect, onClose }) {
const [activeCategory, setActiveCategory] = useState('smileys');
const [searchQuery, setSearchQuery] = useState('');
const currentCategory = emojiCategories.find(c => c.id === activeCategory);
const filteredEmojis = searchQuery
? emojiCategories.flatMap(c => c.emojis)
: currentCategory?.emojis || [];
return (
<div className="emoji-picker">
<div className="emoji-picker-header">
<input
type="text"
className="emoji-search"
placeholder="Search emoji..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="emoji-categories">
{emojiCategories.map(cat => (
<button
key={cat.id}
className={`emoji-category-btn ${activeCategory === cat.id ? 'active' : ''}`}
onClick={() => { setActiveCategory(cat.id); setSearchQuery(''); }}
title={cat.name}
>
{cat.icon}
</button>
))}
</div>
<div className="emoji-grid">
{filteredEmojis.slice(0, 64).map((emoji, idx) => (
<button
key={idx}
className="emoji-btn"
onClick={() => onSelect?.(emoji)}
>
{emoji}
</button>
))}
</div>
<div className="emoji-picker-footer">
<span className="emoji-preview">
{currentCategory?.name || 'Search Results'}
</span>
</div>
</div>
);
}

View file

@ -1,185 +0,0 @@
import React, { useState, useRef } from 'react';
const existingEmojis = [
{ id: 1, name: 'pepehype', image: '🐸', animated: false },
{ id: 2, name: 'monkaS', image: '😰', animated: false },
{ id: 3, name: 'KEKW', image: '🤣', animated: false },
{ id: 4, name: 'Pog', image: '😮', animated: true },
{ id: 5, name: 'catJAM', image: '🐱', animated: true },
{ id: 6, name: 'aethex_logo', image: '🔺', animated: false },
];
const existingStickers = [
{ id: 1, name: 'Wave', emoji: '👋', description: 'A friendly wave' },
{ id: 2, name: 'Love', emoji: '❤️', description: 'Show some love' },
{ id: 3, name: 'Party', emoji: '🎉', description: 'Party time!' },
];
export default function EmojiUpload({ type = 'emoji', onUpload, onClose }) {
const [activeTab, setActiveTab] = useState(type);
const [uploadedFiles, setUploadedFiles] = useState([]);
const [emojiName, setEmojiName] = useState('');
const [stickerDescription, setStickerDescription] = useState('');
const [relatedEmoji, setRelatedEmoji] = useState('');
const fileInputRef = useRef(null);
const handleFileSelect = (e) => {
const files = Array.from(e.target.files);
const newFiles = files.map(file => ({
id: Date.now() + Math.random(),
file,
name: file.name.split('.')[0],
preview: URL.createObjectURL(file),
size: (file.size / 1024).toFixed(1) + ' KB',
animated: file.type === 'image/gif',
}));
setUploadedFiles(prev => [...prev, ...newFiles]);
};
const handleDrop = (e) => {
e.preventDefault();
const files = Array.from(e.dataTransfer.files);
const newFiles = files.map(file => ({
id: Date.now() + Math.random(),
file,
name: file.name.split('.')[0],
preview: URL.createObjectURL(file),
size: (file.size / 1024).toFixed(1) + ' KB',
animated: file.type === 'image/gif',
}));
setUploadedFiles(prev => [...prev, ...newFiles]);
};
const removeFile = (fileId) => {
setUploadedFiles(prev => prev.filter(f => f.id !== fileId));
};
const updateFileName = (fileId, name) => {
setUploadedFiles(prev => prev.map(f =>
f.id === fileId ? { ...f, name } : f
));
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="emoji-upload-modal" onClick={(e) => e.stopPropagation()}>
<div className="emoji-upload-header">
<h2>Upload {activeTab === 'emoji' ? 'Emoji' : 'Sticker'}</h2>
<button className="close-btn" onClick={onClose}></button>
</div>
<div className="emoji-upload-tabs">
<button
className={`upload-tab ${activeTab === 'emoji' ? 'active' : ''}`}
onClick={() => setActiveTab('emoji')}
>
😀 Emoji
</button>
<button
className={`upload-tab ${activeTab === 'sticker' ? 'active' : ''}`}
onClick={() => setActiveTab('sticker')}
>
🎨 Sticker
</button>
</div>
<div className="emoji-upload-content">
{/* Existing items */}
<div className="existing-items">
<h3>{activeTab === 'emoji' ? 'Server Emoji' : 'Server Stickers'}</h3>
<div className="items-grid">
{activeTab === 'emoji' ? (
existingEmojis.map(emoji => (
<div key={emoji.id} className="item-card">
<div className="item-preview">{emoji.image}</div>
<span className="item-name">:{emoji.name}:</span>
{emoji.animated && <span className="animated-badge">GIF</span>}
</div>
))
) : (
existingStickers.map(sticker => (
<div key={sticker.id} className="item-card sticker">
<div className="item-preview">{sticker.emoji}</div>
<span className="item-name">{sticker.name}</span>
</div>
))
)}
</div>
<div className="slots-info">
{activeTab === 'emoji' ? (
<span>6/50 slots used ({existingEmojis.filter(e => e.animated).length}/50 animated)</span>
) : (
<span>3/15 sticker slots used</span>
)}
</div>
</div>
{/* Upload zone */}
<div
className="upload-zone"
onDragOver={(e) => e.preventDefault()}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
accept={activeTab === 'emoji' ? 'image/png,image/gif' : 'image/png,image/apng'}
multiple
onChange={handleFileSelect}
hidden
/>
<div className="upload-icon">📤</div>
<p>Drag and drop or click to upload</p>
<span className="upload-hint">
{activeTab === 'emoji'
? 'PNG or GIF, 256KB max, 128x128 recommended'
: 'PNG or APNG, 512KB max, 320x320 size'
}
</span>
</div>
{/* Pending uploads */}
{uploadedFiles.length > 0 && (
<div className="pending-uploads">
<h3>Ready to Upload</h3>
{uploadedFiles.map(file => (
<div key={file.id} className="pending-file">
<img src={file.preview} alt="" className="pending-preview" />
<div className="pending-info">
<input
type="text"
value={file.name}
onChange={(e) => updateFileName(file.id, e.target.value)}
placeholder="Name"
className="pending-name-input"
/>
<span className="pending-size">{file.size}</span>
{file.animated && <span className="animated-badge">GIF</span>}
</div>
<button
className="pending-remove"
onClick={() => removeFile(file.id)}
>
</button>
</div>
))}
</div>
)}
</div>
<div className="emoji-upload-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button
className="upload-btn"
disabled={uploadedFiles.length === 0}
onClick={() => onUpload?.(uploadedFiles)}
>
Upload {uploadedFiles.length > 0 ? `(${uploadedFiles.length})` : ''}
</button>
</div>
</div>
</div>
);
}

View file

@ -1,392 +0,0 @@
import React, { useState } from 'react';
const mockEvents = [
{
id: 1,
title: 'Monthly Community Townhall',
description: 'Join us for our monthly Q&A session with the AeThex team. Ask questions, get updates, and connect with the community.',
type: 'stage',
channel: 'community-stage',
startTime: new Date(Date.now() + 2 * 60 * 60 * 1000), // 2 hours from now
endTime: new Date(Date.now() + 4 * 60 * 60 * 1000),
host: { name: 'Anderson', avatar: 'A', color: '#ff0000' },
interested: 234,
image: null,
recurring: 'monthly',
},
{
id: 2,
title: 'Game Night: Among Us',
description: 'Weekly game night! This week we\'re playing Among Us. Join the voice channel 10 minutes early.',
type: 'voice',
channel: 'gaming-voice',
startTime: new Date(Date.now() + 26 * 60 * 60 * 1000), // Tomorrow
endTime: new Date(Date.now() + 29 * 60 * 60 * 1000),
host: { name: 'Sarah', avatar: 'S', color: '#ffa500' },
interested: 89,
image: null,
recurring: 'weekly',
},
{
id: 3,
title: 'AeThex SDK Workshop',
description: 'Learn how to integrate the AeThex SDK into your projects. Hands-on coding session with live Q&A.',
type: 'external',
location: 'https://workshop.aethex.com',
startTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000), // 3 days
endTime: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000 + 2 * 60 * 60 * 1000),
host: { name: 'Trevor', avatar: 'T', color: '#0066ff' },
interested: 456,
image: null,
recurring: null,
},
];
export default function EventsPanel({ onClose, onCreateEvent }) {
const [activeTab, setActiveTab] = useState('upcoming');
const [selectedEvent, setSelectedEvent] = useState(null);
const formatDate = (date) => {
const now = new Date();
const diff = date - now;
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
const hours = Math.floor(diff / (1000 * 60 * 60));
if (hours === 0) {
const minutes = Math.floor(diff / (1000 * 60));
return `In ${minutes} minutes`;
}
return `In ${hours} hours`;
} else if (days === 1) {
return 'Tomorrow';
} else {
return date.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
}
};
const formatTime = (date) => {
return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
};
const getTypeIcon = (type) => {
switch (type) {
case 'stage': return '📢';
case 'voice': return '🔊';
case 'external': return '🔗';
default: return '📅';
}
};
if (selectedEvent) {
return (
<EventDetail
event={selectedEvent}
onBack={() => setSelectedEvent(null)}
formatDate={formatDate}
formatTime={formatTime}
getTypeIcon={getTypeIcon}
/>
);
}
return (
<div className="events-panel">
<div className="events-header">
<h2>Events</h2>
<div className="events-actions">
<button className="create-event-btn" onClick={onCreateEvent}>
+ Create Event
</button>
<button className="events-close" onClick={onClose}></button>
</div>
</div>
<div className="events-tabs">
<button
className={`events-tab ${activeTab === 'upcoming' ? 'active' : ''}`}
onClick={() => setActiveTab('upcoming')}
>
Upcoming
</button>
<button
className={`events-tab ${activeTab === 'recurring' ? 'active' : ''}`}
onClick={() => setActiveTab('recurring')}
>
Recurring
</button>
<button
className={`events-tab ${activeTab === 'past' ? 'active' : ''}`}
onClick={() => setActiveTab('past')}
>
Past
</button>
</div>
<div className="events-list">
{mockEvents.map(event => (
<div
key={event.id}
className="event-card"
onClick={() => setSelectedEvent(event)}
>
<div className="event-date-badge">
<span className="event-month">
{event.startTime.toLocaleDateString('en-US', { month: 'short' })}
</span>
<span className="event-day">
{event.startTime.getDate()}
</span>
</div>
<div className="event-info">
<div className="event-timing">
<span className="event-time-label">{formatDate(event.startTime)}</span>
<span className="event-time">
{formatTime(event.startTime)} - {formatTime(event.endTime)}
</span>
</div>
<h3 className="event-title">
<span className="event-type-icon">{getTypeIcon(event.type)}</span>
{event.title}
</h3>
<p className="event-description">{event.description}</p>
<div className="event-meta">
<div className="event-host">
<div
className="host-avatar"
style={event.host.color ? { background: event.host.color } : undefined}
>
{event.host.avatar}
</div>
<span>Hosted by {event.host.name}</span>
</div>
<div className="event-interested">
<span className="interested-count"> {event.interested} interested</span>
</div>
</div>
{event.recurring && (
<div className="recurring-badge">
🔄 Repeats {event.recurring}
</div>
)}
</div>
</div>
))}
</div>
{mockEvents.length === 0 && (
<div className="events-empty">
<span className="empty-icon">📅</span>
<h3>No upcoming events</h3>
<p>Create an event to bring your community together!</p>
<button className="create-event-btn" onClick={onCreateEvent}>
Create Event
</button>
</div>
)}
</div>
);
}
function EventDetail({ event, onBack, formatDate, formatTime, getTypeIcon }) {
const [isInterested, setIsInterested] = useState(false);
return (
<div className="event-detail">
<button className="back-btn" onClick={onBack}>
Back to Events
</button>
<div className="event-detail-header">
<div className="event-type-badge">
{getTypeIcon(event.type)} {event.type.charAt(0).toUpperCase() + event.type.slice(1)}
</div>
<h1>{event.title}</h1>
</div>
<div className="event-detail-content">
<div className="detail-section">
<h4>📅 Date & Time</h4>
<p>{formatDate(event.startTime)}</p>
<p className="time-range">
{formatTime(event.startTime)} - {formatTime(event.endTime)}
</p>
{event.recurring && (
<p className="recurring-info">🔄 Repeats {event.recurring}</p>
)}
</div>
<div className="detail-section">
<h4>📍 Location</h4>
{event.type === 'external' ? (
<a href={event.location} className="external-link">
{event.location}
</a>
) : (
<p>#{event.channel}</p>
)}
</div>
<div className="detail-section">
<h4>📝 Description</h4>
<p>{event.description}</p>
</div>
<div className="detail-section">
<h4>👤 Host</h4>
<div className="host-info">
<div
className="host-avatar large"
style={event.host.color ? { background: event.host.color } : undefined}
>
{event.host.avatar}
</div>
<span>{event.host.name}</span>
</div>
</div>
<div className="interested-section">
<span> {event.interested + (isInterested ? 1 : 0)} interested</span>
</div>
</div>
<div className="event-detail-actions">
<button
className={`interested-btn ${isInterested ? 'active' : ''}`}
onClick={() => setIsInterested(!isInterested)}
>
{isInterested ? '⭐ Interested' : '☆ Mark as Interested'}
</button>
<button className="share-event-btn">
📤 Share
</button>
</div>
</div>
);
}
// Event creation modal
export function CreateEventModal({ onSubmit, onClose }) {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [type, setType] = useState('voice');
const [date, setDate] = useState('');
const [startTime, setStartTime] = useState('');
const [endTime, setEndTime] = useState('');
const handleSubmit = () => {
if (!title.trim() || !date || !startTime) return;
onSubmit?.({
title,
description,
type,
date,
startTime,
endTime,
});
onClose?.();
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="create-event-modal" onClick={(e) => e.stopPropagation()}>
<div className="modal-header">
<h2>Create Event</h2>
<button className="modal-close" onClick={onClose}></button>
</div>
<div className="modal-content">
<div className="form-group">
<label>Event Type</label>
<div className="event-type-options">
<button
className={`type-option ${type === 'stage' ? 'active' : ''}`}
onClick={() => setType('stage')}
>
📢 Stage
</button>
<button
className={`type-option ${type === 'voice' ? 'active' : ''}`}
onClick={() => setType('voice')}
>
🔊 Voice
</button>
<button
className={`type-option ${type === 'external' ? 'active' : ''}`}
onClick={() => setType('external')}
>
🔗 External
</button>
</div>
</div>
<div className="form-group">
<label>Event Title</label>
<input
type="text"
placeholder="What's your event called?"
value={title}
onChange={(e) => setTitle(e.target.value)}
maxLength={100}
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
placeholder="Tell people what this event is about..."
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
/>
</div>
<div className="form-row">
<div className="form-group">
<label>Date</label>
<input
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
</div>
<div className="form-group">
<label>Start Time</label>
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
/>
</div>
<div className="form-group">
<label>End Time</label>
<input
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
/>
</div>
</div>
</div>
<div className="modal-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button
className="submit-btn"
onClick={handleSubmit}
disabled={!title.trim() || !date || !startTime}
>
Create Event
</button>
</div>
</div>
</div>
);
}

View file

@ -1,180 +0,0 @@
import React, { useState, useRef, useCallback } from 'react';
export default function FileUpload({ onUpload, onClose, maxSize = 25 }) {
const [files, setFiles] = useState([]);
const [isDragging, setIsDragging] = useState(false);
const [comment, setComment] = useState('');
const [isSpoiler, setIsSpoiler] = useState(false);
const fileInputRef = useRef(null);
const handleDragOver = useCallback((e) => {
e.preventDefault();
setIsDragging(true);
}, []);
const handleDragLeave = useCallback((e) => {
e.preventDefault();
setIsDragging(false);
}, []);
const handleDrop = useCallback((e) => {
e.preventDefault();
setIsDragging(false);
const droppedFiles = Array.from(e.dataTransfer.files);
addFiles(droppedFiles);
}, []);
const handleFileSelect = (e) => {
const selectedFiles = Array.from(e.target.files);
addFiles(selectedFiles);
};
const addFiles = (newFiles) => {
const processedFiles = newFiles.map((file) => ({
file,
id: Date.now() + Math.random(),
name: file.name,
size: file.size,
type: file.type,
preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : null,
progress: 100, // Simulated upload progress
}));
setFiles((prev) => [...prev, ...processedFiles]);
};
const removeFile = (id) => {
setFiles((prev) => {
const removed = prev.find((f) => f.id === id);
if (removed?.preview) {
URL.revokeObjectURL(removed.preview);
}
return prev.filter((f) => f.id !== id);
});
};
const formatSize = (bytes) => {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
const getFileIcon = (type) => {
if (type.startsWith('image/')) return '🖼️';
if (type.startsWith('video/')) return '🎬';
if (type.startsWith('audio/')) return '🎵';
if (type.includes('pdf')) return '📄';
if (type.includes('zip') || type.includes('rar')) return '📦';
if (type.includes('text') || type.includes('document')) return '📝';
return '📎';
};
const handleUpload = () => {
onUpload?.({
files,
comment,
isSpoiler,
});
onClose?.();
};
const totalSize = files.reduce((acc, f) => acc + f.size, 0);
const isOverLimit = totalSize > maxSize * 1024 * 1024;
return (
<div className="file-upload-modal">
<div className="file-upload-header">
<h3>Upload to #general</h3>
<button className="upload-close" onClick={onClose}></button>
</div>
<div
className={`file-drop-zone ${isDragging ? 'dragging' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<input
ref={fileInputRef}
type="file"
multiple
onChange={handleFileSelect}
style={{ display: 'none' }}
/>
<div className="drop-zone-content">
<span className="drop-icon">📁</span>
<p>Drag & drop files here or click to browse</p>
<span className="drop-hint">Max file size: {maxSize} MB</span>
</div>
</div>
{files.length > 0 && (
<div className="file-list">
{files.map((file) => (
<div key={file.id} className="file-item">
{file.preview ? (
<img src={file.preview} alt={file.name} className="file-preview" />
) : (
<div className="file-icon">{getFileIcon(file.type)}</div>
)}
<div className="file-info">
<div className="file-name">{file.name}</div>
<div className="file-size">{formatSize(file.size)}</div>
</div>
<button
className="file-remove"
onClick={() => removeFile(file.id)}
>
</button>
</div>
))}
</div>
)}
<div className="upload-options">
<label className="spoiler-option">
<input
type="checkbox"
checked={isSpoiler}
onChange={(e) => setIsSpoiler(e.target.checked)}
/>
<span>Mark as spoiler</span>
</label>
</div>
<div className="upload-comment">
<textarea
placeholder="Add a comment (optional)"
value={comment}
onChange={(e) => setComment(e.target.value)}
rows={2}
/>
</div>
{isOverLimit && (
<div className="upload-warning">
Total size exceeds {maxSize} MB limit. Some files may not upload.
</div>
)}
<div className="upload-footer">
<div className="upload-summary">
{files.length} file{files.length !== 1 ? 's' : ''} {formatSize(totalSize)}
</div>
<div className="upload-actions">
<button className="upload-cancel" onClick={onClose}>Cancel</button>
<button
className="upload-submit"
disabled={files.length === 0}
onClick={handleUpload}
>
Upload
</button>
</div>
</div>
</div>
);
}

View file

@ -1,293 +0,0 @@
import React, { useState } from 'react';
const mockPosts = [
{
id: 1,
title: 'How to set up AeThex Passport for your game?',
author: { name: 'Trevor', avatar: 'T', color: '#ff0000' },
content: 'I want to integrate Passport authentication into my Unity game. What are the steps?',
tags: ['question', 'passport', 'unity'],
replies: 12,
reactions: { '👍': 24, '❤️': 5 },
pinned: true,
createdAt: '2 hours ago',
lastActivity: '15 minutes ago',
},
{
id: 2,
title: '[Tutorial] Complete Nexus SDK Integration Guide',
author: { name: 'Sarah', avatar: 'S', color: '#ffa500' },
content: 'Step-by-step guide for integrating the Nexus SDK into your application...',
tags: ['tutorial', 'nexus', 'sdk'],
replies: 45,
reactions: { '🔥': 89, '👍': 67, '❤️': 23 },
pinned: true,
createdAt: '1 day ago',
lastActivity: '30 minutes ago',
},
{
id: 3,
title: 'Bug: Voice chat not working on Linux',
author: { name: 'DevUser_456', avatar: 'D' },
content: 'Getting an error when trying to join voice channels on Ubuntu 22.04...',
tags: ['bug', 'linux', 'voice'],
replies: 8,
reactions: { '👍': 12 },
pinned: false,
createdAt: '5 hours ago',
lastActivity: '1 hour ago',
},
{
id: 4,
title: 'Show off your AeThex-powered projects!',
author: { name: 'Anderson', avatar: 'A', color: '#5865f2' },
content: 'Share what you\'ve built with AeThex tools and get feedback from the community.',
tags: ['showcase', 'community'],
replies: 156,
reactions: { '🚀': 234, '❤️': 145, '👏': 89 },
pinned: false,
createdAt: '3 days ago',
lastActivity: '10 minutes ago',
},
];
const tags = [
{ id: 'all', label: 'All Posts', color: '#5865f2' },
{ id: 'question', label: 'Question', color: '#3ba55d' },
{ id: 'tutorial', label: 'Tutorial', color: '#faa61a' },
{ id: 'bug', label: 'Bug Report', color: '#ed4245' },
{ id: 'showcase', label: 'Showcase', color: '#9b59b6' },
{ id: 'discussion', label: 'Discussion', color: '#747f8d' },
];
export default function ForumChannel({ channel, onClose }) {
const [activeTag, setActiveTag] = useState('all');
const [sortBy, setSortBy] = useState('activity');
const [search, setSearch] = useState('');
const [selectedPost, setSelectedPost] = useState(null);
const channelData = channel || {
name: 'help-forum',
description: 'Get help with AeThex products and services',
guidelines: 'Be respectful, search before posting, use appropriate tags.',
};
const filteredPosts = mockPosts.filter(post => {
const matchesTag = activeTag === 'all' || post.tags.includes(activeTag);
const matchesSearch = !search ||
post.title.toLowerCase().includes(search.toLowerCase()) ||
post.content.toLowerCase().includes(search.toLowerCase());
return matchesTag && matchesSearch;
});
const sortedPosts = [...filteredPosts].sort((a, b) => {
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
return 0;
});
if (selectedPost) {
return (
<ForumPost
post={selectedPost}
onBack={() => setSelectedPost(null)}
/>
);
}
return (
<div className="forum-channel">
<div className="forum-header">
<div className="forum-title">
<span className="forum-icon">💬</span>
<h2>{channelData.name}</h2>
</div>
<p className="forum-description">{channelData.description}</p>
</div>
<div className="forum-toolbar">
<div className="forum-search">
<span className="search-icon">🔍</span>
<input
type="text"
placeholder="Search posts..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="forum-filters">
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="sort-select"
>
<option value="activity">Recent Activity</option>
<option value="newest">Newest</option>
<option value="oldest">Oldest</option>
<option value="popular">Most Popular</option>
</select>
</div>
<button className="new-post-btn">
+ New Post
</button>
</div>
<div className="forum-tags">
{tags.map(tag => (
<button
key={tag.id}
className={`tag-btn ${activeTag === tag.id ? 'active' : ''}`}
style={activeTag === tag.id ? { background: tag.color } : undefined}
onClick={() => setActiveTag(tag.id)}
>
{tag.label}
</button>
))}
</div>
<div className="forum-posts">
{sortedPosts.map(post => (
<div
key={post.id}
className={`forum-post-card ${post.pinned ? 'pinned' : ''}`}
onClick={() => setSelectedPost(post)}
>
{post.pinned && <span className="pinned-badge">📌 Pinned</span>}
<div className="post-main">
<div
className="post-avatar"
style={post.author.color ? { background: post.author.color } : undefined}
>
{post.author.avatar}
</div>
<div className="post-content">
<h3 className="post-title">{post.title}</h3>
<p className="post-preview">{post.content}</p>
<div className="post-tags">
{post.tags.map(tagId => {
const tag = tags.find(t => t.id === tagId);
return tag ? (
<span
key={tagId}
className="post-tag"
style={{ background: tag.color }}
>
{tag.label}
</span>
) : null;
})}
</div>
</div>
</div>
<div className="post-meta">
<div className="post-stats">
<span className="reply-count">💬 {post.replies}</span>
{Object.entries(post.reactions).map(([emoji, count]) => (
<span key={emoji} className="reaction-count">{emoji} {count}</span>
))}
</div>
<div className="post-time">
<span className="author-name">{post.author.name}</span>
<span></span>
<span>Last activity {post.lastActivity}</span>
</div>
</div>
</div>
))}
</div>
{filteredPosts.length === 0 && (
<div className="forum-empty">
<span className="empty-icon">📭</span>
<p>No posts found</p>
</div>
)}
</div>
);
}
function ForumPost({ post, onBack }) {
const [replyContent, setReplyContent] = useState('');
const replies = [
{ id: 1, author: { name: 'Marcus', avatar: 'M' }, content: 'Have you checked the documentation? There\'s a great guide at docs.aethex.com/passport', time: '1 hour ago', reactions: { '👍': 5 } },
{ id: 2, author: { name: 'Sarah', avatar: 'S', color: '#ffa500' }, content: 'I wrote a tutorial for this! Check out post #2 in this forum.', time: '45 minutes ago', reactions: { '❤️': 3 } },
];
return (
<div className="forum-post-detail">
<button className="back-btn" onClick={onBack}>
Back to posts
</button>
<div className="post-header">
<h1>{post.title}</h1>
<div className="post-author-info">
<div
className="author-avatar"
style={post.author.color ? { background: post.author.color } : undefined}
>
{post.author.avatar}
</div>
<span className="author-name">{post.author.name}</span>
<span className="post-date">Posted {post.createdAt}</span>
</div>
</div>
<div className="post-body">
<p>{post.content}</p>
</div>
<div className="post-reactions">
{Object.entries(post.reactions).map(([emoji, count]) => (
<button key={emoji} className="reaction-btn">
{emoji} {count}
</button>
))}
<button className="reaction-btn add">+</button>
</div>
<div className="post-replies">
<h3>Replies ({replies.length})</h3>
{replies.map(reply => (
<div key={reply.id} className="reply-card">
<div
className="reply-avatar"
style={reply.author.color ? { background: reply.author.color } : undefined}
>
{reply.author.avatar}
</div>
<div className="reply-content">
<div className="reply-header">
<span className="reply-author">{reply.author.name}</span>
<span className="reply-time">{reply.time}</span>
</div>
<p>{reply.content}</p>
<div className="reply-actions">
{Object.entries(reply.reactions).map(([emoji, count]) => (
<button key={emoji} className="reaction-btn small">
{emoji} {count}
</button>
))}
<button className="reply-btn">Reply</button>
</div>
</div>
</div>
))}
</div>
<div className="reply-input">
<textarea
placeholder="Write a reply..."
value={replyContent}
onChange={(e) => setReplyContent(e.target.value)}
/>
<button className="send-reply-btn" disabled={!replyContent.trim()}>
Send Reply
</button>
</div>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show more