say it here
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Public Message Board</title>
<!-- Load Tailwind CSS from CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Use Inter font family -->
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap');
body {
font-family: 'Inter', sans-serif;
background-color: #0d1117; /* Dark background for a calm feel */
color: #c9d1d9; /* Light text */
min-height: 100vh;
}
.note-card {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.06);
}
</style>
<!-- Firebase SDK Imports -->
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-app.js";
import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-auth.js";
import { getFirestore, collection, addDoc, onSnapshot, query, serverTimestamp, setLogLevel } from "https://www.gstatic.com/firebasejs/11.6.1/firebase-firestore.js";
// IMPORTANT: Global variables provided by the Canvas environment
const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id';
const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : null;
const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null;
let db;
let auth;
let userId = null;
let notesCollectionRef = null;
// Utility function to safely parse a date and format it
function formatTimestamp(timestamp) {
if (timestamp && timestamp.toDate) {
return timestamp.toDate().toLocaleString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
return 'Unknown Time';
}
// --- 1. FIREBASE INITIALIZATION AND AUTHENTICATION ---
async function initializeFirebase() {
if (!firebaseConfig) {
console.error("Firebase configuration is missing.");
return;
}
setLogLevel('debug'); // Enable debug logging for Firestore
const app = initializeApp(firebaseConfig);
db = getFirestore(app);
auth = getAuth(app);
// Authentication setup
try {
if (initialAuthToken) {
await signInWithCustomToken(auth, initialAuthToken);
} else {
await signInAnonymously(auth);
}
} catch (error) {
console.error("Firebase Auth Error:", error);
}
// Listen for auth state changes to get the user ID
onAuthStateChanged(auth, (user) => {
if (user) {
userId = user.uid;
} else {
// Fallback to a random ID if anonymous sign-in failed (shouldn't happen)
userId = crypto.randomUUID();
}
// Initialize Firestore references after getting the userId
// Public data path: /artifacts/{appId}/public/data/notes
notesCollectionRef = collection(db, 'artifacts', appId, 'public', 'data', 'notes');
// Start listeners once authenticated and references are set
setupNoteListener();
document.getElementById('userIdDisplay').textContent = userId;
document.getElementById('postNoteForm').classList.remove('hidden');
document.getElementById('loadingIndicator').classList.add('hidden');
});
}
// --- 2. REAL-TIME NOTE LISTENER ---
function setupNoteListener() {
if (!notesCollectionRef) return;
const q = query(notesCollectionRef); // Fetching all notes in the public collection
onSnapshot(q, (snapshot) => {
const notesContainer = document.getElementById('notesContainer');
const noteDocs = [];
snapshot.forEach((doc) => {
noteDocs.push({ id: doc.id, ...doc.data() });
});
// IMPORTANT: Sort in memory since Firestore orderBy() is discouraged.
// Sort by timestamp descending (newest first)
noteDocs.sort((a, b) => {
const timeA = a.timestamp ? a.timestamp.toMillis() : 0;
const timeB = b.timestamp ? b.timestamp.toMillis() : 0;
return timeB - timeA;
});
// Clear and render new list
notesContainer.innerHTML = '';
if (noteDocs.length === 0) {
notesContainer.innerHTML = '<p class="text-center text-gray-500 py-8">Be the first to leave a message.</p>';
} else {
noteDocs.forEach(note => {
const noteCard = document.createElement('div');
noteCard.className = 'note-card p-4 bg-gray-800 rounded-lg border border-gray-700 hover:border-blue-500 transition-colors';
// Sanitize and display message content
const messageText = note.message ? note.message.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') : 'Empty Message';
noteCard.innerHTML = `
<p class="text-lg mb-2 whitespace-pre-wrap">${messageText}</p>
<p class="text-xs text-gray-500 mt-3 border-t border-gray-700 pt-2">
Posted on ${formatTimestamp(note.timestamp)}
</p>
`;
notesContainer.appendChild(noteCard);
});
}
}, (error) => {
console.error("Firestore Snapshot Error:", error);
document.getElementById('notesContainer').innerHTML = '<p class="text-center text-red-500 py-8">Error loading messages. Please check the console.</p>';
});
}
// --- 3. NOTE SUBMISSION ---
async function postNote(message) {
if (!notesCollectionRef || !userId) {
console.error("Firestore not initialized or user not authenticated.");
return;
}
try {
await addDoc(notesCollectionRef, {
message: message,
timestamp: serverTimestamp(), // Use server timestamp for accurate ordering
authorId: userId,
});
} catch (e) {
console.error("Error adding document: ", e);
// Display error message to user
showToast("Failed to post message. Please try again.", 'error');
}
}
// --- 4. UI INTERACTION ---
document.addEventListener('DOMContentLoaded', () => {
const form = document.getElementById('postNoteForm');
const messageInput = document.getElementById('messageInput');
const submitButton = form.querySelector('button[type="submit"]');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const message = messageInput.value.trim();
if (message.length === 0) {
showToast("Your message can't be empty.", 'warning');
return;
}
// Disable button and show loading state
submitButton.disabled = true;
submitButton.innerHTML = `<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg> Posting...`;
await postNote(message);
// Re-enable button and clear input
submitButton.disabled = false;
submitButton.innerHTML = 'Post Message';
messageInput.value = '';
showToast("Message posted!", 'success');
});
// Start Firebase initialization
initializeFirebase();
});
// Simple toast notification function (instead of alert)
function showToast(message, type = 'info') {
const toastContainer = document.getElementById('toastContainer');
const toast = document.createElement('div');
let baseClasses = 'fixed top-4 right-4 z-50 p-3 rounded-lg shadow-xl text-white transition-opacity duration-300';
let colorClass = '';
switch(type) {
case 'success':
colorClass = 'bg-green-600';
break;
case 'error':
colorClass = 'bg-red-600';
break;
case 'warning':
colorClass = 'bg-yellow-600';
break;
default:
colorClass = 'bg-blue-600';
}
toast.className = `${baseClasses} ${colorClass}`;
toast.textContent = message;
toastContainer.appendChild(toast);
// Auto-hide the toast
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
</script>
</head>
<body class="p-4 md:p-8">
<!-- Toast Container -->
<div id="toastContainer"></div>
<div class="max-w-2xl mx-auto">
<header class="text-center mb-10 p-6 bg-gray-900 rounded-xl shadow-2xl border-b border-blue-600">
<h1 class="text-4xl font-extrabold text-white mb-2">The Public Canvas</h1>
<p class="text-blue-400">Leave a note for anyone. See what others have shared.</p>
<p class="text-xs mt-3 text-gray-600">Your User ID: <span id="userIdDisplay" class="font-mono text-gray-500">Loading...</span></p>
</header>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="text-center py-10">
<svg class="animate-spin mx-auto h-8 w-8 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<p class="mt-2 text-gray-400">Connecting to the board...</p>
</div>
<!-- Note Submission Form -->
<form id="postNoteForm" class="hidden p-6 mb-10 bg-gray-800 rounded-xl border border-gray-700 shadow-xl">
<label for="messageInput" class="block text-sm font-medium mb-2 text-gray-300">Your Message (Visible to All):</label>
<textarea
id="messageInput"
rows="4"
placeholder="Type your message here..."
class="w-full p-3 mb-4 bg-gray-900 border border-gray-600 rounded-lg focus:ring-blue-500 focus:border-blue-500 text-white resize-none"
required
></textarea>
<button
type="submit"
class="w-full flex justify-center items-center py-3 px-4 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-offset-gray-800 transition duration-150 ease-in-out"
>
Post Message
</button>
</form>
<!-- Notes Display Section -->
<section>
<h2 class="text-2xl font-semibold mb-6 text-gray-200 border-b border-gray-700 pb-2">Recent Messages</h2>
<div id="notesContainer" class="space-y-4">
<!-- Notes will be dynamically inserted here by JavaScript -->
</div>
</section>
</div>
</body>
</html>
Comments
Post a Comment