Spaces:
Running
Running
Update cointube.jsx
Browse files- cointube.jsx +39 -4
cointube.jsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
| 13 |
onSnapshot,
|
| 14 |
setDoc,
|
| 15 |
updateDoc,
|
|
|
|
| 16 |
collection,
|
| 17 |
addDoc,
|
| 18 |
increment,
|
|
@@ -49,6 +50,7 @@ export default function App() {
|
|
| 49 |
const coinIntervalRef = useRef(null);
|
| 50 |
const localCoinBufferRef = useRef(0);
|
| 51 |
|
|
|
|
| 52 |
useEffect(() => {
|
| 53 |
const initFirebase = async () => {
|
| 54 |
try {
|
|
@@ -72,6 +74,7 @@ export default function App() {
|
|
| 72 |
initFirebase();
|
| 73 |
}, []);
|
| 74 |
|
|
|
|
| 75 |
useEffect(() => {
|
| 76 |
if (!firebaseReady || !auth) return;
|
| 77 |
const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
|
|
@@ -88,6 +91,7 @@ export default function App() {
|
|
| 88 |
return () => unsubscribeAuth();
|
| 89 |
}, [firebaseReady]);
|
| 90 |
|
|
|
|
| 91 |
useEffect(() => {
|
| 92 |
if (!firebaseReady || !userId || !db) return;
|
| 93 |
const profileRef = doc(db, 'users', userId);
|
|
@@ -107,9 +111,10 @@ export default function App() {
|
|
| 107 |
return () => unsubscribeProfile();
|
| 108 |
}, [userId, user, firebaseReady]);
|
| 109 |
|
|
|
|
| 110 |
useEffect(() => {
|
| 111 |
if (!firebaseReady || !db) return;
|
| 112 |
-
const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(
|
| 113 |
const unsubscribe = onSnapshot(q, (snapshot) => {
|
| 114 |
const fetchedVideos = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
| 115 |
if (fetchedVideos.length > 0) {
|
|
@@ -121,6 +126,7 @@ export default function App() {
|
|
| 121 |
return () => unsubscribe();
|
| 122 |
}, [firebaseReady]);
|
| 123 |
|
|
|
|
| 124 |
useEffect(() => {
|
| 125 |
if (!firebaseReady || !db) return;
|
| 126 |
const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(20));
|
|
@@ -131,6 +137,7 @@ export default function App() {
|
|
| 131 |
return () => unsubscribe();
|
| 132 |
}, [firebaseReady]);
|
| 133 |
|
|
|
|
| 134 |
useEffect(() => {
|
| 135 |
if (!window.YT) {
|
| 136 |
const tag = document.createElement('script');
|
|
@@ -143,6 +150,7 @@ export default function App() {
|
|
| 143 |
}
|
| 144 |
}, []);
|
| 145 |
|
|
|
|
| 146 |
const saveCoinBuffer = useCallback(() => {
|
| 147 |
if (localCoinBufferRef.current > 0 && userId && db) {
|
| 148 |
const coinsToSave = localCoinBufferRef.current;
|
|
@@ -210,6 +218,19 @@ export default function App() {
|
|
| 210 |
if (playerRef.current) playerRef.current.loadVideoById(video.videoId);
|
| 211 |
};
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
const handleGoogleLogin = async () => {
|
| 214 |
if (!auth) return;
|
| 215 |
const provider = new GoogleAuthProvider();
|
|
@@ -312,19 +333,33 @@ export default function App() {
|
|
| 312 |
<h3 className="text-gray-400 text-sm uppercase font-bold mb-2">Lista de Reproducción</h3>
|
| 313 |
<div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
|
| 314 |
{videos.map((video) => (
|
| 315 |
-
<button key={video.id} onClick={() => loadVideo(video)} className={`w-full text-left p-2 rounded ${video.videoId === currentVideoId ? 'bg-blue-900 border-l-2 border-blue-500' : 'bg-gray-700 hover:bg-gray-600'}`}>
|
| 316 |
-
<div className="text-sm text-white truncate">{video.title}</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
</button>
|
| 318 |
))}
|
| 319 |
</div>
|
| 320 |
</div>
|
|
|
|
|
|
|
| 321 |
<div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
|
| 322 |
<h3 className="text-yellow-500 text-sm uppercase font-bold mb-2">Ranking Global</h3>
|
| 323 |
<div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
|
| 324 |
{leaderboard.map((u, i) => (
|
| 325 |
<div key={u.id || i} className="flex justify-between p-2 bg-gray-700 rounded text-sm">
|
|
|
|
| 326 |
<span className="text-white text-xs md:text-sm truncate mr-2" title={u.email}>
|
| 327 |
-
#{i+1} {u.email}
|
| 328 |
</span>
|
| 329 |
<span className="text-yellow-400 font-mono whitespace-nowrap">{Math.floor(u.coins)}</span>
|
| 330 |
</div>
|
|
|
|
| 13 |
onSnapshot,
|
| 14 |
setDoc,
|
| 15 |
updateDoc,
|
| 16 |
+
deleteDoc, // <--- IMPORTANTE: Agregado para borrar
|
| 17 |
collection,
|
| 18 |
addDoc,
|
| 19 |
increment,
|
|
|
|
| 50 |
const coinIntervalRef = useRef(null);
|
| 51 |
const localCoinBufferRef = useRef(0);
|
| 52 |
|
| 53 |
+
// --- 1. INIT ---
|
| 54 |
useEffect(() => {
|
| 55 |
const initFirebase = async () => {
|
| 56 |
try {
|
|
|
|
| 74 |
initFirebase();
|
| 75 |
}, []);
|
| 76 |
|
| 77 |
+
// --- 2. AUTH ---
|
| 78 |
useEffect(() => {
|
| 79 |
if (!firebaseReady || !auth) return;
|
| 80 |
const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
|
|
|
|
| 91 |
return () => unsubscribeAuth();
|
| 92 |
}, [firebaseReady]);
|
| 93 |
|
| 94 |
+
// --- 3. PROFILE ---
|
| 95 |
useEffect(() => {
|
| 96 |
if (!firebaseReady || !userId || !db) return;
|
| 97 |
const profileRef = doc(db, 'users', userId);
|
|
|
|
| 111 |
return () => unsubscribeProfile();
|
| 112 |
}, [userId, user, firebaseReady]);
|
| 113 |
|
| 114 |
+
// --- 4. VIDEOS ---
|
| 115 |
useEffect(() => {
|
| 116 |
if (!firebaseReady || !db) return;
|
| 117 |
+
const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(50));
|
| 118 |
const unsubscribe = onSnapshot(q, (snapshot) => {
|
| 119 |
const fetchedVideos = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
| 120 |
if (fetchedVideos.length > 0) {
|
|
|
|
| 126 |
return () => unsubscribe();
|
| 127 |
}, [firebaseReady]);
|
| 128 |
|
| 129 |
+
// --- 5. RANKING ---
|
| 130 |
useEffect(() => {
|
| 131 |
if (!firebaseReady || !db) return;
|
| 132 |
const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(20));
|
|
|
|
| 137 |
return () => unsubscribe();
|
| 138 |
}, [firebaseReady]);
|
| 139 |
|
| 140 |
+
// --- 6. YOUTUBE API ---
|
| 141 |
useEffect(() => {
|
| 142 |
if (!window.YT) {
|
| 143 |
const tag = document.createElement('script');
|
|
|
|
| 150 |
}
|
| 151 |
}, []);
|
| 152 |
|
| 153 |
+
// --- LOGICA DE MONEDAS ---
|
| 154 |
const saveCoinBuffer = useCallback(() => {
|
| 155 |
if (localCoinBufferRef.current > 0 && userId && db) {
|
| 156 |
const coinsToSave = localCoinBufferRef.current;
|
|
|
|
| 218 |
if (playerRef.current) playerRef.current.loadVideoById(video.videoId);
|
| 219 |
};
|
| 220 |
|
| 221 |
+
// --- FUNCION NUEVA: BORRAR VIDEO (SOLO ADMIN) ---
|
| 222 |
+
const handleDeleteVideo = async (e, videoId) => {
|
| 223 |
+
e.stopPropagation(); // Evita que al borrar se reproduzca el video
|
| 224 |
+
if (!confirm("¿Estás seguro de que quieres ELIMINAR este video?")) return;
|
| 225 |
+
|
| 226 |
+
try {
|
| 227 |
+
await deleteDoc(doc(db, "videos", videoId));
|
| 228 |
+
} catch (error) {
|
| 229 |
+
console.error("Error borrando:", error);
|
| 230 |
+
alert("No tienes permisos para borrar este video.");
|
| 231 |
+
}
|
| 232 |
+
};
|
| 233 |
+
|
| 234 |
const handleGoogleLogin = async () => {
|
| 235 |
if (!auth) return;
|
| 236 |
const provider = new GoogleAuthProvider();
|
|
|
|
| 333 |
<h3 className="text-gray-400 text-sm uppercase font-bold mb-2">Lista de Reproducción</h3>
|
| 334 |
<div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
|
| 335 |
{videos.map((video) => (
|
| 336 |
+
<button key={video.id} onClick={() => loadVideo(video)} className={`w-full text-left p-2 rounded flex justify-between items-center group ${video.videoId === currentVideoId ? 'bg-blue-900 border-l-2 border-blue-500' : 'bg-gray-700 hover:bg-gray-600'}`}>
|
| 337 |
+
<div className="text-sm text-white truncate flex-1">{video.title}</div>
|
| 338 |
+
|
| 339 |
+
{/* BOTÓN DE BORRAR (SOLO ADMIN) */}
|
| 340 |
+
{user?.email === ADMIN_EMAIL && (
|
| 341 |
+
<span
|
| 342 |
+
onClick={(e) => handleDeleteVideo(e, video.id)}
|
| 343 |
+
className="ml-2 text-red-500 hover:text-red-300 font-bold px-2 py-1 rounded bg-red-900/50 hover:bg-red-900"
|
| 344 |
+
title="Borrar Video"
|
| 345 |
+
>
|
| 346 |
+
X
|
| 347 |
+
</span>
|
| 348 |
+
)}
|
| 349 |
</button>
|
| 350 |
))}
|
| 351 |
</div>
|
| 352 |
</div>
|
| 353 |
+
|
| 354 |
+
{/* RANKING CORREGIDO */}
|
| 355 |
<div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
|
| 356 |
<h3 className="text-yellow-500 text-sm uppercase font-bold mb-2">Ranking Global</h3>
|
| 357 |
<div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
|
| 358 |
{leaderboard.map((u, i) => (
|
| 359 |
<div key={u.id || i} className="flex justify-between p-2 bg-gray-700 rounded text-sm">
|
| 360 |
+
{/* Extrae el nombre antes del @ */}
|
| 361 |
<span className="text-white text-xs md:text-sm truncate mr-2" title={u.email}>
|
| 362 |
+
#{i+1} {u.email ? u.email.split('@')[0] : 'Usuario'}
|
| 363 |
</span>
|
| 364 |
<span className="text-yellow-400 font-mono whitespace-nowrap">{Math.floor(u.coins)}</span>
|
| 365 |
</div>
|