Untitled
unknown
plain_text
9 months ago
41 kB
16
Indexable
import React, { useState, useRef, useEffect } from "react";
import {
Send,
Users,
Hash,
Sun,
Moon,
Smile,
X,
Settings,
EyeOff,
Wifi,
WifiOff,
} from "lucide-react";
// *** YENİ GÜNCELLEME: İstenen adres kullanılıyor (irc.cileksohbet.com) ***
// Nginx Ters Proxy'nin bu adreste WSS trafiğini yakalaması gereklidir.
const IRC_WEBSOCKET_URL = "wss://irc.cileksohbet.com:7777/webirc/websocket/";
const IRCClient = () => {
const [ircClient, setIrcClient] = useState(null);
const [connected, setConnected] = useState(false);
const [connecting, setConnecting] = useState(false);
const [connectionError, setConnectionError] = useState("");
const [currentNick, setCurrentNick] = useState("");
// Kullanıcıları kanallara göre tutmak için bir obje
const [channelUsers, setChannelUsers] = useState({});
const [messages, setMessages] = useState({}); // Mesajları kanallara göre tutmak için
// Kanal listesi: { name: string, active: boolean, users: number }
const [channels, setChannels] = useState([]);
const [currentMessage, setCurrentMessage] = useState("");
const [isMobile, setIsMobile] = useState(window.innerWidth < 768);
const [darkMode, setDarkMode] = useState(true);
const [showUserList, setShowUserList] = useState(true);
const [showChannelList, setShowChannelList] = useState(true);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [userFilter, setUserFilter] = useState("");
const [activeChannelName, setActiveChannelName] = useState("");
const messagesEndRef = useRef(null);
const scrollRef = useRef(null);
const wsRef = useRef(null);
// Emoji picker basit alternatifi
const emojis = ['😊', '😂', '❤️', '👍', '🎉', '😎', '🔥', '✨', '💯', '🙌'];
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
// Varsayılan nick ve şifre, URL yapısına göre ayarlandı
const nick = urlParams.get('nick') || 'Guest' + Math.floor(Math.random() * 10000);
const sifre = urlParams.get('sifre') || '';
setCurrentNick(nick);
if (!connected && !connecting) {
connectToIRC(nick, sifre);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const onResize = () => setIsMobile(window.innerWidth < 768);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
// Aktif kanal mesajları güncellendiğinde en alta kaydır
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages[activeChannelName]]);
const connectToIRC = async (nick, password) => {
setConnecting(true);
setConnectionError("");
try {
// WSS bağlantısı (irc.cileksohbet.com:7004 adresine yönlenir)
const ws = new WebSocket(IRC_WEBSOCKET_URL, "irc");
wsRef.current = ws;
ws.onopen = () => {
console.log('WebSocket bağlantısı açıldı:', IRC_WEBSOCKET_URL);
setIrcClient(ws);
addSystemMessage('🔌 IRC sunucusuna bağlanılıyor...');
// === IRC komutları - her komut \r\n ile bitmeli ===
if (password) {
ws.send(`PASS ${password}\r\n`);
}
ws.send(`NICK ${nick}\r\n`);
ws.send(`USER ${nick} 0 * :${nick}\r\n`);
console.log('IRC komutları gönderildi:', nick);
};
ws.onmessage = (event) => {
const data = event.data;
console.log('◀ IRC:', data);
handleIRCMessage(data);
};
ws.onclose = (event) => {
console.log('WebSocket kapandı:', event.code, event.reason);
setConnected(false);
setConnecting(false);
setConnectionError(`Bağlantı kesildi (${event.code})`);
addSystemMessage('❌ Bağlantı kesildi', 'GLOBAL');
wsRef.current = null;
};
ws.onerror = (error) => {
console.error('WebSocket hatası:', error);
setConnecting(false);
setConnectionError('WebSocket bağlantı hatası. Lütfen irc.cileksohbet.com adresindeki ilgili yapılandırmaları kontrol edin.');
addSystemMessage('❌ Bağlantı hatası oluştu.', 'GLOBAL');
};
} catch (error) {
console.error('Bağlantı hatası:', error);
setConnecting(false);
setConnectionError('Bağlantı kurulamadı: ' + error.message);
addSystemMessage('❌ Bağlantı kurulamadı', 'GLOBAL');
}
};
const handleIRCMessage = (rawMessage) => {
const lines = rawMessage.split(/\r?\n/);
lines.forEach(line => {
if (!line.trim()) return;
console.log('IRC Line:', line);
// PING-PONG
if (line.startsWith('PING')) {
const pingData = line.substring(5).trim();
console.log('▶ PONG:', pingData);
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(`PONG ${pingData}\r\n`);
}
return;
}
// IRC mesaj parsing
const parts = line.split(' ');
// Numeric responses
if (parts.length > 1 && /^\d{3}$/.test(parts[1])) {
const code = parts[1];
// 001: Welcome - bağlantı başarılı
if (code === '001') {
setConnecting(false);
setConnected(true);
const welcomeMsg = line.split(':').slice(2).join(':');
addSystemMessage('✅ ' + (welcomeMsg || 'IRC sunucusuna bağlandı!'), 'GLOBAL');
return;
}
// 002-005: Server info
if (['002', '003', '004', '005'].includes(code)) {
const msg = line.split(':').slice(2).join(':');
if (msg) addSystemMessage('ℹ️ ' + msg, 'GLOBAL');
return;
}
// 375, 372, 376: MOTD
if (['375', '372', '376'].includes(code)) {
const motd = line.split(':').slice(2).join(':');
if (motd) addSystemMessage('📋 ' + motd, 'GLOBAL');
return;
}
// 353: Names list
if (code === '353') {
parseNamesList(line);
return;
}
// 366: End of names
if (code === '366') {
const match = line.match(/(\S+) :End of \/NAMES list/);
if (match) {
const channel = match[1].replace(/[=@*]/, '');
addSystemMessage(`👥 ${channel} kullanıcı listesi alındı`, channel);
}
return;
}
// 332: Topic
if (code === '332') {
const topicMatch = line.match(/(#\S+) :(.+)/);
if (topicMatch) {
addSystemMessage(`📌 ${topicMatch[1]} konusu: ${topicMatch[2]}`, topicMatch[1]);
}
return;
}
// 403: No such channel
if (code === '403') {
const match = line.match(/403 \S+ (\S+) :No such channel/);
if (match) {
addSystemMessage(`❌ Kanal bulunamadı: ${match[1]}`, 'GLOBAL');
}
return;
}
// Diğer numeric kodlar
const msg = line.split(':').slice(2).join(':');
if (msg) addSystemMessage(`[${code}] ${msg}`, 'GLOBAL');
return;
}
// PRIVMSG - Kanal mesajları veya özel mesajlar
if (line.includes('PRIVMSG')) {
parsePrivMsg(line);
return;
}
// JOIN
if (line.includes('JOIN')) {
parseJoin(line);
return;
}
// PART
if (line.includes('PART')) {
parsePart(line);
return;
}
// QUIT
if (line.includes('QUIT')) {
parseQuit(line);
return;
}
// NICK change
if (line.includes('NICK')) {
parseNick(line);
return;
}
// MODE
if (line.includes('MODE')) {
parseMode(line);
return;
}
// Diğer mesajlar
if (line.includes('NOTICE') || line.includes('ERROR')) {
const msg = line.split(':').slice(2).join(':');
if (msg) addSystemMessage('⚠️ ' + msg, 'GLOBAL');
}
});
};
const parsePrivMsg = (line) => {
// :nick!user@host PRIVMSG #channel :message
const match = line.match(/:([^!]+)![^\s]+ PRIVMSG ([^\s]+) :(.+)/);
if (match) {
const [, nick, target, message] = match;
const isChannel = target.startsWith('#');
const channel = isChannel ? target : (nick === currentNick ? target : nick); // Özel mesajlar için
const messageObject = {
id: Date.now() + Math.random(),
user: nick,
mode: getUserMode(nick, isChannel ? channel : activeChannelName), // Özel mesajda mod boş bırakılabilir
message: message,
time: new Date().toLocaleTimeString("tr-TR", {
hour: "2-digit",
minute: "2-digit",
}),
channel: channel
};
setMessages(prev => ({
...prev,
[channel]: [...(prev[channel] || []), messageObject]
}));
// Eğer kanaldan geliyorsa ve aktif kanalda değilsek bildirim
if (isChannel && channel !== activeChannelName) {
// Kanal bildirim rengi eklenebilir
}
}
};
const parseNamesList = (line) => {
// :server 353 nick = #channel :@user1 +user2 user3
const match = line.match(/353 \S+ [=@*] (#\S+) :(.+)/);
if (match) {
const [, channel, usersStr] = match;
const userList = usersStr.trim().split(/\s+/).map(user => {
const modeMatch = user.match(/^([~&@%+])?(.+)/);
return {
nick: modeMatch ? modeMatch[2] : user,
mode: modeMatch ? modeMatch[1] || '' : ''
};
});
console.log('Kullanıcılar:', channel, userList);
setChannelUsers(prev => ({
...prev,
[channel]: userList
}));
// Kanal kullanıcı sayısını güncelle
setChannels(prev => prev.map(ch =>
ch.name === channel ? { ...ch, users: userList.length } : ch
));
}
};
const parseJoin = (line) => {
const match = line.match(/:([^!]+)![^\s]+ JOIN :?(#[^\s]+)/);
if (match) {
const [, nick, channel] = match;
// Kendi join'imizse
if (nick === currentNick) {
const channelExists = channels.find(c => c.name === channel);
if (!channelExists) {
setChannels(prev => [...prev.filter(c => c.name !== channel), { name: channel, active: !activeChannelName, users: 0 }]);
if (!activeChannelName) setActiveChannelName(channel);
}
// Kullanıcı listesini al
setTimeout(() => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(`NAMES ${channel}\r\n`);
}
}, 500);
addSystemMessage(`✅ ${channel} kanalına katıldınız`, channel);
} else {
addSystemMessage(`👋 ${nick} kanla katıldı`, channel);
// Kullanıcı listesine ekle
setChannelUsers(prev => {
const usersInChannel = prev[channel] || [];
if (!usersInChannel.find(u => u.nick === nick)) {
return {
...prev,
[channel]: [...usersInChannel, { nick, mode: '' }]
};
}
return prev;
});
setChannels(prev => prev.map(ch =>
ch.name === channel ? { ...ch, users: (ch.users || 0) + 1 } : ch
));
}
}
};
const parsePart = (line) => {
const match = line.match(/:([^!]+)![^\s]+ PART (#[^\s]+)/);
if (match) {
const [, nick, channel] = match;
addSystemMessage(`👋 ${nick} kanaldan ayrıldı`, channel);
// Kullanıcı listesinden çıkar
setChannelUsers(prev => ({
...prev,
[channel]: (prev[channel] || []).filter(u => u.nick !== nick)
}));
setChannels(prev => prev.map(ch =>
ch.name === channel ? { ...ch, users: Math.max(0, (ch.users || 0) - 1) } : ch
));
// Eğer kendi çıkışımızsa kanalı listeden kaldır
if (nick === currentNick) {
setChannels(prev => prev.filter(c => c.name !== channel));
setChannelUsers(prev => {
const newState = { ...prev };
delete newState[channel];
return newState;
});
setMessages(prev => {
const newState = { ...prev };
delete newState[channel];
return newState;
});
if (activeChannelName === channel) {
setActiveChannelName(channels.find(c => c.name !== channel)?.name || '');
}
}
}
};
const parseQuit = (line) => {
const match = line.match(/:([^!]+)![^\s]+ QUIT :?(.+)?/);
if (match) {
const [, nick, reason] = match;
addSystemMessage(`👋 ${nick} ayrıldı${reason ? ': ' + reason : ''}`, 'GLOBAL');
// Tüm kanallardan çıkar
setChannelUsers(prev => {
const newState = { ...prev };
Object.keys(newState).forEach(channel => {
newState[channel] = newState[channel].filter(u => u.nick !== nick);
});
return newState;
});
setChannels(prev => prev.map(ch => {
const usersInChannel = channelUsers[ch.name] || [];
if (usersInChannel.some(u => u.nick === nick)) {
return { ...ch, users: Math.max(0, ch.users - 1) };
}
return ch;
}));
}
};
const parseNick = (line) => {
const match = line.match(/:([^!]+)![^\s]+ NICK :(.+)/);
if (match) {
const [, oldNick, newNick] = match;
addSystemMessage(`🔄 ${oldNick} artık ${newNick}`, 'GLOBAL');
// Kullanıcı listelerini güncelle
setChannelUsers(prev => {
const newState = { ...prev };
Object.keys(newState).forEach(channel => {
newState[channel] = newState[channel].map(u =>
u.nick === oldNick ? { ...u, nick: newNick } : u
);
});
return newState;
});
// Eğer kendi nickimiz değişiyorsa state'i güncelle
if (oldNick === currentNick) {
setCurrentNick(newNick);
}
}
};
const parseMode = (line) => {
// :setter!user@host MODE #channel +o target
const match = line.match(/:([^!]+)![^\s]+ MODE (#[^\s]+) ([+-][\S]+) (\S+)/);
if (match) {
const [, setter, channel, mode, target] = match;
addSystemMessage(`⚙️ ${setter} → ${target}: ${mode}`, channel);
// Mode değişikliklerini kullanıcı listesine yansıt
const isAdd = mode.startsWith('+');
const modeChar = mode.substring(1); // o, v vb.
setChannelUsers(prev => {
const usersInChannel = prev[channel] || [];
const newUsers = usersInChannel.map(u => {
if (u.nick === target) {
let newMode = u.mode;
// Basit mode yönetimi: sadece tekli mode karakterini tutuyoruz
if (isAdd) {
// Sadece en yüksek mode karakterini tut
newMode = modeChar;
} else if (u.mode === modeChar) {
newMode = '';
}
return { ...u, mode: newMode };
}
return u;
});
return { ...prev, [channel]: newUsers };
});
}
};
const addSystemMessage = (message, channel) => {
const messageObject = {
id: Date.now() + Math.random(),
user: 'SISTEM',
mode: '',
message: message,
time: new Date().toLocaleTimeString("tr-TR", {
hour: "2-digit",
minute: "2-digit",
}),
system: true,
channel: channel
};
if (channel === 'GLOBAL') {
// Tüm kanallara veya sadece genel sisteme ekle
setMessages(prev => ({
...prev,
GLOBAL: [...(prev.GLOBAL || []), messageObject]
}));
} else {
setMessages(prev => ({
...prev,
[channel]: [...(prev[channel] || []), messageObject]
}));
}
};
const getUserMode = (nick, channel) => {
const usersInChannel = channelUsers[channel] || [];
const user = usersInChannel.find(u => u.nick === nick);
return user ? user.mode : '';
};
const sendIRCCommand = (command) => {
if (!wsRef.current || !connected) return;
// IRC komutu gönder
console.log('▶ Komut:', command);
wsRef.current.send(`${command}\r\n`);
};
const sendMessage = (e) => {
e.preventDefault();
if (!currentMessage.trim() || !wsRef.current || !connected) return;
const messageToSend = currentMessage.trim();
// Komut kontrolü
if (messageToSend.startsWith('/')) {
const [command, ...args] = messageToSend.substring(1).split(/\s+/);
switch (command.toLowerCase()) {
case 'join':
if (args[0]) sendIRCCommand(`JOIN ${args[0].startsWith('#') ? args[0] : '#' + args[0]}`);
break;
case 'part':
// Eğer argüman yoksa aktif kanaldan ayrıl
const partChannel = args[0] || activeChannelName;
if (partChannel) sendIRCCommand(`PART ${partChannel}`);
break;
case 'nick':
if (args[0]) sendIRCCommand(`NICK ${args[0]}`);
break;
case 'msg':
case 'privmsg':
if (args[0] && args.length > 1) {
const target = args[0];
const message = args.slice(1).join(' ');
sendIRCCommand(`PRIVMSG ${target} :${message}`);
// Kendi özel mesajımızı göster
const channel = target.startsWith('#') ? target : currentNick; // Özel mesajlar için target
setMessages(prev => ({
...prev,
[channel]: [...(prev[channel] || []), {
id: Date.now(),
user: currentNick,
mode: getUserMode(currentNick, activeChannelName),
message: message,
time: new Date().toLocaleTimeString("tr-TR", {
hour: "2-digit",
minute: "2-digit",
}),
channel: channel
}]
}));
} else {
addSystemMessage('❌ Kullanım: /msg <kullanıcı/kanal> <mesaj>', activeChannelName);
}
break;
case 'quit':
sendIRCCommand('QUIT :Kullanıcı ayrıldı');
break;
default:
addSystemMessage(`❌ Bilinmeyen komut: /${command}`, activeChannelName);
break;
}
} else {
// Normal mesaj
const activeChannel = channels.find(c => c.name === activeChannelName);
if (activeChannel) {
console.log('▶ PRIVMSG:', activeChannel.name, messageToSend);
wsRef.current.send(`PRIVMSG ${activeChannel.name} :${messageToSend}\r\n`);
// Kendi mesajımızı göster
setMessages(prev => ({
...prev,
[activeChannelName]: [...(prev[activeChannelName] || []), {
id: Date.now(),
user: currentNick,
mode: getUserMode(currentNick, activeChannelName),
message: messageToSend,
time: new Date().toLocaleTimeString("tr-TR", {
hour: "2-digit",
minute: "2-digit",
}),
channel: activeChannelName
}]
}));
} else {
addSystemMessage('❌ Aktif kanal yok. Bir kanala katılın: /join #kanal', 'GLOBAL');
}
}
setCurrentMessage("");
};
const switchChannel = (name) => {
if (activeChannelName === name) return;
// Sadece kanal listede varsa ve aktif değilse geçiş yap
const targetChannel = channels.find(c => c.name === name);
if (targetChannel) {
setActiveChannelName(name);
// Kullanıcı listesini yeniden iste
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(`NAMES ${name}\r\n`);
}
}
};
const joinChannel = () => {
const channelName = prompt('Katılmak istediğiniz kanal adını girin (örn: #kanaladi):');
if (channelName && wsRef.current && connected) {
const formattedChannel = channelName.startsWith('#') ? channelName : '#' + channelName;
sendIRCCommand(`JOIN ${formattedChannel}`);
setActiveChannelName(formattedChannel);
}
};
const removeChannel = (name) => {
if (wsRef.current && connected) {
sendIRCCommand(`PART ${name}`);
} else {
// Bağlı değilsek sadece listeden kaldır
setChannels(chs => chs.filter(c => c.name !== name));
if (activeChannelName === name) {
setActiveChannelName(channels.find(c => c.name !== name)?.name || '');
}
}
};
const activeChannel = channels.find(c => c.name === activeChannelName);
const currentUsers = channelUsers[activeChannelName] || [];
const roleColor = (mode) => {
switch (mode) {
case "~": // Founder
return "text-red-600 bg-red-600";
case "&": // Admin
return "text-red-900 bg-red-900";
case "@": // Oper/Operator
return "text-yellow-500 bg-yellow-500";
case "%": // Half-op
return "text-blue-500 bg-blue-500";
case "+": // Voice
return "text-green-500 bg-green-500";
default:
return "text-gray-700 bg-gray-700";
}
};
// Aktif kanalın mesajları + GLOBAL sistem mesajları
const filteredMessages = [
...(messages.GLOBAL || []),
...(messages[activeChannelName] || [])
].sort((a, b) => a.id - b.id); // Zaman damgasına göre sırala
return (
<div className={darkMode ? "dark" : ""}>
<div className="h-screen flex flex-col bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-100">
{/* HEADER */}
<header className="sticky top-0 z-50 flex justify-between items-center border-b border-gray-300 dark:border-gray-700 p-2 bg-white dark:bg-gray-800">
<div className="flex items-center gap-2">
<span className="text-lg">🍓</span>
<h1 className="text-lg font-semibold">
{activeChannelName} ({currentUsers.length})
</h1>
<div className="flex items-center gap-2 text-sm">
{connecting && (
<span className="text-yellow-600">Bağlanıyor...</span>
)}
{connected ? (
<Wifi className="w-4 h-4 text-green-500" />
) : (
<WifiOff className="w-4 h-4 text-red-500" />
)}
{connectionError && (
<span className="text-red-500 text-xs">{connectionError}</span>
)}
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setDarkMode(d => !d)}
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
>
{darkMode ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</button>
<button
onClick={() => setShowSettings(true)}
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
>
<Settings className="w-5 h-5" />
</button>
</div>
</header>
{/* Mobil kanal listesi */}
{isMobile && showChannelList && (
<div className="relative border-b border-gray-400">
<div
className="flex overflow-x-auto whitespace-nowrap w-full p-2"
ref={scrollRef}
>
{channels.map(c => (
<div
key={c.name}
className={`flex-shrink-0 flex items-center gap-1 px-3 py-1 border border-gray-400 rounded-full text-sm cursor-pointer whitespace-nowrap mr-2
${c.name === activeChannelName ? 'bg-blue-500 text-white' : 'bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100'}`}
>
<span onClick={() => switchChannel(c.name)} className="pr-1">
{c.name}
</span>
<X
className={`w-3 h-3 ${c.name === activeChannelName ? 'text-white hover:text-gray-200' : 'text-gray-500 hover:text-gray-700'}`}
onClick={() => removeChannel(c.name)}
/>
</div>
))}
<button
onClick={joinChannel}
className="flex-shrink-0 flex items-center gap-1 px-3 py-1 border border-gray-400 rounded-full text-sm cursor-pointer whitespace-nowrap bg-green-500 text-white"
disabled={!connected}
>
+ Kanal Ekle
</button>
</div>
<button
onClick={() => setShowChannelList(false)}
className="absolute left-2 bottom-0 z-50 translate-y-full w-8 h-8 flex items-center justify-center bg-gray-300 dark:bg-gray-700 shadow-lg rounded-b-lg hover:bg-gray-400 dark:hover:bg-gray-600"
title="Gizle"
>
<EyeOff className="w-5 h-5" />
</button>
</div>
)}
{/* MAIN AREA */}
<div className="flex-1 flex overflow-hidden relative">
{/* MESSAGES */}
<main className="flex-1 flex flex-col overflow-y-auto p-3 bg-white dark:bg-gray-800">
{filteredMessages.length === 0 ? (
<div className="flex-1 flex items-center justify-center text-gray-500">
{connecting ? '🔄 Bağlanıyor...' : connected ? (activeChannelName ? '💬 Henüz mesaj yok' : '💬 Bir kanala katılın: /join #kanal') : '❌ Bağlantı yok'}
</div>
) : (
filteredMessages.map(m => (
<div
key={m.id}
className={`mb-1 text-sm ${m.system ? 'text-gray-500 italic' : ''}`}
>
<span className="text-xs text-gray-400">
[{m.time}]
</span>{" "}
{!m.system && (
<>
{m.mode && (
<span className={`inline-block w-4 h-4 text-center text-xs rounded mr-1 ${roleColor(m.mode).split(' ')[1]} text-white`}>
{m.mode}
</span>
)}
<span className={`font-semibold ${roleColor(m.mode).split(' ')[0]}`}>
{m.user}:
</span>
</>
)}{" "}
{m.message}
</div>
))
)}
<div ref={messagesEndRef} />
</main>
{/* Sağ panel */}
<div className="flex">
{showUserList && (
<aside className="relative w-32 md:w-48 bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-600 flex flex-col">
<div className="p-2 border-b border-gray-300 dark:border-gray-700">
<input
type="text"
placeholder="Kullanıcı ara..."
value={userFilter}
onChange={(e) => setUserFilter(e.target.value)}
className="w-full px-2 py-1 text-xs rounded bg-gray-50 dark:bg-gray-700 border border-gray-300 dark:border-gray-600 focus:outline-none"
disabled={!activeChannelName}
/>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1 text-xs">
{currentUsers.length === 0 && activeChannelName && connected && (
<div className="text-center text-gray-500 text-xs py-4">
Kullanıcı listesi yükleniyor...
</div>
)}
{currentUsers.length > 0 ? (
currentUsers
.filter(u =>
u.nick.toLowerCase().includes(userFilter.toLowerCase())
)
.map(u => (
<div
key={u.nick}
className="flex items-center gap-2 py-1 border-b border-gray-200 dark:border-gray-700"
>
<span
className={`inline-flex items-center justify-center w-4 h-4 rounded text-white text-xs ${
roleColor(u.mode).split(' ')[1]
}`}
>
{u.mode || ''}
</span>
<span
className={`truncate text-xs font-medium ${
roleColor(u.mode).split(' ')[0]
}`}
>
{u.nick}
</span>
</div>
))
) : (
!connected && (
<div className="text-center text-gray-500 text-xs py-4">
Bağlantı bekleniyor...
</div>
)
)}
</div>
{!isMobile && (
<button
onClick={() => setShowUserList(false)}
className="absolute left-0 top-1/2 -translate-y-1/2 w-8 h-8 flex items-center justify-center bg-gray-300 dark:bg-gray-700 shadow rounded-r-lg hover:bg-gray-400 dark:hover:bg-gray-600"
title="Gizle"
>
<EyeOff className="w-5 h-5" />
</button>
)}
</aside>
)}
{!isMobile && showChannelList && (
<aside className="w-32 md:w-48 bg-gray-100 dark:bg-gray-800 border-l border-gray-200 dark:border-gray-600 flex flex-col relative">
<div className="flex justify-between items-center p-2 border-b border-gray-300 dark:border-gray-700 text-xs">
<Hash className="w-4 h-4 mr-1" /> <span>Kanallar</span>
</div>
<div className="flex-1 overflow-y-auto p-2 space-y-1 text-xs">
{channels.map(c => (
<div
key={c.name}
className={`flex items-center justify-between px-3 py-1 rounded text-sm cursor-pointer ${
c.name === activeChannelName
? 'bg-blue-500 text-white'
: 'bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100'
}`}
>
<span
onClick={() => switchChannel(c.name)}
className="truncate flex-1"
>
{c.name}
</span>
<X
className="w-3 h-3 hover:text-red-500"
onClick={() => removeChannel(c.name)}
/>
</div>
))}
<button
onClick={joinChannel}
className="w-full px-3 py-1 rounded text-sm bg-green-500 text-white hover:bg-green-600"
disabled={!connected}
>
+ Kanal Ekle
</button>
</div>
<button
onClick={() => setShowChannelList(false)}
className="absolute left-0 top-1/2 -translate-y-1/2 w-8 h-8 flex items-center justify-center bg-gray-300 dark:bg-gray-700 shadow rounded-r-lg hover:bg-gray-400 dark:hover:bg-gray-600"
title="Gizle"
>
<EyeOff className="w-5 h-5" />
</button>
</aside>
)}
</div>
{/* Açma butonları */}
{!isMobile && (
<div className="fixed right-0 top-1/2 -translate-y-1/2 flex flex-col gap-2 z-50">
{!showUserList && (
<button
onClick={() => setShowUserList(true)}
className="p-2 bg-gray-300 dark:bg-gray-700 rounded-l shadow"
title="Kullanıcı listesi"
>
<Users className="w-5 h-5" />
</button>
)}
{!showChannelList && (
<button
onClick={() => setShowChannelList(true)}
className="p-2 bg-gray-300 dark:bg-gray-700 rounded-l shadow"
title="Kanal listesi"
>
<Hash className="w-5 h-5" />
</button>
)}
</div>
)}
</div>
{/* FOOTER */}
<footer className="p-2 border-t border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 relative">
<form onSubmit={sendMessage} className="flex gap-2 w-full">
<div className="relative flex-1">
<input
value={currentMessage}
onChange={(e) => setCurrentMessage(e.target.value)}
className="w-full px-3 pr-10 py-2 rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder={connected ? "Mesaj veya /komut yaz..." : "Bağlantı bekleniyor..."}
disabled={!connected}
/>
<button
type="button"
onClick={() => setShowEmojiPicker(s => !s)}
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
disabled={!connected}
>
<Smile className="w-5 h-5" />
</button>
</div>
<button
type="submit"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
disabled={!connected || !currentMessage.trim()}
>
<Send className="w-4 h-4" />
</button>
</form>
{showEmojiPicker && (
<div className="absolute bottom-14 right-2 z-50 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg shadow-xl p-3">
<div className="flex flex-wrap gap-2 max-w-xs">
{emojis.map(emoji => (
<button
key={emoji}
type="button"
onClick={() => {
setCurrentMessage(m => m + emoji);
setShowEmojiPicker(false);
}}
className="text-2xl hover:scale-125 transition-transform"
>
{emoji}
</button>
))}
</div>
</div>
)}
</footer>
{/* SETTINGS */}
{showSettings && (
<>
<div
className="fixed inset-0 bg-black bg-opacity-40 z-40"
onClick={() => setShowSettings(false)}
/>
<div
className="fixed bottom-0 left-0 w-full bg-white dark:bg-gray-900 border-t border-gray-400 dark:border-gray-700 shadow-xl z-50"
style={{ height: "70%" }}
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center p-4 border-b border-gray-300 dark:border-gray-700">
<h2 className="text-lg font-semibold">⚙️ Ayarlar</h2>
<button
onClick={() => setShowSettings(false)}
className="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700"
>
<X className="w-5 h-5" />
</button>
</div>
<div className="p-4 overflow-y-auto" style={{ height: "calc(100% - 60px)" }}>
<div className="space-y-6">
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<h3 className="font-semibold mb-3 flex items-center gap-2">
<Wifi className={`w-5 h-5 ${connected ? 'text-green-500' : 'text-red-500'}`} />
Bağlantı Durumu
</h3>
<div className="space-y-2 text-sm">
<p><strong>Durum:</strong> {
connected ? '✅ Bağlı' :
connecting ? '🔄 Bağlanıyor...' :
'❌ Bağlı değil'
}</p>
<p><strong>Kullanıcı:</strong> {currentNick}</p>
<p><strong>Kanal:</strong> {activeChannelName || 'Yok'}</p>
<p><strong>Kullanıcı Sayısı (Aktif Kanal):</strong> {currentUsers.length}</p>
{connectionError && (
<p className="text-red-500"><strong>Hata:</strong> {connectionError}</p>
)}
</div>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<h3 className="font-semibold mb-3">ℹ️ Hakkında</h3>
<div className="space-y-2 text-sm text-gray-600 dark:text-gray-400">
<p>🍓 Çilek Sohbet IRC Client</p>
<p>WebSocket tabanlı modern IRC istemcisi</p>
<p className="text-xs mt-4 pt-4 border-t border-gray-300 dark:border-gray-600">
<strong>ÖNEMLİ NOT:</strong> Bu istemcinin `irc.cileksohbet.com` adresine bağlanması için, bu adreste
(varsayılan WSS/HTTPS portu olan 443) bir **SSL/TLS sertifikalı Nginx Ters Proxy**'nin
kurulu ve UnrealIRCd sunucusuna yönlendirilmiş olması gereklidir.
</p>
</div>
</div>
{connected && (
<button
onClick={() => {
sendIRCCommand('QUIT :Kullanıcı ayrıldı');
setTimeout(() => wsRef.current?.close(), 500); // QUIT gönderildikten sonra kapat
setShowSettings(false);
}}
className="w-full py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 font-semibold"
>
🔌 Bağlantıyı Kes
</button>
)}
{!connected && !connecting && (
<button
onClick={() => {
const nick = prompt('Kullanıcı adınız:', currentNick);
if (nick) {
connectToIRC(nick, '');
}
setShowSettings(false);
}}
className="w-full py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 font-semibold"
>
🔌 Yeniden Bağlan
</button>
)}
</div>
</div>
</div>
</>
)}
</div>
</div>
);
};
export default IRCClient;
Editor is loading...
Leave a Comment