mirror of
https://github.com/m4rcel-lol/custom-discord-ai-chatbot-site.git
synced 2025-12-06 19:03:58 +05:30
feat: Initialize Discord-style AI chat project
Sets up the project structure, dependencies, and basic configuration for a Discord-inspired AI chat application. Includes placeholder components and types, Vite configuration for local development, and basic styling for a dark theme and custom scrollbars. Integrates Gemini API key handling and a service for sending messages.
This commit is contained in:
41
components/Badges.tsx
Normal file
41
components/Badges.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
// Updated to Green "Verified AI" style
|
||||
export const VerifiedBotBadge = () => (
|
||||
<span className="flex items-center gap-[2px] bg-[#23a559] text-white text-[10px] px-[4px] rounded-[4px] h-[15px] select-none align-baseline ml-1.5 font-medium leading-none">
|
||||
<svg aria-label="Verified AI" aria-hidden="false" role="img" width="10" height="10" viewBox="0 0 16 15.2" fill="none" xmlns="http://www.w3.org/2000/svg" className="mt-[1px]">
|
||||
<path d="M7.4,11.17,4.17,8.22,5.55,6.81l1.85,1.69L12.95,3l1.38,1.43L7.4,11.17Z" fill="currentColor"/>
|
||||
</svg>
|
||||
AI
|
||||
</span>
|
||||
);
|
||||
|
||||
export const StaffBadge = () => (
|
||||
<div className="group/badge relative cursor-pointer hover:bg-[#3f4147] rounded p-0.5" title="Discord Staff">
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/m4rcel-lol/assets/27e24d3b15c32ba2d39e54d544b66ba2575e2e3d/865852-discord-clyde.png"
|
||||
alt="Discord Staff"
|
||||
className="w-[22px] h-[22px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const CertifiedBadge = () => (
|
||||
<div className="group/badge relative cursor-pointer hover:bg-[#3f4147] rounded p-0.5" title="Nitro Subscriber Since February 21st 2024">
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/m4rcel-lol/assets/283298c22f1a688c8277e1f6777144731bfdf1e0/67822-opal-nitro-tier.png"
|
||||
alt="Nitro Subscriber"
|
||||
className="w-[22px] h-[22px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const BugHunterBadge = () => (
|
||||
<div className="group/badge relative cursor-pointer hover:bg-[#3f4147] rounded p-0.5" title="This Chatbot has been made by Google">
|
||||
<img
|
||||
src="https://raw.githubusercontent.com/m4rcel-lol/assets/0d09cd59eed6bf752d4dfdcfb21dada876860012/47207-google.png"
|
||||
alt="Made by Google"
|
||||
className="w-[22px] h-[22px] object-contain"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
256
components/ChatArea.tsx
Normal file
256
components/ChatArea.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { Message } from '../types';
|
||||
import { sendMessageToGemini } from '../services/geminiService';
|
||||
import { VerifiedBotBadge, StaffBadge, CertifiedBadge, BugHunterBadge } from './Badges';
|
||||
import MiniProfile from './MiniProfile';
|
||||
|
||||
interface ChatAreaProps {
|
||||
activeChannelName: string;
|
||||
onOpenFullProfile: (user: any) => void;
|
||||
}
|
||||
|
||||
const GEMINI_AVATAR = 'https://raw.githubusercontent.com/m4rcel-lol/assets/f08fbf7f9e51418c372697e2df89ddb28c59efe3/rectangle-gemini-google-icon-symbol-logo-free-png.webp';
|
||||
const USER_AVATAR = 'https://picsum.photos/seed/me/50/50';
|
||||
const GEMINI_BANNER = 'https://raw.githubusercontent.com/m4rcel-lol/assets/13ebd5bfa7abe5ee591107b9a7b411f3e3ae2d13/Gemini-API-IoT-banner_1.original.png';
|
||||
|
||||
const ChatArea: React.FC<ChatAreaProps> = ({ activeChannelName, onOpenFullProfile }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([
|
||||
{
|
||||
id: 'welcome',
|
||||
role: 'model',
|
||||
content: `Hello! I'm your AI assistant. I support **bold**, *italics*, \`code\`, and code blocks:
|
||||
\`\`\`javascript
|
||||
console.log("Hello Discord!");
|
||||
\`\`\`
|
||||
Ask me anything!`,
|
||||
timestamp: new Date(),
|
||||
senderName: 'Gemini AI',
|
||||
avatarUrl: GEMINI_AVATAR
|
||||
}
|
||||
]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
// Popout State
|
||||
const [popoutUser, setPopoutUser] = useState<any>(null);
|
||||
const [popoutPos, setPopoutPos] = useState({ x: 0, y: 0 });
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!inputValue.trim() || isLoading) return;
|
||||
|
||||
const userMsg: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: inputValue,
|
||||
timestamp: new Date(),
|
||||
senderName: 'You',
|
||||
avatarUrl: USER_AVATAR
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, userMsg]);
|
||||
setInputValue('');
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const responseText = await sendMessageToGemini(inputValue, messages);
|
||||
|
||||
const botMsg: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'model',
|
||||
content: responseText,
|
||||
timestamp: new Date(),
|
||||
senderName: 'Gemini AI',
|
||||
avatarUrl: GEMINI_AVATAR
|
||||
};
|
||||
|
||||
setMessages(prev => [...prev, botMsg]);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSendMessage();
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const handleUserClick = (e: React.MouseEvent, msg: Message) => {
|
||||
e.stopPropagation();
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
setPopoutPos({ x: rect.right, y: rect.top });
|
||||
|
||||
setPopoutUser({
|
||||
name: msg.senderName,
|
||||
avatarUrl: msg.avatarUrl,
|
||||
isBot: msg.role === 'model',
|
||||
username: msg.role === 'model' ? 'gemini_ai' : 'current_user',
|
||||
color: msg.role === 'model' ? '#5865F2' : '#f0b232', // Banner/Accent Color
|
||||
theme: msg.role === 'model'
|
||||
? 'linear-gradient(to bottom right, #4c1d95, #1e40af)' // Darker Purple-Blue Gradient for better text visibility
|
||||
: '#232428', // Default Dark Gray for User
|
||||
bannerUrl: msg.role === 'model' ? GEMINI_BANNER : undefined
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col bg-[#313338] h-full relative min-w-0 animate-fade-in">
|
||||
{/* Header */}
|
||||
<div className="h-12 border-b border-[#26272D] flex items-center justify-between px-4 shadow-sm z-10 select-none">
|
||||
<div className="flex items-center gap-2 overflow-hidden">
|
||||
<div className="text-[#80848e] text-2xl font-light">@</div>
|
||||
<h3 className="font-bold text-[#f2f3f5] truncate text-base">{activeChannelName}</h3>
|
||||
|
||||
<div className="flex items-center gap-1.5 ml-2">
|
||||
<div className="w-2.5 h-2.5 bg-[#23a559] rounded-full"></div>
|
||||
</div>
|
||||
|
||||
<VerifiedBotBadge />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div className="flex-1 overflow-y-auto custom-scrollbar p-4 flex flex-col">
|
||||
{/* Welcome Placeholder */}
|
||||
<div className="mt-4 mb-8 px-4 animate-slide-up">
|
||||
<div className="w-20 h-20 rounded-full flex items-center justify-center mb-4 shadow-lg overflow-hidden bg-[#232428]">
|
||||
<img src={GEMINI_AVATAR} alt="Gemini" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white mb-2">
|
||||
<span className="font-bold">{activeChannelName}</span>
|
||||
</h2>
|
||||
<div className="text-[#b5bac1] text-sm flex items-center gap-2">
|
||||
<span>Verified System Bot</span>
|
||||
<span className="w-1 h-1 bg-[#b5bac1] rounded-full"></span>
|
||||
<span>This is a temporary chat conversation with </span> <span>{activeChannelName}</span> <span>Your messages will be lost after refreshing the site.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{messages.map((msg, index) => {
|
||||
const isUser = msg.role === 'user';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={msg.id}
|
||||
className="group flex justify-start w-full hover:bg-[#2e3035] -mx-4 px-8 py-0.5 mt-[1.0625rem] first:mt-0 animate-fade-in"
|
||||
style={{ width: 'calc(100% + 2rem)' }}
|
||||
>
|
||||
<div className="flex gap-4 w-full">
|
||||
|
||||
{/* Avatar */}
|
||||
<div
|
||||
className="flex-shrink-0 mt-0.5 cursor-pointer hover:drop-shadow-md transition-all active:translate-y-[1px]"
|
||||
onClick={(e) => handleUserClick(e, msg)}
|
||||
>
|
||||
<img
|
||||
src={msg.avatarUrl}
|
||||
alt={msg.senderName}
|
||||
className="w-10 h-10 rounded-full hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex flex-col items-start w-full min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span
|
||||
className="text-[#f2f3f5] font-medium hover:underline cursor-pointer text-[1rem]"
|
||||
onClick={(e) => handleUserClick(e, msg)}
|
||||
>
|
||||
{msg.senderName}
|
||||
</span>
|
||||
|
||||
{!isUser && (
|
||||
<div className="flex items-center gap-1 select-none">
|
||||
<VerifiedBotBadge />
|
||||
<div className="flex items-center ml-1 space-x-0.5">
|
||||
<StaffBadge />
|
||||
<CertifiedBadge />
|
||||
<BugHunterBadge />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="text-xs text-[#949ba4] font-medium ml-1 mt-[1px]">
|
||||
{formatTime(msg.timestamp)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="text-[#dbdee1] leading-[1.375rem] w-full markdown-content text-[0.95rem]">
|
||||
<ReactMarkdown>
|
||||
{msg.content}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex items-start gap-4 mt-[1.0625rem] px-4 animate-fade-in">
|
||||
<div className="w-10 h-10 rounded-full bg-[#404249] animate-pulse"></div>
|
||||
<div className="space-y-2 pt-1 w-full max-w-lg">
|
||||
<div className="h-4 bg-[#404249] rounded w-24 animate-pulse"></div>
|
||||
<div className="h-4 bg-[#404249] rounded w-48 animate-pulse"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="px-4 pb-6 pt-2 bg-[#313338]">
|
||||
<div className="relative bg-[#383a40] rounded-lg flex items-center px-4 py-3 transition-colors focus-within:ring-1 focus-within:ring-[#00A8FC]">
|
||||
|
||||
<input
|
||||
type="text"
|
||||
className="bg-transparent flex-1 text-[#dbdee1] outline-none placeholder-[#949ba4] h-[24px] font-normal"
|
||||
placeholder={`Message @${activeChannelName}`}
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isLoading}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isLoading && (
|
||||
<div className="absolute bottom-1 left-4 text-[10px] font-bold text-[#dbdee1] animate-pulse">
|
||||
Gemini AI is typing...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{popoutUser && (
|
||||
<MiniProfile
|
||||
user={popoutUser}
|
||||
position={popoutPos}
|
||||
onClose={() => setPopoutUser(null)}
|
||||
onOpenFullProfile={onOpenFullProfile}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatArea;
|
||||
94
components/DMList.tsx
Normal file
94
components/DMList.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import { User, Plus, X } from 'lucide-react';
|
||||
import { Channel } from '../types';
|
||||
|
||||
interface DMListProps {
|
||||
activeChannelId: string;
|
||||
onSelectChannel: (id: string) => void;
|
||||
}
|
||||
|
||||
const MOCK_CHANNELS: Channel[] = [
|
||||
{ id: '1', name: 'Gemini AI', type: 'dm', status: 'online', avatarUrl: 'https://picsum.photos/seed/gemini/50/50' },
|
||||
{ id: '2', name: 'Wumpus', type: 'dm', status: 'idle', avatarUrl: 'https://picsum.photos/seed/wumpus/50/50' },
|
||||
{ id: '3', name: 'Clyde', type: 'dm', status: 'dnd', avatarUrl: 'https://picsum.photos/seed/clyde/50/50' },
|
||||
{ id: '4', name: 'Nelly', type: 'dm', status: 'offline', avatarUrl: 'https://picsum.photos/seed/nelly/50/50' },
|
||||
];
|
||||
|
||||
const DMList: React.FC<DMListProps> = ({ activeChannelId, onSelectChannel }) => {
|
||||
return (
|
||||
<div className="w-60 bg-[#2b2d31] flex flex-col h-full hidden md:flex">
|
||||
{/* Search / Find Button */}
|
||||
<div className="h-12 shadow-sm flex items-center px-2.5 shadow-md z-10">
|
||||
<button className="w-full text-left text-sm bg-[#1e1f22] text-[#949ba4] px-2 py-1.5 rounded-[4px] truncate">
|
||||
Find or start a conversation
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* List */}
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-0.5 custom-scrollbar">
|
||||
<div className="flex items-center justify-between px-2 pt-2 pb-1 text-[#949ba4] hover:text-[#dbdee1] cursor-pointer">
|
||||
<span className="text-xs font-bold uppercase tracking-wide">Direct Messages</span>
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
|
||||
{MOCK_CHANNELS.map((channel) => (
|
||||
<div
|
||||
key={channel.id}
|
||||
onClick={() => onSelectChannel(channel.id)}
|
||||
className={`group flex items-center gap-3 px-2 py-2 rounded-[4px] cursor-pointer transition-colors ${
|
||||
activeChannelId === channel.id
|
||||
? 'bg-[#404249] text-white'
|
||||
: 'text-[#949ba4] hover:bg-[#35373c] hover:text-[#dbdee1]'
|
||||
}`}
|
||||
>
|
||||
<div className="relative">
|
||||
<img
|
||||
src={channel.avatarUrl}
|
||||
alt={channel.name}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
{channel.status && (
|
||||
<div className={`absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 border-[3px] border-[#2b2d31] rounded-full flex items-center justify-center
|
||||
${channel.status === 'online' ? 'bg-[#23a559]' : ''}
|
||||
${channel.status === 'idle' ? 'bg-[#f0b232]' : ''}
|
||||
${channel.status === 'dnd' ? 'bg-[#f23f43]' : ''}
|
||||
${channel.status === 'offline' ? 'bg-[#80848e]' : ''}
|
||||
`}>
|
||||
{channel.status === 'idle' && <div className="w-2 h-2 bg-[#2b2d31] rounded-full absolute -top-0.5 -left-0.5" />}
|
||||
{channel.status === 'dnd' && <div className="w-1.5 h-0.5 bg-[#2b2d31] rounded-full" />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 truncate">
|
||||
<span className={`font-medium ${activeChannelId === channel.id ? 'text-white' : 'text-[#dbdee1]'}`}>
|
||||
{channel.name}
|
||||
</span>
|
||||
{channel.id === '1' && <span className="ml-1 text-[10px] bg-[#5865F2] text-white px-1 rounded-[3px] py-[1px]">BOT</span>}
|
||||
</div>
|
||||
<X size={14} className="opacity-0 group-hover:opacity-100 text-[#949ba4] hover:text-white" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* User Area */}
|
||||
<div className="bg-[#232428] px-2 py-1.5 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2 group hover:bg-[#3f4147] p-1 rounded-md cursor-pointer -ml-1">
|
||||
<div className="relative">
|
||||
<img src="https://picsum.photos/seed/me/50/50" alt="Me" className="w-8 h-8 rounded-full" />
|
||||
<div className="absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 bg-[#23a559] border-[3px] border-[#232428] rounded-full"></div>
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<div className="font-semibold text-white">Current User</div>
|
||||
<div className="text-[#dbdee1] opacity-60">#1234</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Buttons */}
|
||||
<div className="flex items-center">
|
||||
{/* Minimal controls simulation */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DMList;
|
||||
108
components/FullProfileModal.tsx
Normal file
108
components/FullProfileModal.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React from 'react';
|
||||
import { StaffBadge, CertifiedBadge, BugHunterBadge, VerifiedBotBadge } from './Badges';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
interface FullProfileModalProps {
|
||||
user: {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
isBot: boolean;
|
||||
username: string;
|
||||
color: string;
|
||||
theme?: string;
|
||||
bannerUrl?: string;
|
||||
};
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const FullProfileModal: React.FC<FullProfileModalProps> = ({ user, onClose }) => {
|
||||
return (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 animate-fade-in" onClick={onClose}>
|
||||
<div
|
||||
className="w-[600px] rounded-2xl overflow-hidden shadow-2xl animate-scale-in relative"
|
||||
style={{ background: user.theme || '#111214' }}
|
||||
onClick={(e) => e.stopPropagation()} // Prevent closing when clicking inside
|
||||
>
|
||||
{/* Banner */}
|
||||
<div
|
||||
className="h-[210px] w-full relative bg-cover bg-center"
|
||||
style={{
|
||||
backgroundColor: user.color,
|
||||
backgroundImage: user.bannerUrl ? `url(${user.bannerUrl})` : undefined
|
||||
}}
|
||||
>
|
||||
{/* Close Button */}
|
||||
<div className="absolute top-4 right-4 z-10">
|
||||
<div
|
||||
onClick={onClose}
|
||||
className="w-8 h-8 bg-black/30 hover:bg-black/50 rounded-full flex items-center justify-center cursor-pointer text-white transition-colors backdrop-blur-sm"
|
||||
>
|
||||
<X size={18} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative px-6 pb-6">
|
||||
{/* Avatar */}
|
||||
<div className="absolute -top-[70px] left-6">
|
||||
<div
|
||||
className="w-[136px] h-[136px] rounded-full p-[8px] relative"
|
||||
style={{ background: user.theme || '#111214' }}
|
||||
>
|
||||
<div className="w-full h-full bg-[#111214] rounded-full overflow-hidden relative">
|
||||
<img src={user.avatarUrl} alt={user.name} className="w-full h-full rounded-full object-cover" />
|
||||
</div>
|
||||
<div className="absolute bottom-2 right-2 w-8 h-8 bg-[#23a559] border-[6px] border-[#111214] rounded-full z-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Top Badges */}
|
||||
{user.isBot && (
|
||||
<div className="flex justify-end pt-4 pb-2">
|
||||
<div className="bg-[#232428]/50 rounded-lg p-1.5 flex gap-1.5 border border-[#1e1f22]/50 backdrop-blur-sm">
|
||||
<StaffBadge />
|
||||
<CertifiedBadge />
|
||||
<BugHunterBadge />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!user.isBot && <div className="h-10"></div>}
|
||||
|
||||
{/* Info Card */}
|
||||
<div className={`mt-8 rounded-2xl p-4 ${user.theme ? 'bg-[#232428]/60 backdrop-blur-md' : 'bg-[#232428]'}`}>
|
||||
{/* Name Header */}
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-2xl font-bold text-white">{user.name}</span>
|
||||
{user.isBot && <VerifiedBotBadge />}
|
||||
</div>
|
||||
<div className="text-base text-[#dbdee1] mb-5">{user.username}</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="h-[1px] bg-[#3f4147] w-full mb-4" />
|
||||
|
||||
{/* About Me */}
|
||||
<div className="mb-6">
|
||||
<div className="uppercase text-[12px] font-bold text-[#b5bac1] mb-2">About Me</div>
|
||||
<div className="text-sm text-[#dbdee1] leading-relaxed">
|
||||
{user.isBot
|
||||
? "I am a highly advanced Large Language Model, trained by Google. I can help you with writing, learning, planning, and more. I am designed to be helpful, harmless, and honest."
|
||||
: "Just a human chatting with an AI. Enjoys long walks on the digital beach and dark mode UIs."}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Member Since */}
|
||||
<div className="flex gap-12">
|
||||
<div>
|
||||
<div className="uppercase text-[12px] font-bold text-[#b5bac1] mb-1">Member Since</div>
|
||||
<div className="text-sm text-[#dbdee1]">Feb 21, 2024</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FullProfileModal;
|
||||
112
components/MiniProfile.tsx
Normal file
112
components/MiniProfile.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { StaffBadge, CertifiedBadge, BugHunterBadge, VerifiedBotBadge } from './Badges';
|
||||
|
||||
interface MiniProfileProps {
|
||||
user: {
|
||||
name: string;
|
||||
avatarUrl: string;
|
||||
isBot: boolean;
|
||||
username: string;
|
||||
color: string;
|
||||
theme?: string;
|
||||
bannerUrl?: string;
|
||||
};
|
||||
position: { x: number, y: number };
|
||||
onClose: () => void;
|
||||
onOpenFullProfile: (user: any) => void;
|
||||
}
|
||||
|
||||
const MiniProfile: React.FC<MiniProfileProps> = ({ user, position, onClose, onOpenFullProfile }) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [onClose]);
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
top: Math.min(position.y, window.innerHeight - 400), // Prevent going off bottom
|
||||
left: position.x + 20, // Offset to right
|
||||
background: user.theme || '#232428'
|
||||
};
|
||||
|
||||
const handleAvatarClick = () => {
|
||||
onClose(); // Close mini profile
|
||||
onOpenFullProfile(user); // Open full profile
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={style}
|
||||
className="fixed z-50 w-[300px] rounded-xl shadow-[0_8px_16px_rgba(0,0,0,0.24)] overflow-hidden animate-scale-in"
|
||||
>
|
||||
{/* Banner */}
|
||||
<div
|
||||
className="h-[60px] w-full relative bg-cover bg-center"
|
||||
style={{
|
||||
backgroundColor: user.color,
|
||||
backgroundImage: user.bannerUrl ? `url(${user.bannerUrl})` : undefined
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative px-4 pb-4">
|
||||
{/* Avatar */}
|
||||
<div className="absolute -top-[30px] left-4 group cursor-pointer" onClick={handleAvatarClick}>
|
||||
<div
|
||||
className="w-[80px] h-[80px] rounded-full p-[6px] relative"
|
||||
style={{ background: user.theme || '#232428' }}
|
||||
>
|
||||
<div className="w-full h-full bg-[#232428] rounded-full overflow-hidden relative">
|
||||
<img src={user.avatarUrl} alt={user.name} className="w-full h-full rounded-full transition-opacity group-hover:opacity-80 object-cover" />
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-black/40 rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center text-[10px] text-white font-bold transition-opacity z-10">
|
||||
VIEW PROFILE
|
||||
</div>
|
||||
<div className="absolute bottom-1 right-1 w-5 h-5 bg-[#23a559] border-[4px] border-[#232428] rounded-full z-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges */}
|
||||
{user.isBot && (
|
||||
<div className="flex justify-end pt-3 pb-1">
|
||||
<div className="bg-[#111214]/50 rounded-lg p-1 flex gap-1 border border-[#1e1f22]/50 backdrop-blur-sm">
|
||||
<StaffBadge />
|
||||
<CertifiedBadge />
|
||||
<BugHunterBadge />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!user.isBot && <div className="h-6"></div>}
|
||||
|
||||
{/* Info */}
|
||||
<div className={`mt-8 rounded-lg p-3 pb-4 ${user.theme ? 'bg-[#111214]/60 border border-[#1e1f22]/50 backdrop-blur-md' : 'bg-[#111214]'}`}>
|
||||
<div className="flex items-center flex-wrap gap-1">
|
||||
<span className="text-xl font-bold text-white hover:underline cursor-pointer" onClick={handleAvatarClick}>{user.name}</span>
|
||||
{user.isBot && <VerifiedBotBadge />}
|
||||
</div>
|
||||
<div className="text-sm text-[#dbdee1] mb-3">{user.username}</div>
|
||||
|
||||
<div className="h-[1px] bg-[#2e2f34] w-full mb-3" />
|
||||
|
||||
<div className="uppercase text-[11px] font-bold text-[#b5bac1] mb-1">About Me</div>
|
||||
<div className="text-sm text-[#dbdee1]">
|
||||
{user.isBot
|
||||
? "I am a helpful AI assistant residing in your Discord DMs."
|
||||
: "Just a human chatting with an AI."}
|
||||
</div>
|
||||
|
||||
<div className="uppercase text-[11px] font-bold text-[#b5bac1] mt-3 mb-1">Member Since</div>
|
||||
<div className="text-sm text-[#dbdee1]">Feb 21, 2024</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MiniProfile;
|
||||
38
components/ServerRail.tsx
Normal file
38
components/ServerRail.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Compass, Plus, Download, Home } from 'lucide-react';
|
||||
|
||||
const ServerRail: React.FC = () => {
|
||||
return (
|
||||
<div className="hidden sm:flex flex-col items-center w-[72px] bg-[#1e1f22] py-3 space-y-2 overflow-y-auto no-scrollbar">
|
||||
{/* Home Icon */}
|
||||
<div className="group relative flex items-center justify-center w-12 h-12 bg-[#5865F2] text-white rounded-[16px] cursor-pointer transition-all duration-200 hover:rounded-[16px] hover:bg-[#5865F2]">
|
||||
<Home size={28} />
|
||||
<div className="absolute left-0 w-1 h-8 bg-white rounded-r-full -ml-4" />
|
||||
</div>
|
||||
|
||||
<div className="w-8 h-[2px] bg-[#35363C] rounded-lg mx-auto" />
|
||||
|
||||
{/* Mock Servers */}
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className="group relative flex items-center justify-center w-12 h-12 bg-[#313338] hover:bg-[#5865F2] text-gray-100 hover:text-white rounded-[24px] hover:rounded-[16px] transition-all duration-200 cursor-pointer">
|
||||
<img
|
||||
src={`https://picsum.photos/seed/server${i}/50/50`}
|
||||
alt="Server"
|
||||
className="w-full h-full object-cover rounded-[inherit]"
|
||||
/>
|
||||
<div className="absolute left-0 w-1 h-2 bg-white rounded-r-full -ml-4 opacity-0 group-hover:opacity-100 group-hover:h-5 transition-all duration-200" />
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="group relative flex items-center justify-center w-12 h-12 bg-[#313338] hover:bg-[#23a559] text-[#23a559] hover:text-white rounded-[24px] hover:rounded-[16px] transition-all duration-200 cursor-pointer">
|
||||
<Plus size={24} />
|
||||
</div>
|
||||
|
||||
<div className="group relative flex items-center justify-center w-12 h-12 bg-[#313338] hover:bg-[#23a559] text-[#23a559] hover:text-white rounded-[24px] hover:rounded-[16px] transition-all duration-200 cursor-pointer">
|
||||
<Compass size={24} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerRail;
|
||||
97
components/UserProfileSidebar.tsx
Normal file
97
components/UserProfileSidebar.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import React from 'react';
|
||||
import { StaffBadge, CertifiedBadge, BugHunterBadge, VerifiedBotBadge } from './Badges';
|
||||
|
||||
interface UserProfileSidebarProps {
|
||||
onOpenFullProfile: (user: any) => void;
|
||||
}
|
||||
|
||||
const UserProfileSidebar: React.FC<UserProfileSidebarProps> = ({ onOpenFullProfile }) => {
|
||||
const geminiUser = {
|
||||
name: 'Gemini AI',
|
||||
avatarUrl: 'https://raw.githubusercontent.com/m4rcel-lol/assets/f08fbf7f9e51418c372697e2df89ddb28c59efe3/rectangle-gemini-google-icon-symbol-logo-free-png.webp',
|
||||
isBot: true,
|
||||
username: 'gemini_ai',
|
||||
color: '#5865F2',
|
||||
theme: 'linear-gradient(to bottom right, #4c1d95, #1e40af)', // Darker Purple-Blue Gradient for readability
|
||||
bannerUrl: 'https://raw.githubusercontent.com/m4rcel-lol/assets/13ebd5bfa7abe5ee591107b9a7b411f3e3ae2d13/Gemini-API-IoT-banner_1.original.png'
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="hidden lg:flex flex-col w-[340px] h-full border-l border-[#2e2f34] overflow-y-auto custom-scrollbar animate-fade-in"
|
||||
style={{ background: geminiUser.theme }}
|
||||
>
|
||||
{/* Banner */}
|
||||
<div
|
||||
className="h-[120px] w-full relative bg-cover bg-center"
|
||||
style={{
|
||||
backgroundColor: geminiUser.color,
|
||||
backgroundImage: `url(${geminiUser.bannerUrl})`
|
||||
}}
|
||||
>
|
||||
{/* Pencil icon on hover could go here */}
|
||||
</div>
|
||||
|
||||
{/* Profile Header */}
|
||||
<div className="px-4 pb-3 mb-2 relative">
|
||||
{/* Avatar */}
|
||||
<div className="absolute -top-[44px] left-4" onClick={() => onOpenFullProfile(geminiUser)}>
|
||||
<div className="w-[88px] h-[88px] rounded-full p-[6px] relative group cursor-pointer" style={{ background: geminiUser.theme }}>
|
||||
<div className="w-full h-full bg-[#232428] rounded-full relative overflow-hidden">
|
||||
<img
|
||||
src={geminiUser.avatarUrl}
|
||||
alt="Profile"
|
||||
className="w-full h-full rounded-full transition-opacity group-hover:opacity-80 object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-black/40 rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center text-[10px] text-white font-bold transition-opacity z-10">
|
||||
VIEW PROFILE
|
||||
</div>
|
||||
<div className="absolute bottom-1 right-1 w-6 h-6 bg-[#23a559] border-[5px] border-[#232428] rounded-full z-20"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badges Container */}
|
||||
<div className="flex justify-end pt-3 gap-1">
|
||||
<div className="bg-[#111214]/50 rounded-lg p-1 flex gap-1 border border-[#1e1f22]/50 backdrop-blur-sm">
|
||||
<StaffBadge />
|
||||
<CertifiedBadge />
|
||||
<BugHunterBadge />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Info */}
|
||||
<div className="mt-2 bg-[#111214]/60 p-3 rounded-lg border border-[#1e1f22]/50 backdrop-blur-md">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xl font-bold text-white cursor-pointer hover:underline" onClick={() => onOpenFullProfile(geminiUser)}>Gemini AI</span>
|
||||
<VerifiedBotBadge />
|
||||
</div>
|
||||
<div className="text-sm text-[#dbdee1] mt-0.5">gemini_ai</div>
|
||||
|
||||
<div className="mt-3 border-t border-[#dbdee1]/20 pt-3">
|
||||
<div className="uppercase text-[11px] font-bold text-[#dbdee1]/70 mb-1.5">About Me</div>
|
||||
<div className="text-sm text-[#f2f3f5] leading-relaxed">
|
||||
I am a large language model, trained by Google. I can help you with writing, learning, planning, and more.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<div className="uppercase text-[11px] font-bold text-[#dbdee1]/70 mb-1.5">Member Since</div>
|
||||
<div className="text-sm text-[#f2f3f5]">Feb 21, 2024</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Note */}
|
||||
<div className="mt-4 pt-4 border-t border-[#dbdee1]/20">
|
||||
<div className="uppercase text-[11px] font-bold text-[#dbdee1]/70 mb-2">Note</div>
|
||||
<textarea
|
||||
className="w-full bg-transparent text-xs text-[#dbdee1] placeholder-[#dbdee1]/50 resize-none focus:outline-none h-8"
|
||||
placeholder="Click to add a note"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileSidebar;
|
||||
Reference in New Issue
Block a user