import React, { useState, useEffect, useRef } from 'react'; import { initializeApp } from 'firebase/app'; import { getFirestore, collection, addDoc, onSnapshot, query, doc, updateDoc, arrayUnion, getDoc, setDoc, increment, deleteDoc, where } from 'firebase/firestore'; import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth'; import { Camera, Heart, MessageSquare, MapPin, User, Globe, Plus, X, Send, Image as ImageIcon, Sparkles, Zap, AlertTriangle, Loader2, Info, ShieldCheck } from 'lucide-react'; // --- Firebase Configuration --- const firebaseConfig = JSON.parse(__firebase_config); const app = initializeApp(firebaseConfig); const auth = getAuth(app); const db = getFirestore(app); const appId = typeof __app_id !== 'undefined' ? __app_id : 'urban-canvas-prod'; // --- Helper: Client-Side Image Compression --- // Prevents 1MB Firestore limit issues by shrinking images before upload const compressImage = (base64Str, maxWidth = 800, maxHeight = 800) => { return new Promise((resolve) => { const img = new Image(); img.src = base64Str; img.onload = () => { const canvas = document.createElement('canvas'); let width = img.width; let height = img.height; if (width > height) { if (width > maxWidth) { height *= maxWidth / width; width = maxWidth; } } else { if (height > maxHeight) { width *= maxHeight / height; height = maxHeight; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); // Return highly compressed JPEG to ensure we stay well under 1MB resolve(canvas.toDataURL('image/jpeg', 0.6)); }; }); }; export default function App() { const [user, setUser] = useState(null); const [userData, setUserData] = useState({ name: "Ghost Artist", bio: "", joined: Date.now(), rep: 0 }); const [activeTab, setActiveTab] = useState('feed'); const [posts, setPosts] = useState([]); const [network, setNetwork] = useState([]); const [messages, setMessages] = useState([]); const [requests, setRequests] = useState([]); const [selectedPost, setSelectedPost] = useState(null); const [showUpload, setShowUpload] = useState(false); const [showEditProfile, setShowEditProfile] = useState(false); const [showOnboarding, setShowOnboarding] = useState(false); const [filter, setFilter] = useState('all'); const [viewingPortfolio, setViewingPortfolio] = useState(null); const [uploadPreview, setUploadPreview] = useState(null); const [isUploading, setIsUploading] = useState(false); const [statusMsg, setStatusMsg] = useState(null); const chatEndRef = useRef(null); // --- Auth & Initialization --- useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { await signInWithCustomToken(auth, __initial_auth_token); } else { await signInAnonymously(auth); } } catch (err) { notify("Grid connection offline."); } }; initAuth(); return onAuthStateChanged(auth, setUser); }, []); // --- Utility: Feedback Notifications --- const notify = (text) => { setStatusMsg(text); setTimeout(() => setStatusMsg(null), 3500); }; // --- Real-time Data Listeners --- useEffect(() => { if (!user) return; // 1. Profile Sync const profileRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); const unsubProfile = onSnapshot(profileRef, (docSnap) => { if (docSnap.exists()) { const data = docSnap.data(); setUserData(data); if (data.name === "Ghost Artist") setShowOnboarding(true); } else { // Initial setup for new users setDoc(profileRef, { ...userData, id: user.uid }); setShowOnboarding(true); } }, (err) => console.error("Profile error:", err)); // 2. Global Feed (with automatic moderation filtering) const artCol = collection(db, 'artifacts', appId, 'public', 'data', 'street_art'); const unsubArt = onSnapshot(artCol, (snapshot) => { const docs = snapshot.docs .map(d => ({ id: d.id, ...d.data() })) .filter(d => (d.reports || 0) < 5); // Automatically hide problematic content setPosts(docs.sort((a, b) => b.timestamp - a.timestamp)); const uniqueIds = [...new Set(docs.map(p => p.userId))]; updateNetwork(uniqueIds); }, (err) => console.error("Feed error:", err)); // 3. Encrypted Comms (Community Chat) const chatCol = collection(db, 'artifacts', appId, 'public', 'data', 'community_chat'); const unsubChat = onSnapshot(chatCol, (snapshot) => { const msgs = snapshot.docs.map(d => d.data()).sort((a, b) => a.time - b.time); setMessages(msgs.slice(-60)); }); // 4. Bounties (Wall Requests) const reqCol = collection(db, 'artifacts', appId, 'public', 'data', 'wall_requests'); const unsubReq = onSnapshot(reqCol, (snapshot) => { setRequests(snapshot.docs.map(d => ({ id: d.id, ...d.data() }))); }); return () => { unsubProfile(); unsubArt(); unsubChat(); unsubReq(); }; }, [user]); // Auto-scroll for chat useEffect(() => { if (activeTab === 'network') chatEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages, activeTab]); const updateNetwork = async (ids) => { const networkData = []; for (const id of ids.slice(0, 12)) { const pDoc = await getDoc(doc(db, 'artifacts', appId, 'users', id, 'profile', 'data')); if (pDoc.exists()) networkData.push({ id, ...pDoc.data() }); } setNetwork(networkData); }; // --- Interaction Logic --- const handleLike = async (postId) => { if (!user) return; try { const artRef = doc(db, 'artifacts', appId, 'public', 'data', 'street_art', postId); const postSnap = await getDoc(artRef); if (postSnap.exists()) { const data = postSnap.data(); if (!data.likes?.includes(user.uid)) { // Update post likes await updateDoc(artRef, { likes: arrayUnion(user.uid) }); // Increment artist reputation const creatorRef = doc(db, 'artifacts', appId, 'users', data.userId, 'profile', 'data'); await updateDoc(creatorRef, { rep: increment(1) }); notify("Reputation granted."); } } } catch (e) { notify("Uplink error."); } }; const handleReport = async (postId) => { if (!user) return; try { const artRef = doc(db, 'artifacts', appId, 'public', 'data', 'street_art', postId); await updateDoc(artRef, { reports: increment(1) }); setSelectedPost(null); notify("Flagged for review."); } catch (e) { notify("Report failed."); } }; const handleUpload = async (e) => { e.preventDefault(); if (!uploadPreview || isUploading || !user) return; setIsUploading(true); const form = e.target; try { const compressed = await compressImage(uploadPreview); await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'street_art'), { title: form.artTitle.value || "Untitled", zip: form.artZip.value || "Ghost", area: form.artArea.value || "Unknown", img: compressed, userId: user.uid, userName: userData.name, likes: [], reports: 0, timestamp: Date.now() }); setShowUpload(false); setUploadPreview(null); notify("Mark successful."); } catch (err) { notify("Buffer overflow. File too large."); } finally { setIsUploading(false); } }; const onFileChange = (e) => { const file = e.target.files[0]; if (file) { if (file.size > 8000000) { notify("Max 8MB allowed."); return; } const reader = new FileReader(); reader.onloadend = () => setUploadPreview(reader.result); reader.readAsDataURL(file); } }; const sendMessage = async (e) => { e.preventDefault(); const text = e.target.msg.value.trim(); if (!text || !user) return; e.target.msg.value = ''; await addDoc(collection(db, 'artifacts', appId, 'public', 'data', 'community_chat'), { text, userId: user.uid, userName: userData.name, time: Date.now() }); }; const saveProfile = async (e) => { e.preventDefault(); const name = e.target.uName.value.trim(); const bio = e.target.uBio.value.trim(); if (!name) return notify("Handle required."); const profileRef = doc(db, 'artifacts', appId, 'users', user.uid, 'profile', 'data'); await setDoc(profileRef, { ...userData, name, bio }, { merge: true }); setShowOnboarding(false); setShowEditProfile(false); notify("Intel updated."); }; const filteredPosts = posts.filter(p => { if (viewingPortfolio) return p.userId === viewingPortfolio.id; if (filter === 'mine') return p.userId === user?.uid; return true; }); return (
{/* Dynamic Notifications */} {statusMsg && (
{statusMsg}
)} {/* Header */}
{activeTab === 'feed' && (
{/* Live Artist Network */}
Active Signals LIVE
{network.map(artist => (
{ setViewingPortfolio(artist); setFilter('all'); }} className="flex-shrink-0 text-center w-16 cursor-pointer group">
{artist.name}
))}
{/* Filter Pills */}
{['all', 'mine'].map(f => ( ))} {viewingPortfolio && ( )}
{/* Post Feed */} {filteredPosts.length > 0 ? filteredPosts.map(post => (
setSelectedPost(post)}>
{post.userName}
{new Date(post.timestamp).toDateString()}
{post.zip}

{post.title}

{post.area} District

{post.likes?.length || 0}
)) : (
Grid Silence Detected
)}
)} {activeTab === 'network' && (
Encrypted Uplink
{messages.length === 0 && (
Awaiting Transmission...
)} {messages.map((m, i) => (
{m.userName}
{m.text}
))}
)} {activeTab === 'requests' && (

Bounty Board

Verified contracts for legal wall space.

{requests.map(req => (
{req.location}
${req.budget}
Allocation

"{req.desc}"

))}
)} {activeTab === 'profile' && (

{userData.name}

Master Vandal

"{userData.bio || "Signal strength optimal. Operating in the shadows."}"

{posts.filter(p => p.userId === user?.uid).length}
Marks
{userData.rep || 0}
Rep
L1
Rank
)}
{/* Navigation Dock */} {/* MODALS */} {/* Upload Portal */} {showUpload && (

Deploy Mark

{uploadPreview ? (
) : ( )}
)} {/* Detailed Post Inspection */} {selectedPost && (
{selectedPost.zip}

{selectedPost.title}

Linked: {selectedPost.userName}
Metadata Storage
Captured:
{new Date(selectedPost.timestamp).toLocaleString()}
Location:
{selectedPost.area} District
Status:
Verified Mark
)} {/* First-Time User Setup */} {showOnboarding && (
U

Entry Point

Broadcast your handle to the grid. Remain ghost or become a legend.