dimensionalpulsar commited on
Commit
015b4e8
·
verified ·
1 Parent(s): f859c86

Update cointube.jsx

Browse files
Files changed (1) hide show
  1. cointube.jsx +339 -156
cointube.jsx CHANGED
@@ -4,8 +4,8 @@ import {
4
  getAuth,
5
  onAuthStateChanged,
6
  GoogleAuthProvider,
7
- signInWithRedirect,
8
- getRedirectResult,
9
  signOut
10
  } from 'firebase/auth';
11
  import {
@@ -22,8 +22,8 @@ import {
22
  query,
23
  orderBy,
24
  limit,
25
- writeBatch,
26
- getDocs
27
  } from 'firebase/firestore';
28
 
29
  const firebaseConfig = {
@@ -44,11 +44,13 @@ const VIDEO_COST = 10000;
44
  const DEFAULT_VIDEO_ID = "1rghd2zPhFo";
45
  const DEFAULT_VIDEO_TITLE = "Cuentos de Terror";
46
 
 
47
  const EMAILJS_SERVICE_ID = "service_v18p188";
48
  const EMAILJS_TEMPLATE_ID = "template_wexg1ww";
49
  const EMAILJS_PUBLIC_KEY = "XK6EoFoAqInBoDjIe";
50
 
51
  export default function App() {
 
52
  const [videos, setVideos] = useState([]);
53
  const [leaderboard, setLeaderboard] = useState([]);
54
  const [user, setUser] = useState(null);
@@ -56,8 +58,10 @@ export default function App() {
56
  const [authReady, setAuthReady] = useState(false);
57
  const [userProfile, setUserProfile] = useState(null);
58
  const [currentTab, setCurrentTab] = useState('ganar');
 
59
  const [currentVideoId, setCurrentVideoId] = useState(null);
60
  const [currentVideoTitle, setCurrentVideoTitle] = useState(null);
 
61
  const [ytApiReady, setYtApiReady] = useState(false);
62
 
63
  const playerRef = useRef(null);
@@ -65,21 +69,14 @@ export default function App() {
65
  const localCoinBufferRef = useRef(0);
66
  const initialLoadRef = useRef(true);
67
 
68
- // FIX: Manejo de autenticación robusto para Hugging Face
69
  useEffect(() => {
70
- const checkRedirect = async () => {
71
- try {
72
- await getRedirectResult(auth);
73
- } catch (error) {
74
- console.error("Error en redirección:", error);
75
- }
76
- };
77
- checkRedirect();
78
 
79
- const unsubscribeAuth = onAuthStateChanged(auth, (currentUser) => {
80
- if (currentUser) {
81
- setUser(currentUser);
82
- setUserId(currentUser.uid);
83
  } else {
84
  setUser(null);
85
  setUserId(null);
@@ -91,7 +88,7 @@ export default function App() {
91
  }, []);
92
 
93
  useEffect(() => {
94
- if (!userId || !user) return;
95
  const profileRef = doc(db, 'users', userId);
96
  const unsubscribeProfile = onSnapshot(profileRef, (docSnap) => {
97
  if (docSnap.exists()) {
@@ -105,56 +102,56 @@ export default function App() {
105
  setDoc(profileRef, newProfile).catch(e => console.error(e));
106
  setUserProfile(newProfile);
107
  }
108
- });
109
  return () => unsubscribeProfile();
110
  }, [userId, user]);
111
 
112
- // LÓGICA DE RESETEO AUTOMÁTICO (ADMIN)
113
  useEffect(() => {
114
  if (user?.email !== ADMIN_EMAIL) return;
 
115
  const checkReset = async () => {
116
- try {
117
- const settingsRef = doc(db, 'settings', 'stats');
118
- const settingsSnap = await getDocs(query(collection(db, 'settings')));
119
- if (settingsSnap.empty) return;
120
-
121
- const statsData = settingsSnap.docs[0].data();
122
- const lastResetDate = statsData.lastReset.toDate();
123
- const now = new Date();
124
-
125
- if (now - lastResetDate >= 432000000) {
126
- const batch = writeBatch(db);
127
- const allUsers = await getDocs(collection(db, 'users'));
128
-
129
- allUsers.forEach((uDoc) => {
130
- if (uDoc.data().email !== ADMIN_EMAIL) {
131
- batch.update(uDoc.ref, { coins: 0 });
132
- }
133
- });
134
-
135
- batch.update(settingsRef, { lastReset: serverTimestamp() });
136
- await batch.commit();
137
- alert("CICLO COMPLETADO: Las monedas de todos los usuarios han sido reseteadas.");
138
- }
139
- } catch (err) { console.error("Error Reset:", err); }
140
  };
141
  checkReset();
142
  }, [user]);
143
 
144
- // LISTA DE VIDEOS CON SHUFFLE (ALEATORIO)
145
  useEffect(() => {
146
  const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(50));
147
  const unsubscribe = onSnapshot(q, (snapshot) => {
148
  let fetchedVideos = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
149
 
150
  if (fetchedVideos.length > 0) {
151
- // Mezclar videos aleatoriamente
152
  fetchedVideos = fetchedVideos.sort(() => Math.random() - 0.5);
 
153
  setVideos(fetchedVideos);
154
  if (initialLoadRef.current || !currentVideoId) {
155
- const firstVideo = fetchedVideos[0];
156
- setCurrentVideoId(firstVideo.videoId);
157
- setCurrentVideoTitle(firstVideo.title);
158
  initialLoadRef.current = false;
159
  }
160
  } else {
@@ -170,11 +167,16 @@ export default function App() {
170
  }, [currentVideoId]);
171
 
172
  useEffect(() => {
173
- const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(20));
174
  const unsubscribe = onSnapshot(q, (snapshot) => {
175
  let usersList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
176
- usersList = usersList.filter(u => u.email && u.email.endsWith('@gmail.com'));
177
- setLeaderboard(usersList);
 
 
 
 
 
178
  });
179
  return () => unsubscribe();
180
  }, []);
@@ -226,7 +228,14 @@ export default function App() {
226
  const state = event.data;
227
  if (state === window.YT.PlayerState.PLAYING) startCoinInterval();
228
  else stopCoinInterval();
229
- }, [startCoinInterval, stopCoinInterval]);
 
 
 
 
 
 
 
230
 
231
  useEffect(() => {
232
  if (ytApiReady && userId && !playerRef.current && currentVideoId) {
@@ -238,6 +247,10 @@ export default function App() {
238
  events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange }
239
  });
240
  }
 
 
 
 
241
  }, [ytApiReady, userId, currentVideoId, onPlayerReady, onPlayerStateChange]);
242
 
243
  const loadVideo = (video) => {
@@ -247,142 +260,233 @@ export default function App() {
247
  if (playerRef.current) playerRef.current.loadVideoById(video.videoId);
248
  };
249
 
250
- const handleDeleteVideo = async (e, vId) => {
251
  e.stopPropagation();
252
  if (!confirm("¿Eliminar video?")) return;
253
- try { await deleteDoc(doc(db, "videos", vId)); } catch (err) { console.error(err); }
254
  };
255
 
256
- const handleGiveCoins = async (tId, cC) => {
257
- const amtStr = prompt("Cantidad de monedas a sumar/restar:", "1000");
258
- if (!amtStr) return;
259
- const amt = parseInt(amtStr);
260
- if (!isNaN(amt)) {
261
- await updateDoc(doc(db, 'users', tId), { coins: increment(amt) });
 
 
 
 
 
 
 
262
  }
263
  };
264
 
265
- // FIX: Cambio de Popup a Redirect para Hugging Face
266
- const handleGoogleLogin = () => {
267
- const provider = new GoogleAuthProvider();
268
- signInWithRedirect(auth, provider);
269
- };
270
 
271
- const handleLogout = async () => {
272
- stopCoinInterval();
273
- await signOut(auth);
274
- };
275
 
276
- const sendMassNotification = async () => {
277
- if (!window.emailjs) return alert("EmailJS no cargado.");
278
- if (!confirm("¿Enviar correos de reseteo a todos?")) return;
279
-
280
- const snap = await getDocs(collection(db, 'users'));
281
- snap.forEach((uDoc) => {
282
- const d = uDoc.data();
283
- if (d.email) {
284
  window.emailjs.send(EMAILJS_SERVICE_ID, EMAILJS_TEMPLATE_ID, {
285
- to_email: d.email,
286
- message: "¡Atención! Los puntos se han reseteado para todos los usuarios. ¡Entra ya para ganar más!"
287
  }, EMAILJS_PUBLIC_KEY);
 
288
  }
289
  });
290
- alert("Proceso de envío iniciado.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  };
292
 
293
  if (!authReady) {
294
  return (
295
- <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900">
296
- <div className="animate-spin rounded-full h-10 w-10 border-b-2 border-blue-500 mb-4"></div>
297
- <p className="text-gray-400">Cargando CoinTube...</p>
298
  </div>
299
  );
300
  }
301
 
302
- if (!user) {
303
  return (
304
  <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
305
- <div className="max-w-md w-full bg-gray-800 rounded-3xl shadow-2xl p-10 text-center border border-gray-700">
306
  <div className="flex justify-center mb-6">
307
- <div className="bg-blue-600 p-4 rounded-full">
308
- <svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
309
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
 
310
  </svg>
311
  </div>
312
  </div>
313
- <h1 className="text-3xl font-black text-white mb-2">CoinTube</h1>
314
- <p className="text-gray-400 mb-8">Ve contenido, gana monedas y promociónate.</p>
315
- <button onClick={handleGoogleLogin} className="w-full flex items-center justify-center gap-3 px-6 py-4 bg-white text-gray-900 font-bold rounded-xl hover:bg-gray-100 transition-all">
316
- Empezar con Google
317
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
318
  </div>
319
  </div>
320
  );
321
  }
322
 
323
  return (
324
- <div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-8 bg-gray-900">
325
- <header className="flex justify-between items-center pb-6 mb-6 border-b border-gray-800">
326
- <h1 className="text-3xl font-black text-white tracking-tighter">COIN<span className="text-blue-500">TUBE</span></h1>
327
- <div className="flex items-center gap-5">
 
 
 
 
328
  <div className="text-right">
329
- <p className="text-[10px] text-gray-500">{user.email}</p>
330
- <p className="text-2xl font-black text-yellow-400">{Math.floor(userProfile?.coins || 0)} 🪙</p>
 
 
331
  </div>
332
- <button onClick={handleLogout} className="p-2 bg-gray-800 hover:text-red-400 rounded-lg">
333
- <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/></svg>
 
 
 
 
334
  </button>
335
  </div>
 
336
  </header>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
- <nav className="flex justify-center gap-3 mb-8 bg-gray-800/50 p-1.5 rounded-2xl w-fit mx-auto border border-gray-700">
339
- <button onClick={() => setCurrentTab('ganar')} className={`px-8 py-3 rounded-xl font-bold transition-all ${currentTab === 'ganar' ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/20' : 'text-gray-400 hover:bg-gray-700'}`}>Ver Videos</button>
340
- <button onClick={() => setCurrentTab('promocionar')} className={`px-8 py-3 rounded-xl font-bold transition-all ${currentTab === 'promocionar' ? 'bg-blue-600 text-white shadow-lg shadow-blue-600/20' : 'text-gray-400 hover:bg-gray-700'}`}>Subir Video</button>
341
  {user.email === ADMIN_EMAIL && (
342
- <button onClick={sendMassNotification} className="px-8 py-3 rounded-xl font-bold bg-indigo-600 text-white shadow-lg">📧 Notificar</button>
 
 
 
 
 
343
  )}
344
  </nav>
345
 
346
- <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-8`}>
347
  <div className="lg:w-2/3">
348
- <div className="aspect-video bg-black rounded-3xl overflow-hidden shadow-2xl border border-gray-800">
349
- <div id="player" className="w-full h-full"></div>
 
 
 
 
350
  </div>
351
- <div className="mt-5 p-5 bg-gray-800 rounded-2xl border border-gray-700">
352
- <h3 className="text-xl font-bold text-white">{currentVideoTitle}</h3>
353
- <p className="text-xs text-blue-500 font-bold uppercase tracking-widest mt-1">Reproduciendo</p>
354
  </div>
355
  </div>
356
-
357
- <aside className="lg:w-1/3 flex flex-col gap-6">
358
- <div className="bg-gray-800 rounded-3xl p-6 flex-1 flex flex-col min-h-[300px] border border-gray-700 shadow-xl">
359
- <h3 className="text-gray-500 text-xs font-black uppercase tracking-tighter mb-4">Playlist Random</h3>
360
- <div className="overflow-y-auto flex-1 space-y-3 custom-scrollbar">
361
- {videos.map((v) => (
362
- <button key={v.id} onClick={() => loadVideo(v)} className={`w-full text-left p-3 rounded-xl transition-all border ${v.videoId === currentVideoId ? 'bg-blue-600/20 border-blue-500/50 text-blue-400' : 'bg-gray-700/50 border-transparent text-gray-300 hover:border-gray-600'}`}>
363
- <div className="text-sm truncate font-medium">{v.title}</div>
364
  {user?.email === ADMIN_EMAIL && (
365
- <div onClick={(e) => handleDeleteVideo(e, v.id)} className="text-red-500 text-[10px] mt-1 font-bold">[BORRAR]</div>
366
  )}
367
  </button>
368
  ))}
369
  </div>
370
  </div>
371
-
372
- <div className="bg-gray-800 rounded-3xl p-6 flex-1 flex flex-col min-h-[300px] border border-gray-700 shadow-xl">
373
- <h3 className="text-yellow-500 text-xs font-black uppercase tracking-tighter mb-4">Ranking Global</h3>
374
- <div className="overflow-y-auto flex-1 space-y-2 custom-scrollbar">
375
- {leaderboard.map((u, i) => (
376
- <div key={u.id} className="flex justify-between items-center p-3 bg-gray-700/30 rounded-xl">
377
- <span className="text-sm text-gray-200 font-medium truncate w-32">{u.email?.split('@')[0]}</span>
378
- <div className="flex items-center gap-3">
379
- <span className="text-yellow-400 font-bold text-sm">{Math.floor(u.coins || 0)}</span>
380
- {user?.email === ADMIN_EMAIL && (
381
- <button onClick={() => handleGiveCoins(u.id, u.coins)} className="bg-green-600/20 text-green-500 p-1 rounded-md">+</button>
382
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  </div>
384
- </div>
385
- ))}
 
 
386
  </div>
387
  </div>
388
  </aside>
@@ -399,16 +503,39 @@ function PromoCheck({ user, userProfile, userId, adminEmail, videoCost, db }) {
399
  const isAdmin = user?.email === adminEmail;
400
  const coins = userProfile?.coins || 0;
401
  const hasEnoughCoins = coins >= videoCost;
402
- if (isAdmin || hasEnoughCoins) return <PromoUnlocked userId={userId} isAdmin={isAdmin} videoCost={videoCost} db={db} />;
 
 
403
 
404
  return (
405
- <div className="text-center p-12 bg-gray-800 rounded-3xl shadow-2xl max-w-xl border border-gray-700">
406
- <h2 className="text-3xl font-black text-white mb-2">Puntos Insuficientes</h2>
407
- <p className="text-gray-400 mb-8">Necesitas {videoCost.toLocaleString()} monedas para subir un video.</p>
408
- <div className="w-full bg-gray-700 rounded-full h-4 mb-3 overflow-hidden p-1">
409
- <div className="bg-gradient-to-r from-blue-600 to-blue-400 h-2 rounded-full transition-all duration-1000" style={{ width: `${Math.min((coins / videoCost) * 100, 100)}%` }}></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
410
  </div>
411
- <p className="text-sm font-bold text-yellow-500 mb-8 font-mono">{Math.floor(coins).toLocaleString()} / {videoCost.toLocaleString()}</p>
412
  </div>
413
  );
414
  }
@@ -416,39 +543,95 @@ function PromoCheck({ user, userProfile, userId, adminEmail, videoCost, db }) {
416
  function PromoUnlocked({ userId, isAdmin, videoCost, db }) {
417
  const [url, setUrl] = useState('');
418
  const [title, setTitle] = useState('');
 
419
  const [loading, setLoading] = useState(false);
420
 
 
 
 
 
 
 
421
  const handleSubmit = async (e) => {
422
  e.preventDefault();
423
- const vId = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)?.[1];
424
- if (!vId || !title.trim()) return alert("Link o título inválido");
425
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
  setLoading(true);
 
427
  try {
428
  if (!isAdmin) {
429
- await updateDoc(doc(db, 'users', userId), { coins: increment(-videoCost) });
 
430
  }
431
- await addDoc(collection(db, 'videos'), {
 
 
432
  title: title,
433
- videoId: vId,
434
  addedBy: userId,
435
  createdAt: serverTimestamp(),
436
  isPromoted: !isAdmin
437
  });
438
- alert("¡Video Publicado!");
439
- setUrl(''); setTitle('');
440
- } catch (err) { console.error(err); alert("Error al subir."); } finally { setLoading(false); }
 
 
 
 
 
 
 
441
  };
442
 
443
  return (
444
- <div className="bg-gray-800 p-10 rounded-3xl shadow-2xl max-w-md w-full border-2 border-blue-500/20">
445
- <h2 className="text-3xl font-black text-white mb-8 text-center">{isAdmin ? "MODO ADMIN" : "PROMOCIONAR"}</h2>
446
- <form onSubmit={handleSubmit} className="space-y-5">
447
- <input type="url" placeholder="URL de YouTube" className="w-full p-4 bg-gray-900 text-white rounded-xl border border-gray-700 outline-none focus:border-blue-500 transition-all font-medium" value={url} onChange={e => setUrl(e.target.value)} required />
448
- <input type="text" placeholder="Título del Video" className="w-full p-4 bg-gray-900 text-white rounded-xl border border-gray-700 outline-none focus:border-blue-500 transition-all font-medium" value={title} onChange={e => setTitle(e.target.value)} required maxLength={50} />
449
- <button disabled={loading} className="w-full py-4 bg-blue-600 hover:bg-blue-500 text-white font-black rounded-xl transition-all disabled:opacity-50">
450
- {loading ? 'PUBLICANDO...' : (isAdmin ? 'SUBIR GRATIS' : `PAGAR ${videoCost.toLocaleString()}`)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  </button>
 
 
 
 
 
452
  </form>
453
  </div>
454
  );
 
4
  getAuth,
5
  onAuthStateChanged,
6
  GoogleAuthProvider,
7
+ signInWithRedirect, // Modificado para que funcione en Hugging Face
8
+ getRedirectResult, // Modificado para que funcione en Hugging Face
9
  signOut
10
  } from 'firebase/auth';
11
  import {
 
22
  query,
23
  orderBy,
24
  limit,
25
+ writeBatch, // Agregado para el reseteo masivo
26
+ getDocs // Agregado para el reseteo masivo
27
  } from 'firebase/firestore';
28
 
29
  const firebaseConfig = {
 
44
  const DEFAULT_VIDEO_ID = "1rghd2zPhFo";
45
  const DEFAULT_VIDEO_TITLE = "Cuentos de Terror";
46
 
47
+ // Datos de EmailJS
48
  const EMAILJS_SERVICE_ID = "service_v18p188";
49
  const EMAILJS_TEMPLATE_ID = "template_wexg1ww";
50
  const EMAILJS_PUBLIC_KEY = "XK6EoFoAqInBoDjIe";
51
 
52
  export default function App() {
53
+
54
  const [videos, setVideos] = useState([]);
55
  const [leaderboard, setLeaderboard] = useState([]);
56
  const [user, setUser] = useState(null);
 
58
  const [authReady, setAuthReady] = useState(false);
59
  const [userProfile, setUserProfile] = useState(null);
60
  const [currentTab, setCurrentTab] = useState('ganar');
61
+
62
  const [currentVideoId, setCurrentVideoId] = useState(null);
63
  const [currentVideoTitle, setCurrentVideoTitle] = useState(null);
64
+
65
  const [ytApiReady, setYtApiReady] = useState(false);
66
 
67
  const playerRef = useRef(null);
 
69
  const localCoinBufferRef = useRef(0);
70
  const initialLoadRef = useRef(true);
71
 
 
72
  useEffect(() => {
73
+ // Esto captura el login después de la redirección de Google
74
+ getRedirectResult(auth).catch(error => console.error("Redirect Error:", error));
 
 
 
 
 
 
75
 
76
+ const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
77
+ if (user) {
78
+ setUser(user);
79
+ setUserId(user.uid);
80
  } else {
81
  setUser(null);
82
  setUserId(null);
 
88
  }, []);
89
 
90
  useEffect(() => {
91
+ if (!userId) return;
92
  const profileRef = doc(db, 'users', userId);
93
  const unsubscribeProfile = onSnapshot(profileRef, (docSnap) => {
94
  if (docSnap.exists()) {
 
102
  setDoc(profileRef, newProfile).catch(e => console.error(e));
103
  setUserProfile(newProfile);
104
  }
105
+ }, (error) => console.error(error));
106
  return () => unsubscribeProfile();
107
  }, [userId, user]);
108
 
109
+ // NUEVO: Lógica de Reseteo Diario (Para el admin)
110
  useEffect(() => {
111
  if (user?.email !== ADMIN_EMAIL) return;
112
+
113
  const checkReset = async () => {
114
+ const settingsRef = doc(db, 'settings', 'stats');
115
+ const snap = await getDocs(query(collection(db, 'settings')));
116
+ if (snap.empty) return;
117
+
118
+ const data = snap.docs[0].data();
119
+ const lastReset = data.lastReset.toDate();
120
+ const now = new Date();
121
+
122
+ // 86,400,000 ms = 1 Día (Reseteo Diario)
123
+ if (now - lastReset >= 86400000) {
124
+ const batch = writeBatch(db);
125
+ const usersSnap = await getDocs(collection(db, 'users'));
126
+
127
+ usersSnap.forEach((userDoc) => {
128
+ if (userDoc.data().email !== ADMIN_EMAIL) {
129
+ batch.update(userDoc.ref, { coins: 0 });
130
+ }
131
+ });
132
+
133
+ batch.update(settingsRef, { lastReset: serverTimestamp() });
134
+ await batch.commit();
135
+ alert("Reseteo Diario Completado: Las monedas de los usuarios se reiniciaron a 0.");
136
+ }
 
137
  };
138
  checkReset();
139
  }, [user]);
140
 
 
141
  useEffect(() => {
142
  const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(50));
143
  const unsubscribe = onSnapshot(q, (snapshot) => {
144
  let fetchedVideos = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
145
 
146
  if (fetchedVideos.length > 0) {
147
+ // NUEVO: Randomizar la lista de videos
148
  fetchedVideos = fetchedVideos.sort(() => Math.random() - 0.5);
149
+
150
  setVideos(fetchedVideos);
151
  if (initialLoadRef.current || !currentVideoId) {
152
+ const latestVideo = fetchedVideos[0];
153
+ setCurrentVideoId(latestVideo.videoId);
154
+ setCurrentVideoTitle(latestVideo.title);
155
  initialLoadRef.current = false;
156
  }
157
  } else {
 
167
  }, [currentVideoId]);
168
 
169
  useEffect(() => {
170
+ const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(100));
171
  const unsubscribe = onSnapshot(q, (snapshot) => {
172
  let usersList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
173
+
174
+ usersList = usersList.filter(u =>
175
+ u.email &&
176
+ u.email.endsWith('@gmail.com')
177
+ );
178
+
179
+ setLeaderboard(usersList.slice(0, 20));
180
  });
181
  return () => unsubscribe();
182
  }, []);
 
228
  const state = event.data;
229
  if (state === window.YT.PlayerState.PLAYING) startCoinInterval();
230
  else stopCoinInterval();
231
+
232
+ if (state === window.YT.PlayerState.ENDED) {
233
+ if (currentVideoId === DEFAULT_VIDEO_ID && userId) {
234
+ const profileRef = doc(db, 'users', userId);
235
+ updateDoc(profileRef, { watchedDefaultVideo: true });
236
+ }
237
+ }
238
+ }, [startCoinInterval, stopCoinInterval, currentVideoId, userId]);
239
 
240
  useEffect(() => {
241
  if (ytApiReady && userId && !playerRef.current && currentVideoId) {
 
247
  events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange }
248
  });
249
  }
250
+ if (!userId && playerRef.current) {
251
+ stopCoinInterval();
252
+ playerRef.current = null;
253
+ }
254
  }, [ytApiReady, userId, currentVideoId, onPlayerReady, onPlayerStateChange]);
255
 
256
  const loadVideo = (video) => {
 
260
  if (playerRef.current) playerRef.current.loadVideoById(video.videoId);
261
  };
262
 
263
+ const handleDeleteVideo = async (e, videoId) => {
264
  e.stopPropagation();
265
  if (!confirm("¿Eliminar video?")) return;
266
+ try { await deleteDoc(doc(db, "videos", videoId)); } catch (error) { console.error(error); }
267
  };
268
 
269
+ const handleGiveCoins = async (targetUserId, currentCoins) => {
270
+ const amountStr = prompt("Cantidad de monedas a sumar (usa negativo para restar):", "1000");
271
+ if (!amountStr) return;
272
+ const amount = parseInt(amountStr);
273
+ if (isNaN(amount)) return;
274
+
275
+ try {
276
+ const userRef = doc(db, 'users', targetUserId);
277
+ await updateDoc(userRef, { coins: increment(amount) });
278
+ alert("Monedas actualizadas.");
279
+ } catch (error) {
280
+ console.error("Error:", error);
281
+ alert("Error al dar monedas. Revisa las reglas de Firebase.");
282
  }
283
  };
284
 
285
+ // NUEVO: Función para Email Masivo
286
+ const handleMassEmail = async () => {
287
+ if (!window.emailjs) return alert("EmailJS no está cargado. Revisa tu index.html.");
288
+ if (!confirm("¿Enviar notificación de reseteo a todos los usuarios?")) return;
 
289
 
290
+ const usersSnap = await getDocs(collection(db, 'users'));
291
+ let count = 0;
 
 
292
 
293
+ usersSnap.forEach((uDoc) => {
294
+ const userData = uDoc.data();
295
+ if (userData.email) {
 
 
 
 
 
296
  window.emailjs.send(EMAILJS_SERVICE_ID, EMAILJS_TEMPLATE_ID, {
297
+ to_email: userData.email,
298
+ message: "¡Hola! Los puntos se han reseteado para una nueva ronda. ¡Vuelve a CoinTube y suma monedas viendo videos!"
299
  }, EMAILJS_PUBLIC_KEY);
300
+ count++;
301
  }
302
  });
303
+ alert(`Correos en proceso de envío para ${count} usuarios.`);
304
+ };
305
+
306
+ const handleGoogleLogin = async () => {
307
+ const provider = new GoogleAuthProvider();
308
+ try {
309
+ // FIX DE HUGGING FACE: redirect en vez de popup
310
+ await signInWithRedirect(auth, provider);
311
+ } catch (error) {
312
+ console.error("Error Google:", error);
313
+ }
314
+ };
315
+
316
+ const handleLogout = async () => {
317
+ stopCoinInterval();
318
+ await signOut(auth);
319
  };
320
 
321
  if (!authReady) {
322
  return (
323
+ <div className="flex flex-col items-center justify-center h-full w-full">
324
+ <div className="spinner mb-4"></div>
325
+ <p className="text-lg text-gray-400">Cargando...</p>
326
  </div>
327
  );
328
  }
329
 
330
+ if (authReady && !user) {
331
  return (
332
  <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
333
+ <div className="max-w-md w-full bg-gray-800 rounded-2xl shadow-2xl p-8 text-center border border-gray-700">
334
  <div className="flex justify-center mb-6">
335
+ <div className="bg-blue-600 p-4 rounded-full shadow-lg">
336
+ <svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
337
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"></path>
338
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
339
  </svg>
340
  </div>
341
  </div>
342
+ <h1 className="text-3xl font-bold text-white mb-2 tracking-tight">Bienvenido a CoinTube</h1>
343
+ <p className="text-gray-400 mb-8 text-sm leading-relaxed">
344
+ Descubre contenido increíble, gana monedas virtuales por cada minuto que ves y promociona tus propios videos.<br/><br/>
345
+ <span className="text-yellow-400 font-bold text-base">¡Junta 10,000 monedas para subir tu video!</span>
346
+ </p>
347
+ <div className="space-y-3">
348
+ <button onClick={handleGoogleLogin} className="w-full flex items-center justify-center px-4 py-3 bg-white text-gray-800 font-bold rounded-lg hover:bg-gray-100 transition-all duration-200 shadow-md">
349
+ <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
350
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
351
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
352
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
353
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
354
+ </svg>
355
+ Continuar con Google
356
+ </button>
357
+ </div>
358
+ <p className="mt-6 text-xs text-gray-500">Al continuar, aceptas nuestros términos.</p>
359
  </div>
360
  </div>
361
  );
362
  }
363
 
364
  return (
365
+ <div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-6">
366
+
367
+ <header className="flex justify-between items-center pb-4 mb-4 border-b border-gray-700">
368
+ <h1 className="text-2xl md:text-3xl font-bold text-white">
369
+ Coin<span className="text-blue-500">Tube</span>
370
+ </h1>
371
+
372
+ <div className="flex items-center gap-4">
373
  <div className="text-right">
374
+ <div className="text-xs text-gray-400">{user.email}</div>
375
+ <div className="text-lg font-bold text-yellow-400">
376
+ {Math.floor(userProfile?.coins || 0)} 🪙
377
+ </div>
378
  </div>
379
+
380
+ <button
381
+ onClick={handleLogout}
382
+ className="px-3 py-1 bg-gray-700 text-white text-sm rounded hover:bg-gray-600"
383
+ >
384
+ Salir
385
  </button>
386
  </div>
387
+
388
  </header>
389
+ <a
390
+ href="https://wa.me/+2286051539"
391
+ target="_blank"
392
+ rel="noopener noreferrer"
393
+ className="flex items-center gap-2 bg-green-600 hover:bg-green-500 text-white px-4 py-2 rounded-lg shadow-md w-fit mx-auto mb-4"
394
+ >
395
+ <svg
396
+ xmlns="http://www.w3.org/2000/svg"
397
+ fill="currentColor"
398
+ viewBox="0 0 24 24"
399
+ className="w-6 h-6"
400
+ >
401
+ <path d="M.057 24l1.687-6.163a11.867 11.867 0 01-1.587-5.96C.159 5.3 5.41 0 12.084 0c3.184 0 6.167 1.24 8.413 3.488a11.8 11.8 0 013.5 8.404c-.003 6.673-5.312 11.92-11.987 11.92a11.95 11.95 0 01-5.93-1.575L.057 24zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.593 5.448 0 9.886-4.415 9.889-9.848.003-5.46-4.415-9.89-9.881-9.893-5.466 0-9.887 4.42-9.889 9.878a9.86 9.86 0 001.733 5.57l-.999 3.648 3.755-.948zm11.387-5.464c-.074-.123-.272-.198-.57-.347s-1.758-.868-2.03-.967c-.272-.099-.47-.148-.669.148-.198.297-.768.966-.94 1.164-.173.198-.347.223-.644.074-.297-.148-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.52.149-.173.198-.297.297-.495.099-.198.05-.371-.025-.52-.074-.148-.669-1.611-.916-2.207-.242-.579-.487-.5-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.148.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.718 2.006-1.413.248-.694.248-1.289.173-1.413z"/>
402
+ </svg>
403
+
404
+ <span className="font-bold">Grupo WhatsApp 4000 horas</span>
405
+ </a>
406
+
407
+ <nav className="flex justify-center gap-2 mb-6 bg-gray-800 p-1 rounded-lg w-fit mx-auto">
408
+ <button
409
+ onClick={() => setCurrentTab('ganar')}
410
+ className={`w-[200px] py-2 rounded ${currentTab === 'ganar' ? 'bg-blue-600 text-white' : 'text-gray-400'}`}
411
+ >
412
+ Ver Videos
413
+ </button>
414
+
415
+ <button
416
+ onClick={() => setCurrentTab('promocionar')}
417
+ className={`w-[200px] py-2 rounded ${currentTab === 'promocionar' ? 'bg-blue-600 text-white' : 'text-gray-400'}`}
418
+ >
419
+ Subir Video
420
+ </button>
421
 
422
+ {/* NUEVO: BOTON EMAIL MASIVO */}
 
 
423
  {user.email === ADMIN_EMAIL && (
424
+ <button
425
+ onClick={handleMassEmail}
426
+ className="w-[150px] py-2 rounded bg-indigo-600 hover:bg-indigo-500 text-white font-bold ml-2 shadow"
427
+ >
428
+ 📧 Enviar Correo
429
+ </button>
430
  )}
431
  </nav>
432
 
433
+ <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6`}>
434
  <div className="lg:w-2/3">
435
+ <div className="aspect-video bg-black rounded-lg overflow-hidden shadow-xl flex items-center justify-center">
436
+ {currentVideoId ? (
437
+ <div id="player" className="w-full h-full"></div>
438
+ ) : (
439
+ <div className="text-gray-500 animate-pulse">Cargando último video...</div>
440
+ )}
441
  </div>
442
+ <div className="mt-3 p-3 bg-gray-800 rounded">
443
+ <h3 className="text-white font-medium">{currentVideoTitle || "Cargando..."}</h3>
 
444
  </div>
445
  </div>
446
+ <aside className="lg:w-1/3 flex flex-col gap-4 h-full overflow-hidden">
447
+ <div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
448
+ <h3 className="text-gray-400 text-sm uppercase font-bold mb-2">Lista de Reproducción</h3>
449
+ <div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
450
+ {videos.map((video) => (
451
+ <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'}`}>
452
+ <div className="text-sm text-white truncate flex-1">{video.title}</div>
 
453
  {user?.email === ADMIN_EMAIL && (
454
+ <span onClick={(e) => handleDeleteVideo(e, video.id)} 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 cursor-pointer" title="Borrar Video">X</span>
455
  )}
456
  </button>
457
  ))}
458
  </div>
459
  </div>
460
+
461
+ <div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
462
+ <h3 className="text-yellow-500 text-sm uppercase font-bold mb-2">Ranking Global</h3>
463
+ <div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
464
+ {leaderboard.length > 0 ? (
465
+ leaderboard.map((u, i) => (
466
+ <div key={u.id || i} className="flex justify-between items-center p-2 bg-gray-700 rounded">
467
+ <div className="flex items-center truncate">
468
+ <span className="text-xs text-gray-300 truncate mr-2" title={u.email}>
469
+ {u.email ? u.email.split('@')[0] : 'Usuario'}
470
+ </span>
471
+ </div>
472
+
473
+ <div className="flex items-center gap-2">
474
+ <span className="text-yellow-400 font-mono text-sm whitespace-nowrap">{Math.floor(u.coins || 0)}</span>
475
+ {user?.email === ADMIN_EMAIL && (
476
+ <button
477
+ onClick={() => handleGiveCoins(u.id, u.coins)}
478
+ className="bg-green-600 hover:bg-green-500 text-white text-xs font-bold px-2 py-0.5 rounded shadow"
479
+ title="Dar Monedas"
480
+ >
481
+ +
482
+ </button>
483
+ )}
484
+ </div>
485
  </div>
486
+ ))
487
+ ) : (
488
+ <div className="text-center text-gray-500 text-sm py-4">Esperando usuarios...</div>
489
+ )}
490
  </div>
491
  </div>
492
  </aside>
 
503
  const isAdmin = user?.email === adminEmail;
504
  const coins = userProfile?.coins || 0;
505
  const hasEnoughCoins = coins >= videoCost;
506
+
507
+ if (isAdmin) return <PromoUnlocked userId={userId} isAdmin={true} db={db} />;
508
+ if (hasEnoughCoins) return <PromoUnlocked userId={userId} isAdmin={false} videoCost={videoCost} db={db} />;
509
 
510
  return (
511
+ <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg border border-gray-700">
512
+ <h2 className="text-2xl font-bold text-white mb-2">Insuficientes Monedas</h2>
513
+ <p className="text-gray-400 mb-4">Necesitas {videoCost} monedas para subir un video.</p>
514
+
515
+ <div className="w-full bg-gray-700 rounded-full h-3 mb-4">
516
+ <div className="bg-yellow-400 h-3 rounded-full" style={{ width: `${Math.min((coins / videoCost) * 100, 100)}%` }}></div>
517
+ </div>
518
+ <p className="text-sm text-gray-500 mb-6">{Math.floor(coins)} / {videoCost}</p>
519
+
520
+ <div className="mt-6 border-t border-gray-600 pt-6">
521
+ <h3 className="text-yellow-400 font-bold text-xl mb-2">¿Quieres subir tu video ya?</h3>
522
+ <p className="text-white mb-4 text-sm">Deposita USDT para recargar al instante.</p>
523
+ <div className="bg-gray-900 p-4 rounded-lg">
524
+ <p className="text-green-400 font-bold mb-4 bg-green-900/30 py-2 rounded">1 USD = 3000 Monedas</p>
525
+
526
+ <div className="flex justify-center mb-4">
527
+ <img src="/qr.png" alt="QR Deposito" className="w-32 h-32 rounded-lg border-2 border-white"/>
528
+ </div>
529
+
530
+ <p className="text-xs text-gray-400 mb-1 uppercase font-bold">Dirección USDT (BEP20/TRC20):</p>
531
+ <div className="break-all bg-black p-3 rounded text-xs font-mono text-gray-300 select-all border border-gray-700">
532
+ 0x64531cafffbbc7ab1212d27cbafb4fa58111c84128804edec9d17c49c787e512
533
+ </div>
534
+ <p className="text-xs text-gray-500 mt-3">
535
+ Envía tu comprobante a: <a href={`mailto:${adminEmail}`} className="text-blue-400 hover:underline">{adminEmail}</a>
536
+ </p>
537
+ </div>
538
  </div>
 
539
  </div>
540
  );
541
  }
 
543
  function PromoUnlocked({ userId, isAdmin, videoCost, db }) {
544
  const [url, setUrl] = useState('');
545
  const [title, setTitle] = useState('');
546
+ const [message, setMessage] = useState({ text: '', type: '' });
547
  const [loading, setLoading] = useState(false);
548
 
549
+ const extractYouTubeID = (url) => {
550
+ const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/;
551
+ const match = url.match(regex);
552
+ return match ? match[1] : null;
553
+ };
554
+
555
  const handleSubmit = async (e) => {
556
  e.preventDefault();
557
+ const videoId = extractYouTubeID(url);
 
558
 
559
+ if (!videoId) {
560
+ setMessage({ text: "Enlace de YouTube no válido", type: 'error' });
561
+ return;
562
+ }
563
+ if (!title.trim()) {
564
+ setMessage({ text: "Por favor escribe un título", type: 'error' });
565
+ return;
566
+ }
567
+ if (!db) {
568
+ setMessage({ text: "Error: Base de datos no conectada", type: 'error' });
569
+ return;
570
+ }
571
+
572
  setLoading(true);
573
+
574
  try {
575
  if (!isAdmin) {
576
+ const userRef = doc(db, 'users', userId);
577
+ await updateDoc(userRef, { coins: increment(-videoCost) });
578
  }
579
+
580
+ const videosRef = collection(db, 'videos');
581
+ await addDoc(videosRef, {
582
  title: title,
583
+ videoId: videoId,
584
  addedBy: userId,
585
  createdAt: serverTimestamp(),
586
  isPromoted: !isAdmin
587
  });
588
+
589
+ setMessage({ text: "¡Video agregado con éxito!", type: 'success' });
590
+ setUrl('');
591
+ setTitle('');
592
+ } catch (error) {
593
+ console.error("Error al subir:", error);
594
+ setMessage({ text: "Error al procesar. Intenta de nuevo.", type: 'error' });
595
+ } finally {
596
+ setLoading(false);
597
+ }
598
  };
599
 
600
  return (
601
+ <div className="bg-gray-800 p-8 rounded-lg shadow-xl max-w-md w-full border border-green-900">
602
+ <h2 className="text-2xl font-bold text-white mb-4 text-center">{isAdmin ? "Panel Admin" : "¡Meta Alcanzada!"}</h2>
603
+ <form onSubmit={handleSubmit} className="space-y-4">
604
+ <div>
605
+ <label className="block text-sm text-gray-400 mb-1">Enlace del Video</label>
606
+ <input
607
+ type="url"
608
+ placeholder="https://youtube.com/watch?v=..."
609
+ className="w-full p-3 bg-gray-700 text-white rounded border border-gray-600 focus:border-blue-500 outline-none"
610
+ value={url}
611
+ onChange={(e) => setUrl(e.target.value)}
612
+ required
613
+ />
614
+ </div>
615
+ <div>
616
+ <label className="block text-sm text-gray-400 mb-1">Título del Video</label>
617
+ <input
618
+ type="text"
619
+ placeholder="Ej: Mi Gameplay Épico"
620
+ className="w-full p-3 bg-gray-700 text-white rounded border border-gray-600 focus:border-blue-500 outline-none"
621
+ value={title}
622
+ onChange={(e) => setTitle(e.target.value)}
623
+ required
624
+ maxLength={50}
625
+ />
626
+ </div>
627
+ <button disabled={loading} className="w-full py-3 bg-green-600 hover:bg-green-700 text-white font-bold rounded transition-colors disabled:opacity-50">
628
+ {loading ? 'Procesando...' : (isAdmin ? 'Subir Gratis' : `Pagar ${videoCost} y Subir`)}
629
  </button>
630
+ {message.text && (
631
+ <p className={`text-center font-medium ${message.type === 'error' ? 'text-red-400' : 'text-green-400'}`}>
632
+ {message.text}
633
+ </p>
634
+ )}
635
  </form>
636
  </div>
637
  );