new file: src/frontend/contexts/MessagingContext.jsx

This commit is contained in:
Anderson 2026-02-05 15:17:56 +00:00 committed by GitHub
parent d4456915f0
commit 839d68c20f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
72 changed files with 22125 additions and 246 deletions

View file

@ -1,84 +1,33 @@
import React from 'react';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import DomainVerification from './components/DomainVerification';
import VerifiedDomainBadge from './components/VerifiedDomainBadge';
import './App.css';
import { MessagingProvider } from './contexts/MessagingContext';
import MainLayout from './mockup/MainLayout';
import './index.css';
/**
* Main application component
* Demo of domain verification feature
* AeThex Connect - Discord-style communication platform
*/
function App() {
function AppContent() {
const { user, loading } = useAuth();
const [showVerification, setShowVerification] = React.useState(false);
if (loading) {
return <div className="loading-screen">Loading...</div>;
}
return (
<div className="app">
<header className="app-header">
<h1>AeThex Passport</h1>
<p>Domain Verification</p>
</header>
<main className="app-main">
{user && (
<div className="user-profile">
<div className="profile-header">
<h2>{user.email}</h2>
{user.verifiedDomain && (
<VerifiedDomainBadge
verifiedDomain={user.verifiedDomain}
verifiedAt={user.domainVerifiedAt}
/>
)}
</div>
<div className="profile-section">
<button
onClick={() => setShowVerification(!showVerification)}
className="toggle-button"
>
{showVerification ? 'Hide' : 'Show'} Domain Verification
</button>
{showVerification && (
<div className="verification-container">
<DomainVerification />
</div>
)}
</div>
</div>
)}
<div className="info-section">
<h3>About Domain Verification</h3>
<p>
Domain verification allows you to prove ownership of a domain by adding
a DNS TXT record or connecting a wallet that owns a .aethex blockchain domain.
</p>
<ul>
<li> Verify traditional domains via DNS TXT records</li>
<li> Verify .aethex domains via blockchain</li>
<li> Display verified domain on your profile</li>
<li> Prevent domain impersonation</li>
</ul>
</div>
</main>
<footer className="app-footer">
<p>&copy; 2026 AeThex Corporation. All rights reserved.</p>
</footer>
<div className="flex items-center justify-center h-screen bg-zinc-900">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-purple-500"></div>
</div>
);
}
return <MainLayout />;
}
export default function AppWrapper() {
export default function App() {
return (
<AuthProvider>
<App />
<MessagingProvider>
<AppContent />
</MessagingProvider>
</AuthProvider>
);
}

View file

@ -14,7 +14,8 @@
display: flex;
flex-direction: column;
align-items: center;
justify-cont#0a0a0f;
justify-content: center;
background: #0a0a0f;
color: #e4e4e7;
}

View file

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

View file

@ -1,3 +1,5 @@
@import "tailwindcss";
* {
margin: 0;
padding: 0;
@ -5,39 +7,25 @@
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background: #000000;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0a0a;
color: #e4e4e7;
line-height: 1.5;
}
code {
font-family: 'Fira Code', 'Consolas', 'Monaco', 'Courier New', monospace;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
width: 8px;
}
::-webkit-scrollbar-track {
background: #18181b;
background: #111;
}
::-webkit-scrollbar-thumb {
background: #3f3f46;
border-radius: 5px;
background: #333;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #52525b;
}
::selection {
background: rgba(139, 92, 246, 0.3);
color: #e4e4e7;
background: #444;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,65 +1,112 @@
import React from "react";
import React, { useState } from 'react';
import UserSettingsBar from './UserSettingsBar';
import VoiceChannel from './VoiceChannel';
const channels = [
{ category: 'Announcements', items: [
{ id: 'updates', icon: '📢', name: 'updates', badge: 3 },
{ id: 'changelog', icon: '📜', name: 'changelog' },
]},
{ category: 'Development', items: [
{ id: 'general', icon: '#', name: 'general' },
{ id: 'api-discussion', icon: '#', name: 'api-discussion' },
{ id: 'passport-development', icon: '#', name: 'passport-development' },
]},
{ category: 'Support', items: [
{ id: 'help', icon: '❓', name: 'help' },
{ id: 'bug-reports', icon: '🐛', name: 'bug-reports' },
]},
];
const voiceChannels = [
{ id: 'nexus-lounge', name: 'Nexus Lounge', users: [
{ id: 1, name: 'Trevor', avatar: 'T', color: '#ff0000', speaking: true, muted: false },
{ id: 2, name: 'Sarah', avatar: 'S', color: '#ffa500', speaking: false, muted: true },
{ id: 3, name: 'DevUser_2847', avatar: 'D', color: '#666', speaking: false, muted: false, streaming: true },
]},
{ id: 'collab-space', name: 'Collab Space', users: [] },
];
export default function ChannelSidebar({ onCreateChannel, onSettingsClick, onJoinVoice }) {
const [activeChannel, setActiveChannel] = useState('general');
const [expandedVoice, setExpandedVoice] = useState(['nexus-lounge']);
const [hoveredCategory, setHoveredCategory] = useState(null);
export default function ChannelSidebar() {
return (
<div className="channel-sidebar w-72 bg-[#0f0f0f] border-r border-[#1a1a1a] flex flex-col">
{/* Server Header */}
<div className="server-header p-4 border-b border-[#1a1a1a] font-bold text-base flex items-center justify-between">
<div className="channel-sidebar">
<div className="server-header">
<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>
<span className="server-badge foundation">Official</span>
</div>
{/* Channel List */}
<div className="channel-list flex-1 overflow-y-auto py-2">
<div className="channel-category px-4 pt-4 pb-2 text-xs uppercase tracking-widest text-gray-500 font-bold">Announcements</div>
<div className="channel-item flex items-center gap-2 px-4 py-2 mx-2 rounded cursor-pointer text-sm hover:bg-[#1a1a1a]">
<span className="channel-icon">📢</span>
<span className="channel-name flex-1">updates</span>
<span className="channel-badge text-xs bg-red-600 text-white px-2 rounded-full">3</span>
<div className="channel-list">
{channels.map((category) => (
<div key={category.category}>
<div
className="channel-category"
onMouseEnter={() => setHoveredCategory(category.category)}
onMouseLeave={() => setHoveredCategory(null)}
>
{category.category}
{hoveredCategory === category.category && onCreateChannel && (
<button
className="category-add-btn"
onClick={(e) => { e.stopPropagation(); onCreateChannel(); }}
title="Create Channel"
>
+
</button>
)}
</div>
<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>
{category.items.map((channel) => (
<div
key={channel.id}
className={`channel-item${activeChannel === channel.id ? ' active' : ''}`}
onClick={() => setActiveChannel(channel.id)}
>
<span className="channel-icon">{channel.icon}</span>
<span className="channel-name">{channel.name}</span>
{channel.badge && <span className="channel-badge">{channel.badge}</span>}
</div>
<div 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>
))}
{/* Voice Channels */}
<div
className="channel-category"
onMouseEnter={() => setHoveredCategory('voice')}
onMouseLeave={() => setHoveredCategory(null)}
>
Voice Channels
{hoveredCategory === 'voice' && onCreateChannel && (
<button
className="category-add-btn"
onClick={(e) => { e.stopPropagation(); onCreateChannel(); }}
title="Create Channel"
>
+
</button>
)}
</div>
{voiceChannels.map((vc) => (
<VoiceChannel
key={vc.id}
channel={vc}
expanded={expandedVoice.includes(vc.id)}
onToggle={() => {
setExpandedVoice((prev) =>
prev.includes(vc.id)
? prev.filter((id) => id !== vc.id)
: [...prev, vc.id]
);
}}
onJoin={onJoinVoice}
/>
))}
</div>
<UserSettingsBar onSettingsClick={onSettingsClick} />
</div>
);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,107 @@
import React, { useState } from 'react';
const categories = [
{ id: 'trending', label: 'Trending', icon: '🔥' },
{ id: 'reactions', label: 'Reactions', icon: '😂' },
{ id: 'actions', label: 'Actions', icon: '👋' },
{ id: 'anime', label: 'Anime', icon: '🎌' },
{ id: 'memes', label: 'Memes', icon: '🐸' },
{ id: 'cats', label: 'Cats', icon: '🐱' },
{ id: 'dogs', label: 'Dogs', icon: '🐕' },
{ id: 'gaming', label: 'Gaming', icon: '🎮' },
];
const mockGifs = [
{ id: 1, url: '#', preview: '👍', title: 'Thumbs Up' },
{ id: 2, url: '#', preview: '😂', title: 'Laughing' },
{ id: 3, url: '#', preview: '🎉', title: 'Celebration' },
{ id: 4, url: '#', preview: '❤️', title: 'Heart' },
{ id: 5, url: '#', preview: '🔥', title: 'Fire' },
{ id: 6, url: '#', preview: '👀', title: 'Eyes' },
{ id: 7, url: '#', preview: '💀', title: 'Skull' },
{ id: 8, url: '#', preview: '🙌', title: 'Raised Hands' },
{ id: 9, url: '#', preview: '😭', title: 'Crying' },
{ id: 10, url: '#', preview: '🤔', title: 'Thinking' },
{ id: 11, url: '#', preview: '👏', title: 'Clapping' },
{ id: 12, url: '#', preview: '😎', title: 'Cool' },
];
export default function GifPicker({ onSelect, onClose }) {
const [search, setSearch] = useState('');
const [activeCategory, setActiveCategory] = useState('trending');
const [favorites, setFavorites] = useState([1, 5]);
const filteredGifs = search
? mockGifs.filter(g => g.title.toLowerCase().includes(search.toLowerCase()))
: mockGifs;
return (
<div className="gif-picker">
<div className="gif-picker-header">
<div className="gif-search">
<span className="search-icon">🔍</span>
<input
type="text"
placeholder="Search Tenor"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{search && (
<button className="clear-search" onClick={() => setSearch('')}></button>
)}
</div>
</div>
{!search && (
<div className="gif-categories">
{categories.map(cat => (
<button
key={cat.id}
className={`gif-category ${activeCategory === cat.id ? 'active' : ''}`}
onClick={() => setActiveCategory(cat.id)}
title={cat.label}
>
<span className="cat-icon">{cat.icon}</span>
<span className="cat-label">{cat.label}</span>
</button>
))}
</div>
)}
<div className="gif-tabs">
<button className="gif-tab active">GIFs</button>
<button className="gif-tab">Favorites</button>
</div>
<div className="gif-grid">
{filteredGifs.map(gif => (
<div
key={gif.id}
className="gif-item"
onClick={() => onSelect?.(gif)}
title={gif.title}
>
<div className="gif-preview">{gif.preview}</div>
<button
className={`gif-favorite ${favorites.includes(gif.id) ? 'active' : ''}`}
onClick={(e) => {
e.stopPropagation();
setFavorites(prev =>
prev.includes(gif.id)
? prev.filter(id => id !== gif.id)
: [...prev, gif.id]
);
}}
>
{favorites.includes(gif.id) ? '⭐' : '☆'}
</button>
</div>
))}
</div>
<div className="gif-footer">
<span className="powered-by">Powered by Tenor</span>
</div>
</div>
);
}

View file

@ -0,0 +1,185 @@
import React, { useState } from 'react';
const mockInvites = [
{ code: 'aethex123', channel: 'general', uses: 45, maxUses: 0, expiresAt: null, createdBy: 'Trevor', createdAt: '2025-01-10T10:00:00' },
{ code: 'devjoin', channel: 'api-discussion', uses: 12, maxUses: 50, expiresAt: '2026-02-15T00:00:00', createdBy: 'Sarah', createdAt: '2025-02-01T14:30:00' },
{ code: 'labsonly', channel: 'labs-chat', uses: 8, maxUses: 25, expiresAt: '2026-03-01T00:00:00', createdBy: 'DevUser_2847', createdAt: '2025-02-03T09:15:00' },
{ code: 'temp7day', channel: 'help', uses: 3, maxUses: 10, expiresAt: '2026-02-12T00:00:00', createdBy: 'Trevor', createdAt: '2025-02-05T16:45:00' },
];
export default function InviteManager({ onClose, onCreate }) {
const [invites, setInvites] = useState(mockInvites);
const [showCreateModal, setShowCreateModal] = useState(false);
const [copiedCode, setCopiedCode] = useState(null);
// Create invite form state
const [selectedChannel, setSelectedChannel] = useState('general');
const [maxUses, setMaxUses] = useState(0);
const [expiresIn, setExpiresIn] = useState('never');
const channels = ['general', 'api-discussion', 'help', 'announcements', 'labs-chat'];
const formatDate = (dateStr) => {
if (!dateStr) return 'Never';
const date = new Date(dateStr);
const now = new Date();
const diff = date.getTime() - now.getTime();
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
if (days < 0) return 'Expired';
if (days === 0) return 'Today';
if (days === 1) return 'Tomorrow';
return `${days} days`;
};
const copyInvite = (code) => {
navigator.clipboard.writeText(`https://aethex.gg/invite/${code}`);
setCopiedCode(code);
setTimeout(() => setCopiedCode(null), 2000);
};
const deleteInvite = (code) => {
setInvites(prev => prev.filter(inv => inv.code !== code));
};
const createInvite = () => {
const newCode = Math.random().toString(36).substring(2, 10);
const expiresAt = expiresIn === 'never' ? null :
new Date(Date.now() + parseInt(expiresIn) * 1000).toISOString();
setInvites(prev => [{
code: newCode,
channel: selectedChannel,
uses: 0,
maxUses: maxUses,
expiresAt,
createdBy: 'You',
createdAt: new Date().toISOString(),
}, ...prev]);
setShowCreateModal(false);
copyInvite(newCode);
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="invite-manager-modal" onClick={(e) => e.stopPropagation()}>
<div className="invite-manager-header">
<h2> Server Invites</h2>
<button className="invite-manager-close" onClick={onClose}></button>
</div>
<div className="invite-manager-actions">
<button className="create-invite-btn" onClick={() => setShowCreateModal(true)}>
+ Create Invite
</button>
</div>
<div className="invite-list">
{invites.length === 0 ? (
<div className="no-invites">
<p>No active invites</p>
<span>Create an invite to share your server</span>
</div>
) : (
<>
<div className="invite-list-header">
<span className="col-code">Invite Code</span>
<span className="col-channel">Channel</span>
<span className="col-uses">Uses</span>
<span className="col-expires">Expires</span>
<span className="col-creator">Created By</span>
<span className="col-actions"></span>
</div>
{invites.map(invite => (
<div key={invite.code} className="invite-item">
<div className="invite-code">
<code>{invite.code}</code>
<button
className={`copy-btn ${copiedCode === invite.code ? 'copied' : ''}`}
onClick={() => copyInvite(invite.code)}
>
{copiedCode === invite.code ? '✓' : '📋'}
</button>
</div>
<div className="invite-channel">#{invite.channel}</div>
<div className="invite-uses">
{invite.uses}{invite.maxUses > 0 ? ` / ${invite.maxUses}` : ''}
</div>
<div className="invite-expires">{formatDate(invite.expiresAt)}</div>
<div className="invite-creator">{invite.createdBy}</div>
<div className="invite-actions">
<button
className="delete-invite-btn"
onClick={() => deleteInvite(invite.code)}
title="Delete invite"
>
🗑
</button>
</div>
</div>
))}
</>
)}
</div>
<div className="invite-manager-footer">
<span className="invite-hint">
Invite links: aethex.gg/invite/CODE
</span>
</div>
{showCreateModal && (
<div className="create-invite-overlay" onClick={() => setShowCreateModal(false)}>
<div className="create-invite-modal" onClick={(e) => e.stopPropagation()}>
<h3>Create Invite Link</h3>
<div className="create-field">
<label>Invite to Channel</label>
<select value={selectedChannel} onChange={(e) => setSelectedChannel(e.target.value)}>
{channels.map(ch => (
<option key={ch} value={ch}>#{ch}</option>
))}
</select>
</div>
<div className="create-field">
<label>Expire After</label>
<select value={expiresIn} onChange={(e) => setExpiresIn(e.target.value)}>
<option value="1800">30 minutes</option>
<option value="3600">1 hour</option>
<option value="21600">6 hours</option>
<option value="43200">12 hours</option>
<option value="86400">1 day</option>
<option value="604800">7 days</option>
<option value="never">Never</option>
</select>
</div>
<div className="create-field">
<label>Max Uses</label>
<select value={maxUses} onChange={(e) => setMaxUses(parseInt(e.target.value))}>
<option value="0">No limit</option>
<option value="1">1 use</option>
<option value="5">5 uses</option>
<option value="10">10 uses</option>
<option value="25">25 uses</option>
<option value="50">50 uses</option>
<option value="100">100 uses</option>
</select>
</div>
<div className="create-actions">
<button className="cancel-btn" onClick={() => setShowCreateModal(false)}>
Cancel
</button>
<button className="generate-btn" onClick={createInvite}>
Generate Invite
</button>
</div>
</div>
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,146 @@
import React, { useState } from 'react';
export default function InviteModal({ server, channel, onClose }) {
const [inviteLink, setInviteLink] = useState('https://aethex.gg/invite/Xk9Q3p5');
const [copied, setCopied] = useState(false);
const [expiresIn, setExpiresIn] = useState('7');
const [maxUses, setMaxUses] = useState('0');
const [showAdvanced, setShowAdvanced] = useState(false);
const serverName = server?.name || 'AeThex Foundation';
const channelName = channel?.name || 'general';
const copyLink = () => {
navigator.clipboard.writeText(inviteLink);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
const generateNewLink = () => {
// Simulate generating a new invite code
const code = Math.random().toString(36).substring(2, 8);
setInviteLink(`https://aethex.gg/invite/${code}`);
setCopied(false);
};
const friends = [
{ id: 1, name: 'Sarah', avatar: 'S', gradient: 'linear-gradient(135deg, #ffa500, #ff8c00)', status: 'online' },
{ id: 2, name: 'Marcus', avatar: 'M', gradient: 'linear-gradient(135deg, #0066ff, #003380)', status: 'online' },
{ id: 3, name: 'JohnDev', avatar: 'J', status: 'idle' },
];
return (
<div className="modal-overlay" onClick={onClose}>
<div className="invite-modal" onClick={(e) => e.stopPropagation()}>
<div className="invite-header">
<h2>Invite friends to {serverName}</h2>
<span className="invite-channel">#{channelName}</span>
<button className="modal-close" onClick={onClose}></button>
</div>
<div className="invite-content">
<div className="invite-search">
<input
type="text"
placeholder="Search for friends"
className="friend-search-input"
/>
</div>
<div className="invite-friends-list">
{friends.map((friend) => (
<div key={friend.id} className="invite-friend-item">
<div
className="invite-friend-avatar"
style={{ background: friend.gradient || '#36393f' }}
>
{friend.avatar}
</div>
<span className="invite-friend-name">{friend.name}</span>
<button className="invite-btn">Invite</button>
</div>
))}
</div>
<div className="invite-link-section">
<label>OR, SEND A SERVER INVITE LINK TO A FRIEND</label>
<div className="invite-link-row">
<input
type="text"
value={inviteLink}
readOnly
className="invite-link-input"
/>
<button
className={`copy-link-btn ${copied ? 'copied' : ''}`}
onClick={copyLink}
>
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
<p className="link-expires">
Your invite link expires in 7 days.
<button className="edit-link-btn" onClick={() => setShowAdvanced(!showAdvanced)}>
Edit invite link
</button>
</p>
</div>
{showAdvanced && (
<div className="invite-advanced">
<div className="advanced-option">
<label>EXPIRE AFTER</label>
<select
value={expiresIn}
onChange={(e) => setExpiresIn(e.target.value)}
className="advanced-select"
>
<option value="1800">30 minutes</option>
<option value="3600">1 hour</option>
<option value="21600">6 hours</option>
<option value="43200">12 hours</option>
<option value="86400">1 day</option>
<option value="7">7 days</option>
<option value="0">Never</option>
</select>
</div>
<div className="advanced-option">
<label>MAX NUMBER OF USES</label>
<select
value={maxUses}
onChange={(e) => setMaxUses(e.target.value)}
className="advanced-select"
>
<option value="0">No limit</option>
<option value="1">1 use</option>
<option value="5">5 uses</option>
<option value="10">10 uses</option>
<option value="25">25 uses</option>
<option value="50">50 uses</option>
<option value="100">100 uses</option>
</select>
</div>
<label className="temporary-toggle">
<input type="checkbox" />
<span>Grant temporary membership</span>
</label>
<button className="generate-new-btn" onClick={generateNewLink}>
Generate a New Link
</button>
</div>
)}
</div>
<div className="invite-footer">
<div className="invite-tip">
<span className="tip-icon">💡</span>
<span>Customize your invite link with Server Boost</span>
</div>
</div>
</div>
</div>
);
}

View file

@ -1,17 +1,199 @@
import React from "react";
import ServerList from "./ServerList";
import ChannelSidebar from "./ChannelSidebar";
import ChatArea from "./ChatArea";
import MemberSidebar from "./MemberSidebar";
import "./global.css";
import React, { useState, useCallback } from 'react';
import ServerList from './ServerList';
import ChannelSidebar from './ChannelSidebar';
import ChatArea from './ChatArea';
import MemberSidebar from './MemberSidebar';
import SearchPanel from './SearchPanel';
import ThreadPanel from './ThreadPanel';
import ServerSettingsModal from './ServerSettingsModal';
import UserProfileCard from './UserProfileCard';
import DMsView from './DMsView';
import UserSettingsModal from './UserSettingsModal';
import ContextMenu from './ContextMenu';
import PinnedMessagesPanel from './PinnedMessagesPanel';
import CreateChannelModal from './CreateChannelModal';
import InviteModal from './InviteModal';
import NotificationsPanel from './NotificationsPanel';
import VideoCall from './VideoCall';
import './mockup.css';
export default function MainLayout() {
// UI State
const [showSearch, setShowSearch] = useState(false);
const [showThread, setShowThread] = useState(false);
const [threadParent, setThreadParent] = useState(null);
const [showServerSettings, setShowServerSettings] = useState(false);
const [showUserProfile, setShowUserProfile] = useState(false);
const [selectedUser, setSelectedUser] = useState(null);
// New feature states
const [showDMs, setShowDMs] = useState(false);
const [showUserSettings, setShowUserSettings] = useState(false);
const [showPinnedMessages, setShowPinnedMessages] = useState(false);
const [showCreateChannel, setShowCreateChannel] = useState(false);
const [showInvite, setShowInvite] = useState(false);
const [showNotifications, setShowNotifications] = useState(false);
const [showVideoCall, setShowVideoCall] = useState(false);
// Context menu state
const [contextMenu, setContextMenu] = useState({ visible: false, x: 0, y: 0, type: 'message', data: null });
// Current server/channel context for modals
const [currentServer, setCurrentServer] = useState({
id: '1',
name: 'AeThex Gaming',
icon: '🎮'
});
const handleOpenSearch = () => {
setShowSearch(true);
setShowThread(false);
};
const handleOpenThread = (message) => {
setThreadParent(message);
setShowThread(true);
setShowSearch(false);
};
const handleOpenUserProfile = (user) => {
setSelectedUser(user);
setShowUserProfile(true);
};
// Context menu handlers
const handleContextMenu = useCallback((e, type, data) => {
e.preventDefault();
setContextMenu({
visible: true,
x: e.clientX,
y: e.clientY,
type,
data
});
}, []);
const closeContextMenu = useCallback(() => {
setContextMenu(prev => ({ ...prev, visible: false }));
}, []);
const handleContextMenuAction = useCallback((action) => {
console.log('Context menu action:', action, contextMenu.data);
closeContextMenu();
// Handle actions
if (action === 'pin') setShowPinnedMessages(true);
if (action === 'create_thread') handleOpenThread(contextMenu.data);
}, [contextMenu.data, closeContextMenu]);
// Video call handlers
const handleJoinVoice = useCallback(() => {
setShowVideoCall(true);
}, []);
return (
<div className="connect-container flex h-screen">
<ServerList />
<ChannelSidebar />
<ChatArea />
<MemberSidebar />
<div className="connect-container" onClick={closeContextMenu}>
{showDMs ? (
<>
<ServerList
onOpenSettings={() => setShowServerSettings(true)}
onDMsClick={() => setShowDMs(true)}
selectedDMs={true}
/>
<DMsView onClose={() => setShowDMs(false)} />
</>
) : (
<>
<ServerList
onOpenSettings={() => setShowServerSettings(true)}
onDMsClick={() => setShowDMs(true)}
/>
<ChannelSidebar
onCreateChannel={() => setShowCreateChannel(true)}
onSettingsClick={() => setShowUserSettings(true)}
onJoinVoice={handleJoinVoice}
/>
<ChatArea
onOpenSearch={handleOpenSearch}
onOpenThread={handleOpenThread}
onPinnedClick={() => setShowPinnedMessages(true)}
onNotificationsClick={() => setShowNotifications(true)}
onContextMenu={handleContextMenu}
/>
<MemberSidebar onMemberClick={handleOpenUserProfile} />
</>
)}
{/* Overlays */}
{showSearch && (
<SearchPanel onClose={() => setShowSearch(false)} />
)}
{showThread && threadParent && (
<ThreadPanel
parentMessage={threadParent}
onClose={() => setShowThread(false)}
/>
)}
{showServerSettings && (
<ServerSettingsModal onClose={() => setShowServerSettings(false)} />
)}
{showUserProfile && selectedUser && (
<UserProfileCard
user={selectedUser}
onClose={() => setShowUserProfile(false)}
/>
)}
{/* New Modals */}
{showUserSettings && (
<UserSettingsModal onClose={() => setShowUserSettings(false)} />
)}
{showPinnedMessages && (
<PinnedMessagesPanel onClose={() => setShowPinnedMessages(false)} />
)}
{showCreateChannel && (
<CreateChannelModal
onClose={() => setShowCreateChannel(false)}
onSubmit={(data) => {
console.log('Create channel:', data);
setShowCreateChannel(false);
}}
/>
)}
{showInvite && (
<InviteModal
server={currentServer}
onClose={() => setShowInvite(false)}
/>
)}
{showNotifications && (
<NotificationsPanel onClose={() => setShowNotifications(false)} />
)}
{showVideoCall && (
<VideoCall
channel={{ name: 'General Voice' }}
onLeave={() => setShowVideoCall(false)}
/>
)}
{/* Context Menu */}
{contextMenu.visible && (
<ContextMenu
x={contextMenu.x}
y={contextMenu.y}
type={contextMenu.type}
data={contextMenu.data}
onAction={handleContextMenuAction}
onClose={closeContextMenu}
/>
)}
</div>
);
}

View file

@ -0,0 +1,139 @@
import React from 'react';
// Simple markdown-like parsing for Discord-style formatting
export function parseMarkdown(text) {
if (!text) return null;
const elements = [];
let remaining = text;
let key = 0;
const patterns = [
// Code blocks (```)
{ regex: /```(\w+)?\n?([\s\S]*?)```/g, render: (match, lang, code) => (
<pre key={key++} className="code-block" data-language={lang}>
<code>{code}</code>
</pre>
)},
// Inline code (`)
{ regex: /`([^`]+)`/g, render: (match, code) => (
<code key={key++} className="inline-code">{code}</code>
)},
// Spoiler (||text||)
{ regex: /\|\|([^|]+)\|\|/g, render: (match, content) => (
<span key={key++} className="spoiler" onClick={(e) => e.target.classList.toggle('revealed')}>
{content}
</span>
)},
// Bold (**text**)
{ regex: /\*\*([^*]+)\*\*/g, render: (match, content) => (
<strong key={key++}>{content}</strong>
)},
// Italic (*text* or _text_)
{ regex: /(?:\*|_)([^*_]+)(?:\*|_)/g, render: (match, content) => (
<em key={key++}>{content}</em>
)},
// Strikethrough (~~text~~)
{ regex: /~~([^~]+)~~/g, render: (match, content) => (
<del key={key++}>{content}</del>
)},
// Underline (__text__)
{ regex: /__([^_]+)__/g, render: (match, content) => (
<u key={key++}>{content}</u>
)},
// Links [text](url)
{ regex: /\[([^\]]+)\]\(([^)]+)\)/g, render: (match, text, url) => (
<a key={key++} href={url} target="_blank" rel="noopener noreferrer" className="message-link">
{text}
</a>
)},
// Auto-link URLs
{ regex: /(https?:\/\/[^\s<]+)/g, render: (match, url) => (
<a key={key++} href={url} target="_blank" rel="noopener noreferrer" className="message-link">
{url}
</a>
)},
// Mentions (@user)
{ regex: /@(\w+)/g, render: (match, user) => (
<span key={key++} className="mention">@{user}</span>
)},
// Channel mentions (#channel)
{ regex: /#(\w+)/g, render: (match, channel) => (
<span key={key++} className="channel-mention">#{channel}</span>
)},
// Role mentions (@&role)
{ regex: /@&(\w+)/g, render: (match, role) => (
<span key={key++} className="role-mention">@{role}</span>
)},
// Emoji :emoji_name:
{ regex: /:(\w+):/g, render: (match, name) => {
const emojiMap = {
smile: '😊', laughing: '😆', heart: '❤️', fire: '🔥',
rocket: '🚀', thumbsup: '👍', thumbsdown: '👎', check: '✅',
x: '❌', star: '⭐', eyes: '👀', thinking: '🤔',
aethex: '🔺', foundation: '🔴', corporation: '🔵', labs: '🟠',
};
const emoji = emojiMap[name.toLowerCase()];
return emoji ? <span key={key++} className="emoji">{emoji}</span> : match;
}},
// Block quote (> text)
{ regex: /^>\s(.+)$/gm, render: (match, content) => (
<blockquote key={key++} className="block-quote">{content}</blockquote>
)},
];
// Process special patterns
// For simplicity, we'll do a basic render with some patterns
const processedText = text
.replace(/```(\w+)?\n?([\s\S]*?)```/g, '⟦CODE_BLOCK⟧$1⟦SEP⟧$2⟦END_CODE⟧')
.replace(/`([^`]+)`/g, '⟦INLINE_CODE⟧$1⟦END_INLINE⟧')
.replace(/\|\|([^|]+)\|\|/g, '⟦SPOILER⟧$1⟦END_SPOILER⟧')
.replace(/\*\*([^*]+)\*\*/g, '⟦BOLD⟧$1⟦END_BOLD⟧')
.replace(/~~([^~]+)~~/g, '⟦STRIKE⟧$1⟦END_STRIKE⟧')
.replace(/__([^_]+)__/g, '⟦UNDERLINE⟧$1⟦END_UNDERLINE⟧')
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '⟦ITALIC⟧$1⟦END_ITALIC⟧')
.replace(/(?<!_)_([^_]+)_(?!_)/g, '⟦ITALIC⟧$1⟦END_ITALIC⟧');
return <span dangerouslySetInnerHTML={{ __html: renderMarkdown(text) }} />;
}
function renderMarkdown(text) {
let html = text
// Escape HTML
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
// Code blocks
.replace(/```(\w+)?\n?([\s\S]*?)```/g, '<pre class="code-block"><code>$2</code></pre>')
// Inline code
.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
// Spoilers
.replace(/\|\|([^|]+)\|\|/g, '<span class="spoiler">$1</span>')
// Bold
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
// Strikethrough
.replace(/~~([^~]+)~~/g, '<del>$1</del>')
// Underline
.replace(/__([^_]+)__/g, '<u>$1</u>')
// Italic
.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>')
// Mentions
.replace(/@(\w+)/g, '<span class="mention">@$1</span>')
// Channel mentions
.replace(/#(\w[-\w]*)/g, '<span class="channel-mention">#$1</span>')
// Links
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" class="message-link" target="_blank">$1</a>')
// Auto URLs
.replace(/(https?:\/\/[^\s<]+)/g, '<a href="$1" class="message-link" target="_blank">$1</a>')
// Block quotes
.replace(/^&gt;\s(.+)$/gm, '<blockquote class="block-quote">$1</blockquote>')
// Newlines
.replace(/\n/g, '<br>');
return html;
}
export default function MarkdownText({ children }) {
if (typeof children !== 'string') return children;
return parseMarkdown(children);
}

View file

@ -0,0 +1,155 @@
import React, { useState } from 'react';
const mockMembers = [
{ id: 1, name: 'Trevor', tag: '#0001', avatar: 'T', status: 'online', roles: ['founder', 'foundation'], joinedAt: '2024-01-15' },
{ id: 2, name: 'Sarah', tag: '#1234', avatar: 'S', status: 'online', roles: ['foundation', 'corporation'], joinedAt: '2024-02-20' },
{ id: 3, name: 'DevUser_2847', tag: '#9876', avatar: 'D', status: 'idle', roles: ['labs'], joinedAt: '2024-03-10' },
{ id: 4, name: 'Alex', tag: '#5555', avatar: 'A', status: 'dnd', roles: ['corporation'], joinedAt: '2024-04-05' },
{ id: 5, name: 'Jordan', tag: '#7777', avatar: 'J', status: 'offline', roles: [], joinedAt: '2024-05-12' },
{ id: 6, name: 'Casey', tag: '#3333', avatar: 'C', status: 'online', roles: ['labs', 'corporation'], joinedAt: '2024-06-01' },
{ id: 7, name: 'Morgan', tag: '#2222', avatar: 'M', status: 'online', roles: ['foundation'], joinedAt: '2024-06-15' },
{ id: 8, name: 'Riley', tag: '#4444', avatar: 'R', status: 'offline', roles: [], joinedAt: '2024-07-20' },
];
const allRoles = [
{ id: 'founder', name: 'Founder', color: '#ff0000' },
{ id: 'foundation', name: 'Foundation', color: '#ff0000' },
{ id: 'corporation', name: 'Corporation', color: '#0066ff' },
{ id: 'labs', name: 'Labs', color: '#ffa500' },
];
export default function MemberListPanel({ onClose }) {
const [searchQuery, setSearchQuery] = useState('');
const [filterRole, setFilterRole] = useState('all');
const [selectedMember, setSelectedMember] = useState(null);
const [showRoleMenu, setShowRoleMenu] = useState(false);
const filteredMembers = mockMembers.filter(member => {
const matchesSearch = member.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
member.tag.includes(searchQuery);
const matchesRole = filterRole === 'all' || member.roles.includes(filterRole);
return matchesSearch && matchesRole;
});
const getMemberRoles = (member) => {
return allRoles.filter(role => member.roles.includes(role.id));
};
const getStatusColor = (status) => {
const colors = { online: '#3ba55d', idle: '#faa61a', dnd: '#ed4245', offline: '#747f8d' };
return colors[status] || colors.offline;
};
const toggleMemberRole = (memberId, roleId) => {
// Mock implementation - in real app would update backend
console.log(`Toggle role ${roleId} for member ${memberId}`);
};
return (
<div className="member-list-panel">
<div className="member-list-header">
<h2>Members {mockMembers.length}</h2>
<button className="member-list-close" onClick={onClose}></button>
</div>
<div className="member-list-filters">
<input
type="text"
className="member-search"
placeholder="Search members..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<select
className="role-filter"
value={filterRole}
onChange={(e) => setFilterRole(e.target.value)}
>
<option value="all">All Roles</option>
{allRoles.map(role => (
<option key={role.id} value={role.id}>{role.name}</option>
))}
<option value="">No Roles</option>
</select>
</div>
<div className="member-list-content">
{filteredMembers.length === 0 ? (
<div className="no-members">No members found</div>
) : (
filteredMembers.map(member => (
<div
key={member.id}
className={`member-list-item ${selectedMember === member.id ? 'selected' : ''}`}
onClick={() => setSelectedMember(member.id)}
>
<div className="member-avatar-wrapper">
<div className="member-avatar">{member.avatar}</div>
<div
className="member-status-dot"
style={{ background: getStatusColor(member.status) }}
/>
</div>
<div className="member-info">
<div className="member-name-row">
<span className="member-name">{member.name}</span>
<span className="member-tag">{member.tag}</span>
</div>
<div className="member-roles">
{getMemberRoles(member).map(role => (
<span
key={role.id}
className="member-role-badge"
style={{ borderColor: role.color, color: role.color }}
>
{role.name}
</span>
))}
</div>
</div>
<div className="member-actions">
<button
className="member-action-btn"
onClick={(e) => {
e.stopPropagation();
setSelectedMember(member.id);
setShowRoleMenu(!showRoleMenu);
}}
title="Manage Roles"
>
🎭
</button>
<button className="member-action-btn" title="Message">💬</button>
<button className="member-action-btn" title="More"></button>
</div>
{selectedMember === member.id && showRoleMenu && (
<div className="role-assignment-menu" onClick={(e) => e.stopPropagation()}>
<div className="role-menu-header">Manage Roles</div>
{allRoles.map(role => (
<label key={role.id} className="role-menu-item">
<input
type="checkbox"
checked={member.roles.includes(role.id)}
onChange={() => toggleMemberRole(member.id, role.id)}
/>
<span className="role-dot" style={{ background: role.color }}></span>
<span>{role.name}</span>
</label>
))}
</div>
)}
</div>
))
)}
</div>
<div className="member-list-footer">
<button className="prune-members-btn">Prune Members</button>
<button className="invite-members-btn">Invite People</button>
</div>
</div>
);
}

View file

@ -1,38 +1,67 @@
import React from "react";
import React from 'react';
const members = [
{ section: "Foundation Team — 8", users: [
{ name: "Anderson", avatar: "A", status: "online", avatarBg: "from-red-600 to-red-800" },
{ name: "Trevor", avatar: "T", status: "online", avatarBg: "from-red-600 to-red-800" },
]},
{ section: "Labs Team — 12", users: [
{ name: "Sarah", avatar: "S", status: "labs", avatarBg: "from-orange-400 to-orange-700", activity: "Testing v2.0" },
]},
{ section: "Developers — 47", users: [
{ name: "Marcus", avatar: "M", status: "in-game", avatarBg: "bg-[#1a1a1a]", activity: "Building" },
{ name: "DevUser_2847", avatar: "D", status: "online", avatarBg: "bg-[#1a1a1a]" },
]},
{ section: "Community — 61", users: [
{ name: "JohnDev", avatar: "J", status: "offline", avatarBg: "bg-[#1a1a1a]" },
]},
const memberSections = [
{
title: 'Foundation Team',
count: 8,
members: [
{ id: 1, name: 'Anderson', tag: 'Anderson#0001', initial: 'A', gradient: 'linear-gradient(135deg, #ff0000, #cc0000)', status: 'online', banner: '#ff0000', customStatus: '🔥 Building AeThex', badge: 'Founder', roles: [{ name: 'Founder', color: '#ff0000' }, { name: 'Foundation', color: '#cc0000' }], about: 'Creator of AeThex ecosystem. Building the future of developer tools.' },
{ id: 2, name: 'Trevor', tag: 'Trevor#0042', initial: 'T', gradient: 'linear-gradient(135deg, #ff0000, #cc0000)', status: 'online', banner: '#cc0000', badge: 'Foundation', roles: [{ name: 'Foundation', color: '#cc0000' }, { name: 'Developer', color: '#5865f2' }], about: 'Lead developer on authentication systems.' },
],
},
{
title: 'Labs Team',
count: 12,
members: [
{ id: 3, name: 'Sarah', tag: 'Sarah#1337', initial: 'S', gradient: 'linear-gradient(135deg, #ffa500, #ff8c00)', status: 'labs', banner: '#ffa500', activity: 'Testing v2.0', badge: 'Labs', roles: [{ name: 'Labs', color: '#ffa500' }, { name: 'Tester', color: '#3ba55d' }], about: 'Experimental features lead. Breaking things so you don\'t have to.' },
],
},
{
title: 'Developers',
count: 47,
members: [
{ id: 4, name: 'Marcus', tag: 'Marcus#2048', initial: 'M', gradient: 'linear-gradient(135deg, #0066ff, #003380)', status: 'in-game', activity: 'Building', banner: '#0066ff', roles: [{ name: 'Developer', color: '#5865f2' }], about: 'Full-stack developer passionate about clean code.' },
{ id: 5, name: 'DevUser_2847', tag: 'DevUser_2847#2847', initial: 'D', status: 'online', roles: [{ name: 'Member', color: '#99aab5' }] },
],
},
{
title: 'Community',
count: 61,
members: [
{ id: 6, name: 'JohnDev', tag: 'JohnDev#9999', initial: 'J', status: 'idle', roles: [{ name: 'Member', color: '#99aab5' }] },
],
},
];
export default function MemberSidebar() {
export default function MemberSidebar({ onMemberClick }) {
return (
<div className="member-sidebar w-72 bg-[#0f0f0f] border-l border-[#1a1a1a] flex flex-col">
<div className="member-header p-4 border-b border-[#1a1a1a] text-xs uppercase tracking-widest text-gray-500">Members 128</div>
<div className="member-list flex-1 overflow-y-auto py-3">
{members.map((section, i) => (
<div key={i} className="member-section mb-4">
<div className="member-section-title px-4 py-2 text-xs uppercase tracking-wider text-gray-500 font-bold">{section.section}</div>
{section.users.map((user, j) => (
<div key={j} className="member-item flex items-center gap-3 px-4 py-1.5 cursor-pointer hover:bg-[#1a1a1a]">
<div className={`member-avatar-small w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm relative bg-gradient-to-tr ${user.avatarBg}`}>
{user.avatar}
<div className={`online-indicator absolute -bottom-0.5 -right-0.5 w-3 h-3 rounded-full border-2 border-[#0f0f0f] ${user.status === "online" ? "bg-green-400" : user.status === "in-game" ? "bg-blue-500" : user.status === "labs" ? "bg-orange-400" : "bg-gray-700"}`}></div>
<div className="member-sidebar">
<div className="member-header">Members 128</div>
<div className="member-list">
{memberSections.map((section) => (
<div key={section.title} className="member-section">
<div className="member-section-title">{section.title} {section.count}</div>
{section.members.map((member) => (
<div
key={member.id}
className="member-item"
onClick={() => onMemberClick?.(member)}
style={{ cursor: 'pointer' }}
>
<div
className="member-avatar-small"
style={member.gradient ? { background: member.gradient } : undefined}
>
{member.initial}
{member.status && (
<div className={`online-indicator ${member.status}`}></div>
)}
</div>
<div className="member-name flex-1 text-sm">{user.name}</div>
{user.activity && <div className="member-activity text-xs text-gray-500">{user.activity}</div>}
<div className="member-name">{member.name}</div>
{member.activity && (
<div className="member-activity">{member.activity}</div>
)}
</div>
))}
</div>

View file

@ -0,0 +1,84 @@
import React, { useState } from 'react';
export default function MessageActions({ message, onReact, onReply, onEdit, onDelete, onPin, onThread }) {
const [showMore, setShowMore] = useState(false);
const quickReactions = ['👍', '❤️', '😂', '😮', '😢', '😡'];
return (
<div className="message-actions">
<div className="quick-reactions">
{quickReactions.map((emoji, idx) => (
<button
key={idx}
className="reaction-btn"
onClick={() => onReact?.(emoji)}
title={`React with ${emoji}`}
>
{emoji}
</button>
))}
<button className="reaction-btn add-reaction" title="Add Reaction">
😀+
</button>
</div>
<div className="action-buttons">
<button className="action-btn" onClick={() => onReply?.()} title="Reply">
</button>
<button className="action-btn" onClick={() => onThread?.()} title="Create Thread">
🧵
</button>
{message?.isOwn && (
<button className="action-btn" onClick={() => onEdit?.()} title="Edit">
</button>
)}
<button
className="action-btn more-btn"
onClick={() => setShowMore(!showMore)}
title="More"
>
</button>
</div>
{showMore && (
<div className="more-menu">
<div className="menu-item" onClick={() => onPin?.()}>
📌 {message?.isPinned ? 'Unpin' : 'Pin'} Message
</div>
<div className="menu-item">
📋 Copy Text
</div>
<div className="menu-item">
🔗 Copy Link
</div>
<div className="menu-item">
🚩 Mark Unread
</div>
<div className="menu-divider"></div>
<div className="menu-item">
💬 Reply
</div>
<div className="menu-item">
🧵 Create Thread
</div>
<div className="menu-divider"></div>
<div className="menu-item">
🚫 Report Message
</div>
{message?.isOwn && (
<>
<div className="menu-divider"></div>
<div className="menu-item danger" onClick={() => onDelete?.()}>
🗑 Delete Message
</div>
</>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,273 @@
import React, { useState } from 'react';
// Moderation action modal - for kicks, bans, timeouts
export default function ModerationModal({ type, user, onSubmit, onClose }) {
const [reason, setReason] = useState('');
const [deleteMessages, setDeleteMessages] = useState('none');
const [duration, setDuration] = useState('1h');
const typeConfig = {
kick: {
title: 'Kick Member',
icon: '👢',
action: 'Kick',
color: '#faa61a',
description: `Are you sure you want to kick ${user?.name}? They will be able to rejoin with a new invite.`,
},
ban: {
title: 'Ban Member',
icon: '🔨',
action: 'Ban',
color: '#ed4245',
description: `Are you sure you want to ban ${user?.name}? They will not be able to rejoin unless unbanned.`,
showDeleteMessages: true,
},
timeout: {
title: 'Timeout Member',
icon: '⏰',
action: 'Timeout',
color: '#faa61a',
description: `Temporarily mute ${user?.name}. They won't be able to send messages, react, or join voice.`,
showDuration: true,
},
warn: {
title: 'Warn Member',
icon: '⚠️',
action: 'Warn',
color: '#faa61a',
description: `Send a warning to ${user?.name}. This will be logged in the audit log.`,
},
};
const config = typeConfig[type] || typeConfig.kick;
const durations = [
{ value: '60s', label: '60 seconds' },
{ value: '5m', label: '5 minutes' },
{ value: '10m', label: '10 minutes' },
{ value: '1h', label: '1 hour' },
{ value: '1d', label: '1 day' },
{ value: '1w', label: '1 week' },
];
const deleteOptions = [
{ value: 'none', label: "Don't delete any" },
{ value: '1h', label: 'Previous hour' },
{ value: '24h', label: 'Previous 24 hours' },
{ value: '7d', label: 'Previous 7 days' },
];
const handleSubmit = () => {
onSubmit?.({
type,
userId: user?.id,
reason,
deleteMessages: config.showDeleteMessages ? deleteMessages : undefined,
duration: config.showDuration ? duration : undefined,
});
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="moderation-modal" onClick={(e) => e.stopPropagation()}>
<div className="mod-header" style={{ borderColor: config.color }}>
<span className="mod-icon">{config.icon}</span>
<h2>{config.title}</h2>
</div>
<div className="mod-content">
<div className="mod-user">
<div
className="mod-user-avatar"
style={user?.color ? { background: user.color } : undefined}
>
{user?.avatar || user?.name?.[0]}
</div>
<div className="mod-user-info">
<span className="mod-user-name">{user?.name}</span>
{user?.tag && <span className="mod-user-tag">{user.tag}</span>}
</div>
</div>
<p className="mod-description">{config.description}</p>
{config.showDuration && (
<div className="mod-field">
<label>Duration</label>
<select
value={duration}
onChange={(e) => setDuration(e.target.value)}
className="mod-select"
>
{durations.map(d => (
<option key={d.value} value={d.value}>{d.label}</option>
))}
</select>
</div>
)}
{config.showDeleteMessages && (
<div className="mod-field">
<label>Delete message history</label>
<select
value={deleteMessages}
onChange={(e) => setDeleteMessages(e.target.value)}
className="mod-select"
>
{deleteOptions.map(d => (
<option key={d.value} value={d.value}>{d.label}</option>
))}
</select>
</div>
)}
<div className="mod-field">
<label>Reason (optional)</label>
<textarea
placeholder="Enter a reason..."
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={3}
/>
<span className="field-hint">This will be recorded in the audit log</span>
</div>
</div>
<div className="mod-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button
className="action-btn"
style={{ background: config.color }}
onClick={handleSubmit}
>
{config.action}
</button>
</div>
</div>
</div>
);
}
// User notes modal
export function UserNotesModal({ user, onSave, onClose }) {
const [note, setNote] = useState(user?.note || '');
return (
<div className="modal-overlay" onClick={onClose}>
<div className="notes-modal" onClick={(e) => e.stopPropagation()}>
<div className="notes-header">
<h2>Note about {user?.name}</h2>
<button className="modal-close" onClick={onClose}></button>
</div>
<div className="notes-content">
<textarea
placeholder="Click to add a note..."
value={note}
onChange={(e) => setNote(e.target.value)}
rows={5}
/>
<span className="notes-hint">Only you can see this note</span>
</div>
<div className="notes-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button
className="save-btn"
onClick={() => onSave?.(note)}
>
Save Note
</button>
</div>
</div>
</div>
);
}
// Report user modal
export function ReportModal({ user, message, onSubmit, onClose }) {
const [category, setCategory] = useState('');
const [details, setDetails] = useState('');
const categories = [
{ value: 'harassment', label: 'Harassment or Bullying' },
{ value: 'spam', label: 'Spam or Scam' },
{ value: 'hate', label: 'Hate Speech or Discrimination' },
{ value: 'nsfw', label: 'Inappropriate Content' },
{ value: 'impersonation', label: 'Impersonation' },
{ value: 'threats', label: 'Threats or Violence' },
{ value: 'other', label: 'Other' },
];
return (
<div className="modal-overlay" onClick={onClose}>
<div className="report-modal" onClick={(e) => e.stopPropagation()}>
<div className="report-header">
<span className="report-icon">🚩</span>
<h2>Report {message ? 'Message' : 'User'}</h2>
</div>
<div className="report-content">
{user && (
<div className="report-target">
<div
className="target-avatar"
style={user.color ? { background: user.color } : undefined}
>
{user.avatar || user.name?.[0]}
</div>
<span className="target-name">{user.name}</span>
</div>
)}
{message && (
<div className="reported-message">
<div className="reported-content">{message.text}</div>
</div>
)}
<div className="report-field">
<label>What's the issue?</label>
<div className="category-options">
{categories.map(cat => (
<button
key={cat.value}
className={`category-btn ${category === cat.value ? 'selected' : ''}`}
onClick={() => setCategory(cat.value)}
>
{cat.label}
</button>
))}
</div>
</div>
<div className="report-field">
<label>Additional details (optional)</label>
<textarea
placeholder="Provide more context about this issue..."
value={details}
onChange={(e) => setDetails(e.target.value)}
rows={3}
/>
</div>
<div className="report-notice">
<span className="notice-icon"></span>
<p>Reports are confidential. The reported user won't know who reported them.</p>
</div>
</div>
<div className="report-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button
className="submit-btn"
onClick={() => onSubmit?.({ category, details })}
disabled={!category}
>
Submit Report
</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,233 @@
import React, { useState } from 'react';
const nitroFeatures = [
{ id: 'animated-avatar', icon: '👤', title: 'Animated Avatar', description: 'Use GIFs as your profile picture' },
{ id: 'animated-emoji', icon: '😄', title: 'Animated Emoji', description: 'Use animated emojis anywhere' },
{ id: 'larger-uploads', icon: '📁', title: '500MB Uploads', description: 'Share large files up to 500MB' },
{ id: 'hd-streaming', icon: '📺', title: 'HD Streaming', description: 'Stream in 4K at 60fps' },
{ id: 'custom-tag', icon: '🏷️', title: 'Custom Tag', description: 'Choose your own 4-digit tag' },
{ id: 'server-boost', icon: '🚀', title: '2 Free Boosts', description: 'Boost your favorite servers' },
{ id: 'profile-banner', icon: '🖼️', title: 'Profile Banner', description: 'Add a custom banner to your profile' },
{ id: 'custom-themes', icon: '🎨', title: 'Custom Themes', description: 'Personalize your client' },
{ id: 'super-reactions', icon: '✨', title: 'Super Reactions', description: 'Send enhanced animated reactions' },
{ id: 'soundboard', icon: '🔊', title: 'More Sounds', description: 'Upload more soundboard sounds' },
];
const plans = [
{
id: 'nitro',
name: 'Nitro',
price: '$9.99',
period: '/month',
yearlyPrice: '$99.99',
features: nitroFeatures.map(f => f.id),
popular: true,
},
{
id: 'nitro-basic',
name: 'Nitro Basic',
price: '$2.99',
period: '/month',
yearlyPrice: '$29.99',
features: ['animated-emoji', 'larger-uploads', 'custom-tag'],
popular: false,
},
];
const boostPerks = [
{ level: 1, boosts: 2, perks: ['50 extra emoji slots', '128kbps audio', 'Animated server icon', 'Custom invite background'] },
{ level: 2, boosts: 7, perks: ['100 extra emoji slots', '256kbps audio', '50MB upload limit', 'Server banner'] },
{ level: 3, boosts: 14, perks: ['250 extra emoji slots', '384kbps audio', '100MB upload limit', 'Vanity URL'] },
];
export default function NitroPanel({ onClose }) {
const [activeTab, setActiveTab] = useState('subscribe');
const [billingCycle, setBillingCycle] = useState('monthly');
const [selectedPlan, setSelectedPlan] = useState('nitro');
return (
<div className="nitro-panel">
<div className="nitro-header">
<div className="nitro-branding">
<span className="nitro-logo">🚀</span>
<h1>Nitro</h1>
</div>
<button className="nitro-close" onClick={onClose}></button>
</div>
<div className="nitro-tabs">
<button
className={`nitro-tab ${activeTab === 'subscribe' ? 'active' : ''}`}
onClick={() => setActiveTab('subscribe')}
>
Subscribe
</button>
<button
className={`nitro-tab ${activeTab === 'boost' ? 'active' : ''}`}
onClick={() => setActiveTab('boost')}
>
Server Boost
</button>
<button
className={`nitro-tab ${activeTab === 'gift' ? 'active' : ''}`}
onClick={() => setActiveTab('gift')}
>
Gift
</button>
</div>
{activeTab === 'subscribe' && (
<div className="nitro-content">
<div className="nitro-hero">
<h2>Unleash more fun</h2>
<p>Subscribe to Nitro to unlock features and perks</p>
</div>
<div className="billing-toggle">
<button
className={`billing-option ${billingCycle === 'monthly' ? 'active' : ''}`}
onClick={() => setBillingCycle('monthly')}
>
Monthly
</button>
<button
className={`billing-option ${billingCycle === 'yearly' ? 'active' : ''}`}
onClick={() => setBillingCycle('yearly')}
>
Yearly <span className="save-badge">Save 16%</span>
</button>
</div>
<div className="plans-grid">
{plans.map(plan => (
<div
key={plan.id}
className={`plan-card ${plan.popular ? 'popular' : ''} ${selectedPlan === plan.id ? 'selected' : ''}`}
onClick={() => setSelectedPlan(plan.id)}
>
{plan.popular && <div className="popular-badge">Most Popular</div>}
<h3>{plan.name}</h3>
<div className="plan-price">
<span className="price">{billingCycle === 'yearly' ? plan.yearlyPrice : plan.price}</span>
<span className="period">{billingCycle === 'yearly' ? '/year' : plan.period}</span>
</div>
<ul className="plan-features">
{nitroFeatures.filter(f => plan.features.includes(f.id)).map(feature => (
<li key={feature.id}>
<span className="check"></span>
{feature.title}
</li>
))}
</ul>
<button className="select-plan-btn">
{selectedPlan === plan.id ? 'Selected' : 'Select'}
</button>
</div>
))}
</div>
<button className="subscribe-btn">
Subscribe to {plans.find(p => p.id === selectedPlan)?.name}
</button>
</div>
)}
{activeTab === 'boost' && (
<div className="nitro-content">
<div className="boost-hero">
<span className="boost-icon">🚀</span>
<h2>Server Boost</h2>
<p>Unlock perks for your favorite communities</p>
</div>
<div className="boost-levels">
{boostPerks.map(level => (
<div key={level.level} className="boost-level-card">
<div className="level-header">
<span className="level-badge">Level {level.level}</span>
<span className="boost-requirement">{level.boosts} Boosts</span>
</div>
<ul className="level-perks">
{level.perks.map((perk, idx) => (
<li key={idx}>
<span className="perk-check"></span>
{perk}
</li>
))}
</ul>
</div>
))}
</div>
<div className="boost-cta">
<button className="boost-btn">
🚀 Boost a Server
</button>
<p className="boost-price">$4.99/month or included with Nitro</p>
</div>
</div>
)}
{activeTab === 'gift' && (
<div className="nitro-content">
<div className="gift-hero">
<span className="gift-icon">🎁</span>
<h2>Gift Nitro</h2>
<p>Send the gift of Nitro to a friend</p>
</div>
<div className="gift-options">
<div className="gift-card">
<h3>Nitro</h3>
<div className="gift-durations">
<button className="duration-btn active">1 Month - $9.99</button>
<button className="duration-btn">1 Year - $99.99</button>
</div>
</div>
<div className="gift-card">
<h3>Nitro Basic</h3>
<div className="gift-durations">
<button className="duration-btn active">1 Month - $2.99</button>
<button className="duration-btn">1 Year - $29.99</button>
</div>
</div>
</div>
<div className="gift-recipient">
<label>Send to</label>
<input type="text" placeholder="Enter a username" />
</div>
<div className="gift-message">
<label>Add a message (optional)</label>
<textarea placeholder="Write a message to include with your gift..." rows={3} />
</div>
<button className="purchase-gift-btn">
🎁 Purchase Gift
</button>
</div>
)}
<div className="nitro-footer">
<a href="#" className="nitro-link">Nitro Terms</a>
<span></span>
<a href="#" className="nitro-link">Paid Services Terms</a>
</div>
</div>
);
}
// Nitro badge component
export function NitroBadge({ type = 'full' }) {
if (type === 'icon') {
return <span className="nitro-badge-icon">🚀</span>;
}
return (
<div className="nitro-badge">
<span className="badge-icon">🚀</span>
<span className="badge-text">Nitro</span>
</div>
);
}

View file

@ -0,0 +1,158 @@
import React, { useState } from 'react';
export default function NotificationSettings({ server, channel, onClose, onSave }) {
const isChannelSettings = !!channel;
const name = channel?.name || server?.name || 'AeThex Foundation';
const [notifSetting, setNotifSetting] = useState('all');
const [suppressEveryone, setSuppressEveryone] = useState(false);
const [suppressRoles, setSuppressRoles] = useState(false);
const [muteUntil, setMuteUntil] = useState('none');
const [mobilePush, setMobilePush] = useState(true);
const notifOptions = [
{ id: 'all', label: 'All Messages', desc: 'Get notified for all messages' },
{ id: 'mentions', label: 'Only @mentions', desc: 'Only notify when mentioned' },
{ id: 'nothing', label: 'Nothing', desc: 'Mute all notifications' },
];
if (isChannelSettings) {
notifOptions.unshift({ id: 'default', label: 'Default', desc: 'Use server default setting' });
}
const muteOptions = [
{ value: 'none', label: 'Not muted' },
{ value: '15min', label: 'For 15 minutes' },
{ value: '1hour', label: 'For 1 hour' },
{ value: '8hours', label: 'For 8 hours' },
{ value: '24hours', label: 'For 24 hours' },
{ value: 'forever', label: 'Until I turn it back on' },
];
const handleSave = () => {
onSave?.({
notifSetting,
suppressEveryone,
suppressRoles,
muteUntil,
mobilePush,
});
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="notification-settings-modal" onClick={(e) => e.stopPropagation()}>
<div className="notif-header">
<h2>🔔 Notification Settings</h2>
<button className="notif-close" onClick={onClose}></button>
</div>
<div className="notif-target">
<span className="target-icon">{isChannelSettings ? '#' : '🏠'}</span>
<span className="target-name">{name}</span>
</div>
<div className="notif-content">
<div className="notif-section">
<h3>Notification Frequency</h3>
<div className="notif-options">
{notifOptions.map(opt => (
<label
key={opt.id}
className={`notif-option ${notifSetting === opt.id ? 'selected' : ''}`}
>
<input
type="radio"
name="notifSetting"
value={opt.id}
checked={notifSetting === opt.id}
onChange={() => setNotifSetting(opt.id)}
/>
<div className="option-content">
<span className="option-label">{opt.label}</span>
<span className="option-desc">{opt.desc}</span>
</div>
</label>
))}
</div>
</div>
<div className="notif-section">
<h3>Mute {isChannelSettings ? 'Channel' : 'Server'}</h3>
<select
className="mute-select"
value={muteUntil}
onChange={(e) => setMuteUntil(e.target.value)}
>
{muteOptions.map(opt => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div className="notif-section">
<h3>Suppress</h3>
<div className="suppress-options">
<label className="suppress-option">
<input
type="checkbox"
checked={suppressEveryone}
onChange={(e) => setSuppressEveryone(e.target.checked)}
/>
<span>Suppress @everyone and @here</span>
</label>
<label className="suppress-option">
<input
type="checkbox"
checked={suppressRoles}
onChange={(e) => setSuppressRoles(e.target.checked)}
/>
<span>Suppress All Role @mentions</span>
</label>
</div>
</div>
<div className="notif-section">
<div className="toggle-row">
<div className="toggle-info">
<h3>Mobile Push Notifications</h3>
<p>Receive push notifications on mobile</p>
</div>
<label className="toggle-switch">
<input
type="checkbox"
checked={mobilePush}
onChange={(e) => setMobilePush(e.target.checked)}
/>
<span className="toggle-slider"></span>
</label>
</div>
</div>
{!isChannelSettings && (
<div className="notif-section channel-overrides">
<h3>Channel Overrides</h3>
<p className="override-hint">You can customize notifications for individual channels</p>
<div className="override-list">
<div className="override-item">
<span>#announcements</span>
<span className="override-value">All Messages</span>
</div>
<div className="override-item">
<span>#spam</span>
<span className="override-value">Muted</span>
</div>
</div>
<button className="add-override-btn">+ Add Override</button>
</div>
)}
</div>
<div className="notif-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button className="save-btn" onClick={handleSave}>Done</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,189 @@
import React, { useState } from 'react';
const allNotifications = [
{
id: 1,
type: 'mention',
server: 'AeThex Foundation',
serverIcon: 'F',
serverGradient: 'linear-gradient(135deg, #ff0000, #990000)',
channel: 'general',
author: 'Trevor',
authorAvatar: 'T',
message: '@Anderson check out the new auth flow!',
time: '2 minutes ago',
unread: true,
},
{
id: 2,
type: 'reply',
server: 'AeThex Labs',
serverIcon: 'L',
serverGradient: 'linear-gradient(135deg, #ffa500, #ff8c00)',
channel: 'testing',
author: 'Sarah',
authorAvatar: 'S',
message: 'Great point! I\'ll add that to the test suite.',
time: '15 minutes ago',
unread: true,
},
{
id: 3,
type: 'dm',
author: 'Marcus',
authorAvatar: 'M',
authorGradient: 'linear-gradient(135deg, #0066ff, #003380)',
message: 'The API integration is ready for review!',
time: '1 hour ago',
unread: true,
},
{
id: 4,
type: 'friend-request',
author: 'NewDev123',
authorAvatar: 'N',
message: 'sent you a friend request.',
time: '2 hours ago',
unread: false,
},
{
id: 5,
type: 'server-invite',
server: 'Dev Community',
serverIcon: 'DC',
author: 'JohnDev',
message: 'invited you to join the server.',
time: '3 hours ago',
unread: false,
},
];
export default function NotificationsPanel({ onClose, onNotificationClick }) {
const [filter, setFilter] = useState('all');
const [notifications, setNotifications] = useState(allNotifications);
const filters = [
{ id: 'all', label: 'All' },
{ id: 'mentions', label: 'Mentions' },
{ id: 'unreads', label: 'Unreads' },
];
const filteredNotifications = notifications.filter((n) => {
if (filter === 'mentions') return n.type === 'mention';
if (filter === 'unreads') return n.unread;
return true;
});
const markAllRead = () => {
setNotifications((prev) => prev.map((n) => ({ ...n, unread: false })));
};
const clearAll = () => {
setNotifications([]);
};
const getNotificationIcon = (type) => {
switch (type) {
case 'mention': return '@';
case 'reply': return '↩️';
case 'dm': return '💬';
case 'friend-request': return '👋';
case 'server-invite': return '✉️';
default: return '🔔';
}
};
return (
<div className="notifications-panel">
<div className="notifications-header">
<h3>Inbox</h3>
<div className="notifications-actions">
<button className="mark-read-btn" onClick={markAllRead}>Mark All Read</button>
<button className="clear-btn" onClick={clearAll}>Clear All</button>
</div>
<button className="notifications-close" onClick={onClose}></button>
</div>
<div className="notifications-filters">
{filters.map((f) => (
<button
key={f.id}
className={`filter-btn ${filter === f.id ? 'active' : ''}`}
onClick={() => setFilter(f.id)}
>
{f.label}
</button>
))}
</div>
<div className="notifications-list">
{filteredNotifications.length === 0 ? (
<div className="notifications-empty">
<span className="empty-icon">📭</span>
<p>You're all caught up!</p>
</div>
) : (
filteredNotifications.map((notification) => (
<div
key={notification.id}
className={`notification-item ${notification.unread ? 'unread' : ''}`}
onClick={() => onNotificationClick?.(notification)}
>
<div className="notification-icon-container">
{notification.type === 'dm' || notification.type === 'friend-request' ? (
<div
className="notification-avatar"
style={{ background: notification.authorGradient || '#36393f' }}
>
{notification.authorAvatar}
</div>
) : notification.serverIcon ? (
<div
className="notification-server-icon"
style={{ background: notification.serverGradient || '#36393f' }}
>
{notification.serverIcon}
</div>
) : (
<div className="notification-type-icon">
{getNotificationIcon(notification.type)}
</div>
)}
<div className="notification-type-badge">
{getNotificationIcon(notification.type)}
</div>
</div>
<div className="notification-content">
<div className="notification-header-row">
{notification.server && (
<>
<span className="notification-server">{notification.server}</span>
<span className="notification-separator"></span>
<span className="notification-channel">#{notification.channel}</span>
</>
)}
{notification.type === 'dm' && (
<span className="notification-author">{notification.author}</span>
)}
{notification.type === 'friend-request' && (
<span className="notification-author">{notification.author}</span>
)}
</div>
<div className="notification-message">
{notification.author && notification.type !== 'dm' && notification.type !== 'friend-request' && (
<strong>{notification.author}: </strong>
)}
{notification.message}
</div>
<div className="notification-time">{notification.time}</div>
</div>
{notification.unread && <div className="unread-dot" />}
</div>
))
)}
</div>
</div>
);
}

View file

@ -0,0 +1,92 @@
import React from 'react';
const pinnedMessages = [
{
id: 1,
author: 'Anderson',
avatar: 'A',
gradient: 'linear-gradient(135deg, #ff0000, #0066ff, #ffa500)',
badge: 'Founder',
time: 'Jan 15, 2026',
text: 'Welcome to AeThex Connect! Please read the rules in #rules and introduce yourself in #introductions. The Trinity awaits! 🔥',
pinnedBy: 'Anderson',
},
{
id: 2,
author: 'Trevor',
avatar: 'T',
gradient: 'linear-gradient(135deg, #ff0000, #cc0000)',
badge: 'Foundation',
time: 'Jan 20, 2026',
text: 'Important: Authentication v2.1.0 is now live! All users should update their Passport credentials. Check #updates for migration guide.',
pinnedBy: 'Trevor',
},
{
id: 3,
author: 'Sarah',
avatar: 'S',
gradient: 'linear-gradient(135deg, #ffa500, #ff8c00)',
badge: 'Labs',
time: 'Feb 1, 2026',
text: 'Nexus Engine v2.0 is officially out of beta! Huge thanks to everyone who helped test. Read the changelog in #changelog 🚀',
pinnedBy: 'Sarah',
},
];
export default function PinnedMessagesPanel({ channelName, onClose, onJumpToMessage }) {
return (
<div className="pinned-panel">
<div className="pinned-header">
<span className="pinned-icon">📌</span>
<span className="pinned-title">Pinned Messages</span>
<span className="pinned-channel">#{channelName || 'general'}</span>
<button className="pinned-close" onClick={onClose}></button>
</div>
<div className="pinned-content">
{pinnedMessages.length === 0 ? (
<div className="pinned-empty">
<span className="empty-icon">📌</span>
<p>This channel doesn't have any pinned messages... yet.</p>
</div>
) : (
<div className="pinned-list">
{pinnedMessages.map((msg) => (
<div key={msg.id} className="pinned-message">
<div className="pinned-message-header">
<div
className="pinned-avatar"
style={{ background: msg.gradient || '#36393f' }}
>
{msg.avatar}
</div>
<div className="pinned-author-info">
<span className="pinned-author">{msg.author}</span>
{msg.badge && <span className={`pinned-badge ${msg.badge.toLowerCase()}`}>{msg.badge}</span>}
<span className="pinned-time">{msg.time}</span>
</div>
</div>
<div className="pinned-message-text">{msg.text}</div>
<div className="pinned-message-footer">
<span className="pinned-by">Pinned by {msg.pinnedBy}</span>
<button
className="jump-to-btn"
onClick={() => onJumpToMessage?.(msg)}
>
Jump
</button>
</div>
</div>
))}
</div>
)}
</div>
<div className="pinned-footer">
<span className="protip">
<strong>PROTIP:</strong> You can pin up to 50 messages in a channel.
</span>
</div>
</div>
);
}

View file

@ -0,0 +1,234 @@
import React, { useState } from 'react';
export default function PollCreator({ onSubmit, onClose }) {
const [question, setQuestion] = useState('');
const [options, setOptions] = useState(['', '']);
const [duration, setDuration] = useState('24h');
const [multiSelect, setMultiSelect] = useState(false);
const durations = [
{ value: '1h', label: '1 Hour' },
{ value: '4h', label: '4 Hours' },
{ value: '8h', label: '8 Hours' },
{ value: '24h', label: '24 Hours' },
{ value: '3d', label: '3 Days' },
{ value: '7d', label: '1 Week' },
];
const addOption = () => {
if (options.length < 10) {
setOptions([...options, '']);
}
};
const removeOption = (index) => {
if (options.length > 2) {
setOptions(options.filter((_, i) => i !== index));
}
};
const updateOption = (index, value) => {
setOptions(options.map((opt, i) => i === index ? value : opt));
};
const handleSubmit = () => {
if (!question.trim() || options.filter(o => o.trim()).length < 2) return;
onSubmit?.({
question,
options: options.filter(o => o.trim()),
duration,
multiSelect,
});
onClose?.();
};
const validOptions = options.filter(o => o.trim()).length;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="poll-creator" onClick={(e) => e.stopPropagation()}>
<div className="poll-header">
<h2>Create Poll</h2>
<button className="poll-close" onClick={onClose}></button>
</div>
<div className="poll-content">
<div className="poll-field">
<label>Question</label>
<textarea
placeholder="Ask something..."
value={question}
onChange={(e) => setQuestion(e.target.value)}
maxLength={300}
rows={2}
/>
<span className="char-count">{question.length}/300</span>
</div>
<div className="poll-field">
<label>Answers</label>
<div className="poll-options">
{options.map((opt, idx) => (
<div key={idx} className="poll-option">
<span className="option-number">{idx + 1}</span>
<input
type="text"
placeholder={`Option ${idx + 1}`}
value={opt}
onChange={(e) => updateOption(idx, e.target.value)}
maxLength={55}
/>
{options.length > 2 && (
<button
className="remove-option"
onClick={() => removeOption(idx)}
>
</button>
)}
</div>
))}
</div>
{options.length < 10 && (
<button className="add-option" onClick={addOption}>
+ Add another answer
</button>
)}
</div>
<div className="poll-field">
<label>Duration</label>
<select
value={duration}
onChange={(e) => setDuration(e.target.value)}
className="poll-select"
>
{durations.map(d => (
<option key={d.value} value={d.value}>{d.label}</option>
))}
</select>
</div>
<div className="poll-toggle">
<label className="toggle-label">
<input
type="checkbox"
checked={multiSelect}
onChange={(e) => setMultiSelect(e.target.checked)}
/>
<span className="toggle-switch"></span>
<span>Allow multiple answers</span>
</label>
</div>
</div>
<div className="poll-preview">
<h4>Preview</h4>
<div className="preview-card">
<div className="preview-question">{question || 'Your question here'}</div>
<div className="preview-options">
{options.filter(o => o.trim()).map((opt, idx) => (
<div key={idx} className="preview-option">
<div className="option-circle" />
<span>{opt}</span>
</div>
))}
</div>
<div className="preview-footer">
<span>0 votes</span>
<span></span>
<span>{durations.find(d => d.value === duration)?.label} remaining</span>
</div>
</div>
</div>
<div className="poll-footer">
<button className="poll-cancel" onClick={onClose}>Cancel</button>
<button
className="poll-submit"
onClick={handleSubmit}
disabled={!question.trim() || validOptions < 2}
>
Create Poll
</button>
</div>
</div>
</div>
);
}
// Poll display component for messages
export function Poll({ poll, onVote }) {
const [selected, setSelected] = useState([]);
const [hasVoted, setHasVoted] = useState(false);
const totalVotes = poll.options.reduce((sum, opt) => sum + (opt.votes || 0), 0);
const handleVote = (optionIndex) => {
if (hasVoted) return;
if (poll.multiSelect) {
setSelected(prev =>
prev.includes(optionIndex)
? prev.filter(i => i !== optionIndex)
: [...prev, optionIndex]
);
} else {
setSelected([optionIndex]);
}
};
const submitVote = () => {
if (selected.length === 0) return;
onVote?.(selected);
setHasVoted(true);
};
return (
<div className="poll-message">
<div className="poll-question">{poll.question}</div>
<div className="poll-options-list">
{poll.options.map((opt, idx) => {
const percentage = hasVoted && totalVotes > 0
? Math.round((opt.votes / totalVotes) * 100)
: 0;
return (
<button
key={idx}
className={`poll-vote-option ${selected.includes(idx) ? 'selected' : ''} ${hasVoted ? 'voted' : ''}`}
onClick={() => handleVote(idx)}
disabled={hasVoted}
>
{hasVoted && (
<div className="vote-bar" style={{ width: `${percentage}%` }} />
)}
<div className="vote-content">
{!hasVoted && (
<div className={`vote-checkbox ${poll.multiSelect ? 'multi' : ''}`}>
{selected.includes(idx) && '✓'}
</div>
)}
<span className="vote-text">{opt.text}</span>
{hasVoted && (
<span className="vote-percentage">{percentage}%</span>
)}
</div>
</button>
);
})}
</div>
{!hasVoted && selected.length > 0 && (
<button className="submit-vote" onClick={submitVote}>
Vote
</button>
)}
<div className="poll-info">
<span>{totalVotes} vote{totalVotes !== 1 ? 's' : ''}</span>
<span></span>
<span>{poll.timeLeft}</span>
</div>
</div>
);
}

View file

@ -0,0 +1,216 @@
import React, { useState, useEffect, useCallback } from 'react';
const recentItems = [
{ type: 'channel', id: 'general', name: 'general', server: 'AeThex Foundation', icon: '#' },
{ type: 'dm', id: 'dm-1', name: 'Trevor', avatar: 'T', color: '#ff0000' },
{ type: 'channel', id: 'api-discussion', name: 'api-discussion', server: 'AeThex Foundation', icon: '#' },
];
const allItems = [
// Channels
{ type: 'channel', id: 'general', name: 'general', server: 'AeThex Foundation', icon: '#' },
{ type: 'channel', id: 'updates', name: 'updates', server: 'AeThex Foundation', icon: '📢' },
{ type: 'channel', id: 'api-discussion', name: 'api-discussion', server: 'AeThex Foundation', icon: '#' },
{ type: 'channel', id: 'bug-reports', name: 'bug-reports', server: 'AeThex Foundation', icon: '🐛' },
{ type: 'channel', id: 'nexus-lounge', name: 'Nexus Lounge', server: 'AeThex Foundation', icon: '🔊', isVoice: true },
{ type: 'channel', id: 'labs-general', name: 'general', server: 'AeThex Labs', icon: '#' },
{ type: 'channel', id: 'experiments', name: 'experiments', server: 'AeThex Labs', icon: '🧪' },
// Users/DMs
{ type: 'user', id: 'u-1', name: 'Trevor', tag: '#0001', avatar: 'T', color: '#ff0000' },
{ type: 'user', id: 'u-2', name: 'Sarah', tag: '#0042', avatar: 'S', color: '#ffa500' },
{ type: 'user', id: 'u-3', name: 'Marcus', tag: '#1234', avatar: 'M', color: '#0066ff' },
{ type: 'user', id: 'u-4', name: 'Anderson', tag: '#0001', avatar: 'A', color: '#5865f2' },
// Servers
{ type: 'server', id: 's-1', name: 'AeThex Foundation', icon: 'F', color: '#ff0000' },
{ type: 'server', id: 's-2', name: 'AeThex Labs', icon: 'L', color: '#ffa500' },
{ type: 'server', id: 's-3', name: 'AeThex Corporation', icon: 'C', color: '#0066ff' },
];
export default function QuickSwitcher({ onSelect, onClose }) {
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const [filter, setFilter] = useState('all'); // all, channels, users, servers
const getFilteredItems = useCallback(() => {
let items = query ? allItems : recentItems;
if (query) {
const lowerQuery = query.toLowerCase();
items = items.filter(item =>
item.name.toLowerCase().includes(lowerQuery) ||
(item.server && item.server.toLowerCase().includes(lowerQuery))
);
}
if (filter !== 'all') {
const typeMap = {
channels: 'channel',
users: ['user', 'dm'],
servers: 'server',
};
const types = typeMap[filter];
items = items.filter(item =>
Array.isArray(types) ? types.includes(item.type) : item.type === types
);
}
return items.slice(0, 10);
}, [query, filter]);
const filteredItems = getFilteredItems();
useEffect(() => {
setSelectedIndex(0);
}, [query, filter]);
const handleKeyDown = useCallback((e) => {
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]) {
onSelect?.(filteredItems[selectedIndex]);
onClose?.();
}
break;
case 'Escape':
e.preventDefault();
onClose?.();
break;
case 'Tab':
e.preventDefault();
// Cycle through filters
const filters = ['all', 'channels', 'users', 'servers'];
const currentIdx = filters.indexOf(filter);
setFilter(filters[(currentIdx + 1) % filters.length]);
break;
}
}, [filteredItems, selectedIndex, filter, onSelect, onClose]);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
const getItemIcon = (item) => {
if (item.type === 'channel') {
return item.isVoice ? '🔊' : item.icon;
}
if (item.type === 'user' || item.type === 'dm') {
return (
<div
className="qs-avatar"
style={item.color ? { background: item.color } : undefined}
>
{item.avatar}
</div>
);
}
if (item.type === 'server') {
return (
<div
className="qs-server-icon"
style={item.color ? { background: item.color } : undefined}
>
{item.icon}
</div>
);
}
};
return (
<div className="modal-overlay dark" onClick={onClose}>
<div className="quick-switcher" onClick={(e) => e.stopPropagation()}>
<div className="qs-search">
<input
type="text"
placeholder="Where would you like to go?"
value={query}
onChange={(e) => setQuery(e.target.value)}
autoFocus
/>
</div>
<div className="qs-filters">
<button
className={`qs-filter ${filter === 'all' ? 'active' : ''}`}
onClick={() => setFilter('all')}
>
All
</button>
<button
className={`qs-filter ${filter === 'channels' ? 'active' : ''}`}
onClick={() => setFilter('channels')}
>
# Channels
</button>
<button
className={`qs-filter ${filter === 'users' ? 'active' : ''}`}
onClick={() => setFilter('users')}
>
@ Users
</button>
<button
className={`qs-filter ${filter === 'servers' ? 'active' : ''}`}
onClick={() => setFilter('servers')}
>
🏠 Servers
</button>
</div>
<div className="qs-results">
{!query && (
<div className="qs-section-header">Recent</div>
)}
{filteredItems.map((item, idx) => (
<div
key={item.id}
className={`qs-item ${idx === selectedIndex ? 'selected' : ''}`}
onClick={() => {
onSelect?.(item);
onClose?.();
}}
onMouseEnter={() => setSelectedIndex(idx)}
>
<span className="qs-icon">{getItemIcon(item)}</span>
<div className="qs-item-info">
<span className="qs-item-name">{item.name}</span>
{item.tag && <span className="qs-item-tag">{item.tag}</span>}
{item.server && <span className="qs-item-server">{item.server}</span>}
</div>
{item.type === 'channel' && <span className="qs-type">#</span>}
{item.type === 'user' && <span className="qs-type">@</span>}
</div>
))}
{filteredItems.length === 0 && (
<div className="qs-empty">
<p>No results found</p>
</div>
)}
</div>
<div className="qs-footer">
<span className="qs-hint">
<kbd></kbd><kbd></kbd> to navigate
</span>
<span className="qs-hint">
<kbd>Enter</kbd> to select
</span>
<span className="qs-hint">
<kbd>Tab</kbd> to filter
</span>
<span className="qs-hint">
<kbd>Esc</kbd> to close
</span>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,54 @@
import React from 'react';
export default function ReplyPreview({ message, onCancel, onClose }) {
if (!message) return null;
return (
<div className="reply-preview">
<div className="reply-line" />
<div className="reply-content">
<span className="replying-to">Replying to</span>
<span
className="reply-author"
style={{ color: message.badge === 'foundation' ? '#ff0000' :
message.badge === 'labs' ? '#ffa500' :
message.badge === 'corporation' ? '#0066ff' : '#b9bbbe' }}
>
{message.author}
</span>
<span className="reply-text">{message.text?.substring(0, 100)}{message.text?.length > 100 ? '...' : ''}</span>
</div>
<button className="reply-cancel" onClick={onCancel || onClose}></button>
</div>
);
}
export function MessageWithReply({ message, replyTo }) {
if (!replyTo) return null;
return (
<div className="message-reply-context">
<div className="reply-connector">
<div className="reply-line-vertical" />
<div className="reply-line-horizontal" />
</div>
<div
className="reply-avatar-mini"
style={{ background: replyTo.avatar?.gradient || '#36393f' }}
>
{replyTo.avatar?.initial || replyTo.author?.charAt(0)}
</div>
<span
className="reply-author-mini"
style={{ color: replyTo.badge === 'foundation' ? '#ff0000' :
replyTo.badge === 'labs' ? '#ffa500' :
replyTo.badge === 'corporation' ? '#0066ff' : '#b9bbbe' }}
>
{replyTo.author}
</span>
<span className="reply-text-mini">
{replyTo.text?.substring(0, 50)}{replyTo.text?.length > 50 ? '...' : ''}
</span>
</div>
);
}

View file

@ -0,0 +1,272 @@
import React, { useState } from 'react';
const permissions = [
{ category: 'General', items: [
{ id: 'view_channels', label: 'View Channels', description: 'Members can view channels by default' },
{ id: 'manage_channels', label: 'Manage Channels', description: 'Create, edit, and delete channels', dangerous: true },
{ id: 'manage_roles', label: 'Manage Roles', description: 'Create and edit roles below this one', dangerous: true },
{ id: 'manage_emoji', label: 'Manage Emoji & Stickers', description: 'Add, remove, and manage custom emojis' },
{ id: 'view_audit', label: 'View Audit Log', description: 'View a log of all moderation actions' },
{ id: 'manage_webhooks', label: 'Manage Webhooks', description: 'Create, edit, and delete webhooks' },
{ id: 'manage_server', label: 'Manage Server', description: 'Change server name, region, etc.', dangerous: true },
]},
{ category: 'Membership', items: [
{ id: 'create_invite', label: 'Create Invite', description: 'Invite new people to this server' },
{ id: 'change_nickname', label: 'Change Nickname', description: 'Change their own nickname' },
{ id: 'manage_nicknames', label: 'Manage Nicknames', description: 'Change nicknames of other members' },
{ id: 'kick_members', label: 'Kick Members', description: 'Remove members from the server', dangerous: true },
{ id: 'ban_members', label: 'Ban Members', description: 'Permanently ban members', dangerous: true },
{ id: 'timeout_members', label: 'Timeout Members', description: 'Temporarily mute members' },
]},
{ category: 'Text', items: [
{ id: 'send_messages', label: 'Send Messages', description: 'Send messages in text channels' },
{ id: 'send_tts', label: 'Send TTS Messages', description: 'Send text-to-speech messages' },
{ id: 'manage_messages', label: 'Manage Messages', description: 'Delete and pin messages from others', dangerous: true },
{ id: 'embed_links', label: 'Embed Links', description: 'Links will show a preview' },
{ id: 'attach_files', label: 'Attach Files', description: 'Upload images and files' },
{ id: 'add_reactions', label: 'Add Reactions', description: 'React to messages with emoji' },
{ id: 'use_external_emoji', label: 'Use External Emoji', description: 'Use emoji from other servers' },
{ id: 'mention_everyone', label: 'Mention @everyone', description: 'Can use @everyone and @here', dangerous: true },
]},
{ category: 'Voice', items: [
{ id: 'connect', label: 'Connect', description: 'Join voice channels' },
{ id: 'speak', label: 'Speak', description: 'Talk in voice channels' },
{ id: 'video', label: 'Video', description: 'Share camera in voice channels' },
{ id: 'mute_members', label: 'Mute Members', description: 'Mute other members' },
{ id: 'deafen_members', label: 'Deafen Members', description: 'Deafen other members' },
{ id: 'move_members', label: 'Move Members', description: 'Move members between voice channels' },
{ id: 'use_vad', label: 'Use Voice Activity', description: 'Use voice activity detection' },
{ id: 'priority_speaker', label: 'Priority Speaker', description: 'Be more easily heard' },
]},
];
export default function RoleEditor({ role, onSave, onClose }) {
const [roleName, setRoleName] = useState(role?.name || 'New Role');
const [roleColor, setRoleColor] = useState(role?.color || '#99aab5');
const [rolePermissions, setRolePermissions] = useState(role?.permissions || {});
const [displaySeparately, setDisplaySeparately] = useState(role?.displaySeparately || false);
const [mentionable, setMentionable] = useState(role?.mentionable || false);
const [activeTab, setActiveTab] = useState('display');
const colors = [
'#99aab5', '#1abc9c', '#2ecc71', '#3498db', '#9b59b6',
'#e91e63', '#f1c40f', '#e67e22', '#e74c3c', '#95a5a6',
'#607d8b', '#11806a', '#1f8b4c', '#206694', '#71368a',
'#ad1457', '#c27c0e', '#a84300', '#992d22', '#979c9f',
];
const togglePermission = (permId) => {
setRolePermissions(prev => ({
...prev,
[permId]: prev[permId] === true ? false : prev[permId] === false ? null : true
}));
};
const getPermState = (permId) => {
const state = rolePermissions[permId];
if (state === true) return 'granted';
if (state === false) return 'denied';
return 'inherit';
};
return (
<div className="role-editor">
<div className="role-editor-sidebar">
<div className="role-preview">
<div className="role-color-preview" style={{ background: roleColor }} />
<input
type="text"
value={roleName}
onChange={(e) => setRoleName(e.target.value)}
className="role-name-input"
/>
</div>
<div className="editor-tabs">
<button
className={`editor-tab ${activeTab === 'display' ? 'active' : ''}`}
onClick={() => setActiveTab('display')}
>
Display
</button>
<button
className={`editor-tab ${activeTab === 'permissions' ? 'active' : ''}`}
onClick={() => setActiveTab('permissions')}
>
Permissions
</button>
<button
className={`editor-tab ${activeTab === 'members' ? 'active' : ''}`}
onClick={() => setActiveTab('members')}
>
Members
</button>
</div>
</div>
<div className="role-editor-content">
<div className="editor-header">
<h2>Edit Role {roleName}</h2>
<button className="editor-close" onClick={onClose}></button>
</div>
{activeTab === 'display' && (
<div className="editor-section">
<div className="setting-group">
<label>Role Name</label>
<input
type="text"
value={roleName}
onChange={(e) => setRoleName(e.target.value)}
className="text-input"
/>
</div>
<div className="setting-group">
<label>Role Color</label>
<div className="color-grid">
{colors.map(color => (
<button
key={color}
className={`color-btn ${roleColor === color ? 'selected' : ''}`}
style={{ background: color }}
onClick={() => setRoleColor(color)}
/>
))}
</div>
<div className="custom-color">
<input
type="color"
value={roleColor}
onChange={(e) => setRoleColor(e.target.value)}
/>
<input
type="text"
value={roleColor}
onChange={(e) => setRoleColor(e.target.value)}
className="color-text-input"
/>
</div>
</div>
<div className="setting-group">
<label className="toggle-setting">
<div className="setting-info">
<span>Display role members separately</span>
<span className="setting-desc">Show members with this role in a separate category</span>
</div>
<input
type="checkbox"
checked={displaySeparately}
onChange={(e) => setDisplaySeparately(e.target.checked)}
/>
</label>
</div>
<div className="setting-group">
<label className="toggle-setting">
<div className="setting-info">
<span>Allow anyone to @mention this role</span>
<span className="setting-desc">Members can mention this role in chat</span>
</div>
<input
type="checkbox"
checked={mentionable}
onChange={(e) => setMentionable(e.target.checked)}
/>
</label>
</div>
<div className="role-icon-section">
<label>Role Icon</label>
<div className="role-icon-upload">
<div className="icon-placeholder">🎭</div>
<button className="upload-icon-btn">Upload Image</button>
<span className="icon-hint">Recommended 64x64</span>
</div>
</div>
</div>
)}
{activeTab === 'permissions' && (
<div className="editor-section permissions-section">
<div className="permissions-warning">
Be careful! Granting dangerous permissions can allow members to harm your server.
</div>
{permissions.map(category => (
<div key={category.category} className="permission-category">
<h3>{category.category}</h3>
{category.items.map(perm => (
<div
key={perm.id}
className={`permission-item ${perm.dangerous ? 'dangerous' : ''}`}
>
<div className="perm-info">
<span className="perm-label">{perm.label}</span>
<span className="perm-description">{perm.description}</span>
</div>
<div className="perm-toggle">
<button
className={`toggle-btn ${getPermState(perm.id) === 'denied' ? 'active' : ''}`}
onClick={() => setRolePermissions(prev => ({ ...prev, [perm.id]: false }))}
>
</button>
<button
className={`toggle-btn neutral ${getPermState(perm.id) === 'inherit' ? 'active' : ''}`}
onClick={() => setRolePermissions(prev => ({ ...prev, [perm.id]: null }))}
>
/
</button>
<button
className={`toggle-btn ${getPermState(perm.id) === 'granted' ? 'active' : ''}`}
onClick={() => setRolePermissions(prev => ({ ...prev, [perm.id]: true }))}
>
</button>
</div>
</div>
))}
</div>
))}
</div>
)}
{activeTab === 'members' && (
<div className="editor-section">
<div className="members-header">
<input type="text" placeholder="Search members" className="members-search" />
<button className="add-members-btn">+ Add Members</button>
</div>
<div className="role-members-list">
<div className="member-item">
<div className="member-avatar" style={{ background: '#ff0000' }}>T</div>
<span className="member-name">Trevor</span>
<button className="remove-member-btn"></button>
</div>
<div className="member-item">
<div className="member-avatar" style={{ background: '#ffa500' }}>S</div>
<span className="member-name">Sarah</span>
<button className="remove-member-btn"></button>
</div>
</div>
</div>
)}
<div className="editor-footer">
<button className="delete-role-btn">Delete Role</button>
<div className="footer-actions">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button
className="save-btn"
onClick={() => onSave?.({ name: roleName, color: roleColor, permissions: rolePermissions, displaySeparately, mentionable })}
>
Save Changes
</button>
</div>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,176 @@
import React, { useState } from 'react';
export default function SearchPanel({ onClose, onSearch }) {
const [query, setQuery] = useState('');
const [filter, setFilter] = useState('all');
const [results, setResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
const filters = [
{ id: 'all', label: 'All', icon: '🔍' },
{ id: 'messages', label: 'Messages', icon: '💬' },
{ id: 'files', label: 'Files', icon: '📎' },
{ id: 'links', label: 'Links', icon: '🔗' },
{ id: 'images', label: 'Images', icon: '🖼️' },
{ id: 'mentions', label: 'Mentions', icon: '@' },
{ id: 'pinned', label: 'Pinned', icon: '📌' },
];
const recentSearches = [
'authentication update',
'from: Anderson',
'has: link',
'in: general',
'before: 2026-01-15',
];
const mockResults = [
{
type: 'message',
channel: '#general',
author: 'Trevor',
avatar: 'T',
gradient: 'linear-gradient(135deg, #ff0000, #cc0000)',
content: 'Just pushed the authentication updates...',
timestamp: '10:34 AM',
highlight: 'authentication',
},
{
type: 'message',
channel: '#api-discussion',
author: 'Marcus',
avatar: 'M',
gradient: 'linear-gradient(135deg, #0066ff, #003380)',
content: 'The new auth flow is working perfectly now',
timestamp: 'Yesterday',
highlight: 'auth',
},
{
type: 'file',
channel: '#development',
author: 'Sarah',
filename: 'auth-documentation.pdf',
size: '2.4 MB',
timestamp: 'Jan 15',
},
];
const handleSearch = (e) => {
e.preventDefault();
if (!query.trim()) return;
setIsSearching(true);
// Simulate search
setTimeout(() => {
setResults(mockResults);
setIsSearching(false);
}, 500);
};
return (
<div className="search-panel">
<div className="search-header">
<form className="search-form" onSubmit={handleSearch}>
<span className="search-icon">🔍</span>
<input
type="text"
className="search-input"
placeholder="Search messages, files, and more..."
value={query}
onChange={(e) => setQuery(e.target.value)}
autoFocus
/>
{query && (
<button type="button" className="clear-btn" onClick={() => setQuery('')}>
</button>
)}
</form>
<button className="close-search" onClick={onClose}></button>
</div>
<div className="search-filters">
{filters.map(f => (
<button
key={f.id}
className={`filter-btn ${filter === f.id ? 'active' : ''}`}
onClick={() => setFilter(f.id)}
>
<span>{f.icon}</span>
<span>{f.label}</span>
</button>
))}
</div>
<div className="search-content">
{!query && (
<div className="search-suggestions">
<h4>Recent Searches</h4>
{recentSearches.map((search, idx) => (
<div
key={idx}
className="recent-search"
onClick={() => setQuery(search)}
>
<span className="history-icon">🕐</span>
<span>{search}</span>
</div>
))}
<h4>Search Options</h4>
<div className="search-options">
<div className="option"><code>from:</code> user</div>
<div className="option"><code>mentions:</code> user</div>
<div className="option"><code>has:</code> link, embed, file</div>
<div className="option"><code>before:</code> date</div>
<div className="option"><code>after:</code> date</div>
<div className="option"><code>in:</code> channel</div>
</div>
</div>
)}
{isSearching && (
<div className="search-loading">
<div className="spinner"></div>
<span>Searching...</span>
</div>
)}
{results.length > 0 && !isSearching && (
<div className="search-results">
<h4>{results.length} results found</h4>
{results.map((result, idx) => (
<div key={idx} className="search-result">
{result.type === 'message' && (
<>
<div className="result-avatar" style={{ background: result.gradient }}>
{result.avatar}
</div>
<div className="result-content">
<div className="result-header">
<span className="result-author">{result.author}</span>
<span className="result-channel">{result.channel}</span>
<span className="result-time">{result.timestamp}</span>
</div>
<div className="result-text">{result.content}</div>
</div>
</>
)}
{result.type === 'file' && (
<>
<div className="result-icon">📄</div>
<div className="result-content">
<div className="result-filename">{result.filename}</div>
<div className="result-meta">
{result.size} {result.channel} {result.timestamp}
</div>
</div>
</>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}

View file

@ -0,0 +1,133 @@
import React from 'react';
export default function ServerBanner({ server, isCollapsed }) {
const serverData = server || {
name: 'AeThex Foundation',
icon: 'F',
gradient: 'linear-gradient(135deg, #ff0000, #990000)',
banner: 'linear-gradient(180deg, #ff0000 0%, transparent 100%)',
description: 'Official AeThex Foundation server',
boostLevel: 3,
boostCount: 14,
memberCount: 128,
onlineCount: 47,
features: ['ANIMATED_BANNER', 'ANIMATED_ICON', 'BANNER', 'INVITE_SPLASH', 'VANITY_URL', 'VIP_REGIONS'],
vanityUrl: 'aethex',
};
const boostFeatures = [
{ level: 1, features: ['50 Emoji Slots', '128 Kbps Audio', 'Custom Invite Background', 'Animated Server Icon'] },
{ level: 2, features: ['100 Emoji Slots', '256 Kbps Audio', 'Server Banner', '50 MB Uploads', 'Custom Role Icons'] },
{ level: 3, features: ['250 Emoji Slots', '384 Kbps Audio', 'Vanity URL', '100 MB Uploads', 'Animated Banner'] },
];
if (isCollapsed) {
return (
<div className="server-banner-mini">
<div className="banner-gradient" style={{ background: serverData.banner }}></div>
<span className="server-name-mini">{serverData.name}</span>
</div>
);
}
return (
<div className="server-banner">
<div className="banner-image" style={{ background: serverData.banner }}>
<div className="banner-overlay">
<div className="banner-content">
<h2 className="banner-title">{serverData.name}</h2>
<p className="banner-description">{serverData.description}</p>
</div>
</div>
</div>
<div className="server-info-bar">
<div className="server-stats">
<div className="stat">
<span className="stat-icon">👥</span>
<span className="stat-value">{serverData.memberCount}</span>
<span className="stat-label">Members</span>
</div>
<div className="stat">
<span className="stat-indicator online"></span>
<span className="stat-value">{serverData.onlineCount}</span>
<span className="stat-label">Online</span>
</div>
</div>
<div className="boost-status">
<span className="boost-icon">🚀</span>
<span className="boost-level">Level {serverData.boostLevel}</span>
<span className="boost-count">{serverData.boostCount} Boosts</span>
</div>
</div>
{serverData.boostLevel > 0 && (
<div className="boost-perks">
<div className="perks-header">
<span className="perks-icon"></span>
<span>Server Boost Perks</span>
</div>
<div className="perks-list">
{boostFeatures.slice(0, serverData.boostLevel).map((tier) => (
tier.features.map((feature, idx) => (
<div key={`${tier.level}-${idx}`} className="perk-item">
<span className="perk-check"></span>
<span className="perk-name">{feature}</span>
</div>
))
))}
</div>
</div>
)}
{serverData.vanityUrl && (
<div className="vanity-url">
<span className="vanity-label">Vanity URL</span>
<span className="vanity-link">aethex.gg/{serverData.vanityUrl}</span>
</div>
)}
</div>
);
}
// Progress bar for boost goals
export function BoostProgress({ currentBoosts, currentLevel }) {
const levels = [
{ level: 1, required: 2 },
{ level: 2, required: 7 },
{ level: 3, required: 14 },
];
const nextLevel = levels.find((l) => l.level > currentLevel);
const prevLevel = levels.find((l) => l.level === currentLevel) || { required: 0 };
if (!nextLevel) {
return (
<div className="boost-progress max-level">
<span className="boost-label">Maximum Level Reached!</span>
<div className="boost-bar">
<div className="boost-fill" style={{ width: '100%' }}></div>
</div>
<span className="boost-count">{currentBoosts} Boosts</span>
</div>
);
}
const progress = ((currentBoosts - prevLevel.required) / (nextLevel.required - prevLevel.required)) * 100;
return (
<div className="boost-progress">
<div className="boost-header">
<span className="current-level">Level {currentLevel}</span>
<span className="next-level">Level {nextLevel.level}</span>
</div>
<div className="boost-bar">
<div className="boost-fill" style={{ width: `${Math.min(progress, 100)}%` }}></div>
</div>
<span className="boost-count">
{currentBoosts} / {nextLevel.required} Boosts
</span>
</div>
);
}

View file

@ -0,0 +1,199 @@
import React, { useState } from 'react';
const categories = [
{ id: 'home', label: 'Home', icon: '🏠' },
{ id: 'gaming', label: 'Gaming', icon: '🎮' },
{ id: 'music', label: 'Music', icon: '🎵' },
{ id: 'entertainment', label: 'Entertainment', icon: '🎬' },
{ id: 'education', label: 'Education', icon: '📚' },
{ id: 'science', label: 'Science & Tech', icon: '🔬' },
{ id: 'crypto', label: 'Crypto', icon: '💰' },
];
const featuredServers = [
{
id: 1,
name: 'AeThex Foundation',
description: 'Official home of AeThex. Security, authentication, and core infrastructure discussions.',
icon: 'F',
gradient: 'linear-gradient(135deg, #ff0000, #990000)',
members: '128.5K',
online: '24.2K',
verified: true,
partnered: true,
banner: 'linear-gradient(135deg, #1a0000, #ff0000)',
tags: ['Security', 'Development', 'Official'],
},
{
id: 2,
name: 'AeThex Labs',
description: 'Experimental features, beta testing, and bleeding-edge technology development.',
icon: 'L',
gradient: 'linear-gradient(135deg, #ffa500, #ff6b00)',
members: '45.2K',
online: '8.1K',
verified: true,
partnered: false,
banner: 'linear-gradient(135deg, #1a0f00, #ffa500)',
tags: ['Beta', 'Innovation', 'Research'],
},
{
id: 3,
name: 'GameForge Community',
description: 'The ultimate gaming community. LFG, tournaments, and game discussions.',
icon: '🎮',
members: '892.1K',
online: '156.3K',
verified: true,
partnered: true,
banner: 'linear-gradient(135deg, #2d1b69, #5865f2)',
tags: ['Gaming', 'LFG', 'Tournaments'],
},
{
id: 4,
name: 'Lofi Beats',
description: '24/7 lofi hip hop radio - beats to relax/study to',
icon: '🎧',
members: '1.2M',
online: '89.4K',
verified: true,
partnered: true,
banner: 'linear-gradient(135deg, #1a1a2e, #16213e)',
tags: ['Music', 'Chill', 'Study'],
},
];
const popularServers = [
{ id: 5, name: 'Python', icon: '🐍', members: '456K', tags: ['Programming'] },
{ id: 6, name: 'Anime Hub', icon: '🎌', members: '1.8M', tags: ['Anime'] },
{ id: 7, name: 'Art & Design', icon: '🎨', members: '234K', tags: ['Creative'] },
{ id: 8, name: 'Crypto Trading', icon: '📈', members: '567K', tags: ['Crypto'] },
{ id: 9, name: 'Movie Night', icon: '🎬', members: '123K', tags: ['Movies'] },
{ id: 10, name: 'Book Club', icon: '📚', members: '89K', tags: ['Books'] },
];
export default function ServerDiscovery({ onJoin, onClose }) {
const [search, setSearch] = useState('');
const [activeCategory, setActiveCategory] = useState('home');
return (
<div className="server-discovery">
<div className="discovery-sidebar">
<div className="discovery-header">
<h2>Discover</h2>
</div>
<div className="discovery-search">
<span className="search-icon">🔍</span>
<input
type="text"
placeholder="Explore servers"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="discovery-nav">
{categories.map(cat => (
<button
key={cat.id}
className={`discovery-nav-item ${activeCategory === cat.id ? 'active' : ''}`}
onClick={() => setActiveCategory(cat.id)}
>
<span className="nav-icon">{cat.icon}</span>
<span>{cat.label}</span>
</button>
))}
</div>
</div>
<div className="discovery-content">
<button className="discovery-close" onClick={onClose}></button>
<section className="discovery-section">
<h3>Featured Communities</h3>
<div className="featured-grid">
{featuredServers.map(server => (
<div key={server.id} className="featured-card">
<div
className="card-banner"
style={{ background: server.banner }}
>
{server.verified && <span className="verified-badge"></span>}
{server.partnered && <span className="partner-badge">🤝</span>}
</div>
<div className="card-content">
<div
className="card-icon"
style={server.gradient ? { background: server.gradient } : undefined}
>
{server.icon}
</div>
<h4 className="card-name">{server.name}</h4>
<p className="card-description">{server.description}</p>
<div className="card-tags">
{server.tags.map(tag => (
<span key={tag} className="tag">{tag}</span>
))}
</div>
<div className="card-stats">
<span className="stat">
<span className="online-dot" />
{server.online} Online
</span>
<span className="stat">
👥 {server.members} Members
</span>
</div>
</div>
<button
className="join-btn"
onClick={() => onJoin?.(server)}
>
Join
</button>
</div>
))}
</div>
</section>
<section className="discovery-section">
<h3>Popular Right Now</h3>
<div className="popular-grid">
{popularServers.map(server => (
<div key={server.id} className="popular-card">
<div className="popular-icon">{server.icon}</div>
<div className="popular-info">
<h4>{server.name}</h4>
<span className="popular-members">{server.members} members</span>
</div>
<button
className="join-btn small"
onClick={() => onJoin?.(server)}
>
Join
</button>
</div>
))}
</div>
</section>
<section className="discovery-section">
<h3>Browse by Category</h3>
<div className="category-grid">
{categories.filter(c => c.id !== 'home').map(cat => (
<button
key={cat.id}
className="category-card"
onClick={() => setActiveCategory(cat.id)}
>
<span className="category-icon">{cat.icon}</span>
<span className="category-label">{cat.label}</span>
</button>
))}
</div>
</section>
</div>
</div>
);
}

View file

@ -0,0 +1,256 @@
import React, { useState } from 'react';
const mockData = {
overview: {
totalMembers: 12847,
membersChange: '+342',
activeMembers: 3241,
activeChange: '+12%',
messagesPerDay: 8432,
messagesChange: '-5%',
joinRate: 89,
retentionRate: 76,
},
growth: [
{ date: 'Jan 29', joins: 45, leaves: 12 },
{ date: 'Jan 30', joins: 52, leaves: 8 },
{ date: 'Jan 31', joins: 38, leaves: 15 },
{ date: 'Feb 1', joins: 67, leaves: 10 },
{ date: 'Feb 2', joins: 89, leaves: 7 },
{ date: 'Feb 3', joins: 72, leaves: 11 },
{ date: 'Feb 4', joins: 85, leaves: 9 },
],
topChannels: [
{ name: 'general', messages: 2341, percentage: 28 },
{ name: 'gaming', messages: 1892, percentage: 22 },
{ name: 'off-topic', messages: 1456, percentage: 17 },
{ name: 'development', messages: 1234, percentage: 15 },
{ name: 'announcements', messages: 892, percentage: 11 },
],
peakHours: [
{ hour: '12AM', activity: 15 },
{ hour: '4AM', activity: 8 },
{ hour: '8AM', activity: 25 },
{ hour: '12PM', activity: 65 },
{ hour: '4PM', activity: 85 },
{ hour: '8PM', activity: 100 },
],
demographics: {
regions: [
{ name: 'North America', percentage: 42 },
{ name: 'Europe', percentage: 31 },
{ name: 'Asia', percentage: 18 },
{ name: 'Other', percentage: 9 },
],
platforms: [
{ name: 'Desktop', percentage: 58 },
{ name: 'Mobile', percentage: 35 },
{ name: 'Web', percentage: 7 },
],
},
invites: [
{ code: 'abc123', uses: 234, creator: 'Admin' },
{ code: 'def456', uses: 156, creator: 'Mod' },
{ code: 'ghi789', uses: 89, creator: 'Member' },
],
};
export default function ServerInsights({ onClose }) {
const [activeTab, setActiveTab] = useState('overview');
const [timeRange, setTimeRange] = useState('7d');
const tabs = [
{ id: 'overview', label: 'Overview', icon: '📊' },
{ id: 'growth', label: 'Growth', icon: '📈' },
{ id: 'engagement', label: 'Engagement', icon: '💬' },
{ id: 'audience', label: 'Audience', icon: '👥' },
];
return (
<div className="server-insights">
<div className="insights-header">
<h2>📊 Server Insights</h2>
<div className="insights-controls">
<select
className="time-range-select"
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
>
<option value="24h">Last 24 hours</option>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
<option value="90d">Last 90 days</option>
</select>
<button className="insights-close" onClick={onClose}></button>
</div>
</div>
<div className="insights-tabs">
{tabs.map(tab => (
<button
key={tab.id}
className={`insights-tab ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
<span>{tab.icon}</span> {tab.label}
</button>
))}
</div>
<div className="insights-content">
{activeTab === 'overview' && (
<div className="insights-overview">
<div className="stat-cards">
<div className="stat-card">
<div className="stat-icon">👥</div>
<div className="stat-info">
<span className="stat-value">{mockData.overview.totalMembers.toLocaleString()}</span>
<span className="stat-label">Total Members</span>
<span className="stat-change positive">{mockData.overview.membersChange}</span>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">🟢</div>
<div className="stat-info">
<span className="stat-value">{mockData.overview.activeMembers.toLocaleString()}</span>
<span className="stat-label">Active Members</span>
<span className="stat-change positive">{mockData.overview.activeChange}</span>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">💬</div>
<div className="stat-info">
<span className="stat-value">{mockData.overview.messagesPerDay.toLocaleString()}</span>
<span className="stat-label">Messages/Day</span>
<span className="stat-change negative">{mockData.overview.messagesChange}</span>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">📥</div>
<div className="stat-info">
<span className="stat-value">{mockData.overview.joinRate}%</span>
<span className="stat-label">Join Rate</span>
</div>
</div>
</div>
<div className="insights-chart">
<h3>Member Growth</h3>
<div className="simple-chart">
{mockData.growth.map((day, idx) => (
<div key={idx} className="chart-bar-container">
<div className="chart-bar joins" style={{ height: `${day.joins}%` }} title={`+${day.joins} joins`} />
<div className="chart-bar leaves" style={{ height: `${day.leaves}%` }} title={`-${day.leaves} leaves`} />
<span className="chart-label">{day.date}</span>
</div>
))}
</div>
</div>
</div>
)}
{activeTab === 'growth' && (
<div className="insights-growth">
<div className="growth-stats">
<div className="growth-stat">
<span className="growth-number positive">+{mockData.growth.reduce((a, d) => a + d.joins, 0)}</span>
<span className="growth-label">New Members</span>
</div>
<div className="growth-stat">
<span className="growth-number negative">-{mockData.growth.reduce((a, d) => a + d.leaves, 0)}</span>
<span className="growth-label">Members Left</span>
</div>
<div className="growth-stat">
<span className="growth-number">{mockData.overview.retentionRate}%</span>
<span className="growth-label">Retention Rate</span>
</div>
</div>
<div className="top-invites">
<h3>Top Invite Links</h3>
{mockData.invites.map((invite, idx) => (
<div key={idx} className="invite-row">
<span className="invite-code">discord.gg/{invite.code}</span>
<span className="invite-uses">{invite.uses} uses</span>
<span className="invite-creator">by {invite.creator}</span>
</div>
))}
</div>
</div>
)}
{activeTab === 'engagement' && (
<div className="insights-engagement">
<div className="top-channels">
<h3>Most Active Channels</h3>
{mockData.topChannels.map((channel, idx) => (
<div key={idx} className="channel-stat">
<span className="channel-rank">#{idx + 1}</span>
<span className="channel-name">#{channel.name}</span>
<div className="channel-bar">
<div
className="channel-bar-fill"
style={{ width: `${channel.percentage}%` }}
/>
</div>
<span className="channel-messages">{channel.messages.toLocaleString()}</span>
</div>
))}
</div>
<div className="peak-hours">
<h3>Peak Activity Hours</h3>
<div className="hours-chart">
{mockData.peakHours.map((hour, idx) => (
<div key={idx} className="hour-bar-container">
<div
className="hour-bar"
style={{ height: `${hour.activity}%` }}
/>
<span className="hour-label">{hour.hour}</span>
</div>
))}
</div>
</div>
</div>
)}
{activeTab === 'audience' && (
<div className="insights-audience">
<div className="audience-section">
<h3>Top Regions</h3>
{mockData.demographics.regions.map((region, idx) => (
<div key={idx} className="demographic-row">
<span className="demo-name">{region.name}</span>
<div className="demo-bar">
<div
className="demo-bar-fill"
style={{ width: `${region.percentage}%` }}
/>
</div>
<span className="demo-percentage">{region.percentage}%</span>
</div>
))}
</div>
<div className="audience-section">
<h3>Platforms</h3>
{mockData.demographics.platforms.map((platform, idx) => (
<div key={idx} className="demographic-row">
<span className="demo-name">{platform.name}</span>
<div className="demo-bar">
<div
className="demo-bar-fill"
style={{ width: `${platform.percentage}%` }}
/>
</div>
<span className="demo-percentage">{platform.percentage}%</span>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}

View file

@ -1,30 +1,79 @@
import React from "react";
import React, { useState } from 'react';
const servers = [
{ id: "foundation", label: "F", active: true, className: "foundation" },
{ id: "corporation", label: "C", active: false, className: "corporation" },
{ id: "labs", label: "L", active: false, className: "labs" },
{ id: "divider" },
{ id: "community1", label: "AG", active: false, className: "community" },
{ id: "community2", label: "RD", active: false, className: "community" },
{ id: "add", label: "+", active: false, className: "community" },
{ id: 'home', name: 'Direct Messages', initial: '🏠', type: 'home', notifications: 4 },
{ divider: true },
{ id: 'foundation', name: 'AeThex Foundation', initial: 'F', type: 'foundation', notifications: 0 },
{ id: 'corporation', name: 'AeThex Corporation', initial: 'C', type: 'corporation', notifications: 12 },
{ id: 'labs', name: 'AeThex Labs', initial: 'L', type: 'labs', notifications: 0, hasUpdate: true },
{ divider: true },
{ id: 'gaming', name: 'AeThex Gaming', initial: 'AG', type: 'community', notifications: 0 },
{ id: 'dev', name: 'Dev Community', initial: 'DC', type: 'community', notifications: 3 },
{ id: 'music', name: 'Music Lounge', initial: '🎵', type: 'community', notifications: 0 },
{ divider: true },
{ id: 'add', name: 'Add a Server', initial: '+', type: 'add' },
{ id: 'explore', name: 'Explore Servers', initial: '🧭', type: 'explore' },
];
export default function ServerList() {
export default function ServerList({ activeServer, setActiveServer, onOpenDMs, onDMsClick, selectedDMs, onOpenSettings }) {
const [hoveredServer, setHoveredServer] = useState(null);
return (
<div className="server-list">
{servers.map((server, idx) => {
if (server.divider) {
return <div key={`divider-${idx}`} className="server-divider" />;
}
const isActive = activeServer === server.id || (server.id === 'home' && selectedDMs);
const isHovered = hoveredServer === server.id;
let className = 'server-icon';
if (server.type !== 'home' && server.type !== 'add' && server.type !== 'explore') {
className += ` ${server.type}`;
}
if (isActive) className += ' active';
return (
<div className="server-list flex flex-col items-center py-3 gap-3 w-20 bg-[#0d0d0d] border-r border-[#1a1a1a]">
{servers.map((srv, i) =>
srv.id === "divider" ? (
<div key={i} className="server-divider w-10 h-0.5 bg-[#1a1a1a] my-1" />
) : (
<div
key={srv.id}
className={`server-icon ${srv.className} ${srv.active ? "active" : ""} w-14 h-14 rounded-xl flex items-center justify-center font-bold text-lg cursor-pointer transition-all relative`}
key={server.id}
className={className}
onClick={() => {
if (server.id === 'home') {
onOpenDMs?.();
onDMsClick?.();
}
setActiveServer?.(server.id);
}}
onMouseEnter={() => setHoveredServer(server.id)}
onMouseLeave={() => setHoveredServer(null)}
title={server.name}
style={server.type === 'home' ? { background: isActive ? '#5865f2' : '#36393f' } :
server.type === 'add' ? { background: '#36393f', color: '#3ba55d' } :
server.type === 'explore' ? { background: '#36393f', color: '#3ba55d' } : undefined}
>
{srv.label}
{server.initial}
{/* Notification Badge */}
{server.notifications > 0 && (
<div className="notification-badge">
{server.notifications > 99 ? '99+' : server.notifications}
</div>
)}
{/* Update dot */}
{server.hasUpdate && !server.notifications && (
<div className="update-dot"></div>
)}
{/* Tooltip */}
{isHovered && (
<div className="server-tooltip">
{server.name}
</div>
)
)}
</div>
);
})}
</div>
);
}

View file

@ -0,0 +1,159 @@
import React, { useState } from 'react';
export default function ServerSettingsModal({ server, onClose }) {
const [activeTab, setActiveTab] = useState('overview');
const serverData = server || {
name: 'AeThex Foundation',
icon: 'F',
gradient: 'linear-gradient(135deg, #ff0000, #990000)',
description: 'Official AeThex Foundation server. Security, authentication, and core infrastructure discussions.',
region: 'US East',
memberCount: 128,
boostLevel: 3,
boostCount: 14,
};
const tabs = [
{ id: 'overview', label: 'Overview', icon: '📋' },
{ id: 'roles', label: 'Roles', icon: '🎭' },
{ id: 'emoji', label: 'Emoji', icon: '😀' },
{ id: 'stickers', label: 'Stickers', icon: '🎨' },
{ id: 'moderation', label: 'Moderation', icon: '🛡️' },
{ id: 'audit-log', label: 'Audit Log', icon: '📜' },
{ id: 'integrations', label: 'Integrations', icon: '🔗' },
{ id: 'members', label: 'Members', icon: '👥' },
{ id: 'invites', label: 'Invites', icon: '✉️' },
{ id: 'bans', label: 'Bans', icon: '🚫' },
];
return (
<div className="modal-overlay" onClick={onClose}>
<div className="server-settings-modal" onClick={(e) => e.stopPropagation()}>
<div className="settings-sidebar">
<div className="settings-server-info">
<div className="settings-server-icon" style={{ background: serverData.gradient }}>
{serverData.icon}
</div>
<div className="settings-server-name">{serverData.name}</div>
</div>
<div className="settings-nav">
{tabs.map(tab => (
<div
key={tab.id}
className={`settings-nav-item ${activeTab === tab.id ? 'active' : ''}`}
onClick={() => setActiveTab(tab.id)}
>
<span className="nav-icon">{tab.icon}</span>
<span>{tab.label}</span>
</div>
))}
<div className="settings-divider"></div>
<div className="settings-nav-item danger">
<span className="nav-icon">🗑</span>
<span>Delete Server</span>
</div>
</div>
</div>
<div className="settings-content">
<div className="settings-header">
<h2>{tabs.find(t => t.id === activeTab)?.label}</h2>
<button className="settings-close" onClick={onClose}></button>
</div>
{activeTab === 'overview' && (
<div className="settings-overview">
<div className="setting-group">
<label>Server Name</label>
<input type="text" className="setting-input" defaultValue={serverData.name} />
</div>
<div className="setting-group">
<label>Server Icon</label>
<div className="icon-upload">
<div className="current-icon" style={{ background: serverData.gradient }}>
{serverData.icon}
</div>
<button className="upload-btn">Upload Image</button>
</div>
</div>
<div className="setting-group">
<label>Description</label>
<textarea
className="setting-textarea"
defaultValue={serverData.description}
rows={3}
/>
</div>
<div className="setting-group">
<label>Server Region</label>
<select className="setting-select" defaultValue={serverData.region}>
<option>US East</option>
<option>US West</option>
<option>Europe</option>
<option>Asia</option>
</select>
</div>
<div className="boost-info">
<div className="boost-level">
<span className="boost-icon">🚀</span>
<span>Level {serverData.boostLevel}</span>
</div>
<div className="boost-count">{serverData.boostCount} Boosts</div>
</div>
</div>
)}
{activeTab === 'roles' && (
<div className="settings-roles">
<div className="roles-header">
<button className="create-role-btn">+ Create Role</button>
</div>
<div className="roles-list">
<div className="role-item">
<span className="role-color" style={{ background: '#ff0000' }}></span>
<span className="role-name">Founder</span>
<span className="role-members">1 member</span>
</div>
<div className="role-item">
<span className="role-color" style={{ background: '#ff0000' }}></span>
<span className="role-name">Foundation</span>
<span className="role-members">8 members</span>
</div>
<div className="role-item">
<span className="role-color" style={{ background: '#0066ff' }}></span>
<span className="role-name">Corporation</span>
<span className="role-members">24 members</span>
</div>
<div className="role-item">
<span className="role-color" style={{ background: '#ffa500' }}></span>
<span className="role-name">Labs</span>
<span className="role-members">12 members</span>
</div>
<div className="role-item">
<span className="role-color" style={{ background: '#99aab5' }}></span>
<span className="role-name">@everyone</span>
<span className="role-members">128 members</span>
</div>
</div>
</div>
)}
{activeTab !== 'overview' && activeTab !== 'roles' && (
<div className="settings-placeholder">
<span className="placeholder-icon">🔧</span>
<p>Settings for {tabs.find(t => t.id === activeTab)?.label} coming soon</p>
</div>
)}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,247 @@
import React, { useState } from 'react';
const builtInTemplates = [
{ id: 'gaming', name: 'Gaming', icon: '🎮', description: 'Perfect for gaming communities', channels: ['general', 'voice-chat', 'lfg', 'clips', 'memes'], roles: ['Admin', 'Moderator', 'Member'] },
{ id: 'school', name: 'School Club', icon: '📚', description: 'For study groups and school clubs', channels: ['announcements', 'homework-help', 'resources', 'off-topic', 'voice-study'], roles: ['Teacher', 'TA', 'Student'] },
{ id: 'creator', name: 'Content Creator', icon: '🎬', description: 'For streamers and creators', channels: ['announcements', 'stream-chat', 'clips', 'fan-art', 'suggestions'], roles: ['Creator', 'Mod', 'Subscriber', 'Fan'] },
{ id: 'friends', name: 'Friends', icon: '👥', description: 'Casual hangout space', channels: ['general', 'memes', 'music', 'gaming'], roles: ['Admin', 'Member'] },
{ id: 'local', name: 'Local Community', icon: '🏘️', description: 'For local groups and neighborhoods', channels: ['announcements', 'events', 'marketplace', 'discussion'], roles: ['Organizer', 'Member'] },
];
const myTemplates = [
{ id: 'my-dev', name: 'Dev Team Setup', icon: '💻', description: 'My development team template', channels: ['general', 'backend', 'frontend', 'devops', 'standup'], roles: ['Lead', 'Senior', 'Junior', 'Intern'], createdAt: '2025-01-15' },
];
export default function ServerTemplate({ mode = 'apply', currentServer, onClose, onApply, onSave }) {
const [activeTab, setActiveTab] = useState('browse');
const [selectedTemplate, setSelectedTemplate] = useState(null);
const [templateName, setTemplateName] = useState('');
const [templateDesc, setTemplateDesc] = useState('');
const [includeChannels, setIncludeChannels] = useState(true);
const [includeRoles, setIncludeRoles] = useState(true);
const [includeSettings, setIncludeSettings] = useState(true);
const handleApplyTemplate = () => {
if (selectedTemplate) {
onApply?.(selectedTemplate);
}
};
const handleSaveTemplate = () => {
onSave?.({
name: templateName,
description: templateDesc,
includeChannels,
includeRoles,
includeSettings,
});
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="server-template-modal" onClick={(e) => e.stopPropagation()}>
<div className="template-header">
<h2>📋 Server Templates</h2>
<button className="template-close" onClick={onClose}></button>
</div>
<div className="template-tabs">
<button
className={`template-tab ${activeTab === 'browse' ? 'active' : ''}`}
onClick={() => setActiveTab('browse')}
>
Browse Templates
</button>
<button
className={`template-tab ${activeTab === 'my-templates' ? 'active' : ''}`}
onClick={() => setActiveTab('my-templates')}
>
My Templates
</button>
<button
className={`template-tab ${activeTab === 'create' ? 'active' : ''}`}
onClick={() => setActiveTab('create')}
>
Create Template
</button>
</div>
<div className="template-content">
{activeTab === 'browse' && (
<div className="template-browse">
<p className="template-intro">
Start your server with a template to get channels, roles, and settings pre-configured.
</p>
<div className="template-grid">
{builtInTemplates.map(template => (
<div
key={template.id}
className={`template-card ${selectedTemplate?.id === template.id ? 'selected' : ''}`}
onClick={() => setSelectedTemplate(template)}
>
<div className="template-icon">{template.icon}</div>
<div className="template-info">
<h3>{template.name}</h3>
<p>{template.description}</p>
</div>
<div className="template-preview">
<div className="preview-section">
<span className="preview-label">Channels:</span>
<span className="preview-count">{template.channels.length}</span>
</div>
<div className="preview-section">
<span className="preview-label">Roles:</span>
<span className="preview-count">{template.roles.length}</span>
</div>
</div>
</div>
))}
</div>
{selectedTemplate && (
<div className="template-detail">
<h3>{selectedTemplate.icon} {selectedTemplate.name}</h3>
<div className="detail-section">
<h4>Channels</h4>
<div className="detail-items">
{selectedTemplate.channels.map(ch => (
<span key={ch} className="detail-item">#{ch}</span>
))}
</div>
</div>
<div className="detail-section">
<h4>Roles</h4>
<div className="detail-items">
{selectedTemplate.roles.map(role => (
<span key={role} className="detail-item role">@{role}</span>
))}
</div>
</div>
</div>
)}
</div>
)}
{activeTab === 'my-templates' && (
<div className="my-templates">
{myTemplates.length === 0 ? (
<div className="no-templates">
<span className="no-templates-icon">📁</span>
<p>No saved templates</p>
<span>Create a template from your server to save it here</span>
</div>
) : (
<div className="template-list">
{myTemplates.map(template => (
<div
key={template.id}
className={`template-list-item ${selectedTemplate?.id === template.id ? 'selected' : ''}`}
onClick={() => setSelectedTemplate(template)}
>
<div className="template-icon">{template.icon}</div>
<div className="template-info">
<h3>{template.name}</h3>
<p>{template.description}</p>
<span className="template-date">Created {template.createdAt}</span>
</div>
<div className="template-actions">
<button className="template-action-btn" title="Edit"></button>
<button className="template-action-btn" title="Delete">🗑</button>
</div>
</div>
))}
</div>
)}
</div>
)}
{activeTab === 'create' && (
<div className="create-template">
<p className="create-intro">
Create a template from your current server configuration.
</p>
<div className="create-form">
<div className="form-group">
<label>Template Name</label>
<input
type="text"
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
placeholder="My Server Template"
/>
</div>
<div className="form-group">
<label>Description</label>
<textarea
value={templateDesc}
onChange={(e) => setTemplateDesc(e.target.value)}
placeholder="Describe what this template is for..."
rows={3}
/>
</div>
<div className="form-group">
<label>Include in Template</label>
<div className="include-options">
<label className="include-option">
<input
type="checkbox"
checked={includeChannels}
onChange={(e) => setIncludeChannels(e.target.checked)}
/>
<span>Channels & Categories</span>
</label>
<label className="include-option">
<input
type="checkbox"
checked={includeRoles}
onChange={(e) => setIncludeRoles(e.target.checked)}
/>
<span>Roles & Permissions</span>
</label>
<label className="include-option">
<input
type="checkbox"
checked={includeSettings}
onChange={(e) => setIncludeSettings(e.target.checked)}
/>
<span>Server Settings</span>
</label>
</div>
</div>
<div className="template-warning">
<span></span>
<span>Templates do not include messages, members, or server-specific content.</span>
</div>
</div>
</div>
)}
</div>
<div className="template-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
{activeTab === 'create' ? (
<button
className="save-btn"
onClick={handleSaveTemplate}
disabled={!templateName.trim()}
>
Create Template
</button>
) : (
<button
className="apply-btn"
onClick={handleApplyTemplate}
disabled={!selectedTemplate}
>
Use Template
</button>
)}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,186 @@
import React, { useState } from 'react';
const slowModePresets = [
{ value: 0, label: 'Off' },
{ value: 5, label: '5s' },
{ value: 10, label: '10s' },
{ value: 15, label: '15s' },
{ value: 30, label: '30s' },
{ value: 60, label: '1m' },
{ value: 120, label: '2m' },
{ value: 300, label: '5m' },
{ value: 600, label: '10m' },
{ value: 900, label: '15m' },
{ value: 1800, label: '30m' },
{ value: 3600, label: '1h' },
{ value: 7200, label: '2h' },
{ value: 21600, label: '6h' },
];
export default function SlowModeSettings({ channel, onSave, onClose }) {
const [slowMode, setSlowMode] = useState(channel?.slowMode || 0);
const [customMode, setCustomMode] = useState(false);
const [customSeconds, setCustomSeconds] = useState('');
const [exemptRoles, setExemptRoles] = useState(channel?.exemptRoles || []);
const [showExemptSettings, setShowExemptSettings] = useState(false);
const roles = [
{ id: 'admin', name: 'Admin', color: '#ff0000' },
{ id: 'mod', name: 'Moderator', color: '#5865f2' },
{ id: 'helper', name: 'Helper', color: '#3ba55d' },
{ id: 'vip', name: 'VIP', color: '#faa61a' },
{ id: 'booster', name: 'Server Booster', color: '#ff73fa' },
];
const formatDuration = (seconds) => {
if (seconds === 0) return 'Off';
if (seconds < 60) return `${seconds} second${seconds !== 1 ? 's' : ''}`;
if (seconds < 3600) return `${Math.floor(seconds / 60)} minute${seconds >= 120 ? 's' : ''}`;
return `${Math.floor(seconds / 3600)} hour${seconds >= 7200 ? 's' : ''}`;
};
const handlePresetClick = (value) => {
setSlowMode(value);
setCustomMode(false);
};
const handleCustomApply = () => {
const value = parseInt(customSeconds);
if (!isNaN(value) && value >= 0 && value <= 21600) {
setSlowMode(value);
setCustomMode(false);
}
};
const toggleExemptRole = (roleId) => {
setExemptRoles(prev =>
prev.includes(roleId)
? prev.filter(id => id !== roleId)
: [...prev, roleId]
);
};
const handleSave = () => {
onSave?.({
slowMode,
exemptRoles,
});
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="slowmode-modal" onClick={(e) => e.stopPropagation()}>
<div className="slowmode-header">
<h2> Slowmode Settings</h2>
<button className="slowmode-close" onClick={onClose}></button>
</div>
<div className="slowmode-content">
<div className="slowmode-channel">
<span className="channel-icon">#</span>
<span className="channel-name">{channel?.name || 'general'}</span>
</div>
<div className="slowmode-section">
<h3>Slowmode Duration</h3>
<p className="slowmode-description">
Members can only send one message per {formatDuration(slowMode)}.
</p>
<div className="slowmode-presets">
{slowModePresets.map(preset => (
<button
key={preset.value}
className={`preset-btn ${slowMode === preset.value && !customMode ? 'active' : ''}`}
onClick={() => handlePresetClick(preset.value)}
>
{preset.label}
</button>
))}
<button
className={`preset-btn ${customMode ? 'active' : ''}`}
onClick={() => setCustomMode(true)}
>
Custom
</button>
</div>
{customMode && (
<div className="custom-input">
<input
type="number"
min="0"
max="21600"
value={customSeconds}
onChange={(e) => setCustomSeconds(e.target.value)}
placeholder="Seconds (0-21600)"
/>
<button className="apply-custom" onClick={handleCustomApply}>
Apply
</button>
</div>
)}
<div className="slowmode-slider">
<input
type="range"
min="0"
max="21600"
step="5"
value={slowMode}
onChange={(e) => setSlowMode(parseInt(e.target.value))}
/>
<span className="slider-value">{formatDuration(slowMode)}</span>
</div>
</div>
<div className="slowmode-section">
<div
className="section-header-toggle"
onClick={() => setShowExemptSettings(!showExemptSettings)}
>
<h3>Exempt Roles</h3>
<span className="toggle-icon">{showExemptSettings ? '▼' : '▶'}</span>
</div>
{showExemptSettings && (
<div className="exempt-roles">
<p className="exempt-description">
These roles won't be affected by slowmode.
</p>
{roles.map(role => (
<label key={role.id} className="exempt-role">
<input
type="checkbox"
checked={exemptRoles.includes(role.id)}
onChange={() => toggleExemptRole(role.id)}
/>
<span
className="role-name"
style={{ color: role.color }}
>
@{role.name}
</span>
</label>
))}
</div>
)}
</div>
<div className="slowmode-info">
<span className="info-icon"></span>
<span>
Slowmode helps prevent spam by limiting how often members can send messages.
Members with "Manage Channel" permission are always exempt.
</span>
</div>
</div>
<div className="slowmode-footer">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button className="save-btn" onClick={handleSave}>Save Changes</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,129 @@
import React, { useState } from 'react';
const sounds = [
{ id: 1, name: 'Airhorn', emoji: '📯', duration: '1.2s', category: 'effects' },
{ id: 2, name: 'Cricket', emoji: '🦗', duration: '2.0s', category: 'effects' },
{ id: 3, name: 'Sad Trombone', emoji: '📯', duration: '1.8s', category: 'effects' },
{ id: 4, name: 'Clap', emoji: '👏', duration: '0.5s', category: 'effects' },
{ id: 5, name: 'Dun Dun Dun', emoji: '🎵', duration: '1.5s', category: 'effects' },
{ id: 6, name: 'Wow', emoji: '😮', duration: '0.8s', category: 'voice' },
{ id: 7, name: 'Bruh', emoji: '😐', duration: '0.5s', category: 'voice' },
{ id: 8, name: "Let's Go", emoji: '🔥', duration: '0.7s', category: 'voice' },
{ id: 9, name: 'GG', emoji: '🎮', duration: '0.6s', category: 'voice' },
{ id: 10, name: 'Oof', emoji: '💀', duration: '0.4s', category: 'voice' },
{ id: 11, name: 'Windows Error', emoji: '💻', duration: '0.8s', category: 'memes' },
{ id: 12, name: 'Vine Boom', emoji: '💥', duration: '0.6s', category: 'memes' },
];
export default function Soundboard({ onPlay, onClose }) {
const [search, setSearch] = useState('');
const [activeCategory, setActiveCategory] = useState('all');
const [volume, setVolume] = useState(50);
const [playing, setPlaying] = useState(null);
const [favorites, setFavorites] = useState([1, 8, 12]);
const categories = [
{ id: 'all', label: 'All' },
{ id: 'favorites', label: '⭐ Favorites' },
{ id: 'effects', label: 'Effects' },
{ id: 'voice', label: 'Voice' },
{ id: 'memes', label: 'Memes' },
];
const filteredSounds = sounds.filter(s => {
const matchesSearch = !search || s.name.toLowerCase().includes(search.toLowerCase());
const matchesCategory = activeCategory === 'all' ||
(activeCategory === 'favorites' ? favorites.includes(s.id) : s.category === activeCategory);
return matchesSearch && matchesCategory;
});
const handlePlay = (sound) => {
setPlaying(sound.id);
onPlay?.(sound);
// Simulate sound duration
setTimeout(() => setPlaying(null), parseFloat(sound.duration) * 1000);
};
const toggleFavorite = (e, soundId) => {
e.stopPropagation();
setFavorites(prev =>
prev.includes(soundId)
? prev.filter(id => id !== soundId)
: [...prev, soundId]
);
};
return (
<div className="soundboard">
<div className="soundboard-header">
<h3>Soundboard</h3>
<button className="soundboard-close" onClick={onClose}></button>
</div>
<div className="soundboard-search">
<span className="search-icon">🔍</span>
<input
type="text"
placeholder="Search sounds"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
<div className="soundboard-categories">
{categories.map(cat => (
<button
key={cat.id}
className={`sb-category ${activeCategory === cat.id ? 'active' : ''}`}
onClick={() => setActiveCategory(cat.id)}
>
{cat.label}
</button>
))}
</div>
<div className="soundboard-grid">
{filteredSounds.map(sound => (
<button
key={sound.id}
className={`sound-btn ${playing === sound.id ? 'playing' : ''}`}
onClick={() => handlePlay(sound)}
>
<span className="sound-emoji">{sound.emoji}</span>
<span className="sound-name">{sound.name}</span>
<span className="sound-duration">{sound.duration}</span>
<button
className={`sound-favorite ${favorites.includes(sound.id) ? 'active' : ''}`}
onClick={(e) => toggleFavorite(e, sound.id)}
>
{favorites.includes(sound.id) ? '⭐' : '☆'}
</button>
{playing === sound.id && <div className="sound-playing-indicator" />}
</button>
))}
</div>
{filteredSounds.length === 0 && (
<div className="soundboard-empty">
<span>No sounds found</span>
</div>
)}
<div className="soundboard-footer">
<div className="volume-control">
<span className="volume-icon">🔊</span>
<input
type="range"
min="0"
max="100"
value={volume}
onChange={(e) => setVolume(Number(e.target.value))}
className="volume-slider"
/>
<span className="volume-value">{volume}%</span>
</div>
<button className="upload-sound-btn">+ Upload Sound</button>
</div>
</div>
);
}

View file

@ -0,0 +1,152 @@
import React, { useState } from 'react';
export default function StageChannel({ channel, onClose, onLeave }) {
const [isLive, setIsLive] = useState(true);
const [isSpeaker, setIsSpeaker] = useState(false);
const [handRaised, setHandRaised] = useState(false);
const [muted, setMuted] = useState(true);
const stageData = channel || {
name: 'Community Townhall',
topic: 'Q&A with the AeThex Team - Monthly Update',
startedAt: '2 hours ago',
listeners: 234,
};
const speakers = [
{ id: 1, name: 'Anderson', avatar: 'A', color: '#ff0000', speaking: true, role: 'Host' },
{ id: 2, name: 'Trevor', avatar: 'T', color: '#0066ff', speaking: false, role: 'Speaker' },
{ id: 3, name: 'Sarah', avatar: 'S', color: '#ffa500', speaking: false, role: 'Speaker' },
];
const audience = [
{ id: 4, name: 'Marcus', avatar: 'M', handRaised: true },
{ id: 5, name: 'DevUser_123', avatar: 'D', handRaised: false },
{ id: 6, name: 'Player_456', avatar: 'P', handRaised: true },
{ id: 7, name: 'CoolGamer', avatar: 'C', handRaised: false },
{ id: 8, name: 'TechFan', avatar: 'T', handRaised: false },
];
const handleRequestSpeak = () => {
setHandRaised(!handRaised);
};
return (
<div className="stage-channel">
<div className="stage-header">
<div className="stage-info">
<div className="stage-live-badge">
<span className="live-dot" />
LIVE
</div>
<h2>{stageData.name}</h2>
<p className="stage-topic">{stageData.topic}</p>
</div>
<button className="stage-close" onClick={onClose}></button>
</div>
<div className="stage-content">
<div className="stage-section">
<div className="section-header">
<span className="speaker-icon">🎙</span>
<span>Speakers {speakers.length}</span>
</div>
<div className="speakers-grid">
{speakers.map(speaker => (
<div
key={speaker.id}
className={`speaker-card ${speaker.speaking ? 'speaking' : ''}`}
>
<div
className="speaker-avatar"
style={{ background: speaker.color }}
>
{speaker.avatar}
{speaker.speaking && <div className="speaking-ring" />}
</div>
<div className="speaker-name">{speaker.name}</div>
<div className="speaker-role">{speaker.role}</div>
</div>
))}
</div>
</div>
<div className="stage-section">
<div className="section-header">
<span className="audience-icon">👥</span>
<span>Audience {audience.length}</span>
</div>
<div className="audience-grid">
{audience.map(user => (
<div key={user.id} className="audience-member">
<div className="audience-avatar">{user.avatar}</div>
<span className="audience-name">{user.name}</span>
{user.handRaised && <span className="hand-raised"></span>}
</div>
))}
</div>
</div>
</div>
<div className="stage-stats">
<span>👁 {stageData.listeners} listening</span>
<span></span>
<span>Started {stageData.startedAt}</span>
</div>
<div className="stage-controls">
{isSpeaker ? (
<>
<button
className={`stage-btn ${muted ? 'muted' : ''}`}
onClick={() => setMuted(!muted)}
>
{muted ? '🔇' : '🎤'}
<span>{muted ? 'Unmute' : 'Mute'}</span>
</button>
<button className="stage-btn" onClick={() => setIsSpeaker(false)}>
👥
<span>Move to Audience</span>
</button>
</>
) : (
<button
className={`stage-btn request ${handRaised ? 'active' : ''}`}
onClick={handleRequestSpeak}
>
<span>{handRaised ? 'Lower Hand' : 'Request to Speak'}</span>
</button>
)}
<button className="stage-btn leave" onClick={onLeave}>
📞
<span>Leave Stage</span>
</button>
</div>
</div>
);
}
// Stage channel in sidebar
export function StageChannelPreview({ channel, onJoin }) {
return (
<div className="stage-preview">
<div className="stage-preview-header">
<span className="stage-icon">📢</span>
<span className="stage-name">{channel.name}</span>
<div className="stage-live-dot" />
</div>
{channel.topic && (
<div className="stage-preview-topic">{channel.topic}</div>
)}
<div className="stage-preview-info">
<span className="speaker-count">🎙 {channel.speakers} speaking</span>
<span className="listener-count">👁 {channel.listeners}</span>
</div>
<button className="stage-join-btn" onClick={onJoin}>
Join Stage
</button>
</div>
);
}

View file

@ -0,0 +1,142 @@
import React, { useState } from 'react';
const stickerPacks = [
{
id: 'default',
name: 'Wumpus',
icon: '🐙',
stickers: [
{ id: 1, emoji: '👋', name: 'Wave' },
{ id: 2, emoji: '❤️', name: 'Love' },
{ id: 3, emoji: '😭', name: 'Cry' },
{ id: 4, emoji: '🎉', name: 'Party' },
{ id: 5, emoji: '😎', name: 'Cool' },
{ id: 6, emoji: '🤔', name: 'Think' },
]
},
{
id: 'aethex',
name: 'AeThex',
icon: '🔺',
stickers: [
{ id: 7, emoji: '🔥', name: 'Fire' },
{ id: 8, emoji: '⚡', name: 'Lightning' },
{ id: 9, emoji: '🚀', name: 'Launch' },
{ id: 10, emoji: '💎', name: 'Gem' },
{ id: 11, emoji: '🔮', name: 'Crystal' },
{ id: 12, emoji: '⭐', name: 'Star' },
]
},
{
id: 'gaming',
name: 'Gaming',
icon: '🎮',
stickers: [
{ id: 13, emoji: '🏆', name: 'Trophy' },
{ id: 14, emoji: '💪', name: 'Strong' },
{ id: 15, emoji: '🎯', name: 'Target' },
{ id: 16, emoji: '⚔️', name: 'Swords' },
{ id: 17, emoji: '🛡️', name: 'Shield' },
{ id: 18, emoji: '👑', name: 'Crown' },
]
}
];
export default function StickerPicker({ onSelect, onClose }) {
const [search, setSearch] = useState('');
const [activePack, setActivePack] = useState('default');
const [recentStickers, setRecentStickers] = useState([1, 7, 13]);
const currentPack = stickerPacks.find(p => p.id === activePack);
const allStickers = stickerPacks.flatMap(p => p.stickers);
const filteredStickers = search
? allStickers.filter(s => s.name.toLowerCase().includes(search.toLowerCase()))
: currentPack?.stickers || [];
const handleSelect = (sticker) => {
setRecentStickers(prev => [sticker.id, ...prev.filter(id => id !== sticker.id)].slice(0, 6));
onSelect?.(sticker);
};
return (
<div className="sticker-picker">
<div className="sticker-header">
<div className="sticker-search">
<span className="search-icon">🔍</span>
<input
type="text"
placeholder="Search stickers"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
</div>
</div>
{!search && recentStickers.length > 0 && (
<div className="sticker-section">
<div className="sticker-section-header">
<span>Recently Used</span>
</div>
<div className="sticker-grid small">
{recentStickers.map(id => {
const sticker = allStickers.find(s => s.id === id);
if (!sticker) return null;
return (
<div
key={`recent-${id}`}
className="sticker-item small"
onClick={() => handleSelect(sticker)}
title={sticker.name}
>
{sticker.emoji}
</div>
);
})}
</div>
</div>
)}
<div className="sticker-packs-nav">
{stickerPacks.map(pack => (
<button
key={pack.id}
className={`pack-btn ${activePack === pack.id ? 'active' : ''}`}
onClick={() => setActivePack(pack.id)}
title={pack.name}
>
{pack.icon}
</button>
))}
<div className="pack-divider" />
<button className="pack-btn add" title="Get more stickers">+</button>
</div>
<div className="sticker-section">
{!search && (
<div className="sticker-section-header">
<span>{currentPack?.name}</span>
</div>
)}
<div className="sticker-grid">
{filteredStickers.map(sticker => (
<div
key={sticker.id}
className="sticker-item"
onClick={() => handleSelect(sticker)}
title={sticker.name}
>
<div className="sticker-preview">{sticker.emoji}</div>
</div>
))}
</div>
</div>
{search && filteredStickers.length === 0 && (
<div className="sticker-empty">
<span>No stickers found</span>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,238 @@
import React, { useState } from 'react';
export default function StreamOverlay({ onStart, onClose }) {
const [streamType, setStreamType] = useState('screen');
const [source, setSource] = useState(null);
const [quality, setQuality] = useState('720p');
const [fps, setFps] = useState('30');
const streamSources = {
screen: [
{ id: 'screen-1', name: 'Screen 1', type: 'Screen', preview: '🖥️' },
{ id: 'screen-2', name: 'Screen 2', type: 'Screen', preview: '🖥️' },
],
window: [
{ id: 'vscode', name: 'Visual Studio Code', type: 'Window', preview: '💻' },
{ id: 'chrome', name: 'Google Chrome', type: 'Window', preview: '🌐' },
{ id: 'terminal', name: 'Terminal', type: 'Window', preview: '⌨️' },
{ id: 'game', name: 'Game.exe', type: 'Game', preview: '🎮' },
],
};
const qualities = [
{ value: '480p', label: '480p' },
{ value: '720p', label: '720p' },
{ value: '1080p', label: '1080p', nitro: true },
{ value: '1440p', label: '1440p', nitro: true },
{ value: '4k', label: '4K (Source)', nitro: true },
];
const framerates = [
{ value: '15', label: '15 FPS' },
{ value: '30', label: '30 FPS' },
{ value: '60', label: '60 FPS', nitro: true },
];
const handleStart = () => {
if (!source) return;
onStart?.({
type: streamType,
source,
quality,
fps,
});
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="stream-overlay" onClick={(e) => e.stopPropagation()}>
<div className="stream-header">
<h2>📺 Go Live</h2>
<button className="stream-close" onClick={onClose}></button>
</div>
<div className="stream-content">
<div className="stream-tabs">
<button
className={`stream-tab ${streamType === 'screen' ? 'active' : ''}`}
onClick={() => { setStreamType('screen'); setSource(null); }}
>
🖥 Screen
</button>
<button
className={`stream-tab ${streamType === 'window' ? 'active' : ''}`}
onClick={() => { setStreamType('window'); setSource(null); }}
>
🪟 Window
</button>
</div>
<div className="source-grid">
{streamSources[streamType]?.map(src => (
<button
key={src.id}
className={`source-card ${source?.id === src.id ? 'selected' : ''}`}
onClick={() => setSource(src)}
>
<div className="source-preview">{src.preview}</div>
<div className="source-info">
<span className="source-name">{src.name}</span>
<span className="source-type">{src.type}</span>
</div>
{source?.id === src.id && (
<span className="source-check"></span>
)}
</button>
))}
</div>
<div className="stream-settings">
<h3>Stream Quality</h3>
<div className="setting-row">
<label>Resolution</label>
<div className="quality-options">
{qualities.map(q => (
<button
key={q.value}
className={`quality-btn ${quality === q.value ? 'active' : ''} ${q.nitro ? 'nitro' : ''}`}
onClick={() => setQuality(q.value)}
disabled={q.nitro}
>
{q.label}
{q.nitro && <span className="nitro-badge">🚀</span>}
</button>
))}
</div>
</div>
<div className="setting-row">
<label>Frame Rate</label>
<div className="quality-options">
{framerates.map(f => (
<button
key={f.value}
className={`quality-btn ${fps === f.value ? 'active' : ''} ${f.nitro ? 'nitro' : ''}`}
onClick={() => setFps(f.value)}
disabled={f.nitro}
>
{f.label}
{f.nitro && <span className="nitro-badge">🚀</span>}
</button>
))}
</div>
</div>
<div className="nitro-upsell">
<span className="nitro-icon">🚀</span>
<div className="upsell-text">
<span>Stream in higher quality with Nitro</span>
<a href="#">Learn more</a>
</div>
</div>
</div>
<div className="stream-preview-section">
<h3>Preview</h3>
<div className="stream-preview-box">
{source ? (
<div className="preview-content">
<span className="preview-icon">{source.preview}</span>
<span className="preview-label">{source.name}</span>
</div>
) : (
<div className="preview-placeholder">
Select a source to preview
</div>
)}
</div>
</div>
</div>
<div className="stream-footer">
<div className="stream-info">
{source && (
<span>Streaming {source.name} at {quality} {fps}fps</span>
)}
</div>
<div className="stream-actions">
<button className="cancel-btn" onClick={onClose}>Cancel</button>
<button
className="go-live-btn"
onClick={handleStart}
disabled={!source}
>
Go Live
</button>
</div>
</div>
</div>
</div>
);
}
// Stream viewer component (when watching someone's stream)
export function StreamViewer({ stream, onClose }) {
const [isFullscreen, setIsFullscreen] = useState(false);
const [volume, setVolume] = useState(100);
return (
<div className={`stream-viewer ${isFullscreen ? 'fullscreen' : ''}`}>
<div className="stream-video">
<div className="stream-placeholder">
<span className="stream-icon">📺</span>
<span className="stream-title">{stream?.source?.name || 'Stream'}</span>
</div>
</div>
<div className="stream-controls">
<div className="stream-info-bar">
<div className="streamer-info">
<div
className="streamer-avatar"
style={stream?.user?.color ? { background: stream.user.color } : undefined}
>
{stream?.user?.avatar || 'S'}
</div>
<div className="streamer-details">
<span className="streamer-name">{stream?.user?.name || 'Streamer'}</span>
<span className="viewer-count">👁 {stream?.viewers || 0} viewers</span>
</div>
</div>
<div className="viewer-controls">
<div className="volume-control">
<button className="control-btn" onClick={() => setVolume(v => v > 0 ? 0 : 100)}>
{volume === 0 ? '🔇' : '🔊'}
</button>
<input
type="range"
min="0"
max="100"
value={volume}
onChange={(e) => setVolume(Number(e.target.value))}
className="volume-slider"
/>
</div>
<button className="control-btn" onClick={() => setIsFullscreen(!isFullscreen)}>
{isFullscreen ? '⛶' : '⛶'}
</button>
<button className="control-btn leave" onClick={onClose}>
</button>
</div>
</div>
</div>
</div>
);
}
// Stream badge for voice channel
export function StreamBadge({ stream }) {
return (
<div className="stream-badge">
<span className="badge-icon">📺</span>
<span className="badge-label">LIVE</span>
</div>
);
}

View file

@ -0,0 +1,114 @@
import React, { useState } from 'react';
export default function ThreadPanel({ thread, onClose }) {
const [replyText, setReplyText] = useState('');
const threadData = thread || {
name: 'Authentication Discussion',
parentMessage: {
author: 'Trevor',
avatar: 'T',
gradient: 'linear-gradient(135deg, #ff0000, #cc0000)',
content: 'Just pushed the authentication updates. All services should automatically migrate to the new protocols within 24 hours.',
timestamp: '10:34 AM',
},
replies: [
{
id: 1,
author: 'Marcus',
avatar: 'M',
gradient: 'linear-gradient(135deg, #0066ff, #003380)',
content: 'Great work! How does this affect the API rate limiting?',
timestamp: '10:36 AM',
},
{
id: 2,
author: 'Trevor',
avatar: 'T',
gradient: 'linear-gradient(135deg, #ff0000, #cc0000)',
content: 'Rate limiting remains the same. The auth tokens now include refresh capabilities though.',
timestamp: '10:38 AM',
},
{
id: 3,
author: 'Sarah',
avatar: 'S',
gradient: 'linear-gradient(135deg, #ffa500, #ff8c00)',
content: 'Testing this on Labs now. Will report back in a few hours.',
timestamp: '10:45 AM',
},
],
memberCount: 4,
messageCount: 3,
};
const handleSendReply = () => {
if (!replyText.trim()) return;
// Handle sending reply
setReplyText('');
};
return (
<div className="thread-panel">
<div className="thread-header">
<div className="thread-title">
<span className="thread-icon">🧵</span>
<span>{threadData.name}</span>
</div>
<div className="thread-meta">
<span>{threadData.memberCount} members</span>
<span></span>
<span>{threadData.messageCount} messages</span>
</div>
<button className="thread-close" onClick={onClose}></button>
</div>
<div className="thread-parent">
<div className="parent-message">
<div className="message-avatar" style={{ background: threadData.parentMessage.gradient }}>
{threadData.parentMessage.avatar}
</div>
<div className="message-content">
<div className="message-header">
<span className="message-author">{threadData.parentMessage.author}</span>
<span className="message-time">{threadData.parentMessage.timestamp}</span>
</div>
<div className="message-text">{threadData.parentMessage.content}</div>
</div>
</div>
<div className="thread-line"></div>
</div>
<div className="thread-replies">
{threadData.replies.map(reply => (
<div key={reply.id} className="thread-reply">
<div className="message-avatar small" style={{ background: reply.gradient }}>
{reply.avatar}
</div>
<div className="message-content">
<div className="message-header">
<span className="message-author">{reply.author}</span>
<span className="message-time">{reply.timestamp}</span>
</div>
<div className="message-text">{reply.content}</div>
</div>
</div>
))}
</div>
<div className="thread-input-container">
<input
type="text"
className="thread-input"
placeholder="Reply to thread..."
value={replyText}
onChange={(e) => setReplyText(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleSendReply()}
/>
<button className="thread-send" onClick={handleSendReply}>
</button>
</div>
</div>
);
}

View file

@ -0,0 +1,28 @@
import React from 'react';
export default function TypingIndicator({ users }) {
if (!users || users.length === 0) return null;
const getTypingText = () => {
if (users.length === 1) {
return <><strong>{users[0].name}</strong> is typing</>;
} else if (users.length === 2) {
return <><strong>{users[0].name}</strong> and <strong>{users[1].name}</strong> are typing</>;
} else if (users.length === 3) {
return <><strong>{users[0].name}</strong>, <strong>{users[1].name}</strong>, and <strong>{users[2].name}</strong> are typing</>;
} else {
return <><strong>Several people</strong> are typing</>;
}
};
return (
<div className="typing-indicator">
<div className="typing-dots">
<span className="dot"></span>
<span className="dot"></span>
<span className="dot"></span>
</div>
<span className="typing-text">{getTypingText()}...</span>
</div>
);
}

View file

@ -0,0 +1,165 @@
import React from 'react';
export default function UserProfileCard({ user, onClose, onMessage, onCall, onAddFriend }) {
const defaultUser = {
id: 1,
name: 'Anderson',
tag: 'Anderson#0001',
initial: 'A',
gradient: 'linear-gradient(135deg, #ff0000, #0066ff, #ffa500)',
banner: 'linear-gradient(135deg, #ff0000 0%, #0066ff 50%, #ffa500 100%)',
status: 'online',
customStatus: 'Building AeThex',
about: 'Founder of AeThex. Building the future of metaverse communication and decentralized identity.',
memberSince: 'Jan 2024',
roles: [
{ name: 'Founder', color: '#ff0000' },
{ name: 'Foundation', color: '#ff0000' },
],
activity: {
type: 'developing',
name: 'AeThex Connect',
details: 'Working on messaging system',
elapsed: '2h 34m',
},
};
// Merge user prop with defaults
const userData = { ...defaultUser, ...user };
const statusColors = {
online: '#3ba55d',
idle: '#faa61a',
dnd: '#ed4245',
offline: '#747f8d',
'in-game': '#5865f2',
labs: '#ffa500',
};
const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) {
onClose?.();
}
};
return (
<div className="modal-overlay" onClick={handleOverlayClick}>
<div className="user-profile-card">
<button
onClick={onClose}
style={{
position: 'absolute',
top: '8px',
right: '8px',
background: 'rgba(0,0,0,0.5)',
border: 'none',
color: '#fff',
width: '24px',
height: '24px',
borderRadius: '50%',
cursor: 'pointer',
zIndex: 10,
}}
>
</button>
<div className="profile-banner" style={{ background: userData.banner || '#2f3136' }}></div>
<div className="profile-avatar-section">
<div className="profile-avatar-wrapper">
<div className="profile-avatar" style={{ background: userData.gradient || '#36393f' }}>
{userData.initial || userData.name?.charAt(0) || '?'}
</div>
<div
className="profile-status-indicator"
style={{ background: statusColors[userData.status] || statusColors.offline }}
></div>
</div>
<div className="profile-badges">
{(userData.roles || []).slice(0, 3).map((role, idx) => (
<span key={idx} className="profile-badge" style={{ background: role.color }}>
{role.name.charAt(0)}
</span>
))}
</div>
</div>
<div className="profile-info">
<h2 className="profile-name">{userData.name}</h2>
<span className="profile-tag">{userData.tag || userData.name}</span>
{userData.customStatus && (
<div className="profile-custom-status">
<span className="status-emoji">💻</span>
{userData.customStatus}
</div>
)}
</div>
<div className="profile-divider"></div>
{userData.activity && (
<div className="profile-section">
<h3 className="section-title">ACTIVITY</h3>
<div className="activity-card">
<div className="activity-icon">🔨</div>
<div className="activity-info">
<div className="activity-name">{userData.activity.name || userData.activity}</div>
<div className="activity-details">{userData.activity.details || ''}</div>
<div className="activity-elapsed">{userData.activity.elapsed || ''}</div>
</div>
</div>
</div>
)}
{userData.about && (
<div className="profile-section">
<h3 className="section-title">ABOUT ME</h3>
<p className="about-text">{userData.about}</p>
</div>
)}
<div className="profile-section">
<h3 className="section-title">MEMBER SINCE</h3>
<p className="member-since">{userData.memberSince || 'Jan 2024'}</p>
</div>
{userData.roles && userData.roles.length > 0 && (
<div className="profile-section">
<h3 className="section-title">ROLES {userData.roles.length}</h3>
<div className="roles-list">
{userData.roles.map((role, idx) => (
<span key={idx} className="role-tag" style={{ borderColor: role.color }}>
<span className="role-dot" style={{ background: role.color }}></span>
{role.name}
</span>
))}
</div>
</div>
)}
<div className="profile-section">
<h3 className="section-title">NOTE</h3>
<textarea
className="note-input"
placeholder="Click to add a note"
defaultValue=""
/>
</div>
<div className="profile-actions">
<button className="profile-btn primary" onClick={() => onMessage?.()}>
💬 Message
</button>
<button className="profile-btn" onClick={() => onCall?.()}>
📞 Call
</button>
<button className="profile-btn" onClick={() => onAddFriend?.()}>
👋 Add Friend
</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,105 @@
import React, { useState } from 'react';
export default function UserSettingsBar({ user, onSettingsClick }) {
const [isMuted, setIsMuted] = useState(false);
const [isDeafened, setIsDeafened] = useState(false);
const [showStatusMenu, setShowStatusMenu] = useState(false);
const [status, setStatus] = useState('online');
const currentUser = user || {
name: 'Anderson',
tag: '#0001',
avatar: 'A',
gradient: 'linear-gradient(135deg, #ff0000, #0066ff, #ffa500)',
};
const statuses = [
{ id: 'online', label: 'Online', color: '#3ba55d' },
{ id: 'idle', label: 'Idle', color: '#faa61a' },
{ id: 'dnd', label: 'Do Not Disturb', color: '#ed4245' },
{ id: 'invisible', label: 'Invisible', color: '#747f8d' },
];
const toggleMute = () => {
setIsMuted(!isMuted);
if (isDeafened && !isMuted) setIsDeafened(false);
};
const toggleDeafen = () => {
setIsDeafened(!isDeafened);
if (!isDeafened) setIsMuted(true);
};
return (
<div className="user-settings-bar">
<div className="user-info-section" onClick={() => setShowStatusMenu(!showStatusMenu)}>
<div className="user-avatar-container">
<div
className="user-avatar-small"
style={{ background: currentUser.gradient }}
>
{currentUser.avatar}
</div>
<div
className="status-indicator"
style={{ background: statuses.find(s => s.id === status)?.color }}
></div>
</div>
<div className="user-details">
<div className="user-display-name">{currentUser.name}</div>
<div className="user-tag">{currentUser.tag}</div>
</div>
{showStatusMenu && (
<div className="status-menu">
{statuses.map(s => (
<div
key={s.id}
className="status-option"
onClick={(e) => {
e.stopPropagation();
setStatus(s.id);
setShowStatusMenu(false);
}}
>
<div className="status-dot" style={{ background: s.color }}></div>
<span>{s.label}</span>
</div>
))}
<div className="status-divider"></div>
<div className="status-option">
<span>📝</span>
<span>Set Custom Status</span>
</div>
</div>
)}
</div>
<div className="user-controls">
<button
className={`control-button ${isMuted ? 'active' : ''}`}
onClick={toggleMute}
title={isMuted ? 'Unmute' : 'Mute'}
>
{isMuted ? '🔇' : '🎤'}
{isMuted && <div className="slash-overlay"></div>}
</button>
<button
className={`control-button ${isDeafened ? 'active' : ''}`}
onClick={toggleDeafen}
title={isDeafened ? 'Undeafen' : 'Deafen'}
>
{isDeafened ? '🔇' : '🎧'}
{isDeafened && <div className="slash-overlay"></div>}
</button>
<button
className="control-button"
title="User Settings"
onClick={onSettingsClick}
>
</button>
</div>
</div>
);
}

View file

@ -0,0 +1,375 @@
import React, { useState } from 'react';
export default function UserSettingsModal({ onClose }) {
const [activeTab, setActiveTab] = useState('my-account');
const settingsSections = [
{
title: 'USER SETTINGS',
items: [
{ id: 'my-account', label: 'My Account', icon: '👤' },
{ id: 'profiles', label: 'Profiles', icon: '🎨' },
{ id: 'privacy', label: 'Privacy & Safety', icon: '🔒' },
{ id: 'family', label: 'Family Center', icon: '👨‍👩‍👧' },
{ id: 'authorized-apps', label: 'Authorized Apps', icon: '🔌' },
{ id: 'devices', label: 'Devices', icon: '📱' },
{ id: 'connections', label: 'Connections', icon: '🔗' },
{ id: 'clips', label: 'Clips', icon: '🎬' },
{ id: 'friend-requests', label: 'Friend Requests', icon: '👋' },
],
},
{
title: 'BILLING SETTINGS',
items: [
{ id: 'nitro', label: 'Nitro', icon: '🚀' },
{ id: 'server-boost', label: 'Server Boost', icon: '💎' },
{ id: 'subscriptions', label: 'Subscriptions', icon: '📋' },
{ id: 'gift-inventory', label: 'Gift Inventory', icon: '🎁' },
{ id: 'billing', label: 'Billing', icon: '💳' },
],
},
{
title: 'APP SETTINGS',
items: [
{ id: 'appearance', label: 'Appearance', icon: '🎨' },
{ id: 'accessibility', label: 'Accessibility', icon: '♿' },
{ id: 'voice-video', label: 'Voice & Video', icon: '🎙️' },
{ id: 'text-images', label: 'Text & Images', icon: '📝' },
{ id: 'notifications', label: 'Notifications', icon: '🔔' },
{ id: 'keybinds', label: 'Keybinds', icon: '⌨️' },
{ id: 'language', label: 'Language', icon: '🌐' },
{ id: 'streamer-mode', label: 'Streamer Mode', icon: '📺' },
{ id: 'advanced', label: 'Advanced', icon: '⚙️' },
],
},
{
title: 'ACTIVITY SETTINGS',
items: [
{ id: 'activity-privacy', label: 'Activity Privacy', icon: '👁️' },
{ id: 'registered-games', label: 'Registered Games', icon: '🎮' },
],
},
];
const handleOverlayClick = (e) => {
if (e.target === e.currentTarget) {
onClose?.();
}
};
return (
<div className="modal-overlay" onClick={handleOverlayClick}>
<div className="user-settings-modal">
<div className="settings-sidebar">
<div className="settings-nav">
{settingsSections.map((section) => (
<div key={section.title}>
<div className="settings-section-title">{section.title}</div>
{section.items.map((item) => (
<div
key={item.id}
className={`settings-nav-item ${activeTab === item.id ? 'active' : ''}`}
onClick={() => setActiveTab(item.id)}
>
<span className="nav-icon">{item.icon}</span>
<span>{item.label}</span>
</div>
))}
<div className="settings-divider"></div>
</div>
))}
<div className="settings-nav-item danger">
<span className="nav-icon">🚪</span>
<span>Log Out</span>
</div>
</div>
</div>
<div className="settings-content">
<div className="settings-header">
<h2>{settingsSections.flatMap((s) => s.items).find((i) => i.id === activeTab)?.label}</h2>
<button className="settings-close" onClick={onClose}></button>
</div>
{activeTab === 'my-account' && (
<div className="settings-overview">
<div className="account-card">
<div className="account-banner" style={{ background: 'linear-gradient(135deg, #ff0000, #0066ff)' }}></div>
<div className="account-info">
<div className="account-avatar" style={{ background: 'linear-gradient(135deg, #ff0000, #0066ff, #ffa500)' }}>A</div>
<div className="account-details">
<h3>Anderson</h3>
<span className="account-tag">Anderson#0001</span>
</div>
<button className="edit-profile-btn">Edit User Profile</button>
</div>
</div>
<div className="account-fields">
<div className="account-field">
<label>DISPLAY NAME</label>
<div className="field-value">
<span>Anderson</span>
<button className="field-edit">Edit</button>
</div>
</div>
<div className="account-field">
<label>USERNAME</label>
<div className="field-value">
<span>anderson</span>
<button className="field-edit">Edit</button>
</div>
</div>
<div className="account-field">
<label>EMAIL</label>
<div className="field-value">
<span>a*****@aethex.dev</span>
<button className="field-edit">Edit</button>
</div>
</div>
<div className="account-field">
<label>PHONE NUMBER</label>
<div className="field-value">
<span>*******4567</span>
<button className="field-edit">Edit</button>
</div>
</div>
</div>
<div className="settings-divider"></div>
<div className="setting-group">
<h4>Password and Authentication</h4>
<button className="settings-btn">Change Password</button>
<div className="two-factor">
<div className="two-factor-info">
<h5>Two-Factor Authentication</h5>
<p>Protect your AeThex account with an extra layer of security.</p>
</div>
<button className="settings-btn primary">Enable 2FA</button>
</div>
</div>
<div className="settings-divider"></div>
<div className="setting-group danger-zone">
<h4>Account Removal</h4>
<p>Disabling your account means you can recover it at any time after taking this action.</p>
<div className="danger-buttons">
<button className="settings-btn danger-outline">Disable Account</button>
<button className="settings-btn danger">Delete Account</button>
</div>
</div>
</div>
)}
{activeTab === 'appearance' && (
<div className="settings-overview">
<div className="setting-group">
<h4>Theme</h4>
<div className="theme-options">
<label className="theme-option">
<input type="radio" name="theme" defaultChecked />
<span className="theme-preview dark">Dark</span>
</label>
<label className="theme-option">
<input type="radio" name="theme" />
<span className="theme-preview light">Light</span>
</label>
<label className="theme-option">
<input type="radio" name="theme" />
<span className="theme-preview sync">Sync with computer</span>
</label>
</div>
</div>
<div className="setting-group">
<h4>Message Display</h4>
<div className="display-options">
<label className="display-option">
<input type="radio" name="display" defaultChecked />
<div className="display-preview cozy">
<span>Cozy</span>
<p>Discord text as it was meant to be.</p>
</div>
</label>
<label className="display-option">
<input type="radio" name="display" />
<div className="display-preview compact">
<span>Compact</span>
<p>Fit more messages on screen at once.</p>
</div>
</label>
</div>
</div>
<div className="setting-group">
<label>Chat Font Scaling 16px</label>
<input type="range" min="12" max="24" defaultValue="16" className="font-slider" />
</div>
<div className="setting-group">
<label>Space Between Message Groups 16px</label>
<input type="range" min="0" max="24" defaultValue="16" className="font-slider" />
</div>
<div className="setting-group">
<label>Zoom Level 100%</label>
<input type="range" min="50" max="200" defaultValue="100" className="font-slider" />
</div>
</div>
)}
{activeTab === 'voice-video' && (
<div className="settings-overview">
<div className="setting-group">
<h4>Voice Settings</h4>
<label>INPUT DEVICE</label>
<select className="setting-select">
<option>Default Built-in Microphone</option>
<option>External Microphone</option>
</select>
<label>OUTPUT DEVICE</label>
<select className="setting-select">
<option>Default Built-in Speakers</option>
<option>External Speakers</option>
<option>Headphones</option>
</select>
</div>
<div className="setting-group">
<label>INPUT VOLUME</label>
<input type="range" min="0" max="100" defaultValue="80" className="volume-slider" />
</div>
<div className="setting-group">
<label>OUTPUT VOLUME</label>
<input type="range" min="0" max="100" defaultValue="100" className="volume-slider" />
</div>
<div className="setting-group">
<h4>Input Mode</h4>
<div className="input-mode-options">
<label className="input-mode">
<input type="radio" name="inputMode" defaultChecked />
<span>Voice Activity</span>
</label>
<label className="input-mode">
<input type="radio" name="inputMode" />
<span>Push to Talk</span>
</label>
</div>
</div>
<div className="setting-group">
<label className="toggle-setting">
<span>Echo Cancellation</span>
<input type="checkbox" defaultChecked />
</label>
<label className="toggle-setting">
<span>Noise Suppression</span>
<input type="checkbox" defaultChecked />
</label>
<label className="toggle-setting">
<span>Automatic Gain Control</span>
<input type="checkbox" defaultChecked />
</label>
</div>
<div className="setting-group">
<h4>Video Settings</h4>
<label>CAMERA</label>
<select className="setting-select">
<option>FaceTime HD Camera</option>
<option>External Webcam</option>
</select>
<button className="settings-btn">Test Video</button>
</div>
</div>
)}
{activeTab === 'notifications' && (
<div className="settings-overview">
<div className="setting-group">
<label className="toggle-setting">
<span>Enable Desktop Notifications</span>
<input type="checkbox" defaultChecked />
</label>
<label className="toggle-setting">
<span>Enable Unread Message Badge</span>
<input type="checkbox" defaultChecked />
</label>
<label className="toggle-setting">
<span>Enable Taskbar Flashing</span>
<input type="checkbox" defaultChecked />
</label>
</div>
<div className="setting-group">
<h4>Sounds</h4>
<label className="toggle-setting">
<span>Message</span>
<input type="checkbox" defaultChecked />
</label>
<label className="toggle-setting">
<span>Deafen</span>
<input type="checkbox" defaultChecked />
</label>
<label className="toggle-setting">
<span>Mute</span>
<input type="checkbox" defaultChecked />
</label>
<label className="toggle-setting">
<span>User Join</span>
<input type="checkbox" defaultChecked />
</label>
<label className="toggle-setting">
<span>User Leave</span>
<input type="checkbox" defaultChecked />
</label>
</div>
</div>
)}
{activeTab === 'keybinds' && (
<div className="settings-overview">
<div className="keybinds-header">
<button className="settings-btn primary">Add a Keybind</button>
</div>
<div className="keybind-list">
<div className="keybind-item">
<div className="keybind-action">Push to Talk</div>
<div className="keybind-key">` (backtick)</div>
<button className="keybind-edit"></button>
<button className="keybind-delete">🗑</button>
</div>
<div className="keybind-item">
<div className="keybind-action">Toggle Mute</div>
<div className="keybind-key">Ctrl + Shift + M</div>
<button className="keybind-edit"></button>
<button className="keybind-delete">🗑</button>
</div>
<div className="keybind-item">
<div className="keybind-action">Toggle Deafen</div>
<div className="keybind-key">Ctrl + Shift + D</div>
<button className="keybind-edit"></button>
<button className="keybind-delete">🗑</button>
</div>
</div>
</div>
)}
{!['my-account', 'appearance', 'voice-video', 'notifications', 'keybinds'].includes(activeTab) && (
<div className="settings-placeholder">
<span className="placeholder-icon">🔧</span>
<p>{settingsSections.flatMap((s) => s.items).find((i) => i.id === activeTab)?.label} settings coming soon</p>
</div>
)}
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,206 @@
import React, { useState } from 'react';
export default function VideoCall({ users, onLeave, onToggleMute, onToggleDeafen, onToggleVideo, onToggleScreenShare }) {
const [isMuted, setIsMuted] = useState(false);
const [isDeafened, setIsDeafened] = useState(false);
const [isVideoOn, setIsVideoOn] = useState(true);
const [isScreenSharing, setIsScreenSharing] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const [focusedUser, setFocusedUser] = useState(null);
const participants = users || [
{ id: 1, name: 'Anderson', avatar: 'A', gradient: 'linear-gradient(135deg, #ff0000, #0066ff, #ffa500)', speaking: true, muted: false, video: true },
{ id: 2, name: 'Trevor', avatar: 'T', gradient: 'linear-gradient(135deg, #ff0000, #cc0000)', speaking: false, muted: true, video: true },
{ id: 3, name: 'Sarah', avatar: 'S', gradient: 'linear-gradient(135deg, #ffa500, #ff8c00)', speaking: false, muted: false, video: false, streaming: true },
{ id: 4, name: 'Marcus', avatar: 'M', gradient: 'linear-gradient(135deg, #0066ff, #003380)', speaking: false, muted: false, video: false },
];
const handleMute = () => {
setIsMuted(!isMuted);
onToggleMute?.(!isMuted);
};
const handleDeafen = () => {
setIsDeafened(!isDeafened);
onToggleDeafen?.(!isDeafened);
};
const handleVideo = () => {
setIsVideoOn(!isVideoOn);
onToggleVideo?.(!isVideoOn);
};
const handleScreenShare = () => {
setIsScreenSharing(!isScreenSharing);
onToggleScreenShare?.(!isScreenSharing);
};
const focusedParticipant = focusedUser ? participants.find((p) => p.id === focusedUser) : participants.find((p) => p.streaming) || participants[0];
const gridParticipants = participants.filter((p) => p.id !== focusedParticipant?.id);
return (
<div className={`video-call-container ${isFullscreen ? 'fullscreen' : ''}`}>
{/* Main/Focused Video Area */}
<div className="video-main">
<div className="video-focused">
{focusedParticipant?.streaming ? (
<div className="screen-share-view">
<div className="screen-content">
<div className="screen-placeholder">
<span className="screen-icon">🖥</span>
<span>{focusedParticipant.name}'s screen</span>
</div>
</div>
<div className="streamer-pip">
<div
className="pip-avatar"
style={{ background: focusedParticipant.gradient }}
>
{focusedParticipant.avatar}
</div>
<span className="pip-name">{focusedParticipant.name}</span>
</div>
</div>
) : focusedParticipant?.video ? (
<div className="video-feed">
<div className="video-placeholder">
<div
className="video-avatar-large"
style={{ background: focusedParticipant.gradient }}
>
{focusedParticipant.avatar}
</div>
</div>
<div className="video-name-overlay">{focusedParticipant.name}</div>
</div>
) : (
<div className="video-audio-only">
<div
className={`audio-avatar ${focusedParticipant?.speaking ? 'speaking' : ''}`}
style={{ background: focusedParticipant?.gradient }}
>
{focusedParticipant?.avatar}
</div>
<div className="audio-name">{focusedParticipant?.name}</div>
{focusedParticipant?.muted && <div className="audio-muted">🔇</div>}
</div>
)}
</div>
{/* Participant Grid */}
<div className="video-grid">
{gridParticipants.map((participant) => (
<div
key={participant.id}
className={`video-tile ${participant.speaking ? 'speaking' : ''}`}
onClick={() => setFocusedUser(participant.id)}
>
{participant.video ? (
<div className="tile-video">
<div
className="tile-avatar"
style={{ background: participant.gradient }}
>
{participant.avatar}
</div>
</div>
) : (
<div className="tile-audio">
<div
className="tile-avatar"
style={{ background: participant.gradient }}
>
{participant.avatar}
</div>
</div>
)}
<div className="tile-info">
<span className="tile-name">{participant.name}</span>
<div className="tile-icons">
{participant.muted && <span>🔇</span>}
{participant.streaming && <span>🖥</span>}
</div>
</div>
</div>
))}
</div>
</div>
{/* Controls Bar */}
<div className="video-controls">
<div className="controls-left">
<div className="call-info">
<span className="call-channel">🔊 Nexus Lounge</span>
<span className="call-duration">1:23:45</span>
</div>
</div>
<div className="controls-center">
<button
className={`control-btn ${isMuted ? 'active' : ''}`}
onClick={handleMute}
title={isMuted ? 'Unmute' : 'Mute'}
>
{isMuted ? '🔇' : '🎙️'}
</button>
<button
className={`control-btn ${isDeafened ? 'active' : ''}`}
onClick={handleDeafen}
title={isDeafened ? 'Undeafen' : 'Deafen'}
>
{isDeafened ? '🔈' : '🔊'}
</button>
<button
className={`control-btn ${isVideoOn ? 'active' : ''}`}
onClick={handleVideo}
title={isVideoOn ? 'Turn Off Camera' : 'Turn On Camera'}
>
{isVideoOn ? '📹' : '📷'}
</button>
<button
className={`control-btn ${isScreenSharing ? 'active sharing' : ''}`}
onClick={handleScreenShare}
title={isScreenSharing ? 'Stop Sharing' : 'Share Screen'}
>
🖥
</button>
<button
className="control-btn activities"
title="Activities"
>
🎮
</button>
<button
className="control-btn leave"
onClick={onLeave}
title="Leave Call"
>
📞
</button>
</div>
<div className="controls-right">
<button
className="control-btn"
title="Show Participants"
>
👥 {participants.length}
</button>
<button
className="control-btn"
title="Chat"
>
💬
</button>
<button
className={`control-btn ${isFullscreen ? 'active' : ''}`}
onClick={() => setIsFullscreen(!isFullscreen)}
title={isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
>
{isFullscreen ? '⛶' : '⛶'}
</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,73 @@
import React, { useState } from 'react';
export default function VoiceChannel({ channel, onJoin, onLeave }) {
const [isExpanded, setIsExpanded] = useState(true);
const [isConnected, setIsConnected] = useState(false);
const voiceChannel = channel || {
name: 'Nexus Lounge',
users: [
{ id: 1, name: 'Anderson', avatar: 'A', gradient: 'linear-gradient(135deg, #ff0000, #0066ff, #ffa500)', speaking: true, muted: false, deafened: false },
{ id: 2, name: 'Sarah', avatar: 'S', gradient: 'linear-gradient(135deg, #ffa500, #ff8c00)', speaking: false, muted: true, deafened: false },
{ id: 3, name: 'Marcus', avatar: 'M', gradient: 'linear-gradient(135deg, #0066ff, #003380)', speaking: false, muted: false, deafened: false, streaming: true },
],
limit: 10,
bitrate: 64,
};
const handleConnect = () => {
setIsConnected(!isConnected);
if (!isConnected && onJoin) onJoin(voiceChannel);
if (isConnected && onLeave) onLeave();
};
return (
<div className="voice-channel-container">
<div className="voice-channel-header" onClick={() => setIsExpanded(!isExpanded)}>
<span className="voice-icon">🔊</span>
<span className="voice-channel-name">{voiceChannel.name}</span>
<span className="voice-user-count">{voiceChannel.users.length}/{voiceChannel.limit}</span>
<span className="expand-icon">{isExpanded ? '▼' : '▶'}</span>
</div>
{isExpanded && (
<div className="voice-users-list">
{voiceChannel.users.map(user => (
<div key={user.id} className={`voice-user ${user.speaking ? 'speaking' : ''}`}>
<div className="voice-user-avatar" style={{ background: user.gradient }}>
{user.avatar}
{user.speaking && <div className="speaking-ring"></div>}
</div>
<span className="voice-user-name">{user.name}</span>
<div className="voice-user-icons">
{user.streaming && <span className="stream-icon" title="Streaming">📺</span>}
{user.muted && <span className="muted-icon" title="Muted">🔇</span>}
{user.deafened && <span className="deafened-icon" title="Deafened">🔈</span>}
</div>
</div>
))}
</div>
)}
{isConnected && (
<div className="voice-connected-panel">
<div className="voice-status">
<span className="connected-text">Voice Connected</span>
<span className="voice-ping">23ms</span>
</div>
<div className="voice-controls">
<button className="voice-control-btn" title="Disconnect" onClick={handleConnect}>
📞
</button>
<button className="voice-control-btn" title="Start Video">
📹
</button>
<button className="voice-control-btn" title="Share Screen">
🖥
</button>
</div>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,193 @@
import React, { useState } from 'react';
export default function WelcomeScreen({ server, onComplete, onClose }) {
const [step, setStep] = useState(0);
const [selectedRoles, setSelectedRoles] = useState([]);
const [selectedChannels, setSelectedChannels] = useState([]);
const serverData = server || {
name: 'AeThex Foundation',
icon: 'F',
gradient: 'linear-gradient(135deg, #ff0000, #990000)',
description: 'Official home of AeThex. Security, authentication, and core infrastructure.',
memberCount: '128.5K',
rules: [
{ id: 1, title: 'Be Respectful', description: 'Treat everyone with respect. No harassment, bullying, or hate speech.' },
{ id: 2, title: 'No Spam', description: 'Avoid excessive messages, self-promotion, or unsolicited advertisements.' },
{ id: 3, title: 'Stay On Topic', description: 'Keep discussions relevant to the channel topic.' },
{ id: 4, title: 'No NSFW Content', description: 'This is a SFW community. Keep content appropriate.' },
{ id: 5, title: 'Follow Discord TOS', description: 'Adhere to Discord\'s Terms of Service and Community Guidelines.' },
],
};
const roleOptions = [
{ id: 'developer', label: '💻 Developer', description: 'I build things with AeThex tools' },
{ id: 'gamer', label: '🎮 Gamer', description: 'I\'m here for gaming content' },
{ id: 'security', label: '🔒 Security', description: 'Interested in security topics' },
{ id: 'creator', label: '🎨 Creator', description: 'I create content' },
{ id: 'announcements', label: '📢 Announcements Only', description: 'Just here for updates' },
];
const channelOptions = [
{ id: 'general', label: '# general', description: 'Main community chat' },
{ id: 'development', label: '# development', description: 'Dev discussions' },
{ id: 'gaming', label: '# gaming', description: 'Gaming chat' },
{ id: 'off-topic', label: '# off-topic', description: 'Random discussions' },
{ id: 'showcase', label: '# showcase', description: 'Share your projects' },
];
const steps = [
{ id: 'welcome', title: 'Welcome' },
{ id: 'rules', title: 'Rules' },
{ id: 'roles', title: 'Pick Roles' },
{ id: 'channels', title: 'Channels' },
];
const handleComplete = () => {
onComplete?.({
roles: selectedRoles,
channels: selectedChannels,
});
};
const toggleRole = (roleId) => {
setSelectedRoles(prev =>
prev.includes(roleId)
? prev.filter(id => id !== roleId)
: [...prev, roleId]
);
};
const toggleChannel = (channelId) => {
setSelectedChannels(prev =>
prev.includes(channelId)
? prev.filter(id => id !== channelId)
: [...prev, channelId]
);
};
return (
<div className="welcome-overlay">
<div className="welcome-screen">
{/* Progress dots */}
<div className="welcome-progress">
{steps.map((s, idx) => (
<div
key={s.id}
className={`progress-dot ${idx === step ? 'active' : ''} ${idx < step ? 'completed' : ''}`}
/>
))}
</div>
{step === 0 && (
<div className="welcome-step">
<div
className="welcome-server-icon"
style={{ background: serverData.gradient }}
>
{serverData.icon}
</div>
<h1>Welcome to {serverData.name}!</h1>
<p className="welcome-description">{serverData.description}</p>
<div className="welcome-stats">
<span>👥 {serverData.memberCount} members</span>
</div>
<button className="welcome-btn primary" onClick={() => setStep(1)}>
Continue
</button>
</div>
)}
{step === 1 && (
<div className="welcome-step">
<h2>📜 Server Rules</h2>
<p className="step-subtitle">Please read and agree to our community rules</p>
<div className="rules-list">
{serverData.rules.map((rule, idx) => (
<div key={rule.id} className="rule-item">
<span className="rule-number">{idx + 1}</span>
<div className="rule-content">
<h4>{rule.title}</h4>
<p>{rule.description}</p>
</div>
</div>
))}
</div>
<div className="welcome-actions">
<button className="welcome-btn secondary" onClick={() => setStep(0)}>
Back
</button>
<button className="welcome-btn primary" onClick={() => setStep(2)}>
I Agree
</button>
</div>
</div>
)}
{step === 2 && (
<div className="welcome-step">
<h2>🎭 Pick Your Roles</h2>
<p className="step-subtitle">Select roles that match your interests</p>
<div className="options-grid">
{roleOptions.map(role => (
<button
key={role.id}
className={`option-btn ${selectedRoles.includes(role.id) ? 'selected' : ''}`}
onClick={() => toggleRole(role.id)}
>
<span className="option-label">{role.label}</span>
<span className="option-description">{role.description}</span>
{selectedRoles.includes(role.id) && (
<span className="option-check"></span>
)}
</button>
))}
</div>
<div className="welcome-actions">
<button className="welcome-btn secondary" onClick={() => setStep(1)}>
Back
</button>
<button className="welcome-btn primary" onClick={() => setStep(3)}>
Continue
</button>
</div>
</div>
)}
{step === 3 && (
<div className="welcome-step">
<h2>📺 Choose Channels</h2>
<p className="step-subtitle">Pick channels you want to see</p>
<div className="options-grid">
{channelOptions.map(channel => (
<button
key={channel.id}
className={`option-btn ${selectedChannels.includes(channel.id) ? 'selected' : ''}`}
onClick={() => toggleChannel(channel.id)}
>
<span className="option-label">{channel.label}</span>
<span className="option-description">{channel.description}</span>
{selectedChannels.includes(channel.id) && (
<span className="option-check"></span>
)}
</button>
))}
</div>
<div className="welcome-actions">
<button className="welcome-btn secondary" onClick={() => setStep(2)}>
Back
</button>
<button className="welcome-btn primary" onClick={handleComplete}>
Finish Setup
</button>
</div>
</div>
)}
<button className="skip-btn" onClick={onClose}>
Skip for now
</button>
</div>
</div>
);
}

View file

@ -0,0 +1,84 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check for stored session on mount
const stored = localStorage.getItem('aethex_user');
if (stored) {
try {
setUser(JSON.parse(stored));
} catch (e) {
localStorage.removeItem('aethex_user');
}
}
setLoading(false);
}, []);
const login = (userData) => {
const userWithDefaults = {
id: Date.now(),
email: userData.email,
username: userData.username || userData.email.split('@')[0],
displayName: userData.displayName || userData.email.split('@')[0],
avatar: null,
createdAt: new Date().toISOString(),
...userData,
};
setUser(userWithDefaults);
localStorage.setItem('aethex_user', JSON.stringify(userWithDefaults));
};
const register = (userData) => {
const newUser = {
id: Date.now(),
email: userData.email,
username: userData.username,
displayName: userData.displayName || userData.username,
avatar: null,
createdAt: new Date().toISOString(),
...userData,
};
setUser(newUser);
localStorage.setItem('aethex_user', JSON.stringify(newUser));
};
const logout = () => {
setUser(null);
localStorage.removeItem('aethex_user');
};
const updateUser = (updates) => {
const updated = { ...user, ...updates };
setUser(updated);
localStorage.setItem('aethex_user', JSON.stringify(updated));
};
return (
<AuthContext.Provider value={{
user,
loading,
isAuthenticated: !!user,
login,
register,
logout,
updateUser,
}}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
export default AuthContext;

View file

@ -1,13 +1,92 @@
import React from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { AuthProvider, useAuth } from "./contexts/AuthContext";
import MainLayout from "./MainLayout";
import LandingPage from "./pages/LandingPage";
import LoginPage from "./pages/LoginPage";
import RegisterPage from "./pages/RegisterPage";
import AboutPage from "./pages/AboutPage";
import FeaturesPage from "./pages/FeaturesPage";
import "./global.css";
// Protected route wrapper
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="loading-screen">
<div className="loading-spinner"></div>
<span>Loading...</span>
</div>
);
}
return isAuthenticated ? children : <Navigate to="/login" replace />;
}
// Public route - redirect to app if already logged in
function PublicRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return (
<div className="loading-screen">
<div className="loading-spinner"></div>
<span>Loading...</span>
</div>
);
}
return !isAuthenticated ? children : <Navigate to="/app" replace />;
}
function App() {
const { login, register, logout } = useAuth();
return (
<Routes>
{/* Public pages */}
<Route path="/" element={<LandingPage />} />
<Route path="/features" element={<FeaturesPage />} />
<Route path="/about" element={<AboutPage />} />
{/* Auth pages */}
<Route path="/login" element={
<PublicRoute>
<LoginPage onLogin={login} />
</PublicRoute>
} />
<Route path="/register" element={
<PublicRoute>
<RegisterPage onRegister={register} />
</PublicRoute>
} />
{/* Protected app */}
<Route path="/app" element={
<ProtectedRoute>
<MainLayout onLogout={logout} />
</ProtectedRoute>
} />
{/* Fallback */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}
const root = document.getElementById("root");
if (root) {
createRoot(root).render(
<React.StrictMode>
<MainLayout />
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);
}

10311
src/frontend/mockup/mockup.css Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,163 @@
import React from 'react';
import { Link } from 'react-router-dom';
export default function AboutPage() {
const team = [
{ name: 'Trevor', role: 'Founder & Lead Developer', avatar: 'T', color: '#ff0000' },
{ name: 'Open Source', role: 'Community Contributors', avatar: '👥', color: '#5865f2' },
];
const timeline = [
{ year: '2024', event: 'AeThex Foundation established', desc: 'Core infrastructure and passport system development begins' },
{ year: '2025', event: 'AeThex Connect launched', desc: 'Open source Discord alternative enters alpha' },
{ year: '2026', event: 'Public beta release', desc: 'Full feature parity achieved, community growth accelerates' },
];
return (
<div className="info-page">
<nav className="info-nav">
<Link to="/" className="nav-brand">
<span className="brand-icon"></span>
<span className="brand-text">AeThex Connect</span>
</Link>
<div className="nav-links">
<Link to="/features">Features</Link>
<Link to="/about" className="active">About</Link>
<a href="https://github.com/AeThex-Corporation" target="_blank" rel="noopener noreferrer">GitHub</a>
</div>
<div className="nav-auth">
<Link to="/login" className="nav-login">Login</Link>
<Link to="/register" className="nav-register">Get Started</Link>
</div>
</nav>
<main className="info-content">
<section className="about-hero">
<h1>About <span className="gradient-text">AeThex</span></h1>
<p className="hero-desc">
Building the future of decentralized communication, one line of code at a time.
</p>
</section>
<section className="about-section">
<h2>Our Mission</h2>
<div className="mission-content">
<div className="mission-text">
<p>
AeThex was founded on a simple belief: <strong>your data belongs to you</strong>.
In a world where major platforms profit from your conversations, we chose a different path.
</p>
<p>
AeThex Connect is our answer to the centralized communication monopoly.
It's not just an alternative to Discord—it's a statement that privacy, security,
and user ownership can coexist with a world-class communication experience.
</p>
<p>
As an open-source project, every line of our code is public.
We believe transparency isn't just nice to have—it's essential for trust.
</p>
</div>
<div className="mission-values">
<div className="value-card">
<span className="value-icon">🔒</span>
<h3>Privacy First</h3>
<p>End-to-end encryption by default. We can't read your messages even if we wanted to.</p>
</div>
<div className="value-card">
<span className="value-icon">🌐</span>
<h3>Open Source</h3>
<p>100% of our code is public. Audit it, fork it, contribute to it.</p>
</div>
<div className="value-card">
<span className="value-icon"></span>
<h3>User Owned</h3>
<p>Your identity, your data, your control. Forever.</p>
</div>
</div>
</div>
</section>
<section className="about-section trinity-section">
<h2>The Trinity</h2>
<p className="section-intro">AeThex operates as three interconnected divisions</p>
<div className="trinity-grid">
<div className="trinity-card foundation">
<div className="trinity-icon">🏛</div>
<h3>Foundation</h3>
<p>The core. Authentication, security infrastructure, and the Passport identity system.</p>
</div>
<div className="trinity-card corporation">
<div className="trinity-icon">🏢</div>
<h3>Corporation</h3>
<p>Enterprise solutions, business integrations, and commercial support services.</p>
</div>
<div className="trinity-card labs">
<div className="trinity-icon">🔬</div>
<h3>Labs</h3>
<p>Experimental features, R&D, and cutting-edge technology development.</p>
</div>
</div>
</section>
<section className="about-section">
<h2>Our Journey</h2>
<div className="timeline">
{timeline.map((item, i) => (
<div key={i} className="timeline-item">
<div className="timeline-year">{item.year}</div>
<div className="timeline-content">
<h3>{item.event}</h3>
<p>{item.desc}</p>
</div>
</div>
))}
</div>
</section>
<section className="about-section">
<h2>The Team</h2>
<div className="team-grid">
{team.map((member, i) => (
<div key={i} className="team-card">
<div className="team-avatar" style={{ background: member.color }}>
{member.avatar}
</div>
<h3>{member.name}</h3>
<p>{member.role}</p>
</div>
))}
</div>
</section>
<section className="about-cta">
<h2>Ready to join the revolution?</h2>
<p>Be part of the community shaping the future of communication.</p>
<div className="cta-actions">
<Link to="/register" className="cta-primary">Create Account</Link>
<a href="https://github.com/AeThex-Corporation/AeThex-Connect" className="cta-secondary" target="_blank" rel="noopener noreferrer">
View on GitHub
</a>
</div>
</section>
</main>
<footer className="info-footer">
<div className="footer-content">
<div className="footer-brand">
<span className="brand-icon"></span>
<span>AeThex Connect</span>
</div>
<div className="footer-links">
<Link to="/features">Features</Link>
<Link to="/about">About</Link>
<Link to="/privacy">Privacy</Link>
<Link to="/terms">Terms</Link>
</div>
<div className="footer-copy">
© 2026 AeThex Corporation. Open source under MIT License.
</div>
</div>
</footer>
</div>
);
}

View file

@ -0,0 +1,159 @@
import React from 'react';
import { Link } from 'react-router-dom';
export default function FeaturesPage() {
const featureCategories = [
{
title: 'Communication',
features: [
{ icon: '💬', name: 'Real-time Messaging', desc: 'Instant message delivery with typing indicators, reactions, and rich embeds' },
{ icon: '🎙️', name: 'Voice Channels', desc: 'Crystal-clear voice chat with noise suppression and echo cancellation' },
{ icon: '📹', name: 'Video Calls', desc: 'HD video calls with screen sharing and virtual backgrounds' },
{ icon: '🧵', name: 'Threads', desc: 'Organized conversations that keep your channels clean' },
{ icon: '📌', name: 'Pinned Messages', desc: 'Save important messages for easy access' },
{ icon: '🔍', name: 'Powerful Search', desc: 'Find any message, file, or conversation instantly' },
]
},
{
title: 'Community',
features: [
{ icon: '🏠', name: 'Servers', desc: 'Create communities with unlimited channels and categories' },
{ icon: '🎭', name: 'Roles & Permissions', desc: 'Granular control over who can do what' },
{ icon: '📢', name: 'Announcements', desc: 'Broadcast to your community with announcement channels' },
{ icon: '📅', name: 'Events', desc: 'Schedule and manage community events' },
{ icon: '🎪', name: 'Stage Channels', desc: 'Host live audio events for large audiences' },
{ icon: '💬', name: 'Forum Channels', desc: 'Organized discussion boards for your community' },
]
},
{
title: 'Security & Privacy',
features: [
{ icon: '🔐', name: 'End-to-End Encryption', desc: 'Your messages are encrypted and only you can read them' },
{ icon: '🛡️', name: 'AeThex Passport', desc: 'Secure, decentralized identity that you own' },
{ icon: '🔒', name: 'Two-Factor Auth', desc: 'Extra security for your account' },
{ icon: '👻', name: 'Ephemeral Messages', desc: 'Self-destructing messages for sensitive conversations' },
{ icon: '🚫', name: 'No Data Selling', desc: 'Your data is never sold to third parties. Ever.' },
{ icon: '📋', name: 'Audit Logs', desc: 'Full transparency on server actions' },
]
},
{
title: 'Customization',
features: [
{ icon: '🎨', name: 'Themes', desc: 'Dark, light, and custom themes to match your style' },
{ icon: '😀', name: 'Custom Emoji', desc: 'Upload your own emoji and stickers' },
{ icon: '🎵', name: 'Soundboard', desc: 'Play sound effects in voice channels' },
{ icon: '🤖', name: 'Bots & Integrations', desc: 'Extend functionality with custom bots' },
{ icon: '⚙️', name: 'Webhooks', desc: 'Connect external services to your channels' },
{ icon: '📊', name: 'Server Insights', desc: 'Analytics to understand your community' },
]
},
];
const comparisons = [
{ feature: 'Open Source', aethex: true, discord: false, slack: false },
{ feature: 'E2E Encryption', aethex: true, discord: false, slack: false },
{ feature: 'Self-Hostable', aethex: true, discord: false, slack: false },
{ feature: 'No Data Collection', aethex: true, discord: false, slack: false },
{ feature: 'Voice/Video', aethex: true, discord: true, slack: true },
{ feature: 'Unlimited History', aethex: true, discord: true, slack: false },
{ feature: 'Custom Bots', aethex: true, discord: true, slack: true },
{ feature: 'Free to Use', aethex: true, discord: true, slack: false },
];
return (
<div className="info-page">
<nav className="info-nav">
<Link to="/" className="nav-brand">
<span className="brand-icon"></span>
<span className="brand-text">AeThex Connect</span>
</Link>
<div className="nav-links">
<Link to="/features" className="active">Features</Link>
<Link to="/about">About</Link>
<a href="https://github.com/AeThex-Corporation" target="_blank" rel="noopener noreferrer">GitHub</a>
</div>
<div className="nav-auth">
<Link to="/login" className="nav-login">Login</Link>
<Link to="/register" className="nav-register">Get Started</Link>
</div>
</nav>
<main className="info-content">
<section className="features-hero">
<h1>Everything you need,<br /><span className="gradient-text">nothing you don't</span></h1>
<p className="hero-desc">
All the features you love from Discord, with the privacy and ownership you deserve.
</p>
</section>
{featureCategories.map((category, idx) => (
<section key={idx} className="feature-category">
<h2>{category.title}</h2>
<div className="features-grid">
{category.features.map((feature, i) => (
<div key={i} className="feature-card">
<span className="feature-icon">{feature.icon}</span>
<h3>{feature.name}</h3>
<p>{feature.desc}</p>
</div>
))}
</div>
</section>
))}
<section className="comparison-section">
<h2>How We Compare</h2>
<div className="comparison-table-container">
<table className="comparison-table">
<thead>
<tr>
<th>Feature</th>
<th className="highlight">AeThex Connect</th>
<th>Discord</th>
<th>Slack</th>
</tr>
</thead>
<tbody>
{comparisons.map((row, i) => (
<tr key={i}>
<td>{row.feature}</td>
<td className="highlight">{row.aethex ? '✓' : '✕'}</td>
<td>{row.discord ? '✓' : '✕'}</td>
<td>{row.slack ? '✓' : '✕'}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section className="features-cta">
<h2>Ready to experience the difference?</h2>
<p>Join thousands of users who've made the switch.</p>
<div className="cta-actions">
<Link to="/register" className="cta-primary">Get Started Free</Link>
<Link to="/login" className="cta-secondary">Already have an account?</Link>
</div>
</section>
</main>
<footer className="info-footer">
<div className="footer-content">
<div className="footer-brand">
<span className="brand-icon"></span>
<span>AeThex Connect</span>
</div>
<div className="footer-links">
<Link to="/features">Features</Link>
<Link to="/about">About</Link>
<Link to="/privacy">Privacy</Link>
<Link to="/terms">Terms</Link>
</div>
<div className="footer-copy">
© 2026 AeThex Corporation. Open source under MIT License.
</div>
</div>
</footer>
</div>
);
}

View file

@ -0,0 +1,202 @@
import React from 'react';
import { Link } from 'react-router-dom';
export default function LandingPage() {
return (
<div className="landing-page">
{/* Navigation */}
<nav className="landing-nav">
<div className="nav-brand">
<span className="brand-icon"></span>
<span className="brand-text">AeThex Connect</span>
</div>
<div className="nav-links">
<Link to="/features">Features</Link>
<Link to="/about">About</Link>
<a href="https://github.com/AeThex-Corporation" target="_blank" rel="noopener noreferrer">GitHub</a>
</div>
<div className="nav-auth">
<Link to="/login" className="nav-login">Login</Link>
<Link to="/register" className="nav-register">Get Started</Link>
</div>
</nav>
{/* Hero Section */}
<section className="hero">
<div className="hero-content">
<div className="hero-badge">
<span className="badge-dot"></span>
Open Source Privacy-First Decentralized
</div>
<h1 className="hero-title">
Connect Without
<br />
<span className="gradient-text">Compromise</span>
</h1>
<p className="hero-subtitle">
The next-generation communication platform built for gamers, developers,
and communities who value privacy, security, and true ownership of their data.
</p>
<div className="hero-actions">
<Link to="/register" className="cta-primary">
<span>Launch Connect</span>
<span className="cta-arrow"></span>
</Link>
<a href="#features" className="cta-secondary">
Explore Features
</a>
</div>
<div className="hero-stats">
<div className="stat">
<span className="stat-value">100%</span>
<span className="stat-label">Open Source</span>
</div>
<div className="stat-divider"></div>
<div className="stat">
<span className="stat-value">E2E</span>
<span className="stat-label">Encrypted</span>
</div>
<div className="stat-divider"></div>
<div className="stat">
<span className="stat-value">0</span>
<span className="stat-label">Data Sold</span>
</div>
</div>
</div>
<div className="hero-visual">
<div className="app-preview">
<div className="preview-header">
<div className="preview-dots">
<span></span><span></span><span></span>
</div>
</div>
<div className="preview-content">
<div className="preview-sidebar">
<div className="preview-server active"></div>
<div className="preview-server"></div>
<div className="preview-server"></div>
</div>
<div className="preview-channels">
<div className="preview-channel"></div>
<div className="preview-channel active"></div>
<div className="preview-channel"></div>
</div>
<div className="preview-chat">
<div className="preview-message"></div>
<div className="preview-message short"></div>
<div className="preview-message"></div>
</div>
</div>
</div>
</div>
</section>
{/* Features Section */}
<section id="features" className="features-section">
<h2 className="section-title">Why AeThex Connect?</h2>
<p className="section-subtitle">
Built from the ground up with the features that matter most
</p>
<div className="features-grid">
<div className="feature-card">
<div className="feature-icon foundation">🔐</div>
<h3>End-to-End Encryption</h3>
<p>Every message, call, and file is encrypted. Only you and your recipients can read them.</p>
</div>
<div className="feature-card">
<div className="feature-icon corporation">🎮</div>
<h3>GameForge Integration</h3>
<p>Rich presence, game invites, and overlay support for your favorite games.</p>
</div>
<div className="feature-card">
<div className="feature-icon labs">📞</div>
<h3>Crystal Clear Calls</h3>
<p>HD voice and video calls with screen sharing, powered by WebRTC.</p>
</div>
<div className="feature-card">
<div className="feature-icon">🌐</div>
<h3>Cross-Platform</h3>
<p>Web, Desktop, iOS, and Android. Your conversations sync everywhere.</p>
</div>
<div className="feature-card">
<div className="feature-icon">🔓</div>
<h3>Open Source</h3>
<p>Fully auditable code. No hidden backdoors. Community-driven development.</p>
</div>
<div className="feature-card">
<div className="feature-icon"></div>
<h3>Blazing Fast</h3>
<p>Optimized for performance. No bloat, no lag, just pure speed.</p>
</div>
</div>
</section>
{/* Trinity Section */}
<section className="trinity-section">
<h2 className="section-title">The AeThex Trinity</h2>
<p className="section-subtitle">Three divisions, one ecosystem</p>
<div className="trinity-cards">
<div className="trinity-card foundation">
<div className="trinity-icon"></div>
<h3>Foundation</h3>
<p>Core security, authentication, and identity. The bedrock of trust.</p>
</div>
<div className="trinity-card corporation">
<div className="trinity-icon"></div>
<h3>Corporation</h3>
<p>Enterprise solutions and professional services. Built for scale.</p>
</div>
<div className="trinity-card labs">
<div className="trinity-icon"></div>
<h3>Labs</h3>
<p>Research and experimental features. Tomorrow's technology today.</p>
</div>
</div>
</section>
{/* CTA Section */}
<section className="cta-section">
<div className="cta-content">
<h2>Ready to Connect?</h2>
<p>Join the community that values your privacy as much as you do.</p>
<Link to="/register" className="cta-primary large">
Create Your Account
</Link>
</div>
</section>
{/* Footer */}
<footer className="landing-footer">
<div className="footer-content">
<div className="footer-brand">
<span className="brand-icon"></span>
<span>AeThex Connect</span>
</div>
<div className="footer-links">
<div className="footer-column">
<h4>Product</h4>
<Link to="/features">Features</Link>
<Link to="/about">About</Link>
<a href="#">Download</a>
</div>
<div className="footer-column">
<h4>Resources</h4>
<a href="#">Documentation</a>
<a href="#">API</a>
<a href="https://github.com/AeThex-Corporation" target="_blank">GitHub</a>
</div>
<div className="footer-column">
<h4>Legal</h4>
<a href="#">Privacy Policy</a>
<a href="#">Terms of Service</a>
<a href="#">Security</a>
</div>
</div>
</div>
<div className="footer-bottom">
<p>© 2026 AeThex Corporation. Open source under MIT License.</p>
</div>
</footer>
</div>
);
}

View file

@ -0,0 +1,107 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
export default function LoginPage({ onLogin }) {
const navigate = useNavigate();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
// Mock login - replace with Supabase auth
if (email && password) {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1000));
// For demo, accept any valid-looking email
if (email.includes('@') && password.length >= 6) {
onLogin?.({ email });
navigate('/app');
} else {
setError('Invalid email or password');
}
}
} catch (err) {
setError('An error occurred. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="auth-page">
<div className="auth-container">
<div className="auth-header">
<Link to="/" className="auth-brand">
<span className="brand-icon"></span>
<span>AeThex Connect</span>
</Link>
</div>
<div className="auth-card">
<h1>Welcome back!</h1>
<p className="auth-subtitle">We're so excited to see you again!</p>
{error && (
<div className="auth-error">
<span></span>
{error}
</div>
)}
<form onSubmit={handleSubmit} className="auth-form">
<div className="form-group">
<label htmlFor="email">EMAIL OR USERNAME <span className="required">*</span></label>
<input
id="email"
type="text"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
autoComplete="email"
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="password">PASSWORD <span className="required">*</span></label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete="current-password"
disabled={loading}
/>
<Link to="/forgot-password" className="forgot-link">Forgot your password?</Link>
</div>
<button type="submit" className="auth-submit" disabled={loading}>
{loading ? 'Logging in...' : 'Log In'}
</button>
</form>
<p className="auth-switch">
Need an account? <Link to="/register">Register</Link>
</p>
</div>
<div className="auth-footer">
<Link to="/"> Back to home</Link>
</div>
</div>
<div className="auth-background">
<div className="bg-gradient"></div>
<div className="bg-pattern"></div>
</div>
</div>
);
}

View file

@ -0,0 +1,183 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
export default function RegisterPage({ onRegister }) {
const navigate = useNavigate();
const [formData, setFormData] = useState({
email: '',
username: '',
displayName: '',
password: '',
confirmPassword: '',
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [agreedToTerms, setAgreedToTerms] = useState(false);
const handleChange = (e) => {
setFormData(prev => ({ ...prev, [e.target.name]: e.target.value }));
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
return;
}
if (formData.password.length < 8) {
setError('Password must be at least 8 characters');
return;
}
if (!agreedToTerms) {
setError('You must agree to the Terms of Service');
return;
}
setLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
onRegister?.({ email: formData.email, username: formData.username });
navigate('/app');
} catch (err) {
setError('Registration failed. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="auth-page">
<div className="auth-container">
<div className="auth-header">
<Link to="/" className="auth-brand">
<span className="brand-icon"></span>
<span>AeThex Connect</span>
</Link>
</div>
<div className="auth-card register-card">
<h1>Create an account</h1>
<p className="auth-subtitle">Join the future of communication</p>
{error && (
<div className="auth-error">
<span></span>
{error}
</div>
)}
<form onSubmit={handleSubmit} className="auth-form">
<div className="form-group">
<label htmlFor="email">EMAIL <span className="required">*</span></label>
<input
id="email"
name="email"
type="email"
value={formData.email}
onChange={handleChange}
required
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="displayName">DISPLAY NAME <span className="required">*</span></label>
<input
id="displayName"
name="displayName"
type="text"
value={formData.displayName}
onChange={handleChange}
required
disabled={loading}
/>
</div>
<div className="form-group">
<label htmlFor="username">USERNAME <span className="required">*</span></label>
<input
id="username"
name="username"
type="text"
value={formData.username}
onChange={handleChange}
required
disabled={loading}
placeholder="lowercase, no spaces"
/>
</div>
<div className="form-group">
<label htmlFor="password">PASSWORD <span className="required">*</span></label>
<input
id="password"
name="password"
type="password"
value={formData.password}
onChange={handleChange}
required
disabled={loading}
minLength={8}
/>
</div>
<div className="form-group">
<label htmlFor="confirmPassword">CONFIRM PASSWORD <span className="required">*</span></label>
<input
id="confirmPassword"
name="confirmPassword"
type="password"
value={formData.confirmPassword}
onChange={handleChange}
required
disabled={loading}
/>
</div>
<label className="terms-checkbox">
<input
type="checkbox"
checked={agreedToTerms}
onChange={(e) => setAgreedToTerms(e.target.checked)}
disabled={loading}
/>
<span>
I agree to the <Link to="/terms">Terms of Service</Link> and{' '}
<Link to="/privacy">Privacy Policy</Link>
</span>
</label>
<button type="submit" className="auth-submit" disabled={loading}>
{loading ? 'Creating account...' : 'Continue'}
</button>
</form>
<p className="auth-switch">
Already have an account? <Link to="/login">Log In</Link>
</p>
</div>
<div className="auth-footer">
<Link to="/"> Back to home</Link>
</div>
</div>
<div className="auth-visual">
<div className="visual-content">
<div className="trinity-logo">
<div className="trinity-ring foundation"></div>
<div className="trinity-ring corporation"></div>
<div className="trinity-ring labs"></div>
</div>
<h2>The Trinity Awaits</h2>
<p>Foundation Corporation Labs</p>
</div>
</div>
</div>
);
}

View file

@ -10,6 +10,8 @@
"dependencies": {
"@stripe/react-stripe-js": "^5.4.1",
"@stripe/stripe-js": "^8.6.1",
"@supabase/supabase-js": "^2.94.1",
"@tailwindcss/postcss": "^4.1.18",
"axios": "^1.13.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -20,9 +22,24 @@
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^5.0.8"
}
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
"integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@ -701,7 +718,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@ -712,7 +728,6 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@ -723,7 +738,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@ -733,14 +747,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@ -1143,6 +1155,342 @@
"node": ">=12.16"
}
},
"node_modules/@supabase/auth-js": {
"version": "2.94.1",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.94.1.tgz",
"integrity": "sha512-Wt/SdmAtNNiqrcBbPlzWojLcE1bQ9OYb8PTaYF6QccFX5JeXZI0sZ01MLNE+E83UK6cK0lw4YznX0D2g08UQng==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.94.1",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.94.1.tgz",
"integrity": "sha512-A7Bx0gnclDNZ4m8+mnO2IEEzMxtUSg7cpPEBF6Ek1LpjIQkC7vvoidiV/RuntnKX43IiVcWV1f2FsAppMagEmQ==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.94.1",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.94.1.tgz",
"integrity": "sha512-N6MTghjHnMZddT48rAj8dIFgedCU97cc1ahQM74Tc+DF4UH7y2+iEfdYV3unJsylpaiWlu92Fy8Lj14Jbrmxog==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.94.1",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.94.1.tgz",
"integrity": "sha512-Wq8olpCAGmN4y2DH2kUdlcakdzNHRCde72BFS8zK5ub46bBeSUoE9DqrfeNFWKaF2gCE/cmK8aTUTorZD9jdtQ==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.94.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.94.1.tgz",
"integrity": "sha512-/Mi18LGyrugPwtfqETfAqEGcBQotY/7IMsTGYgEFdqr8cQq280BVQWjN2wI9KibWtshPp0Ryvil5Uzd5YfM7kA==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.94.1",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.94.1.tgz",
"integrity": "sha512-87vOY8n3WHB3m+a/KeySj07djOQVuRA5qgX5E7db1eDkaZ1of5M+3t/tv6eYYy4BfqxuHMZuCe5uVrO/oyvoow==",
"license": "MIT",
"dependencies": {
"@supabase/auth-js": "2.94.1",
"@supabase/functions-js": "2.94.1",
"@supabase/postgrest-js": "2.94.1",
"@supabase/realtime-js": "2.94.1",
"@supabase/storage-js": "2.94.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@tailwindcss/node": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz",
"integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"enhanced-resolve": "^5.18.3",
"jiti": "^2.6.1",
"lightningcss": "1.30.2",
"magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
"tailwindcss": "4.1.18"
}
},
"node_modules/@tailwindcss/oxide": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz",
"integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==",
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@tailwindcss/oxide-android-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-arm64": "4.1.18",
"@tailwindcss/oxide-darwin-x64": "4.1.18",
"@tailwindcss/oxide-freebsd-x64": "4.1.18",
"@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18",
"@tailwindcss/oxide-linux-arm64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-arm64-musl": "4.1.18",
"@tailwindcss/oxide-linux-x64-gnu": "4.1.18",
"@tailwindcss/oxide-linux-x64-musl": "4.1.18",
"@tailwindcss/oxide-wasm32-wasi": "4.1.18",
"@tailwindcss/oxide-win32-arm64-msvc": "4.1.18",
"@tailwindcss/oxide-win32-x64-msvc": "4.1.18"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz",
"integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz",
"integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz",
"integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz",
"integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz",
"integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz",
"integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz",
"integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz",
"integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz",
"integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz",
"integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
"@emnapi/runtime",
"@tybys/wasm-util",
"@emnapi/wasi-threads",
"tslib"
],
"cpu": [
"wasm32"
],
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/core": "^1.7.1",
"@emnapi/runtime": "^1.7.1",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.1.0",
"@tybys/wasm-util": "^0.10.1",
"tslib": "^2.4.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz",
"integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz",
"integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@tailwindcss/postcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz",
"integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"@tailwindcss/node": "4.1.18",
"@tailwindcss/oxide": "4.1.18",
"postcss": "^8.4.41",
"tailwindcss": "4.1.18"
}
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -1195,6 +1543,21 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz",
"integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@ -1224,6 +1587,15 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
@ -1251,6 +1623,43 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": {
"version": "10.4.24",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/autoprefixer"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001766",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
"autoprefixer": "bin/autoprefixer"
},
"engines": {
"node": "^10 || ^12 || >=14"
},
"peerDependencies": {
"postcss": "^8.1.0"
}
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
@ -1321,9 +1730,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001763",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001763.tgz",
"integrity": "sha512-mh/dGtq56uN98LlNX9qdbKnzINhX0QzhiWBFEkFfsFO4QyCvL8YegrJAazCwXIeqkIob8BlZPGM3xdnY+sgmvQ==",
"version": "1.0.30001768",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz",
"integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==",
"dev": true,
"funding": [
{
@ -1393,6 +1802,15 @@
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@ -1436,6 +1854,19 @@
"node": ">=10.0.0"
}
},
"node_modules/enhanced-resolve": {
"version": "5.19.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz",
"integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
"tapable": "^2.3.0"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@ -1566,6 +1997,20 @@
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1649,6 +2094,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@ -1688,6 +2139,24 @@
"node": ">= 0.4"
}
},
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -1720,6 +2189,255 @@
"node": ">=6"
}
},
"node_modules/lightningcss": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"license": "MPL-2.0",
"dependencies": {
"detect-libc": "^2.0.3"
},
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"lightningcss-android-arm64": "1.30.2",
"lightningcss-darwin-arm64": "1.30.2",
"lightningcss-darwin-x64": "1.30.2",
"lightningcss-freebsd-x64": "1.30.2",
"lightningcss-linux-arm-gnueabihf": "1.30.2",
"lightningcss-linux-arm64-gnu": "1.30.2",
"lightningcss-linux-arm64-musl": "1.30.2",
"lightningcss-linux-x64-gnu": "1.30.2",
"lightningcss-linux-x64-musl": "1.30.2",
"lightningcss-win32-arm64-msvc": "1.30.2",
"lightningcss-win32-x64-msvc": "1.30.2"
}
},
"node_modules/lightningcss-android-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz",
"integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-arm64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz",
"integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-darwin-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz",
"integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-freebsd-x64": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz",
"integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm-gnueabihf": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz",
"integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz",
"integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-arm64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz",
"integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-gnu": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz",
"integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-linux-x64-musl": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz",
"integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-arm64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz",
"integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.30.2",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz",
"integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -1742,6 +2460,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -1782,7 +2509,6 @@
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
@ -1817,14 +2543,12 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
@ -1840,6 +2564,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -1849,6 +2574,13 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -2037,12 +2769,42 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tailwindcss": {
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",

View file

@ -11,6 +11,8 @@
"dependencies": {
"@stripe/react-stripe-js": "^5.4.1",
"@stripe/stripe-js": "^8.6.1",
"@supabase/supabase-js": "^2.94.1",
"@tailwindcss/postcss": "^4.1.18",
"axios": "^1.13.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@ -21,6 +23,9 @@
"@types/react": "^18.2.43",
"@types/react-dom": "^18.2.17",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.24",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^5.0.8"
}
}

View file

@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

View file

@ -0,0 +1,159 @@
import { createClient } from '@supabase/supabase-js';
import { io } from 'socket.io-client';
// Supabase client
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'http://127.0.0.1:3000';
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY || 'sb_publishable_ACJWlzQHlZjBrEguHvfOxg_3BJgxAaH';
export const supabase = createClient(supabaseUrl, supabaseKey);
// Socket.IO client
const socketUrl = import.meta.env.VITE_SOCKET_URL || 'http://localhost:3000';
export const socket = io(socketUrl, {
autoConnect: false,
transports: ['websocket', 'polling'],
});
// Connect socket when user is authenticated
export const connectSocket = (userId) => {
if (!socket.connected) {
socket.auth = { userId };
socket.connect();
}
};
export const disconnectSocket = () => {
if (socket.connected) {
socket.disconnect();
}
};
// Messaging API
export const messagingService = {
// Get all conversations for a user
async getConversations(userId) {
const { data, error } = await supabase
.from('conversation_participants')
.select(`
conversation_id,
conversations (
id,
type,
title,
description,
avatar_url,
created_at,
updated_at
)
`)
.eq('user_id', userId)
.order('conversations(updated_at)', { ascending: false });
if (error) throw error;
return data?.map(p => p.conversations) || [];
},
// Get messages for a conversation
async getMessages(conversationId, limit = 50) {
const { data, error } = await supabase
.from('messages')
.select('*')
.eq('conversation_id', conversationId)
.order('created_at', { ascending: false })
.limit(limit);
if (error) throw error;
return (data || []).reverse();
},
// Send a message
async sendMessage(conversationId, senderId, content) {
const { data, error } = await supabase
.from('messages')
.insert({
conversation_id: conversationId,
sender_id: senderId,
content_encrypted: content, // TODO: Add actual encryption
content_type: 'text',
})
.select()
.single();
if (error) throw error;
// Emit via socket for real-time
socket.emit('message:send', {
conversationId,
message: data,
});
return data;
},
// Create a new conversation
async createConversation(type, title, participantIds) {
const { data: conversation, error: convError } = await supabase
.from('conversations')
.insert({ type, title })
.select()
.single();
if (convError) throw convError;
// Add participants
const participants = participantIds.map(userId => ({
conversation_id: conversation.id,
user_id: userId,
role: 'member',
}));
const { error: partError } = await supabase
.from('conversation_participants')
.insert(participants);
if (partError) throw partError;
return conversation;
},
// Subscribe to new messages in a conversation
subscribeToMessages(conversationId, callback) {
const channel = supabase
.channel(`messages:${conversationId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `conversation_id=eq.${conversationId}`,
},
(payload) => {
callback(payload.new);
}
)
.subscribe();
return () => {
supabase.removeChannel(channel);
};
},
};
// Socket event handlers
socket.on('connect', () => {
console.log('Socket connected:', socket.id);
});
socket.on('disconnect', () => {
console.log('Socket disconnected');
});
socket.on('message:new', (message) => {
console.log('New message received:', message);
// This will be handled by the React context
});
socket.on('error', (error) => {
console.error('Socket error:', error);
});

View file

@ -0,0 +1,22 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
'discord-dark': '#0a0a0a',
'discord-darker': '#050505',
'discord-sidebar': '#0d0d0d',
'discord-channel': '#111111',
'discord-hover': '#1a1a1a',
'foundation': '#fbbf24',
'corporation': '#06b6d4',
'labs': '#a855f7',
}
},
},
plugins: [],
}