modified: astro-site/package.json

This commit is contained in:
MrPiglr 2026-02-28 23:50:09 -07:00
parent f14765f47c
commit df3146abdf
43 changed files with 12244 additions and 3678 deletions

View file

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

View file

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

View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
"exclude": ["node_modules"]
}

View file

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

3355
package-lock.json generated

File diff suppressed because it is too large Load diff

6993
packages/desktop/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -4,8 +4,8 @@
"description": "AeThex Connect Desktop App",
"main": "dist/main/index.js",
"scripts": {
"dev": "concurrently \"npm run dev:main\" \"npm run dev:renderer\"",
"dev:main": "tsc -p tsconfig.main.json && electron dist/main/index.js",
"dev": "concurrently \"npm run dev:renderer\" \"npm run dev:main\"",
"dev:main": "tsc -p tsconfig.main.json && cross-env NODE_ENV=development electron dist/main/index.js",
"dev:renderer": "vite",
"build": "npm run build:main && npm run build:renderer",
"build:main": "tsc -p tsconfig.main.json",
@ -28,7 +28,10 @@
"package.json"
],
"mac": {
"target": ["dmg", "zip"],
"target": [
"dmg",
"zip"
],
"category": "public.app-category.social-networking",
"icon": "assets/icon.icns",
"hardenedRuntime": true,
@ -37,11 +40,18 @@
"entitlementsInherit": "build/entitlements.mac.plist"
},
"win": {
"target": ["nsis", "portable"],
"target": [
"nsis",
"portable"
],
"icon": "assets/icon.ico"
},
"linux": {
"target": ["AppImage", "deb", "rpm"],
"target": [
"AppImage",
"deb",
"rpm"
],
"icon": "assets/icon.png",
"category": "Network"
},
@ -57,10 +67,20 @@
"electron-updater": "^6.1.7"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.0",
"@types/node": "^20.10.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.4",
"autoprefixer": "^10.4.24",
"concurrently": "^8.2.2",
"cross-env": "^10.1.0",
"electron": "^28.1.0",
"electron-builder": "^24.9.1",
"postcss": "^8.5.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwindcss": "^4.2.0",
"typescript": "^5.3.3",
"vite": "^5.0.8"
}

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

View file

@ -6,6 +6,7 @@ import { autoUpdater } from 'electron-updater';
const store = new Store();
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
let isQuitting = false;
// Single instance lock
const gotTheLock = app.requestSingleInstanceLock();
@ -28,15 +29,14 @@ function createWindow() {
minWidth: 800,
minHeight: 600,
backgroundColor: '#1a1a1a',
show: false,
show: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
},
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
frame: process.platform !== 'win32',
icon: path.join(__dirname, '../../assets/icon.png'),
frame: true,
});
// Load app
@ -55,7 +55,7 @@ function createWindow() {
// Minimize to tray instead of closing
mainWindow.on('close', (event) => {
if (!app.isQuitting && store.get('minimizeToTray', true)) {
if (!isQuitting && store.get('minimizeToTray', true)) {
event.preventDefault();
mainWindow?.hide();
@ -153,7 +153,7 @@ function updateTrayMenu() {
{
label: 'Quit',
click: () => {
app.isQuitting = true;
isQuitting = true;
app.quit();
},
},
@ -231,7 +231,7 @@ ipcMain.handle('get-sources', async () => {
thumbnailSize: { width: 150, height: 150 },
});
return sources.map((source) => ({
return sources.map((source: any) => ({
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL(),
@ -317,7 +317,7 @@ app.on('window-all-closed', () => {
});
app.on('before-quit', () => {
app.isQuitting = true;
isQuitting = true;
});
app.on('will-quit', () => {

View file

@ -1,7 +1,43 @@
import React from "react";
import React, { useEffect, useState } from "react";
import { AuthProvider, useAuth } from "./context/AuthContext";
import { AppProvider } from "./context/AppContext";
import { ToastProvider } from "./components/ToastProvider";
import MainLayout from "./components/MainLayout";
import LoginScreen from "./components/LoginScreen";
import { PageLoadingSkeleton } from "./components/Skeletons";
import "./global.css";
const App: React.FC = () => <MainLayout />;
const AppContent: React.FC = () => {
const { user, loading } = useAuth();
const [showSplash, setShowSplash] = useState(true);
useEffect(() => {
// Show splash for at least 1.5s for branding
const timer = setTimeout(() => setShowSplash(false), 1500);
return () => clearTimeout(timer);
}, []);
if (loading || showSplash) {
return <PageLoadingSkeleton />;
}
if (!user) {
return <LoginScreen />;
}
return (
<AppProvider>
<MainLayout />
</AppProvider>
);
};
const App: React.FC = () => (
<AuthProvider>
<ToastProvider>
<AppContent />
</ToastProvider>
</AuthProvider>
);
export default App;

View file

@ -0,0 +1,158 @@
import React from "react";
import { useAuth } from "../context/AuthContext";
const ActivityPanel: React.FC = () => {
const { user } = useAuth();
// Mock activity data - would come from real-time API
const activities = [
{ type: 'gaming', user: 'xKryptic', game: 'Valorant', time: '2h 34m', avatar: 'K' },
{ type: 'streaming', user: 'AeThexDev', platform: 'Twitch', viewers: 142, avatar: 'A' },
{ type: 'listening', user: 'NeonByte', track: 'Blinding Lights', artist: 'The Weeknd', avatar: 'N' },
];
const friends = [
{ name: 'CyberWolf', status: 'online', activity: 'Playing Apex Legends', avatar: 'C' },
{ name: 'PixelArt', status: 'idle', activity: 'Away', avatar: 'P' },
{ name: 'NightOwl', status: 'dnd', activity: 'Do Not Disturb', avatar: 'N' },
{ name: 'TechGuru', status: 'offline', activity: 'Last seen 2h ago', avatar: 'T' },
];
const getStatusColor = (status: string) => {
switch (status) {
case 'online': return 'bg-emerald-500';
case 'idle': return 'bg-amber-500';
case 'dnd': return 'bg-rose-500';
default: return 'bg-gray-500';
}
};
return (
<div className="activity-panel w-72 bg-[#08080a]/90 backdrop-blur-xl border-l border-white/5 flex flex-col">
{/* Panel Header */}
<div className="p-4 border-b border-white/5">
<h3 className="text-sm font-bold text-white/90 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-gradient-to-r from-cyan-500 to-blue-500 animate-pulse" />
Active Now
</h3>
</div>
{/* Live Activities */}
<div className="flex-1 overflow-y-auto">
<div className="p-3">
<div className="text-xs uppercase tracking-wider text-white/40 font-semibold mb-3 px-1">
Live Activity
</div>
{activities.map((activity, i) => (
<div key={i} className="activity-card mb-2 p-3 rounded-xl bg-white/[0.03] hover:bg-white/[0.06] border border-white/5 transition-all cursor-pointer group">
<div className="flex items-center gap-3">
<div className="relative">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-violet-600 to-indigo-600 flex items-center justify-center font-bold text-sm">
{activity.avatar}
</div>
<div className="absolute -bottom-1 -right-1 w-4 h-4 rounded-full bg-[#08080a] flex items-center justify-center">
{activity.type === 'gaming' && <span className="text-[10px]">🎮</span>}
{activity.type === 'streaming' && <span className="text-[10px]">📺</span>}
{activity.type === 'listening' && <span className="text-[10px]">🎵</span>}
</div>
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-white/90 truncate">{activity.user}</div>
<div className="text-xs text-white/50 truncate">
{activity.type === 'gaming' && `Playing ${activity.game}`}
{activity.type === 'streaming' && `Live on ${activity.platform}${activity.viewers} viewers`}
{activity.type === 'listening' && `${activity.track}${activity.artist}`}
</div>
</div>
{activity.type === 'streaming' && (
<div className="px-2 py-1 rounded-md bg-rose-500/20 text-rose-400 text-[10px] font-bold uppercase">
Live
</div>
)}
</div>
</div>
))}
</div>
{/* Friends List */}
<div className="p-3 border-t border-white/5">
<div className="text-xs uppercase tracking-wider text-white/40 font-semibold mb-3 px-1">
Friends {friends.filter(f => f.status !== 'offline').length} Online
</div>
{friends.map((friend, i) => (
<div key={i} className="friend-item flex items-center gap-3 p-2 rounded-lg hover:bg-white/[0.05] cursor-pointer transition-all">
<div className="relative">
<div className={`w-9 h-9 rounded-full flex items-center justify-center font-semibold text-sm ${
friend.status === 'offline' ? 'bg-white/10 text-white/40' : 'bg-gradient-to-br from-cyan-500 to-blue-600'
}`}>
{friend.avatar}
</div>
<div className={`absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-[#08080a] ${getStatusColor(friend.status)}`} />
</div>
<div className="flex-1 min-w-0">
<div className={`text-sm font-medium truncate ${friend.status === 'offline' ? 'text-white/40' : 'text-white/90'}`}>
{friend.name}
</div>
<div className="text-xs text-white/40 truncate">{friend.activity}</div>
</div>
</div>
))}
</div>
{/* Quick Actions */}
<div className="p-3 border-t border-white/5">
<div className="text-xs uppercase tracking-wider text-white/40 font-semibold mb-3 px-1">
Quick Actions
</div>
<div className="grid grid-cols-2 gap-2">
<button className="flex flex-col items-center justify-center p-3 rounded-xl bg-gradient-to-br from-violet-600/20 to-indigo-600/20 border border-violet-500/20 hover:border-violet-500/40 transition-all group">
<span className="text-lg mb-1 group-hover:scale-110 transition-transform">🎮</span>
<span className="text-xs text-white/70">GameForge</span>
</button>
<button className="flex flex-col items-center justify-center p-3 rounded-xl bg-gradient-to-br from-emerald-600/20 to-cyan-600/20 border border-emerald-500/20 hover:border-emerald-500/40 transition-all group">
<span className="text-lg mb-1 group-hover:scale-110 transition-transform">📞</span>
<span className="text-xs text-white/70">Voice Call</span>
</button>
<button className="flex flex-col items-center justify-center p-3 rounded-xl bg-gradient-to-br from-rose-600/20 to-pink-600/20 border border-rose-500/20 hover:border-rose-500/40 transition-all group">
<span className="text-lg mb-1 group-hover:scale-110 transition-transform">🎬</span>
<span className="text-xs text-white/70">Go Live</span>
</button>
<button className="flex flex-col items-center justify-center p-3 rounded-xl bg-gradient-to-br from-amber-600/20 to-orange-600/20 border border-amber-500/20 hover:border-amber-500/40 transition-all group">
<span className="text-lg mb-1 group-hover:scale-110 transition-transform"></span>
<span className="text-xs text-white/70">Nitro</span>
</button>
</div>
</div>
</div>
{/* Now Playing */}
<div className="p-3 border-t border-white/5 bg-gradient-to-r from-violet-600/10 to-transparent">
<div className="flex items-center gap-3">
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center shadow-lg shadow-violet-500/20">
🎵
</div>
<div className="flex-1 min-w-0">
<div className="text-xs text-white/50 uppercase tracking-wider">Now Playing</div>
<div className="text-sm font-medium text-white/90 truncate">Midnight City</div>
<div className="text-xs text-white/50 truncate">M83</div>
</div>
<div className="flex gap-1">
<button className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center hover:bg-white/20 transition-colors">
</button>
<button className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center hover:bg-white/20 transition-colors">
</button>
</div>
</div>
<div className="mt-2 h-1 rounded-full bg-white/10 overflow-hidden">
<div className="h-full w-1/3 rounded-full bg-gradient-to-r from-violet-500 to-purple-500" />
</div>
</div>
</div>
);
};
export default ActivityPanel;

View file

@ -1,65 +1,281 @@
import React from "react";
import React, { useState } from "react";
import { useApp } from "../context/AppContext";
import { useAuth } from "../context/AuthContext";
const ChannelSidebar: React.FC = () => (
<div className="channel-sidebar w-72 bg-[#0f0f0f] border-r border-[#1a1a1a] flex flex-col">
{/* Server Header */}
<div className="server-header p-4 border-b border-[#1a1a1a] font-bold text-base flex items-center justify-between">
<span>AeThex Foundation</span>
<span className="server-badge foundation text-xs px-2 py-1 rounded bg-red-900/20 text-red-500 border border-red-500 uppercase tracking-wider">Official</span>
interface ChannelSidebarProps {
onOpenSettings?: () => void;
}
const ChannelSidebar: React.FC<ChannelSidebarProps> = ({ onOpenSettings }) => {
const { currentServer, channels, currentChannel, selectChannel, loadingChannels } = useApp();
const { user, logout } = useAuth();
const [showSettings, setShowSettings] = useState(false);
const [statusMenu, setStatusMenu] = useState(false);
const textChannels = channels.filter(c => c.channel_type === 'text' || c.channel_type === 'announcement');
const voiceChannels = channels.filter(c => c.channel_type === 'voice' || c.channel_type === 'stage');
const getChannelIcon = (type: string) => {
switch (type) {
case 'voice': return '🔊';
case 'announcement': return '📢';
case 'stage': return '🎭';
default: return '#';
}
};
if (!currentServer) {
return (
<div className="channel-sidebar w-60 bg-[#0c0c0e]/80 backdrop-blur-xl border-r border-white/5 flex flex-col">
{/* Home Header */}
<div className="p-4 border-b border-white/5">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-red-600 via-blue-600 to-orange-500 flex items-center justify-center font-black text-lg shadow-lg">
A
</div>
{/* Channel List */}
<div className="channel-list flex-1 overflow-y-auto py-2">
<div className="channel-category px-4 pt-4 pb-2 text-xs uppercase tracking-widest text-gray-500 font-bold">Announcements</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">📢</span>
<span className="channel-name flex-1">updates</span>
<span className="channel-badge text-xs bg-red-600 text-white px-2 rounded-full">3</span>
</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">📜</span>
<span className="channel-name flex-1">changelog</span>
</div>
<div className="channel-category px-4 pt-4 pb-2 text-xs uppercase tracking-widest text-gray-500 font-bold">Development</div>
<div className="channel-item active flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm bg-[#1a1a1a]">
<span className="channel-icon">#</span>
<span className="channel-name flex-1">general</span>
</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">#</span>
<span className="channel-name flex-1">api-discussion</span>
</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">#</span>
<span className="channel-name flex-1">passport-development</span>
</div>
<div className="channel-category px-4 pt-4 pb-2 text-xs uppercase tracking-widest text-gray-500 font-bold">Support</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon"></span>
<span className="channel-name flex-1">help</span>
</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">🐛</span>
<span className="channel-name flex-1">bug-reports</span>
</div>
<div className="channel-category px-4 pt-4 pb-2 text-xs uppercase tracking-widest text-gray-500 font-bold">Voice Channels</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">🔊</span>
<span className="channel-name flex-1">Nexus Lounge</span>
<span className="text-gray-500 text-xs">3</span>
</div>
</div>
{/* User Presence */}
<div className="user-presence p-3 border-t border-[#1a1a1a] flex items-center gap-3 text-sm">
<div className="user-avatar w-10 h-10 rounded-full flex items-center justify-center font-bold bg-gradient-to-tr from-red-600 via-blue-600 to-orange-400">A</div>
<div className="user-info flex-1">
<div className="user-name font-bold mb-0.5">Anderson</div>
<div className="user-status flex items-center gap-1 text-xs text-gray-500">
<span className="status-dot w-2 h-2 rounded-full bg-green-400 shadow-green-400/50 shadow" />
<span>Building AeThex</span>
<div>
<div className="font-bold text-white">AeThex Connect</div>
<div className="text-xs text-white/40">Home</div>
</div>
</div>
</div>
{/* Quick Access */}
<div className="flex-1 p-3">
<div className="text-[11px] uppercase tracking-wider text-white/30 font-semibold px-2 mb-2">Quick Access</div>
<button className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all text-left group mb-1">
<span className="text-lg">👋</span>
<span className="text-sm text-white/70 group-hover:text-white/90">Friends</span>
</button>
<button className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all text-left group mb-1">
<span className="text-lg">🎮</span>
<span className="text-sm text-white/70 group-hover:text-white/90">GameForge Hub</span>
</button>
<button className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all text-left group mb-1">
<span className="text-lg">🧭</span>
<span className="text-sm text-white/70 group-hover:text-white/90">Discover Servers</span>
</button>
<button className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all text-left group">
<span className="text-lg"></span>
<span className="text-sm text-white/70 group-hover:text-white/90">Nitro</span>
</button>
</div>
{/* User Panel */}
<UserPanel user={user} logout={logout} statusMenu={statusMenu} setStatusMenu={setStatusMenu} onOpenSettings={onOpenSettings} />
</div>
);
}
return (
<div className="channel-sidebar w-60 bg-[#0c0c0e]/80 backdrop-blur-xl border-r border-white/5 flex flex-col">
{/* Server Header with Banner */}
<div className="relative">
{currentServer.banner_url ? (
<div className="h-24 bg-cover bg-center" style={{ backgroundImage: `url(${currentServer.banner_url})` }}>
<div className="absolute inset-0 bg-gradient-to-t from-[#0c0c0e] to-transparent" />
</div>
) : (
<div className="h-12 bg-gradient-to-r from-indigo-600/30 via-violet-600/30 to-purple-600/30" />
)}
<div className="px-4 py-3 flex items-center justify-between relative">
<div className="flex items-center gap-2 flex-1 min-w-0">
<span className="font-bold text-white truncate">{currentServer.name}</span>
{currentServer.is_public && (
<span className="px-1.5 py-0.5 rounded text-[10px] bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 font-medium">
PUBLIC
</span>
)}
</div>
<button className="w-7 h-7 rounded flex items-center justify-center text-white/50 hover:text-white/80 hover:bg-white/10 transition-all">
</button>
</div>
</div>
{/* Server Stats Bar */}
<div className="px-4 pb-3 flex items-center gap-3 text-sm border-b border-white/5">
<div className="flex items-center gap-1.5 text-white/50">
<span className="w-2 h-2 rounded-full bg-emerald-500" />
<span>{currentServer.member_count || 1}</span>
</div>
{currentServer.invite_code && (
<button
onClick={() => navigator.clipboard.writeText(currentServer.invite_code)}
className="ml-auto text-xs px-2 py-1 rounded bg-white/5 text-white/50 hover:text-white/80 hover:bg-white/10 transition-all"
title="Click to copy"
>
Invite: {currentServer.invite_code}
</button>
)}
</div>
{/* Channel List */}
<div className="channel-list flex-1 overflow-y-auto py-3 px-2">
{loadingChannels ? (
<div className="flex flex-col items-center justify-center py-8">
<div className="w-6 h-6 border-2 border-white/20 border-t-white/60 rounded-full animate-spin mb-2" />
<span className="text-xs text-white/40">Loading channels...</span>
</div>
) : (
<>
{textChannels.length > 0 && (
<ChannelGroup
title="Text Channels"
channels={textChannels}
currentChannel={currentChannel}
selectChannel={selectChannel}
getChannelIcon={getChannelIcon}
/>
)}
{voiceChannels.length > 0 && (
<ChannelGroup
title="Voice Channels"
channels={voiceChannels}
currentChannel={currentChannel}
selectChannel={selectChannel}
getChannelIcon={getChannelIcon}
/>
)}
{channels.length === 0 && (
<div className="text-center py-8">
<div className="text-3xl mb-2">📭</div>
<p className="text-sm text-white/40">No channels yet</p>
<p className="text-xs text-white/30 mt-1">Create one to get started!</p>
</div>
)}
</>
)}
</div>
{/* User Panel */}
<UserPanel user={user} logout={logout} statusMenu={statusMenu} setStatusMenu={setStatusMenu} onOpenSettings={onOpenSettings} />
</div>
);
};
// Channel Group Component
const ChannelGroup: React.FC<{
title: string;
channels: any[];
currentChannel: any;
selectChannel: (channel: any) => void;
getChannelIcon: (type: string) => string;
}> = ({ title, channels, currentChannel, selectChannel, getChannelIcon }) => {
const [collapsed, setCollapsed] = useState(false);
return (
<div className="mb-4">
<button
onClick={() => setCollapsed(!collapsed)}
className="w-full flex items-center gap-1 px-2 py-1 text-[11px] uppercase tracking-wider text-white/40 font-semibold hover:text-white/60 transition-colors"
>
<span className={`transition-transform ${collapsed ? '-rotate-90' : ''}`}></span>
{title}
</button>
{!collapsed && channels.map(channel => (
<div
key={channel.id}
onClick={() => selectChannel(channel)}
className={`channel-item flex items-center gap-2 px-2 py-1.5 mx-1 rounded-md cursor-pointer text-sm transition-all group ${
currentChannel?.id === channel.id
? 'bg-white/10 text-white'
: 'text-white/50 hover:bg-white/5 hover:text-white/80'
}`}
>
<span className={`channel-icon text-base ${currentChannel?.id === channel.id ? 'text-white/90' : 'text-white/40'}`}>
{getChannelIcon(channel.channel_type)}
</span>
<span className="channel-name flex-1 truncate">{channel.name}</span>
<div className="opacity-0 group-hover:opacity-100 flex items-center gap-0.5 transition-opacity">
<button className="w-6 h-6 rounded flex items-center justify-center text-white/40 hover:text-white/70 hover:bg-white/10 text-xs">
</button>
</div>
</div>
))}
</div>
);
};
// User Panel Component
const UserPanel: React.FC<{
user: any;
logout: () => void;
statusMenu: boolean;
setStatusMenu: (v: boolean) => void;
onOpenSettings?: () => void;
}> = ({ user, logout, statusMenu, setStatusMenu, onOpenSettings }) => {
const statuses = [
{ id: 'online', label: 'Online', color: 'bg-emerald-500' },
{ id: 'idle', label: 'Idle', color: 'bg-amber-500' },
{ id: 'dnd', label: 'Do Not Disturb', color: 'bg-rose-500' },
{ id: 'offline', label: 'Invisible', color: 'bg-gray-500' },
];
return (
<div className="user-panel p-2 border-t border-white/5 bg-[#08080a]/50">
<div className="flex items-center gap-2 p-2 rounded-lg hover:bg-white/5 transition-all cursor-pointer">
<div className="relative">
<div className="w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm bg-gradient-to-br from-red-600 via-blue-600 to-orange-400">
{user?.avatarUrl ? (
<img src={user.avatarUrl} alt={user.username} className="w-full h-full rounded-full object-cover" />
) : (
<span className="text-white">{user?.username?.charAt(0).toUpperCase() || 'U'}</span>
)}
</div>
<button
onClick={() => setStatusMenu(!statusMenu)}
className="absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full bg-emerald-500 border-2 border-[#0c0c0e] cursor-pointer hover:scale-110 transition-transform"
/>
</div>
<div className="flex-1 min-w-0">
<div className="font-semibold text-sm text-white truncate">{user?.displayName || user?.username}</div>
<div className="text-xs text-white/40 truncate">@{user?.username}</div>
</div>
<div className="flex items-center gap-1">
<button className="w-8 h-8 rounded-lg flex items-center justify-center text-white/40 hover:text-white/70 hover:bg-white/10 transition-all">
🎤
</button>
<button className="w-8 h-8 rounded-lg flex items-center justify-center text-white/40 hover:text-white/70 hover:bg-white/10 transition-all">
🎧
</button>
<button
onClick={onOpenSettings}
className="w-8 h-8 rounded-lg flex items-center justify-center text-white/40 hover:text-white/70 hover:bg-white/10 transition-all"
title="Settings"
>
</button>
</div>
</div>
{/* Status Menu */}
{statusMenu && (
<div className="absolute bottom-16 left-2 right-2 bg-[#111113] rounded-xl border border-white/10 shadow-xl p-2 z-50">
{statuses.map(status => (
<button
key={status.id}
onClick={() => setStatusMenu(false)}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-white/5 transition-all text-left"
>
<span className={`w-3 h-3 rounded-full ${status.color}`} />
<span className="text-sm text-white/80">{status.label}</span>
</button>
))}
<div className="my-2 h-px bg-white/10" />
<button className="w-full flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-white/5 transition-all text-left">
<span className="text-lg"></span>
<span className="text-sm text-white/80">Set Custom Status</span>
</button>
</div>
)}
</div>
);
};
export default ChannelSidebar;

View file

@ -1,41 +1,182 @@
import React from "react";
import React, { useRef, useEffect, useState } from "react";
import { useApp } from "../context/AppContext";
import Message from "./Message";
import MessageInput from "./MessageInput";
const messages = [
{ type: "system", label: "FOUNDATION", text: "Foundation authentication services upgraded to v2.1.0. Enhanced security protocols now active across all AeThex infrastructure.", className: "foundation" },
{ type: "user", author: "Trevor", badge: "Foundation", time: "10:34 AM", text: "Just pushed the authentication updates. All services should automatically migrate to the new protocols within 24 hours.", avatar: "T", avatarBg: "from-red-600 to-red-800" },
{ type: "user", author: "Marcus", time: "10:41 AM", text: "Excellent work! I've been testing the new Passport integration and it's incredibly smooth. The Trinity color-coding in the UI makes it really clear which division is handling what.", avatar: "M", avatarBg: "from-blue-600 to-blue-900" },
{ type: "system", label: "LABS", text: "Nexus Engine v2.0-beta now available for testing. New cross-platform sync reduces latency by 40%. Join #labs-testing to participate.", className: "labs" },
{ type: "user", author: "Sarah", badge: "Labs", time: "11:15 AM", text: "The Nexus v2 parallel compilation is insane. Cut my build time from 3 minutes to under 2. Still some edge cases with complex state synchronization but wow... ⚠️", avatar: "S", avatarBg: "from-orange-400 to-orange-700" },
{ type: "user", author: "Anderson", badge: "Founder", time: "11:47 AM", text: "Love seeing the Trinity infrastructure working in harmony. Foundation keeping everything secure, Labs pushing the boundaries, Corporation delivering production-ready tools. This is exactly the vision.", avatar: "A", avatarBg: "from-red-600 via-blue-600 to-orange-400" },
{ type: "user", author: "DevUser_2847", time: "12:03 PM", text: "Quick question - when using AeThex Studio, does the Terminal automatically connect to all three Trinity divisions, or do I need to configure that?", avatar: "D", avatarBg: "bg-[#1a1a1a]" },
{ type: "system", label: "CORPORATION", text: "AeThex Studio Pro users: New Railway deployment templates available. Optimized configurations for Foundation APIs, Corporation services, and Labs experiments.", className: "corporation" },
];
interface ChatAreaProps {
onToggleMembers?: () => void;
onToggleActivity?: () => void;
showMembers?: boolean;
showActivity?: boolean;
onOpenQuickSwitcher?: () => void;
}
const ChatArea: React.FC = () => (
<div className="chat-area flex flex-col flex-1 bg-[#0a0a0a]">
{/* Chat Header */}
<div className="chat-header px-5 py-4 border-b border-[#1a1a1a] flex items-center gap-3">
<span className="channel-name-header flex-1 font-bold text-base"># general</span>
<div className="chat-tools flex gap-4 text-sm text-gray-500">
<span className="chat-tool cursor-pointer hover:text-blue-500">🔔</span>
<span className="chat-tool cursor-pointer hover:text-blue-500">📌</span>
<span className="chat-tool cursor-pointer hover:text-blue-500">👥 128</span>
<span className="chat-tool cursor-pointer hover:text-blue-500">🔍</span>
const ChatArea: React.FC<ChatAreaProps> = ({
onToggleMembers,
onToggleActivity,
showMembers = true,
showActivity = true,
onOpenQuickSwitcher
}) => {
const { currentChannel, messages, loadingMessages } = useApp();
const messagesEndRef = useRef<HTMLDivElement>(null);
const [typingUsers] = useState<string[]>(['PixelArt']); // Mock typing indicator
// Auto-scroll to bottom on new messages
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
if (!currentChannel) {
return (
<div className="chat-area flex flex-col flex-1 bg-[#0a0a0c] items-center justify-center">
<div className="text-center max-w-md">
<div className="relative mb-6">
<div className="w-24 h-24 mx-auto rounded-3xl bg-gradient-to-br from-indigo-600/20 to-violet-600/20 flex items-center justify-center border border-white/10">
<span className="text-5xl">💬</span>
</div>
<div className="absolute inset-0 bg-gradient-to-r from-indigo-500 to-violet-500 rounded-3xl blur-2xl opacity-20 -z-10" />
</div>
<h2 className="text-2xl font-bold text-white mb-3">No Channel Selected</h2>
<p className="text-white/50 leading-relaxed">
Select a text or voice channel from the sidebar to start chatting with your community.
</p>
</div>
</div>
{/* Messages */}
<div className="chat-messages flex-1 overflow-y-auto px-5 py-5">
{messages.map((msg, i) => (
<Message key={i} {...msg} />
))}
);
}
return (
<div className="chat-area flex flex-col flex-1 bg-[#0a0a0c]">
{/* Enhanced Chat Header */}
<div className="chat-header px-5 py-3 border-b border-white/5 flex items-center gap-3 bg-[#0a0a0c]/80 backdrop-blur-xl">
<div className="flex items-center gap-3 flex-1">
<div className="w-8 h-8 rounded-lg bg-white/5 flex items-center justify-center text-white/60">
#
</div>
<div>
<div className="font-semibold text-white/90">{currentChannel.name}</div>
{currentChannel.description && (
<div className="text-xs text-white/40 truncate max-w-md">{currentChannel.description}</div>
)}
</div>
</div>
<div className="chat-tools flex items-center gap-1">
<button className="w-9 h-9 rounded-lg flex items-center justify-center text-white/50 hover:bg-white/5 hover:text-white/80 transition-all" title="Threads">
💬
</button>
<button className="w-9 h-9 rounded-lg flex items-center justify-center text-white/50 hover:bg-white/5 hover:text-white/80 transition-all" title="Notifications">
🔔
</button>
<button className="w-9 h-9 rounded-lg flex items-center justify-center text-white/50 hover:bg-white/5 hover:text-white/80 transition-all" title="Pinned Messages">
📌
</button>
<button
onClick={onToggleMembers}
className={`w-9 h-9 rounded-lg flex items-center justify-center transition-all ${
showMembers ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80'
}`}
title="Toggle Member List"
>
👥
</button>
<button
onClick={onToggleActivity}
className={`w-9 h-9 rounded-lg flex items-center justify-center transition-all ${
showActivity ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80'
}`}
title="Toggle Activity"
>
</button>
<div className="w-px h-6 bg-white/10 mx-1" />
<button
onClick={onOpenQuickSwitcher}
className="flex items-center gap-2 px-3 py-1.5 text-sm rounded-lg bg-black/30 border border-white/10 text-white/30 hover:text-white/50 hover:border-white/20 transition-all"
>
<span>Search</span>
<kbd className="text-[10px] px-1 py-0.5 bg-white/10 rounded border border-white/10">Ctrl+K</kbd>
</button>
</div>
</div>
{/* Messages Area */}
<div className="chat-messages flex-1 overflow-y-auto">
{loadingMessages ? (
<div className="flex items-center justify-center h-full">
<div className="text-center">
<div className="w-8 h-8 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-3" />
<p className="text-white/50 text-sm">Loading messages...</p>
</div>
</div>
) : messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full">
<div className="relative mb-4">
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-emerald-600/20 to-cyan-600/20 flex items-center justify-center border border-white/10">
<span className="text-4xl">🎉</span>
</div>
</div>
<h3 className="text-lg font-semibold text-white mb-2">Welcome to #{currentChannel.name}!</h3>
<p className="text-white/50 text-sm text-center max-w-sm">
This is the start of the #{currentChannel.name} channel. Send your first message to get the conversation going!
</p>
</div>
) : (
<div className="px-5 py-4">
{/* Channel Start Header */}
<div className="mb-6 pb-6 border-b border-white/5">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-indigo-600/30 to-violet-600/30 flex items-center justify-center mb-4 border border-white/10">
<span className="text-3xl font-bold text-white">#</span>
</div>
<h3 className="text-2xl font-bold text-white mb-1">Welcome to #{currentChannel.name}!</h3>
<p className="text-white/50 text-sm">This is the beginning of the #{currentChannel.name} channel.</p>
</div>
{messages.map((msg, index) => {
const prevMsg = messages[index - 1];
const showAvatar = !prevMsg ||
prevMsg.user_id !== msg.user_id ||
new Date(msg.created_at).getTime() - new Date(prevMsg.created_at).getTime() > 300000;
return (
<Message
key={msg.id}
type="user"
author={msg.display_name || msg.username}
time={new Date(msg.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
text={msg.content}
avatar={msg.avatar_url ? undefined : (msg.username?.charAt(0).toUpperCase() || 'U')}
avatarUrl={msg.avatar_url}
avatarBg="from-indigo-600 to-violet-600"
showAvatar={showAvatar}
compact={!showAvatar}
/>
);
})}
<div ref={messagesEndRef} />
</div>
)}
</div>
{/* Typing Indicator */}
{typingUsers.length > 0 && (
<div className="px-5 py-2 text-sm text-white/50 flex items-center gap-2">
<div className="flex gap-1">
<span className="w-2 h-2 rounded-full bg-white/50 animate-bounce" style={{ animationDelay: '0ms' }} />
<span className="w-2 h-2 rounded-full bg-white/50 animate-bounce" style={{ animationDelay: '150ms' }} />
<span className="w-2 h-2 rounded-full bg-white/50 animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
<span><strong className="text-white/70">{typingUsers.join(', ')}</strong> is typing...</span>
</div>
)}
{/* Message Input */}
<div className="message-input-container px-5 py-5 border-t border-[#1a1a1a]">
<div className="message-input-container px-5 pb-6 pt-2">
<MessageInput />
</div>
</div>
);
};
export default ChatArea;

View file

@ -0,0 +1,319 @@
import React, { useEffect, useRef, useState } from 'react';
export interface ContextMenuItem {
id: string;
label: string;
icon?: React.ReactNode;
shortcut?: string;
danger?: boolean;
disabled?: boolean;
divider?: boolean;
submenu?: ContextMenuItem[];
onClick?: () => void;
}
interface ContextMenuProps {
items: ContextMenuItem[];
position: { x: number; y: number } | null;
onClose: () => void;
}
const ContextMenu: React.FC<ContextMenuProps> = ({ items, position, onClose }) => {
const menuRef = useRef<HTMLDivElement>(null);
const [adjustedPosition, setAdjustedPosition] = useState(position);
const [activeSubmenu, setActiveSubmenu] = useState<string | null>(null);
useEffect(() => {
if (!position || !menuRef.current) return;
const rect = menuRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let x = position.x;
let y = position.y;
// Adjust horizontal position
if (x + rect.width > viewportWidth) {
x = viewportWidth - rect.width - 10;
}
// Adjust vertical position
if (y + rect.height > viewportHeight) {
y = viewportHeight - rect.height - 10;
}
setAdjustedPosition({ x, y });
}, [position]);
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
onClose();
}
};
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleEscape);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [onClose]);
if (!position) return null;
const handleItemClick = (item: ContextMenuItem) => {
if (item.disabled || item.submenu) return;
item.onClick?.();
onClose();
};
return (
<div
ref={menuRef}
className="fixed z-[100] min-w-[200px] bg-[#0c0c0e]/95 backdrop-blur-xl rounded-xl border border-white/10 shadow-2xl py-2 animate-fadeIn"
style={{
left: adjustedPosition?.x ?? position.x,
top: adjustedPosition?.y ?? position.y,
}}
>
{items.map((item, index) => {
if (item.divider) {
return <div key={index} className="h-px bg-white/10 my-2 mx-2" />;
}
return (
<div
key={item.id}
className="relative"
onMouseEnter={() => item.submenu && setActiveSubmenu(item.id)}
onMouseLeave={() => setActiveSubmenu(null)}
>
<button
onClick={() => handleItemClick(item)}
disabled={item.disabled}
className={`
w-full flex items-center gap-3 px-3 py-2 text-sm transition-colors
${item.disabled
? 'text-gray-600 cursor-not-allowed'
: item.danger
? 'text-red-400 hover:bg-red-500/10'
: 'text-gray-200 hover:bg-white/10'
}
`}
>
{item.icon && (
<span className={`w-5 h-5 flex items-center justify-center ${item.disabled ? 'opacity-50' : ''}`}>
{item.icon}
</span>
)}
<span className="flex-1 text-left">{item.label}</span>
{item.shortcut && (
<span className="text-gray-500 text-xs">{item.shortcut}</span>
)}
{item.submenu && (
<svg className="w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)}
</button>
{/* Submenu */}
{item.submenu && activeSubmenu === item.id && (
<div className="absolute left-full top-0 ml-1 min-w-[180px] bg-[#0c0c0e]/95 backdrop-blur-xl rounded-xl border border-white/10 shadow-2xl py-2">
{item.submenu.map((subitem) => (
<button
key={subitem.id}
onClick={() => {
subitem.onClick?.();
onClose();
}}
disabled={subitem.disabled}
className={`
w-full flex items-center gap-3 px-3 py-2 text-sm transition-colors
${subitem.disabled
? 'text-gray-600 cursor-not-allowed'
: subitem.danger
? 'text-red-400 hover:bg-red-500/10'
: 'text-gray-200 hover:bg-white/10'
}
`}
>
{subitem.icon && (
<span className="w-5 h-5 flex items-center justify-center">
{subitem.icon}
</span>
)}
<span className="flex-1 text-left">{subitem.label}</span>
</button>
))}
</div>
)}
</div>
);
})}
</div>
);
};
// Context Menu Hook
interface ContextMenuState {
position: { x: number; y: number } | null;
items: ContextMenuItem[];
}
export const useContextMenu = () => {
const [state, setState] = useState<ContextMenuState>({ position: null, items: [] });
const showContextMenu = (e: React.MouseEvent, items: ContextMenuItem[]) => {
e.preventDefault();
setState({
position: { x: e.clientX, y: e.clientY },
items,
});
};
const hideContextMenu = () => {
setState({ position: null, items: [] });
};
return {
contextMenu: state,
showContextMenu,
hideContextMenu,
};
};
// Pre-defined menu item sets
export const messageContextMenuItems = (params: {
onReply?: () => void;
onEdit?: () => void;
onDelete?: () => void;
onCopy?: () => void;
onPin?: () => void;
isOwner?: boolean;
}): ContextMenuItem[] => [
{
id: 'reply',
label: 'Reply',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" /></svg>,
onClick: params.onReply,
},
{
id: 'copy',
label: 'Copy Text',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /></svg>,
shortcut: 'Ctrl+C',
onClick: params.onCopy,
},
{
id: 'pin',
label: 'Pin Message',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" /></svg>,
onClick: params.onPin,
},
{ id: 'divider1', label: '', divider: true },
...(params.isOwner ? [
{
id: 'edit',
label: 'Edit Message',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" /></svg>,
onClick: params.onEdit,
},
{
id: 'delete',
label: 'Delete Message',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>,
danger: true,
onClick: params.onDelete,
},
] : []),
];
export const userContextMenuItems = (params: {
onProfile?: () => void;
onMessage?: () => void;
onCall?: () => void;
onMute?: () => void;
onBlock?: () => void;
}): ContextMenuItem[] => [
{
id: 'profile',
label: 'View Profile',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>,
onClick: params.onProfile,
},
{
id: 'message',
label: 'Send Message',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" /></svg>,
onClick: params.onMessage,
},
{
id: 'call',
label: 'Start Voice Call',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><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>,
onClick: params.onCall,
},
{ id: 'divider1', label: '', divider: true },
{
id: 'mute',
label: 'Mute',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2" /></svg>,
onClick: params.onMute,
},
{
id: 'block',
label: 'Block',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" /></svg>,
danger: true,
onClick: params.onBlock,
},
];
export const channelContextMenuItems = (params: {
onEdit?: () => void;
onMute?: () => void;
onInvite?: () => void;
onDelete?: () => void;
canManage?: boolean;
}): ContextMenuItem[] => [
{
id: 'invite',
label: 'Invite People',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" /></svg>,
onClick: params.onInvite,
},
{
id: 'mute',
label: 'Mute Channel',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><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>,
onClick: params.onMute,
},
...(params.canManage ? [
{ id: 'divider1', label: '', divider: true },
{
id: 'edit',
label: 'Edit Channel',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>,
onClick: params.onEdit,
},
{
id: 'delete',
label: 'Delete Channel',
icon: <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>,
danger: true,
onClick: params.onDelete,
},
] as ContextMenuItem[] : []),
];
export default ContextMenu;

View file

@ -0,0 +1,291 @@
import React, { useState } from 'react';
interface CreateServerModalProps {
isOpen: boolean;
onClose: () => void;
onCreateServer: (name: string, icon?: string) => Promise<void>;
}
const serverTemplates = [
{ id: 'gaming', name: 'Gaming', icon: '🎮', description: 'For gaming communities' },
{ id: 'friends', name: 'Friends', icon: '👥', description: 'A place to hang out' },
{ id: 'study', name: 'Study Group', icon: '📚', description: 'For learning together' },
{ id: 'creators', name: 'Creators', icon: '🎨', description: 'For content creators' },
{ id: 'local', name: 'Local Community', icon: '🏠', description: 'For local groups' },
{ id: 'custom', name: 'Custom', icon: '⚙️', description: 'Start from scratch' },
];
const CreateServerModal: React.FC<CreateServerModalProps> = ({ isOpen, onClose, onCreateServer }) => {
const [step, setStep] = useState<'select' | 'customize' | 'invite'>('select');
const [selectedTemplate, setSelectedTemplate] = useState<string>('');
const [serverName, setServerName] = useState('');
const [serverIcon, setServerIcon] = useState<string>('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
if (!isOpen) return null;
const handleTemplateSelect = (templateId: string) => {
setSelectedTemplate(templateId);
const template = serverTemplates.find(t => t.id === templateId);
if (template) {
setServerName(`${template.name} Server`);
}
setStep('customize');
};
const handleCreate = async () => {
if (!serverName.trim()) {
setError('Server name is required');
return;
}
setLoading(true);
setError('');
try {
await onCreateServer(serverName, serverIcon);
setStep('invite');
} catch (err: any) {
setError(err.message || 'Failed to create server');
} finally {
setLoading(false);
}
};
const handleClose = () => {
setStep('select');
setSelectedTemplate('');
setServerName('');
setServerIcon('');
setError('');
onClose();
};
const renderStep = () => {
switch (step) {
case 'select':
return (
<div className="p-6">
{/* Header */}
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-white mb-2">Create Your Server</h2>
<p className="text-gray-400">Your server is where you and your friends hang out. Make yours and start talking.</p>
</div>
{/* Templates Grid */}
<div className="grid grid-cols-2 gap-3 mb-6">
{serverTemplates.map((template) => (
<button
key={template.id}
onClick={() => handleTemplateSelect(template.id)}
className="p-4 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 hover:border-indigo-500/50 transition-all text-left group"
>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-600/20 to-purple-600/20 rounded-xl flex items-center justify-center text-2xl group-hover:scale-110 transition-transform">
{template.icon}
</div>
<div>
<p className="font-medium text-white">{template.name}</p>
<p className="text-xs text-gray-500">{template.description}</p>
</div>
</div>
</button>
))}
</div>
{/* Join Server Option */}
<div className="pt-6 border-t border-white/10">
<p className="text-center text-gray-400 mb-4">Have an invite already?</p>
<button className="w-full py-3 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-gray-300 font-medium transition-colors">
Join a Server
</button>
</div>
</div>
);
case 'customize':
return (
<div className="p-6">
{/* Back Button */}
<button
onClick={() => setStep('select')}
className="flex items-center gap-2 text-gray-400 hover:text-white mb-6 transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Back
</button>
{/* Header */}
<div className="text-center mb-8">
<h2 className="text-2xl font-bold text-white mb-2">Customize Your Server</h2>
<p className="text-gray-400">Give your new server a personality with a name and an icon. You can always change it later.</p>
</div>
{/* Server Icon Upload */}
<div className="flex justify-center mb-6">
<div className="relative">
<div className="w-24 h-24 rounded-full bg-gradient-to-br from-indigo-600/30 to-purple-600/30 flex items-center justify-center border-2 border-dashed border-white/20 hover:border-indigo-500/50 cursor-pointer transition-colors overflow-hidden">
{serverIcon ? (
<img src={serverIcon} alt="Server icon" className="w-full h-full object-cover" />
) : (
<div className="text-center">
<svg className="w-8 h-8 text-gray-500 mx-auto mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span className="text-xs text-gray-500">Upload</span>
</div>
)}
</div>
<div className="absolute -bottom-1 -right-1 w-8 h-8 bg-indigo-600 rounded-full flex items-center justify-center cursor-pointer hover:bg-indigo-500 transition-colors">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</div>
</div>
</div>
{/* Server Name Input */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">SERVER NAME</label>
<input
type="text"
value={serverName}
onChange={(e) => setServerName(e.target.value)}
className="w-full px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all"
placeholder="Enter server name"
/>
{error && <p className="text-red-400 text-sm mt-2">{error}</p>}
</div>
{/* Guidelines */}
<p className="text-gray-500 text-xs mb-6">
By creating a server, you agree to AeThex's <span className="text-indigo-400 hover:underline cursor-pointer">Community Guidelines</span>.
</p>
{/* Create Button */}
<button
onClick={handleCreate}
disabled={loading || !serverName.trim()}
className="w-full py-3 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white font-medium rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-indigo-500/25"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Creating...
</span>
) : (
'Create Server'
)}
</button>
</div>
);
case 'invite':
return (
<div className="p-6">
{/* Success Animation */}
<div className="text-center mb-8">
<div className="w-20 h-20 mx-auto mb-4 bg-gradient-to-br from-green-500/20 to-emerald-500/20 rounded-full flex items-center justify-center">
<svg className="w-10 h-10 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-2xl font-bold text-white mb-2">Server Created!</h2>
<p className="text-gray-400">"{serverName}" is ready to go. Invite your friends to start chatting.</p>
</div>
{/* Invite Link */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">INVITE LINK</label>
<div className="flex gap-2">
<input
type="text"
value="aethex.io/invite/AbC123"
readOnly
className="flex-1 px-4 py-3 bg-white/5 border border-white/10 rounded-xl text-gray-300"
/>
<button className="px-4 py-3 bg-indigo-600 hover:bg-indigo-500 rounded-xl text-white font-medium transition-colors">
Copy
</button>
</div>
</div>
{/* Share Options */}
<div className="grid grid-cols-4 gap-3 mb-6">
{['Twitter', 'Discord', 'Reddit', 'More'].map((platform) => (
<button
key={platform}
className="p-3 bg-white/5 hover:bg-white/10 rounded-xl text-gray-400 text-sm transition-colors"
>
{platform}
</button>
))}
</div>
{/* Done Button */}
<button
onClick={handleClose}
className="w-full py-3 bg-white/5 hover:bg-white/10 rounded-xl text-gray-300 font-medium border border-white/10 transition-colors"
>
Done
</button>
</div>
);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={handleClose}
/>
{/* Modal */}
<div className="relative w-full max-w-md bg-[#0a0a0c] rounded-2xl border border-white/10 shadow-2xl animate-fadeIn overflow-hidden">
{/* Background Glow */}
<div className="absolute -top-20 -right-20 w-40 h-40 bg-indigo-600/20 rounded-full blur-3xl" />
<div className="absolute -bottom-20 -left-20 w-40 h-40 bg-purple-600/20 rounded-full blur-3xl" />
{/* Close Button */}
<button
onClick={handleClose}
className="absolute top-4 right-4 w-8 h-8 bg-white/5 hover:bg-white/10 rounded-lg flex items-center justify-center transition-colors z-10"
>
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Content */}
<div className="relative">
{renderStep()}
</div>
{/* Progress Indicator */}
<div className="flex justify-center gap-2 pb-6">
{['select', 'customize', 'invite'].map((s, i) => (
<div
key={s}
className={`w-2 h-2 rounded-full transition-colors ${
step === s ? 'bg-indigo-500' :
['select', 'customize', 'invite'].indexOf(step) > i ? 'bg-indigo-500/50' :
'bg-white/10'
}`}
/>
))}
</div>
</div>
</div>
);
};
export default CreateServerModal;

View file

@ -0,0 +1,356 @@
import React, { useState, useEffect } from 'react';
import { useAuth } from '../context/AuthContext';
const LoginScreen: React.FC = () => {
const { login, register, demoLogin } = useAuth();
const [isRegister, setIsRegister] = useState(false);
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [displayName, setDisplayName] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [showQR, setShowQR] = useState(false);
const [passwordStrength, setPasswordStrength] = useState(0);
// Calculate password strength
useEffect(() => {
let strength = 0;
if (password.length >= 8) strength++;
if (/[A-Z]/.test(password)) strength++;
if (/[0-9]/.test(password)) strength++;
if (/[^A-Za-z0-9]/.test(password)) strength++;
setPasswordStrength(strength);
}, [password]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
if (isRegister) {
await register(username, password, displayName || undefined);
} else {
await login(username, password);
}
} catch (err: any) {
setError(err.message || 'Authentication failed');
} finally {
setLoading(false);
}
};
const handleDemoLogin = async () => {
setError('');
setLoading(true);
try {
await demoLogin();
} catch (err: any) {
setError(err.message || 'Demo login failed');
} finally {
setLoading(false);
}
};
const strengthColors = ['bg-red-500', 'bg-orange-500', 'bg-yellow-500', 'bg-green-500'];
const strengthLabels = ['Weak', 'Fair', 'Good', 'Strong'];
return (
<div className="min-h-screen bg-[#050507] flex overflow-hidden">
{/* Animated Background */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute -top-1/2 -left-1/2 w-full h-full bg-gradient-to-br from-indigo-600/20 via-transparent to-transparent rounded-full blur-3xl animate-pulse" />
<div className="absolute -bottom-1/2 -right-1/2 w-full h-full bg-gradient-to-tl from-purple-600/20 via-transparent to-transparent rounded-full blur-3xl animate-pulse" style={{ animationDelay: '1s' }} />
<div className="absolute top-1/4 right-1/4 w-64 h-64 bg-pink-500/10 rounded-full blur-3xl animate-pulse" style={{ animationDelay: '2s' }} />
{/* Grid pattern */}
<div className="absolute inset-0 opacity-5" style={{
backgroundImage: 'linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)',
backgroundSize: '50px 50px'
}} />
</div>
{/* Left Side - Branding */}
<div className="hidden lg:flex flex-1 items-center justify-center relative p-12">
<div className="max-w-lg text-center">
{/* Logo Animation */}
<div className="relative w-32 h-32 mx-auto mb-8">
<div className="absolute inset-0 bg-gradient-to-tr from-indigo-600 via-purple-600 to-pink-600 rounded-3xl rotate-6 animate-pulse" />
<div className="absolute inset-0 bg-gradient-to-tr from-indigo-600 via-purple-600 to-pink-600 rounded-3xl -rotate-6 opacity-60" />
<div className="relative w-full h-full bg-[#0a0a0c] rounded-3xl flex items-center justify-center border border-white/10">
<span className="text-5xl font-black bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">A</span>
</div>
</div>
<h1 className="text-4xl font-bold text-white mb-4">
Welcome to <span className="bg-gradient-to-r from-indigo-400 via-purple-400 to-pink-400 bg-clip-text text-transparent">AeThex</span>
</h1>
<p className="text-gray-400 text-lg mb-8">
The next generation communication platform for gamers, creators, and communities.
</p>
{/* Features */}
<div className="grid grid-cols-3 gap-4">
{[
{ icon: '🎮', label: 'GameForge' },
{ icon: '📞', label: 'HD Calls' },
{ icon: '🔒', label: 'E2E Encryption' },
].map((feature, i) => (
<div key={i} className="bg-white/5 backdrop-blur-sm rounded-xl p-4 border border-white/5">
<div className="text-2xl mb-2">{feature.icon}</div>
<div className="text-sm text-gray-300">{feature.label}</div>
</div>
))}
</div>
</div>
</div>
{/* Right Side - Form */}
<div className="flex-1 flex items-center justify-center p-6 relative z-10">
<div className="w-full max-w-md">
{/* Mobile Logo */}
<div className="lg:hidden text-center mb-8">
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-tr from-indigo-600 via-purple-600 to-pink-600 flex items-center justify-center">
<span className="text-2xl font-bold text-white">A</span>
</div>
<h1 className="text-2xl font-bold text-white">AeThex Connect</h1>
</div>
{/* Form Card */}
<div className="bg-[#0a0a0c]/80 backdrop-blur-xl rounded-2xl p-8 border border-white/10 shadow-2xl">
<div className="text-center mb-6">
<h2 className="text-2xl font-bold text-white">
{showQR ? 'Scan to Login' : isRegister ? 'Create Account' : 'Welcome Back'}
</h2>
<p className="text-gray-400 mt-1 text-sm">
{showQR ? 'Use the AeThex mobile app to scan' : isRegister ? 'Join millions of users worldwide' : 'Sign in to continue to AeThex'}
</p>
</div>
{error && (
<div className="mb-4 p-3 bg-red-500/10 border border-red-500/30 rounded-xl text-red-400 text-sm flex items-center gap-2">
<svg className="w-5 h-5 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{error}
</div>
)}
{showQR ? (
/* QR Code View */
<div className="text-center py-8">
<div className="w-48 h-48 mx-auto bg-white rounded-2xl p-4 mb-4">
{/* Fake QR pattern */}
<div className="w-full h-full bg-[url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0id2hpdGUiLz48cmVjdCB4PSIxMCIgeT0iMTAiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iYmxhY2siLz48cmVjdCB4PSI3MCIgeT0iMTAiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iYmxhY2siLz48cmVjdCB4PSIxMCIgeT0iNzAiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iYmxhY2siLz48cmVjdCB4PSI0MCIgeT0iNDAiIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCIgZmlsbD0iYmxhY2siLz48L3N2Zz4=')] bg-cover" />
</div>
<p className="text-gray-400 text-sm mb-4">
Open the AeThex app and scan this code
</p>
<button
onClick={() => setShowQR(false)}
className="text-indigo-400 hover:text-indigo-300 text-sm font-medium"
>
Back to login
</button>
</div>
) : (
<form onSubmit={handleSubmit} className="space-y-4">
{isRegister && (
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">Email</label>
<div className="relative">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full px-4 py-3 pl-11 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all"
placeholder="you@example.com"
/>
<svg className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">Username</label>
<div className="relative">
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-4 py-3 pl-11 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all"
placeholder="Enter your username"
required
/>
<svg className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</div>
{isRegister && (
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">Display Name</label>
<div className="relative">
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
className="w-full px-4 py-3 pl-11 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all"
placeholder="How should we call you?"
/>
<svg className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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 10a3 3 0 11-6 0 3 3 0 016 0zm6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-300 mb-1.5">Password</label>
<div className="relative">
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 pl-11 bg-white/5 border border-white/10 rounded-xl text-white placeholder-gray-500 focus:outline-none focus:border-indigo-500/50 focus:ring-2 focus:ring-indigo-500/20 transition-all"
placeholder="Enter your password"
required
/>
<svg className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
</div>
{/* Password Strength Indicator */}
{isRegister && password && (
<div className="mt-2">
<div className="flex gap-1 mb-1">
{[0, 1, 2, 3].map((i) => (
<div
key={i}
className={`h-1 flex-1 rounded-full transition-colors ${
i < passwordStrength ? strengthColors[passwordStrength - 1] : 'bg-white/10'
}`}
/>
))}
</div>
<p className={`text-xs ${strengthColors[passwordStrength - 1]?.replace('bg-', 'text-') || 'text-gray-500'}`}>
{password.length < 8 ? 'Min 8 characters' : strengthLabels[passwordStrength - 1] || 'Weak'}
</p>
</div>
)}
</div>
{!isRegister && (
<div className="flex items-center justify-between text-sm">
<label className="flex items-center gap-2 text-gray-400 cursor-pointer">
<input type="checkbox" className="w-4 h-4 rounded border-white/20 bg-white/5 text-indigo-500 focus:ring-indigo-500/20" />
Remember me
</label>
<button type="button" className="text-indigo-400 hover:text-indigo-300">
Forgot password?
</button>
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full py-3 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-500 hover:to-purple-500 text-white font-medium rounded-xl transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-indigo-500/25 hover:shadow-indigo-500/40 active:scale-[0.98]"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<svg className="w-5 h-5 animate-spin" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
{isRegister ? 'Creating Account...' : 'Signing In...'}
</span>
) : (
isRegister ? 'Create Account' : 'Sign In'
)}
</button>
{/* Social Login */}
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-white/10" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-[#0a0a0c] text-gray-500">or continue with</span>
</div>
</div>
<div className="grid grid-cols-3 gap-3">
{[
{ icon: '🔵', name: 'Discord' },
{ icon: '⚫', name: 'GitHub' },
{ icon: '🔴', name: 'Google' },
].map((provider) => (
<button
key={provider.name}
type="button"
className="flex items-center justify-center gap-2 py-2.5 bg-white/5 hover:bg-white/10 border border-white/10 rounded-xl transition-all text-sm text-gray-300"
>
<span>{provider.icon}</span>
</button>
))}
</div>
{/* QR Login Option */}
{!isRegister && (
<button
type="button"
onClick={() => setShowQR(true)}
className="w-full py-2.5 text-gray-400 hover:text-white text-sm flex items-center justify-center gap-2 transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v1m6 11h2m-6 0h-2v4m0-11v3m0 0h.01M12 12h4.01M16 20h4M4 12h4m12 0h.01M5 8h2a1 1 0 001-1V5a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1zm12 0h2a1 1 0 001-1V5a1 1 0 00-1-1h-2a1 1 0 00-1 1v2a1 1 0 001 1zM5 20h2a1 1 0 001-1v-2a1 1 0 00-1-1H5a1 1 0 00-1 1v2a1 1 0 001 1z" />
</svg>
Log in with QR Code
</button>
)}
<div className="pt-4 border-t border-white/10">
<button
type="button"
onClick={handleDemoLogin}
disabled={loading}
className="w-full py-3 bg-white/5 hover:bg-white/10 text-gray-300 font-medium rounded-xl transition-all border border-white/10 disabled:opacity-50 flex items-center justify-center gap-2"
>
<span className="text-lg">🚀</span>
Try Demo Account
</button>
</div>
<div className="text-center pt-4">
<button
type="button"
onClick={() => setIsRegister(!isRegister)}
className="text-gray-400 hover:text-white text-sm transition-colors"
>
{isRegister ? (
<>Already have an account? <span className="text-indigo-400 font-medium">Sign in</span></>
) : (
<>Don't have an account? <span className="text-indigo-400 font-medium">Register</span></>
)}
</button>
</div>
</form>
)}
</div>
<p className="text-center text-gray-600 text-xs mt-6">
By signing in, you agree to AeThex's <span className="text-gray-500 hover:text-gray-400 cursor-pointer">Terms of Service</span> and <span className="text-gray-500 hover:text-gray-400 cursor-pointer">Privacy Policy</span>
</p>
</div>
</div>
</div>
);
};
export default LoginScreen;

View file

@ -1,16 +1,156 @@
import React from "react";
import React, { useState, useEffect, useCallback } from "react";
import ServerList from "./ServerList";
import ChannelSidebar from "./ChannelSidebar";
import ChatArea from "./ChatArea";
import MemberSidebar from "./MemberSidebar";
import ActivityPanel from "./ActivityPanel";
import SettingsPanel from "./SettingsPanel";
import QuickSwitcher from "./QuickSwitcher";
import CreateServerModal from "./CreateServerModal";
import { useApp } from "../context/AppContext";
import { useToast } from "./ToastProvider";
const MainLayout: React.FC = () => {
const [showActivity, setShowActivity] = useState(true);
const [showMembers, setShowMembers] = useState(true);
const [showSettings, setShowSettings] = useState(false);
const [showQuickSwitcher, setShowQuickSwitcher] = useState(false);
const [showCreateServer, setShowCreateServer] = useState(false);
const { servers, channels, createServer, selectServer, selectChannel } = useApp();
const { addToast } = useToast();
// Global keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ctrl+K or Cmd+K - Quick Switcher
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
setShowQuickSwitcher(true);
}
// Ctrl+, or Cmd+, - Settings
if ((e.ctrlKey || e.metaKey) && e.key === ',') {
e.preventDefault();
setShowSettings(true);
}
// Escape - Close any modal
if (e.key === 'Escape') {
if (showSettings) setShowSettings(false);
if (showQuickSwitcher) setShowQuickSwitcher(false);
if (showCreateServer) setShowCreateServer(false);
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [showSettings, showQuickSwitcher, showCreateServer]);
const handleCreateServer = useCallback(async (name: string, icon?: string) => {
try {
await createServer(name, icon);
addToast({
type: 'success',
title: 'Server Created',
message: `"${name}" is ready to go!`,
});
} catch (error: any) {
addToast({
type: 'error',
title: 'Failed to Create Server',
message: error.message || 'Something went wrong',
});
throw error;
}
}, [createServer, addToast]);
return (
<div className="connect-container flex h-screen">
<ServerList />
<ChannelSidebar />
<ChatArea />
<MemberSidebar />
<div className="connect-container flex h-screen bg-[#050507] overflow-hidden">
{/* Ambient glow effects */}
<div className="fixed top-0 left-1/4 w-96 h-96 bg-indigo-600/10 rounded-full blur-[120px] pointer-events-none" />
<div className="fixed bottom-0 right-1/4 w-96 h-96 bg-purple-600/10 rounded-full blur-[120px] pointer-events-none" />
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-64 h-64 bg-pink-600/5 rounded-full blur-[100px] pointer-events-none" />
<ServerList
onOpenCreateServer={() => setShowCreateServer(true)}
/>
<ChannelSidebar
onOpenSettings={() => setShowSettings(true)}
/>
<ChatArea
onToggleMembers={() => setShowMembers(!showMembers)}
onToggleActivity={() => setShowActivity(!showActivity)}
showMembers={showMembers}
showActivity={showActivity}
onOpenQuickSwitcher={() => setShowQuickSwitcher(true)}
/>
{showMembers && <MemberSidebar />}
{showActivity && <ActivityPanel />}
{/* Modals */}
<SettingsPanel
isOpen={showSettings}
onClose={() => setShowSettings(false)}
/>
<QuickSwitcher
isOpen={showQuickSwitcher}
onClose={() => setShowQuickSwitcher(false)}
servers={servers}
channels={channels}
onSelectServer={selectServer}
onSelectChannel={selectChannel}
onOpenSettings={() => {
setShowQuickSwitcher(false);
setShowSettings(true);
}}
/>
<CreateServerModal
isOpen={showCreateServer}
onClose={() => setShowCreateServer(false)}
onCreateServer={handleCreateServer}
/>
{/* Keyboard shortcut hints - show briefly on load */}
<KeyboardHints />
</div>
);
};
// Component to show keyboard hints briefly
const KeyboardHints: React.FC = () => {
const [show, setShow] = useState(true);
useEffect(() => {
const timer = setTimeout(() => setShow(false), 5000);
return () => clearTimeout(timer);
}, []);
if (!show) return null;
return (
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-4 px-4 py-2 bg-black/80 backdrop-blur-xl rounded-full border border-white/10 text-xs text-gray-400 animate-fadeIn">
<div className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-white/10 rounded text-gray-300">Ctrl</kbd>
<span>+</span>
<kbd className="px-1.5 py-0.5 bg-white/10 rounded text-gray-300">K</kbd>
<span className="ml-1">Quick Switcher</span>
</div>
<div className="w-px h-4 bg-white/10" />
<div className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-white/10 rounded text-gray-300">Ctrl</kbd>
<span>+</span>
<kbd className="px-1.5 py-0.5 bg-white/10 rounded text-gray-300">,</kbd>
<span className="ml-1">Settings</span>
</div>
<button
onClick={() => setShow(false)}
className="ml-2 text-gray-500 hover:text-white"
>
</button>
</div>
);
};

View file

@ -1,43 +1,114 @@
import React from "react";
import { useApp } from "../context/AppContext";
const members = [
{ section: "Foundation Team — 8", users: [
{ name: "Anderson", avatar: "A", status: "online", avatarBg: "from-red-600 to-red-800" },
{ name: "Trevor", avatar: "T", status: "online", avatarBg: "from-red-600 to-red-800" },
]},
{ section: "Labs Team — 12", users: [
{ name: "Sarah", avatar: "S", status: "labs", avatarBg: "from-orange-400 to-orange-700", activity: "Testing v2.0" },
]},
{ section: "Developers — 47", users: [
{ name: "Marcus", avatar: "M", status: "in-game", avatarBg: "bg-[#1a1a1a]", activity: "Building" },
{ name: "DevUser_2847", avatar: "D", status: "online", avatarBg: "bg-[#1a1a1a]" },
]},
{ section: "Community — 61", users: [
{ name: "JohnDev", avatar: "J", status: "offline", avatarBg: "bg-[#1a1a1a]" },
]},
];
const MemberSidebar: React.FC = () => {
const { members, currentServer } = useApp();
const MemberSidebar: React.FC = () => (
<div className="member-sidebar w-72 bg-[#0f0f0f] border-l border-[#1a1a1a] flex flex-col">
<div className="member-header p-4 border-b border-[#1a1a1a] text-xs uppercase tracking-widest text-gray-500">Members 128</div>
<div className="member-list flex-1 overflow-y-auto py-3">
{members.map((section, i) => (
<div key={i} className="member-section mb-4">
<div className="member-section-title px-4 py-2 text-xs uppercase tracking-wider text-gray-500 font-bold">{section.section}</div>
{section.users.map((user, j) => (
<div key={j} className="member-item flex items-center gap-3 px-4 py-1.5 cursor-pointer hover:bg-[#1a1a1a]">
<div className={`member-avatar-small w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm relative bg-gradient-to-tr ${user.avatarBg}`}>
{user.avatar}
<div className={`online-indicator absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-[#0f0f0f] ${user.status === "online" ? "bg-green-400" : user.status === "in-game" ? "bg-blue-500" : user.status === "labs" ? "bg-orange-400" : "bg-gray-700"}`}></div>
if (!currentServer) {
return null;
}
// Group members by role
const grouped = members.reduce((acc, member) => {
const role = member.role || 'member';
if (!acc[role]) acc[role] = [];
acc[role].push(member);
return acc;
}, {} as Record<string, typeof members>);
const roleOrder = ['owner', 'admin', 'moderator', 'member'];
const roleLabels: Record<string, string> = {
owner: '👑 Owner',
admin: '⚡ Admins',
moderator: '🛡️ Moderators',
member: 'Members'
};
const roleColors: Record<string, string> = {
owner: 'text-amber-400',
admin: 'text-rose-400',
moderator: 'text-emerald-400',
member: 'text-white/70'
};
const getStatusColor = (status: string) => {
switch (status) {
case 'online': return 'bg-emerald-500';
case 'away': return 'bg-amber-500';
case 'dnd': return 'bg-rose-500';
default: return 'bg-gray-600';
}
};
const getGradient = (role: string) => {
switch (role) {
case 'owner': return 'from-amber-600 to-orange-600';
case 'admin': return 'from-rose-600 to-pink-600';
case 'moderator': return 'from-emerald-600 to-cyan-600';
default: return 'from-indigo-600 to-violet-600';
}
};
return (
<div className="member-sidebar w-60 bg-[#08080a]/80 backdrop-blur-xl border-l border-white/5 flex flex-col">
<div className="member-header px-4 py-3 border-b border-white/5">
<div className="text-[11px] uppercase tracking-wider text-white/40 font-semibold">
Members {members.length}
</div>
</div>
<div className="member-list flex-1 overflow-y-auto py-2 px-2">
{roleOrder.map(role => {
const roleMembers = grouped[role];
if (!roleMembers || roleMembers.length === 0) return null;
return (
<div key={role} className="member-section mb-3">
<div className={`member-section-title px-2 py-1.5 text-[11px] uppercase tracking-wider font-semibold ${roleColors[role]}`}>
{roleLabels[role]} {roleMembers.length}
</div>
{roleMembers.map((member) => (
<div
key={member.id}
className="member-item group flex items-center gap-2.5 px-2 py-1.5 rounded-lg cursor-pointer hover:bg-white/5 transition-all"
>
<div className={`member-avatar w-8 h-8 rounded-full flex items-center justify-center font-semibold text-xs relative bg-gradient-to-br ${getGradient(member.role)}`}>
{member.avatar_url ? (
<img src={member.avatar_url} alt={member.username} className="w-full h-full rounded-full object-cover" />
) : (
<span className="text-white">{member.username?.charAt(0).toUpperCase() || 'U'}</span>
)}
<div className={`online-indicator absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-[#08080a] ${getStatusColor(member.status)}`} />
</div>
<div className="member-info flex-1 min-w-0">
<div className={`member-name text-sm truncate ${roleColors[member.role] || 'text-white/80'}`}>
{member.nickname || member.display_name || member.username}
</div>
{member.custom_status && (
<div className="text-[11px] text-white/40 truncate">{member.custom_status}</div>
)}
</div>
{/* Hover Actions */}
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<button className="w-6 h-6 rounded flex items-center justify-center text-white/40 hover:text-white/70 hover:bg-white/10">
💬
</button>
</div>
<div className="member-name flex-1 text-sm">{user.name}</div>
{user.activity && <div className="member-activity text-xs text-gray-500">{user.activity}</div>}
</div>
))}
</div>
))}
);
})}
{members.length === 0 && (
<div className="text-center py-8">
<div className="text-3xl mb-2">👥</div>
<p className="text-sm text-white/40">No members yet</p>
</div>
)}
</div>
</div>
);
};
export default MemberSidebar;

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
interface MessageProps {
type: "system" | "user";
@ -9,33 +9,94 @@ interface MessageProps {
time?: string;
text: string;
avatar?: string;
avatarUrl?: string;
avatarBg?: string;
showAvatar?: boolean;
compact?: boolean;
}
const Message: React.FC<MessageProps> = (props) => {
const [showActions, setShowActions] = useState(false);
const { showAvatar = true, compact = false } = props;
if (props.type === "system") {
return (
<div className={`message-system ${props.className} bg-[#0f0f0f] border-l-4 pl-4 pr-4 py-3 mb-4 text-sm`}>
<div className={`system-label ${props.className} text-xs uppercase tracking-wider font-bold mb-1`}>[{props.label}] System Announcement</div>
<div>{props.text}</div>
<div className="flex items-center gap-3 py-2 px-4 my-2">
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
<span className="text-xs text-white/40 uppercase tracking-wider font-medium">
{props.label || 'System'}: {props.text}
</span>
<div className="flex-1 h-px bg-gradient-to-r from-transparent via-white/10 to-transparent" />
</div>
);
}
if (compact) {
return (
<div className="message flex gap-4 mb-5 p-3 rounded transition hover:bg-[#0f0f0f]">
<div className={`message-avatar w-10 h-10 rounded-full flex items-center justify-center font-bold text-base flex-shrink-0 bg-gradient-to-tr ${props.avatarBg}`}>{props.avatar}</div>
<div className="message-content flex-1">
<div className="message-header flex items-baseline gap-3 mb-1">
<span className="message-author font-bold">{props.author}</span>
{props.badge && (
<span className={`message-badge ${props.className} text-xs px-2 py-1 rounded uppercase tracking-wider font-bold`}>{props.badge}</span>
<div
className="message group flex items-start gap-4 py-0.5 px-4 hover:bg-white/[0.02] transition-colors relative"
onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
>
<div className="w-10 flex-shrink-0 text-right">
<span className="text-[10px] text-white/30 opacity-0 group-hover:opacity-100 transition-opacity">
{props.time}
</span>
</div>
<div className="flex-1 text-[15px] text-white/80 leading-relaxed">{props.text}</div>
{showActions && <MessageActions />}
</div>
);
}
return (
<div
className="message group flex gap-4 py-2 px-4 mt-4 hover:bg-white/[0.02] transition-colors rounded-lg relative"
onMouseEnter={() => setShowActions(true)}
onMouseLeave={() => setShowActions(false)}
>
{showAvatar && (
<div className={`message-avatar w-10 h-10 rounded-full flex items-center justify-center font-bold text-sm flex-shrink-0 bg-gradient-to-br ${props.avatarBg || 'from-indigo-600 to-violet-600'}`}>
{props.avatarUrl ? (
<img src={props.avatarUrl} alt={props.author} className="w-full h-full rounded-full object-cover" />
) : (
<span className="text-white">{props.avatar}</span>
)}
<span className="message-time text-xs text-gray-500">{props.time}</span>
</div>
<div className="message-text leading-relaxed text-gray-300">{props.text}</div>
)}
<div className="message-content flex-1 min-w-0">
<div className="message-header flex items-baseline gap-2 mb-0.5">
<span className="message-author font-semibold text-white hover:underline cursor-pointer">
{props.author}
</span>
{props.badge && (
<span className="message-badge text-[10px] px-1.5 py-0.5 rounded bg-indigo-500/20 text-indigo-400 border border-indigo-500/30 uppercase tracking-wider font-bold">
{props.badge}
</span>
)}
<span className="message-time text-xs text-white/30">{props.time}</span>
</div>
<div className="message-text text-[15px] leading-relaxed text-white/80 break-words">
{props.text}
</div>
</div>
{showActions && <MessageActions />}
</div>
);
};
const MessageActions: React.FC = () => (
<div className="absolute -top-4 right-4 flex items-center gap-0.5 p-1 rounded-lg bg-[#111113] border border-white/10 shadow-xl opacity-0 group-hover:opacity-100 transition-all">
<button className="w-8 h-8 rounded flex items-center justify-center text-white/50 hover:bg-white/10 hover:text-white/80 transition-all" title="Add Reaction">
😀
</button>
<button className="w-8 h-8 rounded flex items-center justify-center text-white/50 hover:bg-white/10 hover:text-white/80 transition-all" title="Reply">
</button>
<button className="w-8 h-8 rounded flex items-center justify-center text-white/50 hover:bg-white/10 hover:text-white/80 transition-all" title="More">
</button>
</div>
);
export default Message;

View file

@ -1,16 +1,136 @@
import React from "react";
import React, { useState, useRef } from "react";
import { useApp } from "../context/AppContext";
const MessageInput: React.FC = () => (
<div className="flex items-center gap-2">
<button className="attachButton w-10 h-10 flex items-center justify-center rounded bg-[#1a1a1a] text-xl text-gray-400 mr-2">+</button>
<input
type="text"
className="message-input flex-1 bg-[#0f0f0f] border border-[#1a1a1a] rounded-lg px-4 py-3 text-gray-200 text-sm focus:outline-none focus:border-blue-500 placeholder:text-gray-500"
placeholder="Message #general (Foundation infrastructure channel)"
const MessageInput: React.FC = () => {
const { currentChannel, sendMessage } = useApp();
const [message, setMessage] = useState('');
const [sending, setSending] = useState(false);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const inputRef = useRef<HTMLTextAreaElement>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!message.trim() || sending) return;
setSending(true);
try {
await sendMessage(message);
setMessage('');
} catch (error) {
console.error('Failed to send message:', error);
} finally {
setSending(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSubmit(e);
}
};
const addEmoji = (emoji: string) => {
setMessage(prev => prev + emoji);
setShowEmojiPicker(false);
inputRef.current?.focus();
};
const quickEmojis = ['😀', '😂', '❤️', '👍', '🔥', '✨', '🎉', '💯'];
return (
<div className="relative">
<form onSubmit={handleSubmit} className="flex items-end gap-3 bg-[#111113] rounded-xl p-2 border border-white/5">
{/* Left Actions */}
<div className="flex items-center gap-1">
<button
type="button"
className="w-10 h-10 flex items-center justify-center rounded-lg text-white/40 hover:text-white/70 hover:bg-white/5 transition-all text-lg"
title="Attach File"
>
📎
</button>
<button
type="button"
className="w-10 h-10 flex items-center justify-center rounded-lg text-white/40 hover:text-white/70 hover:bg-white/5 transition-all text-lg"
title="GIF"
>
🖼
</button>
</div>
{/* Input */}
<div className="flex-1 relative">
<textarea
ref={inputRef}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={handleKeyDown}
className="w-full bg-transparent text-white text-[15px] leading-relaxed focus:outline-none placeholder:text-white/30 resize-none min-h-[24px] max-h-[200px] py-2"
placeholder={currentChannel ? `Message #${currentChannel.name}` : 'Select a channel to chat'}
maxLength={2000}
disabled={!currentChannel || sending}
rows={1}
style={{ height: 'auto' }}
onInput={(e) => {
const target = e.target as HTMLTextAreaElement;
target.style.height = 'auto';
target.style.height = Math.min(target.scrollHeight, 200) + 'px';
}}
/>
<button className="sendButton w-10 h-10 flex items-center justify-center rounded bg-blue-600 text-xl text-white ml-2">🎤</button>
</div>
{/* Right Actions */}
<div className="flex items-center gap-1">
<div className="relative">
<button
type="button"
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
className="w-10 h-10 flex items-center justify-center rounded-lg text-white/40 hover:text-white/70 hover:bg-white/5 transition-all text-lg"
title="Emoji"
>
😊
</button>
{/* Quick Emoji Picker */}
{showEmojiPicker && (
<div className="absolute bottom-full right-0 mb-2 p-2 bg-[#111113] rounded-xl border border-white/10 shadow-xl flex gap-1">
{quickEmojis.map(emoji => (
<button
key={emoji}
type="button"
onClick={() => addEmoji(emoji)}
className="w-9 h-9 flex items-center justify-center rounded-lg hover:bg-white/10 transition-all text-xl"
>
{emoji}
</button>
))}
</div>
)}
</div>
<button
type="submit"
disabled={!message.trim() || sending}
className="w-10 h-10 flex items-center justify-center rounded-lg bg-gradient-to-r from-indigo-600 to-violet-600 text-white hover:from-indigo-500 hover:to-violet-500 disabled:opacity-30 disabled:cursor-not-allowed transition-all shadow-lg shadow-indigo-500/20"
>
{sending ? (
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<span className="text-sm"></span>
)}
</button>
</div>
</form>
{/* Character Count */}
{message.length > 1800 && (
<div className={`absolute -top-6 right-2 text-xs ${message.length > 1950 ? 'text-rose-400' : 'text-white/40'}`}>
{message.length}/2000
</div>
)}
</div>
);
};
export default MessageInput;

View file

@ -0,0 +1,289 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
interface QuickSwitcherItem {
id: string;
type: 'server' | 'channel' | 'dm' | 'action';
name: string;
icon?: string;
subtitle?: string;
action?: () => void;
}
interface QuickSwitcherProps {
isOpen: boolean;
onClose: () => void;
servers?: any[];
channels?: any[];
dms?: any[];
onSelectServer?: (serverId: string) => void;
onSelectChannel?: (channelId: string) => void;
onOpenSettings?: () => void;
}
const QuickSwitcher: React.FC<QuickSwitcherProps> = ({
isOpen,
onClose,
servers = [],
channels = [],
dms = [],
onSelectServer,
onSelectChannel,
onOpenSettings,
}) => {
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
const actionItems: QuickSwitcherItem[] = [
{ id: 'settings', type: 'action', name: 'Open Settings', icon: '⚙️', action: onOpenSettings },
{ id: 'create-server', type: 'action', name: 'Create Server', icon: '' },
{ id: 'join-server', type: 'action', name: 'Join Server', icon: '🔗' },
{ id: 'add-friend', type: 'action', name: 'Add Friend', icon: '👤' },
];
const allItems = useMemo(() => {
const items: QuickSwitcherItem[] = [];
// Add servers
servers.forEach(server => {
items.push({
id: `server-${server.id}`,
type: 'server',
name: server.name,
icon: server.icon || '🏠',
subtitle: 'Server',
action: () => onSelectServer?.(server.id),
});
});
// Add channels
channels.forEach(channel => {
items.push({
id: `channel-${channel.id}`,
type: 'channel',
name: channel.name,
icon: channel.channel_type === 'voice' ? '🔊' : '#',
subtitle: 'Channel',
action: () => onSelectChannel?.(channel.id),
});
});
// Add DMs
dms.forEach(dm => {
items.push({
id: `dm-${dm.id}`,
type: 'dm',
name: dm.name,
icon: '💬',
subtitle: 'Direct Message',
});
});
// Add actions
items.push(...actionItems);
return items;
}, [servers, channels, dms, onSelectServer, onSelectChannel, onOpenSettings]);
const filteredItems = useMemo(() => {
if (!query.trim()) {
// Show recent items and actions when no query
return allItems.slice(0, 10);
}
const lowerQuery = query.toLowerCase();
return allItems.filter(item =>
item.name.toLowerCase().includes(lowerQuery) ||
item.subtitle?.toLowerCase().includes(lowerQuery)
);
}, [query, allItems]);
useEffect(() => {
if (isOpen) {
setQuery('');
setSelectedIndex(0);
setTimeout(() => inputRef.current?.focus(), 50);
}
}, [isOpen]);
useEffect(() => {
setSelectedIndex(0);
}, [query]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (!isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(prev => Math.min(prev + 1, filteredItems.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(prev => Math.max(prev - 1, 0));
break;
case 'Enter':
e.preventDefault();
if (filteredItems[selectedIndex]?.action) {
filteredItems[selectedIndex].action();
onClose();
}
break;
case 'Escape':
e.preventDefault();
onClose();
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, filteredItems, selectedIndex, onClose]);
// Scroll selected item into view
useEffect(() => {
if (listRef.current) {
const selectedElement = listRef.current.children[selectedIndex] as HTMLElement;
if (selectedElement) {
selectedElement.scrollIntoView({ block: 'nearest' });
}
}
}, [selectedIndex]);
if (!isOpen) return null;
const getTypeIcon = (type: string, icon?: string) => {
if (icon && icon.length <= 2) return icon;
switch (type) {
case 'server':
return '🏠';
case 'channel':
return '#';
case 'dm':
return '💬';
case 'action':
return icon || '⚡';
default:
return '📄';
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'server':
return 'text-indigo-400';
case 'channel':
return 'text-gray-400';
case 'dm':
return 'text-green-400';
case 'action':
return 'text-purple-400';
default:
return 'text-gray-400';
}
};
return (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-24">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative w-full max-w-xl bg-[#0a0a0c] rounded-2xl border border-white/10 shadow-2xl overflow-hidden animate-fadeIn">
{/* Search Input */}
<div className="relative">
<svg
className="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-500"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Where would you like to go?"
className="w-full pl-12 pr-4 py-4 bg-transparent text-white text-lg placeholder-gray-500 focus:outline-none border-b border-white/10"
/>
<div className="absolute right-4 top-1/2 -translate-y-1/2 flex items-center gap-1 text-gray-500 text-xs">
<kbd className="px-1.5 py-0.5 bg-white/10 rounded border border-white/10">ESC</kbd>
to close
</div>
</div>
{/* Results */}
<div ref={listRef} className="max-h-80 overflow-y-auto py-2">
{filteredItems.length === 0 ? (
<div className="py-8 text-center text-gray-500">
<p className="text-lg mb-1">No results found</p>
<p className="text-sm">Try searching for servers, channels, or friends</p>
</div>
) : (
filteredItems.map((item, index) => (
<button
key={item.id}
onClick={() => {
item.action?.();
onClose();
}}
className={`
w-full flex items-center gap-3 px-4 py-2.5 transition-colors
${index === selectedIndex ? 'bg-white/10' : 'hover:bg-white/5'}
`}
>
<div className={`w-8 h-8 rounded-lg bg-white/5 flex items-center justify-center ${getTypeColor(item.type)}`}>
{getTypeIcon(item.type, item.icon)}
</div>
<div className="flex-1 text-left">
<p className="text-white font-medium">{item.name}</p>
{item.subtitle && (
<p className="text-gray-500 text-xs">{item.subtitle}</p>
)}
</div>
{index === selectedIndex && (
<div className="flex items-center gap-1 text-gray-500 text-xs">
<kbd className="px-1.5 py-0.5 bg-white/10 rounded border border-white/10"></kbd>
to select
</div>
)}
</button>
))
)}
</div>
{/* Footer */}
<div className="px-4 py-3 border-t border-white/10 flex items-center gap-4 text-xs text-gray-500">
<div className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-white/5 rounded border border-white/10"></kbd>
<kbd className="px-1.5 py-0.5 bg-white/5 rounded border border-white/10"></kbd>
<span className="ml-1">Navigate</span>
</div>
<div className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-white/5 rounded border border-white/10"></kbd>
<span className="ml-1">Select</span>
</div>
<div className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-white/5 rounded border border-white/10">ESC</kbd>
<span className="ml-1">Close</span>
</div>
</div>
</div>
</div>
);
};
export default QuickSwitcher;

View file

@ -1,30 +1,234 @@
import React from "react";
import React, { useState } from "react";
import { useApp } from "../context/AppContext";
const servers = [
{ id: "foundation", label: "F", active: true, className: "foundation" },
{ id: "corporation", label: "C", active: false, className: "corporation" },
{ id: "labs", label: "L", active: false, className: "labs" },
{ id: "divider" },
{ id: "community1", label: "AG", active: false, className: "community" },
{ id: "community2", label: "RD", active: false, className: "community" },
{ id: "add", label: "+", active: false, className: "community" },
];
interface ServerListProps {
onOpenCreateServer?: () => void;
}
const ServerList: React.FC = () => (
<div className="server-list flex flex-col items-center py-3 gap-3 w-20 bg-[#0d0d0d] border-r border-[#1a1a1a]">
{servers.map((srv, i) =>
srv.id === "divider" ? (
<div key={i} className="server-divider w-10 h-0.5 bg-[#1a1a1a] my-1" />
) : (
const ServerList: React.FC<ServerListProps> = ({ onOpenCreateServer }) => {
const { servers, currentServer, selectServer, createServer, joinServer } = useApp();
const [showModal, setShowModal] = useState(false);
const [modalMode, setModalMode] = useState<'create' | 'join'>('create');
const [serverName, setServerName] = useState('');
const [inviteCode, setInviteCode] = useState('');
const [error, setError] = useState('');
const [hoveredServer, setHoveredServer] = useState<string | null>(null);
const handleCreate = async () => {
if (!serverName.trim()) return;
try {
const server = await createServer(serverName);
selectServer(server);
setShowModal(false);
setServerName('');
} catch (err: any) {
setError(err.message);
}
};
const handleJoin = async () => {
if (!inviteCode.trim()) return;
try {
const server = await joinServer(inviteCode);
selectServer(server);
setShowModal(false);
setInviteCode('');
} catch (err: any) {
setError(err.message);
}
};
const handleAddServer = () => {
if (onOpenCreateServer) {
onOpenCreateServer();
} else {
setShowModal(true);
setModalMode('create');
setError('');
}
};
return (
<>
<div className="server-list flex flex-col items-center py-3 gap-2 w-[76px] bg-[#08080a]/80 backdrop-blur-xl border-r border-white/5">
{/* AeThex Home Button */}
<div className="relative group mb-1">
<div
className="server-icon w-12 h-12 rounded-2xl flex items-center justify-center font-bold text-lg cursor-pointer transition-all duration-300 bg-gradient-to-br from-indigo-600 via-purple-600 to-pink-600 hover:rounded-xl shadow-lg shadow-indigo-500/20 hover:shadow-indigo-500/40"
title="Home"
>
<span className="text-white font-black text-xl">A</span>
</div>
<div className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-[10px] w-1 h-5 rounded-r-full bg-white opacity-0 group-hover:opacity-100 transition-all" />
</div>
<div className="w-8 h-0.5 bg-white/10 rounded-full my-1" />
{/* Server Icons */}
<div className="flex flex-col gap-2 flex-1 overflow-y-auto scrollbar-hide">
{servers.map((srv) => (
<div
key={srv.id}
className={`server-icon ${srv.className} ${srv.active ? "active" : ""} w-14 h-14 rounded-xl flex items-center justify-center font-bold text-lg cursor-pointer transition-all relative`}
className="relative group"
onMouseEnter={() => setHoveredServer(srv.id)}
onMouseLeave={() => setHoveredServer(null)}
>
{srv.label}
</div>
)
<div className={`absolute left-0 top-1/2 -translate-y-1/2 -translate-x-[10px] w-1 rounded-r-full bg-white transition-all duration-200 ${
currentServer?.id === srv.id ? 'h-10 opacity-100' : hoveredServer === srv.id ? 'h-5 opacity-100' : 'h-0 opacity-0'
}`} />
<div
onClick={() => selectServer(srv)}
className={`server-icon w-12 h-12 flex items-center justify-center font-bold text-base cursor-pointer transition-all duration-200 ${
currentServer?.id === srv.id
? 'rounded-xl bg-gradient-to-br from-indigo-600 to-violet-600 shadow-lg shadow-indigo-500/30'
: 'rounded-3xl bg-[#1a1a1e] hover:rounded-xl hover:bg-indigo-600'
}`}
title={srv.name}
>
{srv.icon_url ? (
<img src={srv.icon_url} alt={srv.name} className={`w-full h-full object-cover transition-all duration-200 ${
currentServer?.id === srv.id ? 'rounded-xl' : 'rounded-3xl group-hover:rounded-xl'
}`} />
) : (
<span className="text-white/90">{srv.name.charAt(0).toUpperCase()}</span>
)}
</div>
{/* Tooltip */}
<div className={`absolute left-full ml-4 top-1/2 -translate-y-1/2 px-3 py-2 bg-[#111113] rounded-lg shadow-xl border border-white/10 whitespace-nowrap z-50 transition-all duration-200 ${
hoveredServer === srv.id ? 'opacity-100 translate-x-0' : 'opacity-0 -translate-x-2 pointer-events-none'
}`}>
<div className="text-sm font-semibold text-white">{srv.name}</div>
{srv.member_count && <div className="text-xs text-white/50">{srv.member_count} members</div>}
<div className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-1 w-2 h-2 bg-[#111113] rotate-45 border-l border-b border-white/10" />
</div>
</div>
))}
</div>
<div className="w-8 h-0.5 bg-white/10 rounded-full my-1" />
{/* Add Server Button */}
<div className="relative group">
<div
onClick={handleAddServer}
className="server-icon w-12 h-12 rounded-3xl flex items-center justify-center text-2xl cursor-pointer bg-[#1a1a1e] text-emerald-500 hover:bg-emerald-600 hover:text-white hover:rounded-xl transition-all duration-200"
title="Add a Server"
>
+
</div>
<div className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-[10px] w-1 h-0 rounded-r-full bg-white group-hover:h-5 transition-all" />
</div>
{/* Discovery Button */}
<div className="relative group">
<div
className="server-icon w-12 h-12 rounded-3xl flex items-center justify-center text-xl cursor-pointer bg-[#1a1a1e] text-white/60 hover:bg-emerald-600 hover:text-white hover:rounded-xl transition-all duration-200"
title="Explore Public Servers"
>
🧭
</div>
</div>
</div>
{/* Enhanced Modal */}
{showModal && (
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center z-50" onClick={() => setShowModal(false)}>
<div className="bg-[#111113] rounded-2xl w-[440px] overflow-hidden shadow-2xl border border-white/10" onClick={e => e.stopPropagation()}>
<div className="relative p-8 pb-4 text-center bg-gradient-to-b from-indigo-600/20 to-transparent">
<div className="absolute top-4 right-4">
<button onClick={() => setShowModal(false)} className="w-8 h-8 rounded-full bg-white/10 flex items-center justify-center text-white/60 hover:text-white hover:bg-white/20 transition-all">
</button>
</div>
<h2 className="text-2xl font-bold text-white mb-2">
{modalMode === 'create' ? 'Create Your Server' : 'Join a Server'}
</h2>
<p className="text-white/50 text-sm">
{modalMode === 'create'
? 'Your server is where you and your friends hang out.'
: 'Enter an invite code to join an existing server.'}
</p>
</div>
<div className="px-8 pt-4">
<div className="flex rounded-xl bg-black/30 p-1">
<button
onClick={() => setModalMode('create')}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all ${
modalMode === 'create' ? 'bg-white/10 text-white shadow' : 'text-white/50 hover:text-white/70'
}`}
>
Create New
</button>
<button
onClick={() => setModalMode('join')}
className={`flex-1 py-2.5 rounded-lg text-sm font-medium transition-all ${
modalMode === 'join' ? 'bg-white/10 text-white shadow' : 'text-white/50 hover:text-white/70'
}`}
>
Join Existing
</button>
</div>
</div>
<div className="p-8 pt-6">
{error && (
<div className="mb-4 p-3 rounded-lg bg-rose-500/20 border border-rose-500/30 text-rose-400 text-sm">
{error}
</div>
)}
{modalMode === 'create' ? (
<>
<label className="block text-xs uppercase tracking-wider text-white/50 font-semibold mb-2">
Server Name
</label>
<input
type="text"
value={serverName}
onChange={(e) => setServerName(e.target.value)}
placeholder="Enter server name..."
className="w-full px-4 py-3.5 bg-black/30 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500/50 transition-all"
autoFocus
/>
<button
onClick={handleCreate}
disabled={!serverName.trim()}
className="w-full mt-6 py-3.5 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-500 hover:to-violet-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-xl transition-all shadow-lg shadow-indigo-500/25"
>
Create Server
</button>
</>
) : (
<>
<label className="block text-xs uppercase tracking-wider text-white/50 font-semibold mb-2">
Invite Code
</label>
<input
type="text"
value={inviteCode}
onChange={(e) => setInviteCode(e.target.value)}
placeholder="abc123"
className="w-full px-4 py-3.5 bg-black/30 border border-white/10 rounded-xl text-white placeholder-white/30 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500/50 transition-all font-mono text-lg tracking-wider text-center"
autoFocus
/>
<button
onClick={handleJoin}
disabled={!inviteCode.trim()}
className="w-full mt-6 py-3.5 bg-gradient-to-r from-emerald-600 to-cyan-600 hover:from-emerald-500 hover:to-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-xl transition-all shadow-lg shadow-emerald-500/25"
>
Join Server
</button>
</>
)}
</div>
</div>
</div>
)}
</>
);
};
export default ServerList;

View file

@ -0,0 +1,605 @@
import React, { useState } from 'react';
import { useAuth } from '../context/AuthContext';
interface SettingsPanelProps {
isOpen: boolean;
onClose: () => void;
}
type SettingsTab = 'account' | 'appearance' | 'voice' | 'notifications' | 'keybinds' | 'advanced';
const SettingsPanel: React.FC<SettingsPanelProps> = ({ isOpen, onClose }) => {
const { user, logout } = useAuth();
const [activeTab, setActiveTab] = useState<SettingsTab>('account');
const [theme, setTheme] = useState('dark');
const [accentColor, setAccentColor] = useState('#6366f1');
const [notifications, setNotifications] = useState({
desktop: true,
sounds: true,
messages: true,
mentions: true,
});
if (!isOpen) return null;
const tabs: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
{
id: 'account',
label: 'My Account',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
),
},
{
id: 'appearance',
label: 'Appearance',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
</svg>
),
},
{
id: 'voice',
label: 'Voice & Video',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
),
},
{
id: 'notifications',
label: 'Notifications',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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>
),
},
{
id: 'keybinds',
label: 'Keybinds',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
),
},
{
id: 'advanced',
label: 'Advanced',
icon: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
),
},
];
const accentColors = [
{ name: 'Indigo', value: '#6366f1' },
{ name: 'Purple', value: '#a855f7' },
{ name: 'Pink', value: '#ec4899' },
{ name: 'Red', value: '#ef4444' },
{ name: 'Orange', value: '#f97316' },
{ name: 'Green', value: '#22c55e' },
{ name: 'Cyan', value: '#06b6d4' },
{ name: 'Blue', value: '#3b82f6' },
];
const renderContent = () => {
switch (activeTab) {
case 'account':
return (
<div className="space-y-6">
{/* Profile Card */}
<div className="bg-gradient-to-br from-indigo-600/20 to-purple-600/20 rounded-2xl p-6 border border-white/10">
<div className="flex items-start gap-4">
<div className="relative">
<div className="w-20 h-20 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-3xl font-bold">
{user?.displayName?.[0]?.toUpperCase() || user?.username?.[0]?.toUpperCase() || 'U'}
</div>
<button className="absolute -bottom-1 -right-1 w-8 h-8 bg-indigo-600 hover:bg-indigo-500 rounded-full flex items-center justify-center border-4 border-[#0a0a0c] transition-colors">
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
</button>
</div>
<div className="flex-1">
<h3 className="text-xl font-bold text-white">{user?.displayName || user?.username}</h3>
<p className="text-gray-400">@{user?.username}</p>
<div className="flex items-center gap-2 mt-2">
<span className="px-2 py-0.5 bg-indigo-500/20 text-indigo-400 text-xs rounded-full border border-indigo-500/30">
PRO
</span>
<span className="text-gray-500 text-sm">Member since 2026</span>
</div>
</div>
<button className="px-4 py-2 bg-white/5 hover:bg-white/10 rounded-lg text-sm text-gray-300 transition-colors border border-white/10">
Edit Profile
</button>
</div>
</div>
{/* Account Details */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Account Details</h4>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<label className="text-xs text-gray-500 uppercase">Username</label>
<div className="flex items-center justify-between mt-1">
<span className="text-white">{user?.username}</span>
<button className="text-indigo-400 hover:text-indigo-300 text-sm">Edit</button>
</div>
</div>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<label className="text-xs text-gray-500 uppercase">Email</label>
<div className="flex items-center justify-between mt-1">
<span className="text-white">{(user as any)?.email || 'Not set'}</span>
<button className="text-indigo-400 hover:text-indigo-300 text-sm">Edit</button>
</div>
</div>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<label className="text-xs text-gray-500 uppercase">Phone Number</label>
<div className="flex items-center justify-between mt-1">
<span className="text-gray-500">Not set</span>
<button className="text-indigo-400 hover:text-indigo-300 text-sm">Add</button>
</div>
</div>
</div>
{/* Password & Security */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Password & Authentication</h4>
<div className="bg-white/5 rounded-xl p-4 border border-white/10 flex items-center justify-between">
<div>
<p className="text-white font-medium">Password</p>
<p className="text-gray-500 text-sm">Last changed 30 days ago</p>
</div>
<button className="px-4 py-2 bg-white/5 hover:bg-white/10 rounded-lg text-sm text-gray-300 transition-colors border border-white/10">
Change Password
</button>
</div>
<div className="bg-white/5 rounded-xl p-4 border border-white/10 flex items-center justify-between">
<div>
<p className="text-white font-medium">Two-Factor Authentication</p>
<p className="text-gray-500 text-sm">Add an extra layer of security</p>
</div>
<button className="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 rounded-lg text-sm text-white transition-colors">
Enable 2FA
</button>
</div>
</div>
{/* Danger Zone */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-red-400 uppercase tracking-wider">Danger Zone</h4>
<div className="bg-red-500/10 rounded-xl p-4 border border-red-500/20">
<div className="flex items-center justify-between">
<div>
<p className="text-white font-medium">Delete Account</p>
<p className="text-gray-500 text-sm">Permanently delete your account and all data</p>
</div>
<button className="px-4 py-2 bg-red-600 hover:bg-red-500 rounded-lg text-sm text-white transition-colors">
Delete Account
</button>
</div>
</div>
</div>
</div>
);
case 'appearance':
return (
<div className="space-y-6">
{/* Theme Selection */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Theme</h4>
<div className="grid grid-cols-3 gap-3">
{['dark', 'light', 'amoled'].map((t) => (
<button
key={t}
onClick={() => setTheme(t)}
className={`p-4 rounded-xl border transition-all ${
theme === t
? 'bg-indigo-500/20 border-indigo-500/50'
: 'bg-white/5 border-white/10 hover:border-white/20'
}`}
>
<div className={`w-full h-16 rounded-lg mb-2 ${
t === 'dark' ? 'bg-[#1a1a1a]' :
t === 'light' ? 'bg-gray-200' :
'bg-black'
}`} />
<span className="text-sm text-gray-300 capitalize">{t}</span>
</button>
))}
</div>
</div>
{/* Accent Color */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Accent Color</h4>
<div className="flex gap-3 flex-wrap">
{accentColors.map((color) => (
<button
key={color.value}
onClick={() => setAccentColor(color.value)}
className={`w-10 h-10 rounded-full transition-all ${
accentColor === color.value ? 'ring-2 ring-white ring-offset-2 ring-offset-[#0a0a0c]' : ''
}`}
style={{ backgroundColor: color.value }}
title={color.name}
/>
))}
</div>
</div>
{/* Message Display */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Message Display</h4>
<div className="space-y-3">
<div className="flex items-center justify-between bg-white/5 rounded-xl p-4 border border-white/10">
<div>
<p className="text-white">Compact Mode</p>
<p className="text-gray-500 text-sm">Reduce spacing between messages</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-white/10 peer-focus:ring-2 peer-focus:ring-indigo-500/50 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div className="flex items-center justify-between bg-white/5 rounded-xl p-4 border border-white/10">
<div>
<p className="text-white">Show Timestamps</p>
<p className="text-gray-500 text-sm">Display time next to messages</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" defaultChecked className="sr-only peer" />
<div className="w-11 h-6 bg-white/10 peer-focus:ring-2 peer-focus:ring-indigo-500/50 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
</div>
</div>
{/* Font Size */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Chat Font Size</h4>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<input
type="range"
min="12"
max="20"
defaultValue="14"
className="w-full accent-indigo-500"
/>
<div className="flex justify-between text-xs text-gray-500 mt-2">
<span>12px</span>
<span>14px</span>
<span>16px</span>
<span>18px</span>
<span>20px</span>
</div>
</div>
</div>
</div>
);
case 'voice':
return (
<div className="space-y-6">
{/* Input Device */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Input Device</h4>
<select className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-indigo-500/50">
<option>Default - Microphone Array</option>
<option>USB Audio Device</option>
<option>Headset Microphone</option>
</select>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-400">Input Volume</span>
<span className="text-sm text-gray-400">75%</span>
</div>
<input type="range" defaultValue="75" className="w-full accent-indigo-500" />
</div>
</div>
{/* Output Device */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Output Device</h4>
<select className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-3 text-white focus:outline-none focus:border-indigo-500/50">
<option>Default - Speakers</option>
<option>USB Audio Device</option>
<option>Headphones</option>
</select>
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-400">Output Volume</span>
<span className="text-sm text-gray-400">100%</span>
</div>
<input type="range" defaultValue="100" className="w-full accent-indigo-500" />
</div>
</div>
{/* Voice Settings */}
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Voice Settings</h4>
<div className="space-y-3">
<div className="flex items-center justify-between bg-white/5 rounded-xl p-4 border border-white/10">
<div>
<p className="text-white">Noise Suppression</p>
<p className="text-gray-500 text-sm">Powered by Krisp AI</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" defaultChecked className="sr-only peer" />
<div className="w-11 h-6 bg-white/10 peer-focus:ring-2 peer-focus:ring-indigo-500/50 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div className="flex items-center justify-between bg-white/5 rounded-xl p-4 border border-white/10">
<div>
<p className="text-white">Echo Cancellation</p>
<p className="text-gray-500 text-sm">Reduce echo during calls</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" defaultChecked className="sr-only peer" />
<div className="w-11 h-6 bg-white/10 peer-focus:ring-2 peer-focus:ring-indigo-500/50 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
<div className="flex items-center justify-between bg-white/5 rounded-xl p-4 border border-white/10">
<div>
<p className="text-white">Push to Talk</p>
<p className="text-gray-500 text-sm">Hold a key to transmit voice</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-white/10 peer-focus:ring-2 peer-focus:ring-indigo-500/50 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
</div>
</div>
{/* Test Audio */}
<div className="bg-gradient-to-r from-indigo-600/20 to-purple-600/20 rounded-xl p-4 border border-indigo-500/20">
<div className="flex items-center gap-4">
<button className="w-12 h-12 bg-indigo-600 hover:bg-indigo-500 rounded-full flex items-center justify-center transition-colors">
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
</button>
<div>
<p className="text-white font-medium">Mic Test</p>
<p className="text-gray-400 text-sm">Click to test your microphone</p>
</div>
</div>
</div>
</div>
);
case 'notifications':
return (
<div className="space-y-6">
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Desktop Notifications</h4>
<div className="space-y-3">
{Object.entries(notifications).map(([key, value]) => (
<div key={key} className="flex items-center justify-between bg-white/5 rounded-xl p-4 border border-white/10">
<div>
<p className="text-white capitalize">{key.replace(/([A-Z])/g, ' $1')}</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={value}
onChange={() => setNotifications({ ...notifications, [key]: !value })}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-white/10 peer-focus:ring-2 peer-focus:ring-indigo-500/50 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
))}
</div>
</div>
{/* Do Not Disturb */}
<div className="bg-orange-500/10 rounded-xl p-4 border border-orange-500/20">
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-orange-500/20 rounded-full flex items-center justify-center">
<svg className="w-5 h-5 text-orange-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
</div>
<div className="flex-1">
<p className="text-white font-medium">Do Not Disturb</p>
<p className="text-gray-400 text-sm">Mute all notifications</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-white/10 peer-focus:ring-2 peer-focus:ring-orange-500/50 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-orange-600"></div>
</label>
</div>
</div>
</div>
);
case 'keybinds':
return (
<div className="space-y-6">
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Keybinds</h4>
{[
{ action: 'Toggle Mute', keys: ['Ctrl', 'Shift', 'M'] },
{ action: 'Toggle Deafen', keys: ['Ctrl', 'Shift', 'D'] },
{ action: 'Push to Talk', keys: ['`'] },
{ action: 'Quick Switcher', keys: ['Ctrl', 'K'] },
{ action: 'Search', keys: ['Ctrl', 'F'] },
{ action: 'Mark as Read', keys: ['Esc'] },
].map((bind, i) => (
<div key={i} className="flex items-center justify-between bg-white/5 rounded-xl p-4 border border-white/10">
<span className="text-white">{bind.action}</span>
<div className="flex items-center gap-1">
{bind.keys.map((key, j) => (
<React.Fragment key={j}>
{j > 0 && <span className="text-gray-600">+</span>}
<kbd className="px-2 py-1 bg-white/10 rounded text-sm text-gray-300 border border-white/10">{key}</kbd>
</React.Fragment>
))}
<button className="ml-3 text-indigo-400 hover:text-indigo-300 text-sm">Edit</button>
</div>
</div>
))}
</div>
</div>
);
case 'advanced':
return (
<div className="space-y-6">
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Developer Mode</h4>
<div className="flex items-center justify-between bg-white/5 rounded-xl p-4 border border-white/10">
<div>
<p className="text-white">Developer Mode</p>
<p className="text-gray-500 text-sm">Show IDs and developer options</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" className="sr-only peer" />
<div className="w-11 h-6 bg-white/10 peer-focus:ring-2 peer-focus:ring-indigo-500/50 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
</div>
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Experimental</h4>
<div className="flex items-center justify-between bg-white/5 rounded-xl p-4 border border-white/10">
<div>
<p className="text-white">Hardware Acceleration</p>
<p className="text-gray-500 text-sm">Use GPU for rendering (requires restart)</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input type="checkbox" defaultChecked className="sr-only peer" />
<div className="w-11 h-6 bg-white/10 peer-focus:ring-2 peer-focus:ring-indigo-500/50 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:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-indigo-600"></div>
</label>
</div>
</div>
<div className="space-y-4">
<h4 className="text-sm font-medium text-gray-400 uppercase tracking-wider">Data</h4>
<div className="grid grid-cols-2 gap-3">
<button className="p-4 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-left transition-colors">
<p className="text-white font-medium">Export Data</p>
<p className="text-gray-500 text-sm">Download your data</p>
</button>
<button className="p-4 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-left transition-colors">
<p className="text-white font-medium">Clear Cache</p>
<p className="text-gray-500 text-sm">Free up space</p>
</button>
</div>
</div>
{/* Version Info */}
<div className="bg-white/5 rounded-xl p-4 border border-white/10">
<div className="flex items-center justify-between">
<div>
<p className="text-white font-medium">AeThex Connect</p>
<p className="text-gray-500 text-sm">Version 1.0.0-beta</p>
</div>
<button className="text-indigo-400 hover:text-indigo-300 text-sm">Check for Updates</button>
</div>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative w-full max-w-5xl h-[85vh] bg-[#0a0a0c] rounded-2xl overflow-hidden border border-white/10 shadow-2xl flex animate-fadeIn">
{/* Sidebar */}
<div className="w-56 bg-white/[0.02] border-r border-white/10 p-4 flex flex-col">
<h2 className="text-lg font-bold text-white px-3 mb-4">Settings</h2>
<nav className="flex-1 space-y-1">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all ${
activeTab === tab.id
? 'bg-white/10 text-white'
: 'text-gray-400 hover:text-white hover:bg-white/5'
}`}
>
{tab.icon}
{tab.label}
</button>
))}
</nav>
{/* Logout Button */}
<div className="pt-4 border-t border-white/10">
<button
onClick={() => {
logout();
onClose();
}}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium text-red-400 hover:text-red-300 hover:bg-red-500/10 transition-all"
>
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
</svg>
Log Out
</button>
</div>
</div>
{/* Content */}
<div className="flex-1 flex flex-col">
{/* Header */}
<div className="p-6 border-b border-white/10 flex items-center justify-between">
<h3 className="text-xl font-bold text-white">
{tabs.find((t) => t.id === activeTab)?.label}
</h3>
<button
onClick={onClose}
className="w-8 h-8 bg-white/5 hover:bg-white/10 rounded-lg flex items-center justify-center transition-colors"
>
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto p-6">
{renderContent()}
</div>
</div>
</div>
</div>
);
};
export default SettingsPanel;

View file

@ -0,0 +1,219 @@
import React from 'react';
// Base Skeleton component
interface SkeletonProps {
className?: string;
animate?: boolean;
style?: React.CSSProperties;
}
export const Skeleton: React.FC<SkeletonProps> = ({ className = '', animate = true, style }) => (
<div
className={`bg-white/5 rounded ${animate ? 'animate-pulse' : ''} ${className}`}
style={style}
/>
);
// Message Skeleton
export const MessageSkeleton: React.FC<{ compact?: boolean }> = ({ compact = false }) => (
<div className={`flex gap-4 px-4 ${compact ? 'py-1' : 'py-3'}`}>
{!compact && <Skeleton className="w-10 h-10 rounded-full flex-shrink-0" />}
<div className="flex-1 space-y-2">
{!compact && (
<div className="flex items-center gap-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
)}
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
</div>
);
// Channel Skeleton
export const ChannelSkeleton: React.FC = () => (
<div className="flex items-center gap-2 px-2 py-1.5">
<Skeleton className="w-5 h-5 rounded" />
<Skeleton className="h-4 flex-1" />
</div>
);
// Member Skeleton
export const MemberSkeleton: React.FC = () => (
<div className="flex items-center gap-3 px-2 py-1.5">
<Skeleton className="w-8 h-8 rounded-full flex-shrink-0" />
<div className="flex-1 space-y-1.5">
<Skeleton className="h-3.5 w-20" />
<Skeleton className="h-2.5 w-16" />
</div>
</div>
);
// Server Skeleton
export const ServerSkeleton: React.FC = () => (
<Skeleton className="w-12 h-12 rounded-2xl flex-shrink-0" />
);
// Chat Area Loading
export const ChatAreaSkeleton: React.FC = () => (
<div className="flex-1 flex flex-col">
{/* Header */}
<div className="h-12 border-b border-white/10 flex items-center px-4 gap-2">
<Skeleton className="w-5 h-5 rounded" />
<Skeleton className="h-4 w-32" />
</div>
{/* Messages */}
<div className="flex-1 p-4 space-y-4">
{Array.from({ length: 8 }).map((_, i) => (
<MessageSkeleton key={i} compact={i % 3 === 2} />
))}
</div>
{/* Input */}
<div className="p-4">
<Skeleton className="h-11 w-full rounded-lg" />
</div>
</div>
);
// Full Page Loading
export const PageLoadingSkeleton: React.FC = () => (
<div className="h-screen w-screen bg-[#050507] flex items-center justify-center">
<div className="flex flex-col items-center gap-4">
<div className="relative">
<div className="w-16 h-16 bg-gradient-to-tr from-indigo-600 via-purple-600 to-pink-600 rounded-2xl animate-pulse" />
<div className="absolute inset-0 w-16 h-16 bg-gradient-to-tr from-indigo-600 via-purple-600 to-pink-600 rounded-2xl animate-ping opacity-20" />
</div>
<div className="flex gap-1">
<div className="w-2 h-2 bg-indigo-500 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
<div className="w-2 h-2 bg-purple-500 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
<div className="w-2 h-2 bg-pink-500 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
</div>
<p className="text-gray-500 text-sm">Loading AeThex Connect...</p>
</div>
</div>
);
// Sidebar Loading
export const SidebarSkeleton: React.FC = () => (
<div className="w-60 bg-white/[0.02] border-r border-white/10 flex flex-col">
{/* Header */}
<div className="h-12 border-b border-white/10 px-4 flex items-center">
<Skeleton className="h-5 w-32" />
</div>
{/* Channels */}
<div className="flex-1 p-2 space-y-4">
{/* Category */}
<div>
<div className="flex items-center gap-1 px-1 py-1">
<Skeleton className="w-3 h-3" />
<Skeleton className="h-3 w-20" />
</div>
<div className="space-y-1 mt-1">
{Array.from({ length: 4 }).map((_, i) => (
<ChannelSkeleton key={i} />
))}
</div>
</div>
{/* Category */}
<div>
<div className="flex items-center gap-1 px-1 py-1">
<Skeleton className="w-3 h-3" />
<Skeleton className="h-3 w-24" />
</div>
<div className="space-y-1 mt-1">
{Array.from({ length: 3 }).map((_, i) => (
<ChannelSkeleton key={i} />
))}
</div>
</div>
</div>
{/* User Panel */}
<div className="h-14 border-t border-white/10 px-3 flex items-center gap-2">
<Skeleton className="w-8 h-8 rounded-full" />
<div className="flex-1 space-y-1">
<Skeleton className="h-3.5 w-20" />
<Skeleton className="h-2.5 w-16" />
</div>
</div>
</div>
);
// Members Sidebar Loading
export const MembersSidebarSkeleton: React.FC = () => (
<div className="w-60 bg-white/[0.02] border-l border-white/10 p-2">
{/* Role Group */}
<div className="space-y-2">
<Skeleton className="h-3 w-20 mx-2" />
{Array.from({ length: 3 }).map((_, i) => (
<MemberSkeleton key={i} />
))}
</div>
{/* Role Group */}
<div className="space-y-2 mt-4">
<Skeleton className="h-3 w-24 mx-2" />
{Array.from({ length: 5 }).map((_, i) => (
<MemberSkeleton key={i} />
))}
</div>
</div>
);
// Server List Loading
export const ServerListSkeleton: React.FC = () => (
<div className="w-[72px] bg-white/[0.02] py-3 flex flex-col items-center gap-2">
<ServerSkeleton />
<div className="w-8 h-0.5 bg-white/10 rounded my-1" />
{Array.from({ length: 4 }).map((_, i) => (
<ServerSkeleton key={i} />
))}
</div>
);
// Full App Loading
export const AppSkeleton: React.FC = () => (
<div className="h-screen w-screen bg-[#050507] flex">
<ServerListSkeleton />
<SidebarSkeleton />
<ChatAreaSkeleton />
<MembersSidebarSkeleton />
</div>
);
// Card Skeleton
export const CardSkeleton: React.FC<{ lines?: number }> = ({ lines = 3 }) => (
<div className="bg-white/5 rounded-xl p-4 border border-white/10 space-y-3">
<div className="flex items-center gap-3">
<Skeleton className="w-10 h-10 rounded-full" />
<div className="flex-1 space-y-2">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-3 w-16" />
</div>
</div>
{Array.from({ length: lines }).map((_, i) => (
<Skeleton key={i} className="h-3 w-full" style={{ width: `${100 - i * 15}%` }} />
))}
</div>
);
// Image Skeleton
export const ImageSkeleton: React.FC<{ aspectRatio?: string }> = ({ aspectRatio = '16/9' }) => (
<div
className="bg-white/5 rounded-lg overflow-hidden animate-pulse"
style={{ aspectRatio }}
>
<div className="w-full h-full flex items-center justify-center">
<svg className="w-10 h-10 text-white/10" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
</div>
);
export default Skeleton;

View file

@ -0,0 +1,215 @@
import React, { createContext, useContext, useState, useCallback } from 'react';
export type ToastType = 'success' | 'error' | 'warning' | 'info';
interface Toast {
id: string;
type: ToastType;
title: string;
message?: string;
duration?: number;
}
interface ToastContextType {
toasts: Toast[];
addToast: (toast: Omit<Toast, 'id'>) => void;
removeToast: (id: string) => void;
}
const ToastContext = createContext<ToastContextType | undefined>(undefined);
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error('useToast must be used within a ToastProvider');
}
return context;
};
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback((toast: Omit<Toast, 'id'>) => {
const id = Math.random().toString(36).substr(2, 9);
const newToast = { ...toast, id };
setToasts((prev) => [...prev, newToast]);
// Auto remove after duration
const duration = toast.duration || 5000;
setTimeout(() => {
removeToast(id);
}, duration);
}, []);
const removeToast = useCallback((id: string) => {
setToasts((prev) => prev.filter((toast) => toast.id !== id));
}, []);
return (
<ToastContext.Provider value={{ toasts, addToast, removeToast }}>
{children}
<ToastContainer toasts={toasts} onRemove={removeToast} />
</ToastContext.Provider>
);
};
interface ToastContainerProps {
toasts: Toast[];
onRemove: (id: string) => void;
}
const ToastContainer: React.FC<ToastContainerProps> = ({ toasts, onRemove }) => {
if (toasts.length === 0) return null;
return (
<div className="fixed bottom-6 right-6 z-[100] flex flex-col gap-3 max-w-sm">
{toasts.map((toast, index) => (
<ToastItem
key={toast.id}
toast={toast}
onRemove={onRemove}
index={index}
/>
))}
</div>
);
};
interface ToastItemProps {
toast: Toast;
onRemove: (id: string) => void;
index: number;
}
const ToastItem: React.FC<ToastItemProps> = ({ toast, onRemove, index }) => {
const iconMap = {
success: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
),
error: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
),
warning: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
),
info: (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
),
};
const colorMap = {
success: {
bg: 'from-green-500/20 to-emerald-500/20',
border: 'border-green-500/30',
icon: 'bg-green-500/20 text-green-400',
progress: 'bg-green-500',
},
error: {
bg: 'from-red-500/20 to-rose-500/20',
border: 'border-red-500/30',
icon: 'bg-red-500/20 text-red-400',
progress: 'bg-red-500',
},
warning: {
bg: 'from-orange-500/20 to-amber-500/20',
border: 'border-orange-500/30',
icon: 'bg-orange-500/20 text-orange-400',
progress: 'bg-orange-500',
},
info: {
bg: 'from-indigo-500/20 to-blue-500/20',
border: 'border-indigo-500/30',
icon: 'bg-indigo-500/20 text-indigo-400',
progress: 'bg-indigo-500',
},
};
const colors = colorMap[toast.type];
const duration = toast.duration || 5000;
return (
<div
className={`
relative overflow-hidden rounded-xl border backdrop-blur-xl
bg-gradient-to-r ${colors.bg} ${colors.border}
shadow-2xl animate-slideIn
`}
style={{ animationDelay: `${index * 50}ms` }}
>
<div className="flex items-start gap-3 p-4">
{/* Icon */}
<div className={`w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 ${colors.icon}`}>
{iconMap[toast.type]}
</div>
{/* Content */}
<div className="flex-1 min-w-0">
<p className="font-medium text-white text-sm">{toast.title}</p>
{toast.message && (
<p className="text-gray-400 text-xs mt-0.5 line-clamp-2">{toast.message}</p>
)}
</div>
{/* Close Button */}
<button
onClick={() => onRemove(toast.id)}
className="w-6 h-6 rounded flex items-center justify-center text-gray-500 hover:text-white hover:bg-white/10 transition-colors flex-shrink-0"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Progress Bar */}
<div className="h-0.5 bg-white/5">
<div
className={`h-full ${colors.progress} animate-shrink`}
style={{
animationDuration: `${duration}ms`,
animationTimingFunction: 'linear',
}}
/>
</div>
{/* Add keyframes for animations */}
<style>{`
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes shrink {
from {
width: 100%;
}
to {
width: 0%;
}
}
.animate-slideIn {
animation: slideIn 0.3s ease-out forwards;
}
.animate-shrink {
animation: shrink linear forwards;
}
`}</style>
</div>
);
};
export default ToastProvider;

View file

@ -0,0 +1,228 @@
import React from 'react';
interface UserProfileCardProps {
user: {
id: string;
username: string;
display_name?: string;
avatar_url?: string;
status?: string;
bio?: string;
banner_color?: string;
created_at?: string;
};
isOpen: boolean;
onClose: () => void;
position?: { x: number; y: number };
}
const UserProfileCard: React.FC<UserProfileCardProps> = ({ user, isOpen, onClose, position }) => {
if (!isOpen) return null;
const badges = [
{ name: 'Early Supporter', icon: '⭐', color: 'from-yellow-500 to-orange-500' },
{ name: 'Active Chatter', icon: '💬', color: 'from-indigo-500 to-purple-500' },
{ name: 'Bug Hunter', icon: '🐛', color: 'from-green-500 to-emerald-500' },
];
const activities = [
{ type: 'playing', name: 'Valorant', detail: 'Competitive Match', icon: '🎮' },
{ type: 'listening', name: 'Spotify', detail: 'Blinding Lights - The Weeknd', icon: '🎵' },
];
const mutualServers = [
{ name: 'Gaming Hub', icon: '🎮' },
{ name: 'Dev Community', icon: '💻' },
{ name: 'Music Lovers', icon: '🎵' },
];
const mutualFriends = [
{ name: 'Alex', avatar: 'A' },
{ name: 'Jordan', avatar: 'J' },
{ name: 'Sam', avatar: 'S' },
];
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={onClose}
/>
{/* Card */}
<div
className="relative w-[340px] bg-[#0a0a0c] rounded-2xl border border-white/10 shadow-2xl overflow-hidden animate-fadeIn"
style={position ? { position: 'absolute', left: position.x, top: position.y } : {}}
>
{/* Banner */}
<div
className="h-28 relative"
style={{
background: user.banner_color || 'linear-gradient(135deg, #6366f1, #a855f7, #ec4899)'
}}
>
{/* Banner Pattern */}
<div className="absolute inset-0 opacity-20" style={{
backgroundImage: 'radial-gradient(circle at 20% 80%, rgba(255,255,255,0.3) 0%, transparent 50%), radial-gradient(circle at 80% 20%, rgba(255,255,255,0.3) 0%, transparent 50%)'
}} />
</div>
{/* Avatar */}
<div className="relative px-4">
<div className="absolute -top-14 left-4">
<div className="relative">
<div className="w-28 h-28 rounded-full bg-gradient-to-br from-indigo-500 to-purple-500 flex items-center justify-center text-4xl font-bold border-[6px] border-[#0a0a0c]">
{user.avatar_url ? (
<img src={user.avatar_url} alt="" className="w-full h-full rounded-full object-cover" />
) : (
user.display_name?.[0]?.toUpperCase() || user.username?.[0]?.toUpperCase()
)}
</div>
{/* Status Indicator */}
<div className={`absolute bottom-1 right-1 w-6 h-6 rounded-full border-4 border-[#0a0a0c] ${
user.status === 'online' ? 'bg-green-500' :
user.status === 'idle' ? 'bg-yellow-500' :
user.status === 'dnd' ? 'bg-red-500' :
'bg-gray-500'
}`} />
</div>
</div>
{/* Actions */}
<div className="flex justify-end pt-3 gap-2">
<button className="p-2 bg-white/5 hover:bg-white/10 rounded-lg transition-colors" title="Send Message">
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
</button>
<button className="p-2 bg-white/5 hover:bg-white/10 rounded-lg transition-colors" title="Voice Call">
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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="p-2 bg-white/5 hover:bg-white/10 rounded-lg transition-colors" title="More">
<svg className="w-5 h-5 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h.01M12 12h.01M19 12h.01M6 12a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0zm7 0a1 1 0 11-2 0 1 1 0 012 0z" />
</svg>
</button>
</div>
</div>
{/* User Info */}
<div className="px-4 pt-10 pb-4">
{/* Name */}
<div className="mb-3">
<h3 className="text-xl font-bold text-white">{user.display_name || user.username}</h3>
<p className="text-gray-400 text-sm">@{user.username}</p>
</div>
{/* Badges */}
<div className="flex flex-wrap gap-2 mb-4">
{badges.map((badge, i) => (
<div
key={i}
className={`px-2 py-1 rounded-full bg-gradient-to-r ${badge.color} text-xs font-medium text-white flex items-center gap-1`}
title={badge.name}
>
<span>{badge.icon}</span>
<span>{badge.name}</span>
</div>
))}
</div>
{/* Bio */}
{user.bio && (
<div className="bg-white/5 rounded-xl p-3 mb-4 border border-white/5">
<p className="text-sm text-gray-300">{user.bio}</p>
</div>
)}
{/* Divider */}
<div className="border-t border-white/10 my-4" />
{/* Activity */}
<div className="mb-4">
<h4 className="text-xs font-medium text-gray-500 uppercase mb-2">Current Activity</h4>
{activities.slice(0, 1).map((activity, i) => (
<div key={i} className="flex items-center gap-3 bg-white/5 rounded-xl p-3 border border-white/5">
<div className="w-10 h-10 bg-white/10 rounded-lg flex items-center justify-center text-xl">
{activity.icon}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-white font-medium capitalize">{activity.type} {activity.name}</p>
<p className="text-xs text-gray-400 truncate">{activity.detail}</p>
</div>
</div>
))}
</div>
{/* Mutual Servers */}
<div className="mb-4">
<h4 className="text-xs font-medium text-gray-500 uppercase mb-2">
{mutualServers.length} Mutual Servers
</h4>
<div className="flex gap-2">
{mutualServers.map((server, i) => (
<div
key={i}
className="w-10 h-10 bg-white/10 rounded-xl flex items-center justify-center text-lg hover:bg-white/20 cursor-pointer transition-colors"
title={server.name}
>
{server.icon}
</div>
))}
{mutualServers.length > 3 && (
<div className="w-10 h-10 bg-white/10 rounded-xl flex items-center justify-center text-xs text-gray-400">
+{mutualServers.length - 3}
</div>
)}
</div>
</div>
{/* Mutual Friends */}
<div>
<h4 className="text-xs font-medium text-gray-500 uppercase mb-2">
{mutualFriends.length} Mutual Friends
</h4>
<div className="flex -space-x-2">
{mutualFriends.slice(0, 5).map((friend, i) => (
<div
key={i}
className="w-8 h-8 bg-gradient-to-br from-indigo-500 to-purple-500 rounded-full flex items-center justify-center text-xs font-bold border-2 border-[#0a0a0c] hover:translate-y-[-2px] cursor-pointer transition-transform"
title={friend.name}
>
{friend.avatar}
</div>
))}
{mutualFriends.length > 5 && (
<div className="w-8 h-8 bg-white/10 rounded-full flex items-center justify-center text-xs text-gray-400 border-2 border-[#0a0a0c]">
+{mutualFriends.length - 5}
</div>
)}
</div>
</div>
{/* Note Section */}
<div className="mt-4 pt-4 border-t border-white/10">
<h4 className="text-xs font-medium text-gray-500 uppercase mb-2">Note</h4>
<textarea
className="w-full bg-white/5 border border-white/10 rounded-lg p-2 text-sm text-gray-300 placeholder-gray-500 resize-none focus:outline-none focus:border-indigo-500/50"
placeholder="Click to add a note"
rows={2}
/>
</div>
</div>
{/* Footer */}
<div className="px-4 pb-4">
<p className="text-xs text-gray-600 text-center">
Member since {user.created_at ? new Date(user.created_at).toLocaleDateString('en-US', { month: 'short', year: 'numeric' }) : 'Unknown'}
</p>
</div>
</div>
</div>
);
};
export default UserProfileCard;

View file

@ -0,0 +1,199 @@
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import api from '../services/api';
import { useAuth } from './AuthContext';
interface Server {
id: string;
name: string;
description?: string;
icon_url?: string;
owner_id: string;
invite_code: string;
member_count: number;
role?: string;
}
interface Channel {
id: string;
server_id: string;
name: string;
description?: string;
channel_type: 'text' | 'voice' | 'announcement' | 'stage';
position: number;
}
interface Member {
id: string;
username: string;
display_name?: string;
avatar_url?: string;
status: string;
custom_status?: string;
role: string;
nickname?: string;
}
interface Message {
id: string;
channel_id: string;
user_id: string;
content: string;
username: string;
display_name?: string;
avatar_url?: string;
created_at: string;
}
interface AppContextType {
servers: Server[];
currentServer: Server | null;
channels: Channel[];
currentChannel: Channel | null;
members: Member[];
messages: Message[];
loadingServers: boolean;
loadingChannels: boolean;
loadingMessages: boolean;
selectServer: (server: Server | null) => void;
selectChannel: (channel: Channel | null) => void;
createServer: (name: string, description?: string) => Promise<Server>;
joinServer: (inviteCode: string) => Promise<Server>;
sendMessage: (content: string) => Promise<void>;
refreshServers: () => Promise<void>;
}
const AppContext = createContext<AppContextType | null>(null);
export function AppProvider({ children }: { children: ReactNode }) {
const { user } = useAuth();
const [servers, setServers] = useState<Server[]>([]);
const [currentServer, setCurrentServer] = useState<Server | null>(null);
const [channels, setChannels] = useState<Channel[]>([]);
const [currentChannel, setCurrentChannel] = useState<Channel | null>(null);
const [members, setMembers] = useState<Member[]>([]);
const [messages, setMessages] = useState<Message[]>([]);
const [loadingServers, setLoadingServers] = useState(false);
const [loadingChannels, setLoadingChannels] = useState(false);
const [loadingMessages, setLoadingMessages] = useState(false);
const refreshServers = useCallback(async () => {
if (!user) return;
setLoadingServers(true);
try {
const data = await api.getServers();
setServers(data.servers || []);
} catch (error) {
console.error('Failed to load servers:', error);
} finally {
setLoadingServers(false);
}
}, [user]);
// Load servers when user logs in
useEffect(() => {
if (user) {
refreshServers();
} else {
setServers([]);
setCurrentServer(null);
setChannels([]);
setCurrentChannel(null);
setMembers([]);
setMessages([]);
}
}, [user, refreshServers]);
// Load channels when server changes
useEffect(() => {
if (!currentServer) {
setChannels([]);
setCurrentChannel(null);
return;
}
setLoadingChannels(true);
Promise.all([
api.getChannels(currentServer.id),
api.getMembers(currentServer.id),
])
.then(([channelData, memberData]) => {
setChannels(channelData.channels || []);
setMembers(memberData.members || []);
// Auto-select first text channel
const firstText = (channelData.channels || []).find((c: Channel) => c.channel_type === 'text');
if (firstText) setCurrentChannel(firstText);
})
.catch(console.error)
.finally(() => setLoadingChannels(false));
}, [currentServer]);
// Load messages when channel changes
useEffect(() => {
if (!currentChannel) {
setMessages([]);
return;
}
setLoadingMessages(true);
api.getMessages(currentChannel.id)
.then(data => setMessages(data.messages || []))
.catch(console.error)
.finally(() => setLoadingMessages(false));
}, [currentChannel]);
const selectServer = (server: Server | null) => {
setCurrentServer(server);
};
const selectChannel = (channel: Channel | null) => {
setCurrentChannel(channel);
};
const createServer = async (name: string, description?: string) => {
const data = await api.createServer(name, description);
await refreshServers();
return data.server;
};
const joinServer = async (inviteCode: string) => {
const data = await api.joinServer(inviteCode);
await refreshServers();
return data.server;
};
const sendMessage = async (content: string) => {
if (!currentChannel) return;
const data = await api.sendMessage(currentChannel.id, content);
setMessages(prev => [...prev, data.message]);
};
return (
<AppContext.Provider value={{
servers,
currentServer,
channels,
currentChannel,
members,
messages,
loadingServers,
loadingChannels,
loadingMessages,
selectServer,
selectChannel,
createServer,
joinServer,
sendMessage,
refreshServers,
}}>
{children}
</AppContext.Provider>
);
}
export function useApp() {
const context = useContext(AppContext);
if (!context) {
throw new Error('useApp must be used within AppProvider');
}
return context;
}

View file

@ -0,0 +1,77 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import api from '../services/api';
interface User {
id: string;
username: string;
displayName?: string;
avatarUrl?: string;
status?: string;
customStatus?: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: (username: string, password: string) => Promise<void>;
register: (username: string, password: string, displayName?: string) => Promise<void>;
demoLogin: () => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for existing token
const token = api.getToken();
if (token) {
api.getMe()
.then(data => setUser(data.user || data))
.catch(() => {
api.logout();
setUser(null);
})
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (username: string, password: string) => {
const data = await api.login(username, password);
setUser(data.user);
};
const register = async (username: string, password: string, displayName?: string) => {
const data = await api.register(username, password, displayName);
setUser(data.user);
};
const demoLogin = async () => {
const data = await api.demoLogin();
setUser(data.user);
};
const logout = () => {
api.logout();
setUser(null);
};
return (
<AuthContext.Provider value={{ user, loading, login, register, demoLogin, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within AuthProvider');
}
return context;
}

View file

@ -1,61 +1,139 @@
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&display=swap');
@import "tailwindcss";
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap');
html, body, #root {
height: 100%;
margin: 0;
padding: 0;
font-family: 'Roboto Mono', monospace;
background: #0a0a0a;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #050507;
color: #e0e0e0;
overflow: hidden;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1000;
}
/* Remove the scanline effect for cleaner look */
.connect-container {
height: 100vh;
display: flex;
background: linear-gradient(135deg, #050507 0%, #0a0a0c 50%, #050507 100%);
}
/* Glassmorphism and accent classes for key elements */
.server-icon, .user-avatar, .member-avatar-small {
background: rgba(26,26,26,0.85);
backdrop-filter: blur(6px);
}
.channel-item.active, .channel-item:hover, .member-item:hover {
background: rgba(26,26,26,0.85);
backdrop-filter: blur(4px);
}
.message-input, .message-input-container {
background: rgba(15,15,15,0.95);
backdrop-filter: blur(4px);
}
/* Hide scrollbars for a cleaner look */
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 8px;
background: #111;
}
::-webkit-scrollbar-thumb {
background: #222;
border-radius: 4px;
width: 6px;
height: 6px;
}
/* Utility classes for gradients, badges, etc. can be extended as needed */
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Hide scrollbar when not hovering */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Selection */
::selection {
background: rgba(99, 102, 241, 0.3);
color: white;
}
/* Focus outline */
:focus-visible {
outline: 2px solid rgba(99, 102, 241, 0.5);
outline-offset: 2px;
}
/* Smooth scrolling */
* {
scroll-behavior: smooth;
}
/* Animation for typing indicator */
@keyframes bounce {
0%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-6px);
}
}
/* Pulse animation for live indicators */
@keyframes pulse-soft {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* Gradient text utility */
.text-gradient {
background: linear-gradient(135deg, #6366f1, #a855f7, #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Glass effect utilities */
.glass {
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
.glass-dark {
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.05);
}
/* Hover lift effect */
.hover-lift {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.hover-lift:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
/* Button press effect */
.press-effect:active {
transform: scale(0.97);
}
/* Fade in animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.3s ease forwards;
}

View file

@ -0,0 +1,148 @@
const API_BASE = 'http://localhost:3000/api';
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
class ApiService {
private token: string | null = null;
setToken(token: string | null) {
this.token = token;
if (token) {
localStorage.setItem('auth_token', token);
} else {
localStorage.removeItem('auth_token');
}
}
getToken(): string | null {
if (!this.token) {
this.token = localStorage.getItem('auth_token');
}
return this.token;
}
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
const token = this.getToken();
const headers: HeadersInit = {
'Content-Type': 'application/json',
...(options.headers || {}),
};
if (token) {
(headers as Record<string, string>)['Authorization'] = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
});
const json = await response.json();
if (!response.ok || json.success === false) {
throw new Error(json.error || 'Request failed');
}
// Handle wrapped response { success, data } format
if (json.data !== undefined) {
return json.data as T;
}
return json as T;
}
// Auth
async register(username: string, password: string, displayName?: string) {
const data = await this.request<{ token: string; user: any }>('/auth/register', {
method: 'POST',
body: JSON.stringify({ username, password, displayName }),
});
this.setToken(data.token);
return data;
}
async login(username: string, password: string) {
const data = await this.request<{ token: string; user: any }>('/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
this.setToken(data.token);
return data;
}
async demoLogin() {
const data = await this.request<{ token: string; user: any }>('/auth/demo', {
method: 'POST',
});
this.setToken(data.token);
return data;
}
async getMe() {
return this.request<{ user: any }>('/auth/me');
}
logout() {
this.setToken(null);
}
// Servers
async getServers() {
return this.request<{ servers: any[] }>('/servers');
}
async createServer(name: string, description?: string) {
return this.request<{ server: any }>('/servers', {
method: 'POST',
body: JSON.stringify({ name, description }),
});
}
async joinServer(inviteCode: string) {
return this.request<{ server: any }>(`/servers/join/${inviteCode}`, {
method: 'POST',
});
}
async getServer(serverId: string) {
return this.request<{ server: any }>(`/servers/${serverId}`);
}
// Channels
async getChannels(serverId: string) {
return this.request<{ channels: any[] }>(`/servers/${serverId}/channels`);
}
async createChannel(serverId: string, name: string, type: 'text' | 'voice' = 'text') {
return this.request<{ channel: any }>(`/servers/${serverId}/channels`, {
method: 'POST',
body: JSON.stringify({ name, channel_type: type }),
});
}
// Members
async getMembers(serverId: string) {
return this.request<{ members: any[] }>(`/servers/${serverId}/members`);
}
// Messages
async getMessages(channelId: string, limit = 50, before?: string) {
let url = `/servers/channels/${channelId}/messages?limit=${limit}`;
if (before) url += `&before=${before}`;
return this.request<{ messages: any[] }>(url);
}
async sendMessage(channelId: string, content: string) {
return this.request<{ message: any }>(`/servers/channels/${channelId}/messages`, {
method: 'POST',
body: JSON.stringify({ content }),
});
}
}
export const api = new ApiService();
export default api;

View file

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./src/renderer/**/*.{js,ts,jsx,tsx,html}",
],
theme: {
extend: {},
},
plugins: [],
}

View file

@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist/main",
"rootDir": "./src/main",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": false,
"moduleResolution": "node"
},
"include": ["src/main/**/*"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src/renderer/**/*"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,31 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import tailwindcss from '@tailwindcss/postcss';
import autoprefixer from 'autoprefixer';
import path from 'path';
export default defineConfig({
plugins: [react()],
root: './src/renderer',
base: './',
build: {
outDir: '../../dist/renderer',
emptyOutDir: true,
},
css: {
postcss: {
plugins: [tailwindcss(), autoprefixer()],
},
},
server: {
port: 5173,
hmr: {
overlay: false,
},
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src/renderer'),
},
},
});

19
scripts/check-schema.js Normal file
View file

@ -0,0 +1,19 @@
const { Pool } = require('pg');
require('dotenv').config();
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
async function check() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL?.replace('?sslmode=require', ''), ssl: true });
// Check existing columns in users table
const cols = await pool.query(`SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'users' ORDER BY ordinal_position`);
console.log('Users table columns:');
cols.rows.forEach(r => console.log(' -', r.column_name, '(' + r.data_type + ')'));
// Check existing tables
const tables = await pool.query(`SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'`);
console.log('\nExisting tables:', tables.rows.map(r => r.table_name).join(', '));
await pool.end();
}
check();

89
scripts/create-tables.js Normal file
View file

@ -0,0 +1,89 @@
const { Pool } = require('pg');
require('dotenv').config();
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
async function createMissingTables() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL?.replace('?sslmode=require', ''), ssl: true });
console.log('Creating missing tables for AeThex Connect...\n');
// Create servers table
console.log('Creating servers table...');
await pool.query(`
CREATE TABLE IF NOT EXISTS servers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
description TEXT,
icon_url TEXT,
banner_url TEXT,
owner_id UUID NOT NULL,
invite_code VARCHAR(20) UNIQUE,
is_public BOOLEAN DEFAULT false,
member_count INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
`);
console.log(' ✓ servers created');
// Create server_members table
console.log('Creating server_members table...');
await pool.query(`
CREATE TABLE IF NOT EXISTS server_members (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
server_id UUID NOT NULL REFERENCES servers(id) ON DELETE CASCADE,
user_id UUID NOT NULL,
nickname VARCHAR(100),
role VARCHAR(20) CHECK (role IN ('owner', 'admin', 'moderator', 'member')) DEFAULT 'member',
joined_at TIMESTAMP DEFAULT NOW(),
CONSTRAINT unique_server_member UNIQUE(server_id, user_id)
)
`);
console.log(' ✓ server_members created');
// Create indexes
console.log('Creating indexes...');
await pool.query(`CREATE INDEX IF NOT EXISTS idx_servers_owner_id ON servers(owner_id)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_servers_invite_code ON servers(invite_code)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_server_members_server_id ON server_members(server_id)`);
await pool.query(`CREATE INDEX IF NOT EXISTS idx_server_members_user_id ON server_members(user_id)`);
console.log(' ✓ indexes created');
// Check existing channels table structure
console.log('\nChecking channels table structure...');
const channelCols = await pool.query(`
SELECT column_name FROM information_schema.columns WHERE table_name = 'channels'
`);
const cols = channelCols.rows.map(r => r.column_name);
console.log(' Columns:', cols.join(', '));
// Add server_id to channels if missing
if (!cols.includes('server_id')) {
console.log(' Adding server_id column to channels...');
await pool.query(`ALTER TABLE channels ADD COLUMN server_id UUID REFERENCES servers(id) ON DELETE CASCADE`);
console.log(' ✓ server_id added');
}
// Check messages table structure
console.log('\nChecking messages table structure...');
const msgCols = await pool.query(`
SELECT column_name FROM information_schema.columns WHERE table_name = 'messages'
`);
const msgColNames = msgCols.rows.map(r => r.column_name);
console.log(' Columns:', msgColNames.join(', '));
// Add channel_id to messages if missing
if (!msgColNames.includes('channel_id')) {
console.log(' Adding channel_id column to messages...');
await pool.query(`ALTER TABLE messages ADD COLUMN channel_id UUID REFERENCES channels(id) ON DELETE CASCADE`);
console.log(' ✓ channel_id added');
}
console.log('\n✅ All tables ready!');
await pool.end();
}
createMissingTables().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});

38
scripts/fix-channels.js Normal file
View file

@ -0,0 +1,38 @@
const { Pool } = require('pg');
require('dotenv').config();
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
async function fixChannels() {
const pool = new Pool({ connectionString: process.env.DATABASE_URL?.replace('?sslmode=require', ''), ssl: true });
console.log('Adding missing columns to channels table...');
await pool.query(`ALTER TABLE channels ADD COLUMN IF NOT EXISTS channel_type VARCHAR(20) DEFAULT 'text'`);
console.log(' ✓ channel_type');
await pool.query(`ALTER TABLE channels ADD COLUMN IF NOT EXISTS position INTEGER DEFAULT 0`);
console.log(' ✓ position');
await pool.query(`ALTER TABLE channels ADD COLUMN IF NOT EXISTS description TEXT`);
console.log(' ✓ description');
await pool.query(`ALTER TABLE channels ADD COLUMN IF NOT EXISTS is_private BOOLEAN DEFAULT false`);
console.log(' ✓ is_private');
await pool.query(`ALTER TABLE channels ADD COLUMN IF NOT EXISTS slowmode_seconds INTEGER DEFAULT 0`);
console.log(' ✓ slowmode_seconds');
await pool.query(`ALTER TABLE channels ADD COLUMN IF NOT EXISTS created_at TIMESTAMP DEFAULT NOW()`);
console.log(' ✓ created_at');
await pool.query(`ALTER TABLE channels ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT NOW()`);
console.log(' ✓ updated_at');
console.log('\n✅ Channels table updated!');
await pool.end();
}
fixChannels().catch(err => {
console.error('Error:', err.message);
process.exit(1);
});

61
scripts/run-migration.js Normal file
View file

@ -0,0 +1,61 @@
const { Pool } = require('pg');
const fs = require('fs');
const path = require('path');
require('dotenv').config();
// Disable SSL verification for Supabase pooler
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
async function runMigration() {
const connectionString = process.env.DATABASE_URL?.replace('?sslmode=require', '');
const pool = new Pool({
connectionString,
ssl: true
});
try {
console.log('Connecting to database...');
await pool.query('SELECT 1');
console.log('Connected!\n');
// Read the migration file
const migrationPath = path.join(__dirname, '../supabase/migrations/20260206000000_users_and_servers.sql');
const sql = fs.readFileSync(migrationPath, 'utf8');
console.log('Running users_and_servers migration...\n');
// Execute the entire SQL file as a single transaction
await pool.query(sql);
console.log('✅ Migration complete!');
// Verify tables exist
const tables = await pool.query(`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('users', 'servers', 'channels', 'messages', 'server_members')
`);
console.log('\nVerified tables created:');
tables.rows.forEach(row => console.log(' ✓', row.table_name));
} catch (error) {
console.error('Migration error:', error.message);
// If tables already exist error, try checking what exists
if (error.message.includes('already exists')) {
console.log('\nSome tables already exist. Checking current state...');
const tables = await pool.query(`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
`);
console.log('Existing tables:', tables.rows.map(r => r.table_name).join(', '));
}
} finally {
await pool.end();
}
}
runMigration();

View file

@ -1,5 +1,8 @@
require('dotenv').config();
// Disable SSL verification for Supabase pooler (development)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
// Use in-memory dev DB if DATABASE_URL is not configured
let pool = null;
let useDevDb = false;

View file

@ -6,47 +6,8 @@ const jwt = require('jsonwebtoken');
*/
function authenticateUser(req, res, next) {
try {
// In development mode, allow requests without auth for testing
// SECURITY: This bypass only works if BOTH conditions are met:
// 1. NODE_ENV is 'development'
// 2. ALLOW_DEV_BYPASS is explicitly set to 'true'
const isDevelopment = process.env.NODE_ENV === 'development';
const allowDevBypass = process.env.ALLOW_DEV_BYPASS === 'true';
if (isDevelopment && allowDevBypass) {
// Check for token, but if not present, use demo user
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
// Use demo user for development
console.warn('⚠️ Development mode: Using demo user without authentication');
req.user = {
id: 'demo-user-123',
email: 'demo@aethex.dev'
};
return next();
}
const token = authHeader.substring(7);
// Try to verify, but don't fail if invalid
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = {
id: decoded.userId,
email: decoded.email
};
} catch {
// Use demo user if token invalid
console.warn('⚠️ Development mode: Invalid token, using demo user');
req.user = {
id: 'demo-user-123',
email: 'demo@aethex.dev'
};
}
return next();
}
// Production: Strict auth required by default
// In development, still require valid tokens but give better error messages
// Production: Strict auth required
const authHeader = req.headers.authorization;

View file

@ -13,17 +13,20 @@ const router = express.Router();
/**
* POST /api/auth/register
* Register a new user
* Register a new user (supports username or email)
*/
router.post('/register', async (req, res) => {
try {
const { email, password, username, displayName } = req.body;
// Support both email and username-only registration
const userIdentifier = username || email;
// Validation
if (!email || !password) {
if (!userIdentifier || !password) {
return res.status(400).json({
success: false,
error: 'Email and password are required'
error: 'Username/email and password are required'
});
}
@ -34,24 +37,11 @@ router.post('/register', async (req, res) => {
});
}
// 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) {
// Check if username is taken
const finalUsername = username?.toLowerCase() || email?.split('@')[0].toLowerCase();
const existingUsername = await db.query(
'SELECT id FROM users WHERE username = $1',
[username.toLowerCase()]
[finalUsername]
);
if (existingUsername.rows.length > 0) {
@ -60,25 +50,24 @@ router.post('/register', async (req, res) => {
error: 'This username is already taken'
});
}
}
// Hash password
const salt = await bcrypt.genSalt(12);
const passwordHash = await bcrypt.hash(password, salt);
// Create user
// Create user (using 'password' column to match existing schema)
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]]
`INSERT INTO users (username, password, display_name, status, created_at)
VALUES ($1, $2, $3, 'online', NOW())
RETURNING id, username, display_name, created_at`,
[finalUsername, passwordHash, displayName || finalUsername]
);
const user = result.rows[0];
// Generate JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
{ userId: user.id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
@ -88,7 +77,6 @@ router.post('/register', async (req, res) => {
data: {
user: {
id: user.id,
email: user.email,
username: user.username,
displayName: user.display_name
},
@ -106,57 +94,60 @@ router.post('/register', async (req, res) => {
/**
* POST /api/auth/login
* Login with email and password
* Login with username and password
*/
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const { email, password, username } = req.body;
// Support both email and username login
const loginId = username || email;
// Validation
if (!email || !password) {
if (!loginId || !password) {
return res.status(400).json({
success: false,
error: 'Email and password are required'
error: 'Username and password are required'
});
}
// Find user
// Find user by username
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()]
`SELECT id, username, password, display_name, verified_domain, avatar_url, premium_tier, status
FROM users WHERE username = $1`,
[loginId.toLowerCase()]
);
if (result.rows.length === 0) {
return res.status(401).json({
success: false,
error: 'Invalid email or password'
error: 'Invalid username or password'
});
}
const user = result.rows[0];
// Verify password
const isValidPassword = await bcrypt.compare(password, user.password_hash);
// Verify password (column is 'password' not 'password_hash')
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
error: 'Invalid email or password'
error: 'Invalid username or password'
});
}
// Generate JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
{ userId: user.id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
// Update last login
// Update status to online
await db.query(
'UPDATE users SET last_login = NOW() WHERE id = $1',
[user.id]
'UPDATE users SET status = $1 WHERE id = $2',
['online', user.id]
);
res.json({
@ -164,12 +155,12 @@ router.post('/login', async (req, res) => {
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
isPremium: user.premium_tier !== 'free',
status: 'online'
},
token
}
@ -189,13 +180,13 @@ router.post('/login', async (req, res) => {
*/
router.post('/demo', async (req, res) => {
try {
const demoEmail = 'demo@aethex.dev';
const demoUsername = 'demo';
// 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]
`SELECT id, username, display_name, verified_domain, avatar_url, premium_tier, status
FROM users WHERE username = $1`,
[demoUsername]
);
let user;
@ -206,10 +197,10 @@ router.post('/demo', async (req, res) => {
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']
`INSERT INTO users (username, password, display_name, verified_domain, status, created_at)
VALUES ($1, $2, $3, $4, 'online', NOW())
RETURNING id, username, display_name, verified_domain, avatar_url, premium_tier, status`,
[demoUsername, passwordHash, 'Demo User', 'demo.aethex']
);
}
@ -217,7 +208,7 @@ router.post('/demo', async (req, res) => {
// Generate JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
{ userId: user.id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
@ -227,12 +218,12 @@ router.post('/demo', async (req, res) => {
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
isPremium: user.premium_tier !== 'free',
status: user.status
},
token
}

View file

@ -0,0 +1,411 @@
/**
* Server Routes
* API endpoints for servers (guilds), channels, and members
*/
const express = require('express');
const router = express.Router();
const { authenticateUser } = require('../middleware/auth');
const db = require('../database/db');
const { v4: uuidv4 } = require('uuid');
/**
* GET /api/servers
* Get all servers the current user is a member of
*/
router.get('/', authenticateUser, async (req, res) => {
try {
const result = await db.query(`
SELECT s.*, sm.role, sm.nickname, sm.joined_at as member_joined_at
FROM servers s
INNER JOIN server_members sm ON s.id = sm.server_id
WHERE sm.user_id = $1
ORDER BY sm.joined_at ASC
`, [req.user.id]);
res.json({
success: true,
servers: result.rows
});
} catch (error) {
console.error('Failed to get servers:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* POST /api/servers
* Create a new server
*/
router.post('/', authenticateUser, async (req, res) => {
try {
const { name, description, icon_url, is_public } = req.body;
if (!name || name.trim().length === 0) {
return res.status(400).json({
success: false,
error: 'Server name is required'
});
}
// Generate invite code
const invite_code = uuidv4().substring(0, 8).toUpperCase();
// Create server
const serverResult = await db.query(`
INSERT INTO servers (name, description, icon_url, owner_id, invite_code, is_public)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *
`, [name.trim(), description || null, icon_url || null, req.user.id, invite_code, is_public || false]);
const server = serverResult.rows[0];
// Add owner as member
await db.query(`
INSERT INTO server_members (server_id, user_id, role)
VALUES ($1, $2, 'owner')
`, [server.id, req.user.id]);
// Create default channels
await db.query(`
INSERT INTO channels (server_id, name, channel_type, position) VALUES
($1, 'general', 'text', 0),
($1, 'voice', 'voice', 1)
`, [server.id]);
res.json({
success: true,
server
});
} catch (error) {
console.error('Failed to create server:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* GET /api/servers/:id
* Get server details
*/
router.get('/:id', authenticateUser, async (req, res) => {
try {
// Check if user is a member
const memberCheck = await db.query(`
SELECT * FROM server_members WHERE server_id = $1 AND user_id = $2
`, [req.params.id, req.user.id]);
if (memberCheck.rows.length === 0) {
return res.status(403).json({
success: false,
error: 'Not a member of this server'
});
}
const result = await db.query(`
SELECT * FROM servers WHERE id = $1
`, [req.params.id]);
if (result.rows.length === 0) {
return res.status(404).json({
success: false,
error: 'Server not found'
});
}
res.json({
success: true,
server: result.rows[0]
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* POST /api/servers/join/:inviteCode
* Join a server by invite code
*/
router.post('/join/:inviteCode', authenticateUser, async (req, res) => {
try {
const server = await db.query(`
SELECT * FROM servers WHERE invite_code = $1
`, [req.params.inviteCode.toUpperCase()]);
if (server.rows.length === 0) {
return res.status(404).json({
success: false,
error: 'Invalid invite code'
});
}
// Check if already a member
const existingMember = await db.query(`
SELECT * FROM server_members WHERE server_id = $1 AND user_id = $2
`, [server.rows[0].id, req.user.id]);
if (existingMember.rows.length > 0) {
return res.json({
success: true,
server: server.rows[0],
message: 'Already a member'
});
}
// Add as member
await db.query(`
INSERT INTO server_members (server_id, user_id, role)
VALUES ($1, $2, 'member')
`, [server.rows[0].id, req.user.id]);
// Update member count
await db.query(`
UPDATE servers SET member_count = member_count + 1 WHERE id = $1
`, [server.rows[0].id]);
res.json({
success: true,
server: server.rows[0]
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* GET /api/servers/:id/channels
* Get channels for a server
*/
router.get('/:id/channels', authenticateUser, async (req, res) => {
try {
// Check membership
const memberCheck = await db.query(`
SELECT * FROM server_members WHERE server_id = $1 AND user_id = $2
`, [req.params.id, req.user.id]);
if (memberCheck.rows.length === 0) {
return res.status(403).json({
success: false,
error: 'Not a member of this server'
});
}
const channels = await db.query(`
SELECT * FROM channels WHERE server_id = $1 ORDER BY position ASC
`, [req.params.id]);
res.json({
success: true,
channels: channels.rows
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* POST /api/servers/:id/channels
* Create a new channel in a server
*/
router.post('/:id/channels', authenticateUser, async (req, res) => {
try {
const { name, description, channel_type } = req.body;
// Check if user is admin/owner
const memberCheck = await db.query(`
SELECT * FROM server_members WHERE server_id = $1 AND user_id = $2 AND role IN ('owner', 'admin')
`, [req.params.id, req.user.id]);
if (memberCheck.rows.length === 0) {
return res.status(403).json({
success: false,
error: 'Permission denied'
});
}
// Get next position
const posResult = await db.query(`
SELECT COALESCE(MAX(position), -1) + 1 as next_pos FROM channels WHERE server_id = $1
`, [req.params.id]);
const channel = await db.query(`
INSERT INTO channels (server_id, name, description, channel_type, position)
VALUES ($1, $2, $3, $4, $5)
RETURNING *
`, [req.params.id, name, description || null, channel_type || 'text', posResult.rows[0].next_pos]);
res.json({
success: true,
channel: channel.rows[0]
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* GET /api/servers/:id/members
* Get members of a server
*/
router.get('/:id/members', authenticateUser, async (req, res) => {
try {
// Check membership
const memberCheck = await db.query(`
SELECT * FROM server_members WHERE server_id = $1 AND user_id = $2
`, [req.params.id, req.user.id]);
if (memberCheck.rows.length === 0) {
return res.status(403).json({
success: false,
error: 'Not a member of this server'
});
}
const members = await db.query(`
SELECT u.id, u.username, u.display_name, u.avatar_url, u.status, u.custom_status,
sm.role, sm.nickname, sm.joined_at
FROM server_members sm
INNER JOIN users u ON sm.user_id = u.id
WHERE sm.server_id = $1
ORDER BY
CASE sm.role
WHEN 'owner' THEN 0
WHEN 'admin' THEN 1
WHEN 'moderator' THEN 2
ELSE 3
END,
sm.joined_at ASC
`, [req.params.id]);
res.json({
success: true,
members: members.rows
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* GET /api/channels/:channelId/messages
* Get messages from a channel
*/
router.get('/channels/:channelId/messages', authenticateUser, async (req, res) => {
try {
const { limit = 50, before } = req.query;
// Get channel and check membership
const channel = await db.query(`SELECT server_id FROM channels WHERE id = $1`, [req.params.channelId]);
if (channel.rows.length === 0) {
return res.status(404).json({ success: false, error: 'Channel not found' });
}
const memberCheck = await db.query(`
SELECT * FROM server_members WHERE server_id = $1 AND user_id = $2
`, [channel.rows[0].server_id, req.user.id]);
if (memberCheck.rows.length === 0) {
return res.status(403).json({ success: false, error: 'Not a member' });
}
let query = `
SELECT m.*, u.username, u.display_name, u.avatar_url
FROM messages m
INNER JOIN users u ON m.user_id = u.id
WHERE m.channel_id = $1
`;
const params = [req.params.channelId];
if (before) {
query += ` AND m.created_at < $2`;
params.push(before);
}
query += ` ORDER BY m.created_at DESC LIMIT $${params.length + 1}`;
params.push(parseInt(limit));
const messages = await db.query(query, params);
res.json({
success: true,
messages: messages.rows.reverse()
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
/**
* POST /api/channels/:channelId/messages
* Send a message to a channel
*/
router.post('/channels/:channelId/messages', authenticateUser, async (req, res) => {
try {
const { content } = req.body;
if (!content || content.trim().length === 0) {
return res.status(400).json({ success: false, error: 'Message content is required' });
}
// Get channel and check membership
const channel = await db.query(`SELECT server_id FROM channels WHERE id = $1`, [req.params.channelId]);
if (channel.rows.length === 0) {
return res.status(404).json({ success: false, error: 'Channel not found' });
}
const memberCheck = await db.query(`
SELECT * FROM server_members WHERE server_id = $1 AND user_id = $2
`, [channel.rows[0].server_id, req.user.id]);
if (memberCheck.rows.length === 0) {
return res.status(403).json({ success: false, error: 'Not a member' });
}
const message = await db.query(`
INSERT INTO messages (channel_id, user_id, content)
VALUES ($1, $2, $3)
RETURNING *
`, [req.params.channelId, req.user.id, content.trim()]);
// Get user info
const user = await db.query(`SELECT username, display_name, avatar_url FROM users WHERE id = $1`, [req.user.id]);
res.json({
success: true,
message: {
...message.rows[0],
...user.rows[0]
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
module.exports = router;

View file

@ -19,6 +19,7 @@ const realtimeMessagesRoutes = require('./routes/realtimeMessagesRoutes');
const voiceCallRoutes = require('./routes/voiceCallRoutes');
const liveKitRoutes = require('./routes/liveKitRoutes');
const chatRoutes = require('./routes/chatRoutes');
const serverRoutes = require('./routes/serverRoutes');
const socketService = require('./services/socketService');
const path = require('path');
@ -91,6 +92,7 @@ app.use('/api/realtime/messages', realtimeMessagesRoutes);
app.use('/api/voice', voiceCallRoutes);
app.use('/api/livekit', liveKitRoutes);
app.use('/api/chat', chatRoutes);
app.use('/api/servers', serverRoutes);
// Initialize Socket.io
const io = socketService.initialize(httpServer);