Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>FireChat Flames</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> | |
| <script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-app.js"></script> | |
| <script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-auth.js"></script> | |
| <script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-database.js"></script> | |
| <script src="https://www.gstatic.com/firebasejs/8.10.0/firebase-storage.js"></script> | |
| <style> | |
| .chat-message::after { | |
| content: ""; | |
| position: absolute; | |
| bottom: 0; | |
| width: 20px; | |
| height: 20px; | |
| background: inherit; | |
| clip-path: polygon(0 0, 100% 100%, 0 100%); | |
| } | |
| .message-left::after { | |
| left: -10px; | |
| transform: rotate(90deg); | |
| } | |
| .message-right::after { | |
| right: -10px; | |
| transform: rotate(-90deg); | |
| } | |
| #messages-container { | |
| scroll-behavior: smooth; | |
| } | |
| .animate-pulse { | |
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; } | |
| 50% { opacity: 0.5; } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gradient-to-br from-gray-900 via-purple-900 to-gray-900 min-h-screen text-white"> | |
| <div class="container mx-auto max-w-6xl h-screen flex flex-col"> | |
| <!-- Header --> | |
| <header class="bg-black bg-opacity-50 backdrop-blur-md p-4 flex items-center justify-between border-b border-purple-500"> | |
| <div class="flex items-center space-x-3"> | |
| <i data-feather="message-square" class="text-purple-400"></i> | |
| <h1 class="text-2xl font-bold bg-gradient-to-r from-purple-400 via-pink-500 to-red-500 bg-clip-text text-transparent">FireChat Flames</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button id="logout-btn" class="p-2 rounded-full hover:bg-purple-900 hover:bg-opacity-50"> | |
| <i data-feather="log-out"></i> | |
| </button> | |
| <button id="profile-btn" class="flex items-center space-x-2 group"> | |
| <div class="w-10 h-10 rounded-full bg-purple-600 flex items-center justify-center overflow-hidden"> | |
| <img id="profile-img" src="" alt="Profile" class="w-full h-full object-cover"> | |
| </div> | |
| <span class="hidden md:block group-hover:text-purple-300 transition">Profile</span> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <div class="flex flex-1 overflow-hidden"> | |
| <!-- Sidebar --> | |
| <div class="w-1/4 bg-black bg-opacity-30 backdrop-blur-md border-r border-purple-900 hidden md:block"> | |
| <div class="p-4 border-b border-purple-900"> | |
| <h2 class="text-lg font-semibold mb-3 text-purple-300">Friends</h2> | |
| <div class="space-y-2 max-h-96 overflow-y-auto" id="friends-list"> | |
| </div> | |
| </div> | |
| <div class="p-4"> | |
| <h2 class="text-lg font-semibold mb-3 text-purple-300">Friend Requests</h2> | |
| <div class="space-y-2 max-h-96 overflow-y-auto" id="requests-list"> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Chat Area --> | |
| <div class="flex-1 flex flex-col"> | |
| <!-- Chat Header --> | |
| <div class="p-3 border-b border-purple-900 bg-black bg-opacity-30 backdrop-blur-md flex items-center"> | |
| <div class="w-10 h-10 rounded-full bg-purple-600 mr-3 overflow-hidden"> | |
| <img src="http://static.photos/people/200x200/3" class="w-full h-full object-cover"> | |
| </div> | |
| <div> | |
| <h2 id="chat-title" class="font-semibold">Global Chat</h2> | |
| <p class="text-xs text-purple-300">Online</p> | |
| </div> | |
| </div> | |
| <!-- Messages --> | |
| <div id="messages-container" class="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-gray-900 to-gray-800"> | |
| </div> | |
| <!-- Message Input --> | |
| <div class="p-3 border-t border-purple-900 bg-black bg-opacity-30 backdrop-blur-md"> | |
| <form id="message-form" class="flex items-center space-x-2"> | |
| <button type="button" id="attach-btn" class="p-2 rounded-full hover:bg-purple-900 hover:bg-opacity-50"> | |
| <i data-feather="paperclip"></i> | |
| </button> | |
| <input type="file" id="file-input" class="hidden" accept="image/*"> | |
| <input type="text" id="message-input" placeholder="Type a message..." | |
| class="flex-1 bg-gray-800 border border-purple-900 rounded-full px-4 py-2 focus:outline-none focus:ring-2 focus:ring-purple-500"> | |
| <button type="submit" class="p-2 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700"> | |
| <i data-feather="send"></i> | |
| </button> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Profile Modal --> | |
| <div id="profile-modal" class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 hidden"> | |
| <div class="bg-gray-900 rounded-xl max-w-md w-full p-6 border border-purple-900"> | |
| <div class="flex justify-between items-center mb-4"> | |
| <h3 class="text-xl font-bold text-purple-400">Edit Profile</h3> | |
| <button id="close-profile-modal" class="text-gray-400 hover:text-white"> | |
| <i data-feather="x"></i> | |
| </button> | |
| </div> | |
| <div class="flex flex-col items-center mb-6"> | |
| <div class="relative mb-4"> | |
| <div class="w-32 h-32 rounded-full bg-purple-900 overflow-hidden border-2 border-purple-600"> | |
| <img id="modal-profile-img" src="" class="w-full h-full object-cover"> | |
| </div> | |
| <button id="change-photo-btn" class="absolute bottom-0 right-0 bg-purple-600 rounded-full p-2 hover:bg-purple-700"> | |
| <i data-feather="camera" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| <input type="file" id="profile-photo-input" class="hidden" accept="image/*"> | |
| <input type="text" id="profile-name-input" placeholder="Your Name" | |
| class="bg-gray-800 border border-purple-900 rounded-full px-4 py-2 text-center focus:outline-none focus:ring-2 focus:ring-purple-500 w-full max-w-xs"> | |
| </div> | |
| <div class="flex justify-end space-x-3"> | |
| <button id="cancel-profile-btn" class="px-4 py-2 rounded-full bg-gray-800 hover:bg-gray-700">Cancel</button> | |
| <button id="save-profile-btn" class="px-4 py-2 rounded-full bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700">Save Changes</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Firebase configuration for direct API calls | |
| const API_BASE_URL = "https://us-central1-app-teste-x.cloudfunctions.net/api"; | |
| let currentUser = null; | |
| let authToken = null; | |
| // Initialize Firebase | |
| const firebaseConfig = { | |
| apiKey: "AIzaSyA4MtyRVloOv6WWR_KyF4bM0UqD91gwykY", | |
| authDomain: "app-teste-x.firebaseapp.com", | |
| projectId: "app-teste-x", | |
| databaseURL: "https://app-teste-x-default-rtdb.firebaseio.com", | |
| storageBucket: "app-teste-x.appspot.com" | |
| }; | |
| firebase.initializeApp(firebaseConfig); | |
| const auth = firebase.auth(); | |
| const database = firebase.database(); | |
| // Initialize Feather Icons | |
| feather.replace(); | |
| // DOM Elements | |
| const messageForm = document.getElementById('message-form'); | |
| const messageInput = document.getElementById('message-input'); | |
| const messagesContainer = document.getElementById('messages-container'); | |
| const fileInput = document.getElementById('file-input'); | |
| const attachBtn = document.getElementById('attach-btn'); | |
| const profileBtn = document.getElementById('profile-btn'); | |
| const profileModal = document.getElementById('profile-modal'); | |
| const closeProfileModal = document.getElementById('close-profile-modal'); | |
| const cancelProfileBtn = document.getElementById('cancel-profile-btn'); | |
| const saveProfileBtn = document.getElementById('save-profile-btn'); | |
| const changePhotoBtn = document.getElementById('change-photo-btn'); | |
| const profilePhotoInput = document.getElementById('profile-photo-input'); | |
| const modalProfileImg = document.getElementById('modal-profile-img'); | |
| const profileImg = document.getElementById('profile-img'); | |
| // Event Listeners | |
| messageForm.addEventListener('submit', sendMessage); | |
| attachBtn.addEventListener('click', () => fileInput.click()); | |
| fileInput.addEventListener('change', handleFileUpload); | |
| profileBtn.addEventListener('click', openProfileModal); | |
| closeProfileModal.addEventListener('click', closeModal); | |
| cancelProfileBtn.addEventListener('click', closeModal); | |
| saveProfileBtn.addEventListener('click', saveProfile); | |
| changePhotoBtn.addEventListener('click', () => profilePhotoInput.click()); | |
| profilePhotoInput.addEventListener('change', handleProfilePhotoChange); | |
| document.getElementById('logout-btn').addEventListener('click', logoutUser); | |
| // Initialize auth state | |
| auth.onAuthStateChanged(async user => { | |
| currentUser = user; | |
| if (user) { | |
| const token = await user.getIdToken(); | |
| authToken = token; | |
| // Fetch user profile | |
| const profileResponse = await fetch(`${API_BASE_URL}/users/${user.uid}`, { | |
| headers: { | |
| 'Authorization': `Bearer ${token}` | |
| } | |
| }); | |
| if (profileResponse.ok) { | |
| const profileData = await profileResponse.json(); | |
| profileImg.src = profileData.photoUrl || ''; | |
| modalProfileImg.src = profileData.photoUrl || ''; | |
| document.getElementById('profile-name-input').value = profileData.name || ''; | |
| } | |
| initChat(); | |
| loadFriends(); | |
| loadFriendRequests(); | |
| } else { | |
| window.location.href = '/login.html'; | |
| } | |
| }); | |
| async function initChat() { | |
| try { | |
| const messagesRef = db.ref('messages').limitToLast(100); | |
| const snapshot = await messagesRef.once('value'); | |
| messagesContainer.innerHTML = ''; | |
| snapshot.forEach((childSnapshot) => { | |
| const msg = childSnapshot.val(); | |
| const isCurrentUser = msg.senderId === currentUser.uid; | |
| if (msg.contentType === 'text') { | |
| appendMessage(msg.content, isCurrentUser, msg.senderName, msg.timestamp); | |
| } else if (msg.contentType === 'image') { | |
| appendImageMessage(msg.content, isCurrentUser, msg.senderName, msg.timestamp); | |
| } | |
| }); | |
| scrollToBottom(); | |
| setupMessageListener(); | |
| } catch (error) { | |
| console.error('Error initializing chat:', error); | |
| } | |
| } | |
| // Functions | |
| async function sendMessage(e) { | |
| e.preventDefault(); | |
| const message = messageInput.value.trim(); | |
| if (message && currentUser) { | |
| try { | |
| const newMessageRef = db.ref('messages').push(); | |
| await newMessageRef.set({ | |
| senderId: currentUser.uid, | |
| senderName: currentUser.displayName, | |
| content: message, | |
| contentType: 'text', | |
| timestamp: firebase.database.ServerValue.TIMESTAMP | |
| }); | |
| appendMessage(message, true, currentUser.displayName, Date.now()); | |
| messageInput.value = ''; | |
| scrollToBottom(); | |
| } catch (error) { | |
| console.error('Error sending message:', error); | |
| } | |
| } | |
| } | |
| // Listen for new messages | |
| function setupMessageListener() { | |
| db.ref('messages').limitToLast(100).on('child_added', (snapshot) => { | |
| const message = snapshot.val(); | |
| const isCurrentUser = message.senderId === currentUser.uid; | |
| if (message.contentType === 'text') { | |
| appendMessage(message.content, isCurrentUser, message.senderName, message.timestamp); | |
| } else if (message.contentType === 'image') { | |
| appendImageMessage(message.content, isCurrentUser, message.senderName, message.timestamp); | |
| } | |
| scrollToBottom(); | |
| }); | |
| } | |
| async function handleFileUpload(e) { | |
| const file = e.target.files[0]; | |
| if (file && currentUser) { | |
| if (file.type.startsWith('image/')) { | |
| try { | |
| const formData = new FormData(); | |
| formData.append('photo', file); | |
| formData.append('uid', currentUser.uid); | |
| const response = await fetch(`${API_BASE_URL}/upload-profile`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${authToken}` | |
| }, | |
| body: formData | |
| }); | |
| if (response.ok) { | |
| const result = await response.json(); | |
| appendImageMessage(result.photoUrl, true, currentUser.displayName, Date.now()); | |
| scrollToBottom(); | |
| } | |
| } catch (error) { | |
| console.error('Error uploading image:', error); | |
| } | |
| } else { | |
| alert('Please select an image file.'); | |
| } | |
| } | |
| fileInput.value = ''; | |
| } | |
| function appendMessage(text, isCurrentUser, senderName, timestamp) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `flex ${isCurrentUser ? 'justify-end' : 'justify-start'}`; | |
| const messageContent = document.createElement('div'); | |
| messageContent.className = `chat-message message-${isCurrentUser ? 'right' : 'left'} relative max-w-xs md:max-w-md ${isCurrentUser ? 'bg-indigo-600' : 'bg-purple-900'} rounded-2xl p-3 ${isCurrentUser ? 'ml-10' : 'mr-10'}`; | |
| if (!isCurrentUser) { | |
| const senderName = document.createElement('div'); | |
| senderName.className = 'font-semibold text-purple-300'; | |
| senderName.textContent = 'Friend'; | |
| messageContent.appendChild(senderName); | |
| } | |
| const messageText = document.createElement('p'); | |
| messageText.textContent = text; | |
| messageContent.appendChild(messageText); | |
| const timeStamp = document.createElement('span'); | |
| timeStamp.className = `text-xs block mt-1 ${isCurrentUser ? 'text-indigo-200 text-right' : 'text-purple-200'}`; | |
| const date = new Date(timestamp); | |
| timeStamp.textContent = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| messageContent.appendChild(timeStamp); | |
| messageDiv.appendChild(messageContent); | |
| messagesContainer.appendChild(messageDiv); | |
| } | |
| function appendImageMessage(imageUrl, isCurrentUser, senderName, timestamp) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = `flex ${isCurrentUser ? 'justify-end' : 'justify-start'}`; | |
| const messageContent = document.createElement('div'); | |
| messageContent.className = `chat-message message-${isCurrentUser ? 'right' : 'left'} relative max-w-xs md:max-w-md ${isCurrentUser ? 'bg-indigo-600' : 'bg-purple-900'} rounded-2xl p-3 ${isCurrentUser ? 'ml-10' : 'mr-10'}`; | |
| const imageContainer = document.createElement('div'); | |
| imageContainer.className = 'mb-2 rounded-lg overflow-hidden'; | |
| const image = document.createElement('img'); | |
| image.src = imageUrl; | |
| image.className = 'w-full h-auto'; | |
| imageContainer.appendChild(image); | |
| messageContent.appendChild(imageContainer); | |
| const timeStamp = document.createElement('span'); | |
| timeStamp.className = `text-xs block mt-1 ${isCurrentUser ? 'text-indigo-200 text-right' : 'text-purple-200'}`; | |
| const date = new Date(timestamp); | |
| timeStamp.textContent = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| messageContent.appendChild(timeStamp); | |
| messageDiv.appendChild(messageContent); | |
| messagesContainer.appendChild(messageDiv); | |
| } | |
| function scrollToBottom() { | |
| messagesContainer.scrollTop = messagesContainer.scrollHeight; | |
| } | |
| function openProfileModal() { | |
| profileModal.classList.remove('hidden'); | |
| } | |
| function closeModal() { | |
| profileModal.classList.add('hidden'); | |
| } | |
| function handleProfilePhotoChange(e) { | |
| const file = e.target.files[0]; | |
| if (file && file.type.startsWith('image/')) { | |
| const reader = new FileReader(); | |
| reader.onload = (event) => { | |
| modalProfileImg.src = event.target.result; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| } | |
| async function saveProfile() { | |
| if (!currentUser) return; | |
| try { | |
| // Upload new profile photo if changed | |
| if (profilePhotoInput.files[0]) { | |
| const formData = new FormData(); | |
| formData.append('photo', profilePhotoInput.files[0]); | |
| formData.append('uid', currentUser.uid); | |
| const uploadResponse = await fetch(`${API_BASE_URL}/upload-profile`, { | |
| method: 'POST', | |
| headers: { | |
| 'Authorization': `Bearer ${authToken}` | |
| }, | |
| body: formData | |
| }); | |
| if (uploadResponse.ok) { | |
| const result = await uploadResponse.json(); | |
| profileImg.src = result.photoUrl; | |
| modalProfileImg.src = result.photoUrl; | |
| // Update auth profile | |
| await auth.currentUser.updateProfile({ | |
| photoURL: result.photoUrl | |
| }); | |
| } | |
| } | |
| // Update profile name if changed | |
| const newName = document.getElementById('profile-name-input').value.trim(); | |
| if (newName && newName !== currentUser.displayName) { | |
| await db.ref(`users/${currentUser.uid}/name`).set(newName); | |
| await auth.currentUser.updateProfile({ | |
| displayName: newName | |
| }); | |
| } | |
| closeModal(); | |
| } catch (error) { | |
| console.error('Error saving profile:', error); | |
| } | |
| } | |
| async function loadFriends() { | |
| if (!currentUser) return; | |
| try { | |
| const friendsRef = db.ref(`friends/${currentUser.uid}`); | |
| const snapshot = await friendsRef.once('value'); | |
| const friends = snapshot.val() || {}; | |
| const friendsList = document.getElementById('friends-list'); | |
| friendsList.innerHTML = ''; | |
| for (const friendId in friends) { | |
| const userSnapshot = await db.ref(`users/${friendId}`).once('value'); | |
| const userData = userSnapshot.val(); | |
| const friendElement = document.createElement('div'); | |
| friendElement.className = 'flex items-center p-2 rounded hover:bg-purple-900 hover:bg-opacity-30 cursor-pointer'; | |
| friendElement.innerHTML = ` | |
| <div class="w-8 h-8 rounded-full bg-purple-600 mr-2 overflow-hidden"> | |
| <img src="${userData?.photoUrl || 'http://static.photos/people/200x200/1'}" class="w-full h-full object-cover"> | |
| </div> | |
| <span>${friends[friendId].name || 'Friend'}</span> | |
| `; | |
| friendsList.appendChild(friendElement); | |
| } | |
| } catch (error) { | |
| console.error('Error loading friends:', error); | |
| } | |
| } | |
| async function loadFriendRequests() { | |
| if (!currentUser) return; | |
| try { | |
| const requestsRef = db.ref(`friendRequests/${currentUser.uid}`); | |
| const snapshot = await requestsRef.once('value'); | |
| const requests = snapshot.val() || {}; | |
| const requestsList = document.getElementById('requests-list'); | |
| requestsList.innerHTML = ''; | |
| for (const requesterId in requests) { | |
| if (requests[requesterId].status === 'pending') { | |
| const userSnapshot = await db.ref(`users/${requesterId}`).once('value'); | |
| const userData = userSnapshot.val(); | |
| const requestElement = document.createElement('div'); | |
| requestElement.className = 'flex items-center justify-between p-2 rounded hover:bg-purple-900 hover:bg-opacity-30'; | |
| requestElement.innerHTML = ` | |
| <div class="flex items-center"> | |
| <div class="w-8 h-8 rounded-full bg-purple-600 mr-2 overflow-hidden"> | |
| <img src="${userData?.photoUrl || 'http://static.photos/people/200x200/2'}" class="w-full h-full object-cover"> | |
| </div> | |
| <span>${requests[requesterId].name || 'User'}</span> | |
| </div> | |
| <div class="flex space-x-1"> | |
| <button onclick="handleFriendRequest('accept', '${requesterId}')" class="p-1 rounded-full bg-green-600 hover:bg-green-700"> | |
| <i data-feather="check" class="w-3 h-3"></i> | |
| </button> | |
| <button onclick="handleFriendRequest('reject', '${requesterId}')" class="p-1 rounded-full bg-red-600 hover:bg-red-700"> | |
| <i data-feather="x" class="w-3 h-3"></i> | |
| </button> | |
| </div> | |
| `; | |
| requestsList.appendChild(requestElement); | |
| } | |
| } | |
| feather.replace(); | |
| } catch (error) { | |
| console.error('Error loading friend requests:', error); | |
| } | |
| } | |
| async function handleFriendRequest(action, requesterId) { | |
| try { | |
| if (action === 'accept') { | |
| // Get requester's name from friend requests | |
| const requestSnapshot = await db.ref(`friendRequests/${currentUser.uid}/${requesterId}`).once('value'); | |
| const requesterData = requestSnapshot.val(); | |
| // Add to each other's friends lists | |
| await db.ref(`friends/${currentUser.uid}/${requesterId}`).set({ | |
| name: requesterData.name | |
| }); | |
| // Get current user's name | |
| const userSnapshot = await db.ref(`users/${currentUser.uid}/name`).once('value'); | |
| const userName = userSnapshot.val() || 'Friend'; | |
| await db.ref(`friends/${requesterId}/${currentUser.uid}`).set({ | |
| name: userName | |
| }); | |
| } | |
| // Remove from friend requests in both cases (accept or reject) | |
| await db.ref(`friendRequests/${currentUser.uid}/${requesterId}`).remove(); | |
| loadFriendRequests(); | |
| loadFriends(); | |
| } catch (error) { | |
| console.error(`Error ${action}ing friend request:`, error); | |
| } | |
| } | |
| async function logoutUser() { | |
| try { | |
| await auth.signOut(); | |
| window.location.href = '/login.html'; | |
| } catch (error) { | |
| console.error('Error signing out:', error); | |
| } | |
| } | |
| // Initial setup | |
| scrollToBottom(); | |
| feather.replace(); | |
| // Initialize Firebase listeners | |
| auth.onAuthStateChanged(async user => { | |
| if (user) { | |
| currentUser = user; | |
| authToken = await user.getIdToken(); | |
| // Load initial data | |
| const userSnapshot = await db.ref(`users/${user.uid}`).once('value'); | |
| const userData = userSnapshot.val(); | |
| if (userData) { | |
| profileImg.src = userData.photoUrl || ''; | |
| modalProfileImg.src = userData.photoUrl || ''; | |
| document.getElementById('profile-name-input').value = userData.name || ''; | |
| } | |
| initChat(); | |
| loadFriends(); | |
| loadFriendRequests(); | |
| } else { | |
| window.location.href = '/login.html'; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> | |