dimensionalpulsar commited on
Commit
1b73938
·
verified ·
1 Parent(s): cad019a

Update cointube.jsx

Browse files
Files changed (1) hide show
  1. cointube.jsx +187 -101
cointube.jsx CHANGED
@@ -45,6 +45,7 @@ const DEFAULT_VIDEO_TITLE = "Cuentos de Terror";
45
 
46
  const EMAILJS_SERVICE_ID = "service_v18p188";
47
  const EMAILJS_TEMPLATE_ID = "template_wexg1ww";
 
48
 
49
  export default function App() {
50
  const [videos, setVideos] = useState([]);
@@ -79,7 +80,7 @@ export default function App() {
79
  }, []);
80
 
81
  useEffect(() => {
82
- if (!userId) return;
83
  const profileRef = doc(db, 'users', userId);
84
  const unsubscribeProfile = onSnapshot(profileRef, (docSnap) => {
85
  if (docSnap.exists()) {
@@ -100,26 +101,31 @@ export default function App() {
100
  useEffect(() => {
101
  if (user?.email !== ADMIN_EMAIL) return;
102
  const checkReset = async () => {
103
- const settingsRef = doc(db, 'settings', 'stats');
104
- const snap = await getDocs(query(collection(db, 'settings')));
105
- if (snap.empty) return;
106
-
107
- const data = snap.docs[0].data();
108
- const lastReset = data.lastReset.toDate();
109
- const now = new Date();
110
-
111
- if (now - lastReset >= 432000000) {
112
- const batch = writeBatch(db);
113
- const usersSnap = await getDocs(collection(db, 'users'));
114
- usersSnap.forEach((uDoc) => {
115
- if (uDoc.data().email !== ADMIN_EMAIL) {
116
- batch.update(uDoc.ref, { coins: 0 });
117
- }
118
- });
119
- batch.update(settingsRef, { lastReset: serverTimestamp() });
120
- await batch.commit();
121
- alert("Ciclo de 5 días completado: Puntos reseteados.");
122
- }
 
 
 
 
 
123
  };
124
  checkReset();
125
  }, [user]);
@@ -133,14 +139,13 @@ export default function App() {
133
  fetchedVideos = fetchedVideos.sort(() => Math.random() - 0.5);
134
  setVideos(fetchedVideos);
135
  if (initialLoadRef.current || !currentVideoId) {
136
- const latestVideo = fetchedVideos[0];
137
- setCurrentVideoId(latestVideo.videoId);
138
- setCurrentVideoTitle(latestVideo.title);
139
  initialLoadRef.current = false;
140
  }
141
  } else {
142
- const def = [{ id: '1', title: DEFAULT_VIDEO_TITLE, videoId: DEFAULT_VIDEO_ID }];
143
- setVideos(def);
144
  if (initialLoadRef.current || !currentVideoId) {
145
  setCurrentVideoId(DEFAULT_VIDEO_ID);
146
  setCurrentVideoTitle(DEFAULT_VIDEO_TITLE);
@@ -216,7 +221,9 @@ export default function App() {
216
  useEffect(() => {
217
  if (ytApiReady && userId && !playerRef.current && currentVideoId) {
218
  playerRef.current = new window.YT.Player('player', {
219
- height: '100%', width: '100%', videoId: currentVideoId,
 
 
220
  playerVars: { 'playsinline': 1, 'autoplay': 1, 'controls': 1, 'modestbranding': 1, 'rel': 0 },
221
  events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange }
222
  });
@@ -236,21 +243,21 @@ export default function App() {
236
 
237
  const handleDeleteVideo = async (e, vId) => {
238
  e.stopPropagation();
239
- if (!confirm("¿Eliminar video?")) return;
240
- try { await deleteDoc(doc(db, "videos", vId)); } catch (e) { console.error(e); }
241
  };
242
 
243
  const handleGiveCoins = async (tId, cC) => {
244
- const amtStr = prompt("Monedas a sumar/restar:", "1000");
245
  if (!amtStr) return;
246
  const amt = parseInt(amtStr);
247
  if (isNaN(amt)) return;
248
- try { await updateDoc(doc(db, 'users', tId), { coins: increment(amt) }); } catch (e) { console.error(e); }
249
  };
250
 
251
  const handleGoogleLogin = async () => {
252
- const p = new GoogleAuthProvider();
253
- try { await signInWithPopup(auth, p); } catch (e) { console.error(e); }
254
  };
255
 
256
  const handleLogout = async () => {
@@ -258,88 +265,141 @@ export default function App() {
258
  await signOut(auth);
259
  };
260
 
261
- const handleMassEmail = async () => {
262
- if (!window.emailjs) return alert("EmailJS no detectado");
263
- if (!confirm("¿Enviar notificación de reseteo a todos?")) return;
 
264
  const snap = await getDocs(collection(db, 'users'));
 
265
  snap.forEach((uDoc) => {
266
- const data = uDoc.data();
267
- if (data.email) {
268
  window.emailjs.send(EMAILJS_SERVICE_ID, EMAILJS_TEMPLATE_ID, {
269
- to_email: data.email,
270
- subject: "¡Reseteo de Puntos!",
271
- message: "Los puntos se han reseteado. ¡Ven a ver videos para ganar más!"
272
- });
 
273
  }
274
  });
275
- alert("Envíos iniciados.");
276
  };
277
 
278
- if (!authReady) return <div className="flex flex-col items-center justify-center h-full w-full"><div className="spinner mb-4"></div><p className="text-gray-400">Cargando...</p></div>;
279
-
280
- if (!user) return (
281
- <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
282
- <div className="max-w-md w-full bg-gray-800 rounded-2xl p-8 text-center border border-gray-700">
283
- <h1 className="text-3xl font-bold text-white mb-8">Bienvenido a CoinTube</h1>
284
- <button onClick={handleGoogleLogin} className="w-full py-3 bg-white text-gray-800 font-bold rounded-lg">Continuar con Google</button>
285
  </div>
286
- </div>
287
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
  return (
290
- <div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-6">
291
- <header className="flex justify-between items-center pb-4 mb-4 border-b border-gray-700">
292
- <h1 className="text-2xl md:text-3xl font-bold text-white">Coin<span className="text-blue-500">Tube</span></h1>
293
- <div className="flex items-center gap-4">
294
- <div className="text-right">
295
- <div className="text-xs text-gray-400">{user.email}</div>
296
- <div className="text-lg font-bold text-yellow-400">{Math.floor(userProfile?.coins || 0)} 🪙</div>
 
 
 
 
 
 
297
  </div>
298
- <button onClick={handleLogout} className="px-3 py-1 bg-gray-700 text-white text-sm rounded">Salir</button>
 
 
299
  </div>
300
  </header>
301
 
302
- <nav className="flex justify-center gap-2 mb-6 bg-gray-800 p-1 rounded-lg w-fit mx-auto">
303
- <button onClick={() => setCurrentTab('ganar')} className={`w-[120px] md:w-[250px] py-2 rounded ${currentTab === 'ganar' ? 'bg-blue-600 text-white' : 'text-gray-400'}`}>Ver Videos</button>
304
- <button onClick={() => setCurrentTab('promocionar')} className={`w-[120px] md:w-[250px] py-2 rounded ${currentTab === 'promocionar' ? 'bg-blue-600 text-white' : 'text-gray-400'}`}>Subir Video</button>
 
 
 
 
 
 
305
  {user.email === ADMIN_EMAIL && (
306
- <button onClick={handleMassEmail} className="px-4 py-2 bg-indigo-600 text-white rounded text-xs font-bold">📧 Notificar</button>
 
 
307
  )}
308
  </nav>
309
 
310
- <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6`}>
311
  <div className="lg:w-2/3">
312
- <div className="aspect-video bg-black rounded-lg overflow-hidden shadow-xl flex items-center justify-center">
313
  <div id="player" className="w-full h-full"></div>
314
  </div>
315
- <div className="mt-3 p-3 bg-gray-800 rounded">
316
- <h3 className="text-white font-medium">{currentVideoTitle}</h3>
 
317
  </div>
318
  </div>
319
- <aside className="lg:w-1/3 flex flex-col gap-4 h-full overflow-hidden">
320
- <div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
321
- <h3 className="text-gray-400 text-sm font-bold mb-2">Lista Aleatoria</h3>
322
- <div className="overflow-y-auto flex-1 space-y-2 custom-scrollbar">
 
 
 
 
323
  {videos.map((v) => (
324
- <button key={v.id} onClick={() => loadVideo(v)} className={`w-full text-left p-2 rounded flex justify-between items-center ${v.videoId === currentVideoId ? 'bg-blue-900 border-l-2 border-blue-500' : 'bg-gray-700 hover:bg-gray-600'}`}>
325
- <div className="text-sm text-white truncate flex-1">{v.title}</div>
326
  {user?.email === ADMIN_EMAIL && (
327
- <span onClick={(e) => handleDeleteVideo(e, v.id)} className="ml-2 text-red-500 font-bold px-2 py-1 rounded bg-red-900/50">X</span>
 
 
328
  )}
329
  </button>
330
  ))}
331
  </div>
332
  </div>
333
- <div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
334
- <h3 className="text-yellow-500 text-sm font-bold mb-2">Ranking</h3>
335
- <div className="overflow-y-auto flex-1 space-y-2 custom-scrollbar">
 
336
  {leaderboard.map((u, i) => (
337
- <div key={u.id || i} className="flex justify-between items-center p-2 bg-gray-700 rounded">
338
- <span className="text-xs text-gray-300 truncate">{u.email?.split('@')[0]}</span>
339
- <div className="flex items-center gap-2">
340
- <span className="text-yellow-400 font-mono text-sm">{Math.floor(u.coins || 0)}</span>
 
 
 
341
  {user?.email === ADMIN_EMAIL && (
342
- <button onClick={() => handleGiveCoins(u.id, u.coins)} className="bg-green-600 text-white text-xs px-2 rounded">+</button>
 
 
343
  )}
344
  </div>
345
  </div>
@@ -361,13 +421,21 @@ function PromoCheck({ user, userProfile, userId, adminEmail, videoCost, db }) {
361
  const coins = userProfile?.coins || 0;
362
  const hasEnoughCoins = coins >= videoCost;
363
  if (isAdmin || hasEnoughCoins) return <PromoUnlocked userId={userId} isAdmin={isAdmin} videoCost={videoCost} db={db} />;
 
364
  return (
365
- <div className="text-center p-8 bg-gray-800 rounded-lg max-w-lg border border-gray-700">
366
- <h2 className="text-2xl font-bold text-white mb-2">Faltan Monedas</h2>
367
- <div className="w-full bg-gray-700 rounded-full h-3 mb-4">
368
- <div className="bg-yellow-400 h-3 rounded-full" style={{ width: `${Math.min((coins / videoCost) * 100, 100)}%` }}></div>
 
 
 
 
 
 
 
 
369
  </div>
370
- <p className="text-gray-500">{Math.floor(coins)} / {videoCost}</p>
371
  </div>
372
  );
373
  }
@@ -380,26 +448,44 @@ function PromoUnlocked({ userId, isAdmin, videoCost, db }) {
380
  const handleSubmit = async (e) => {
381
  e.preventDefault();
382
  const vId = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)?.[1];
383
- if (!vId || !title.trim()) return alert("Enlace no válido");
 
 
384
  setLoading(true);
385
  try {
386
- if (!isAdmin) await updateDoc(doc(db, 'users', userId), { coins: increment(-videoCost) });
 
 
 
387
  await addDoc(collection(db, 'videos'), {
388
- title, videoId: vId, addedBy: userId, createdAt: serverTimestamp(), isPromoted: !isAdmin
 
 
 
 
389
  });
390
- alert("¡Video subido!");
391
  setUrl(''); setTitle('');
392
- } catch (err) { console.error(err); } finally { setLoading(false); }
393
  };
394
 
395
  return (
396
- <form onSubmit={handleSubmit} className="bg-gray-800 p-8 rounded-lg max-w-md w-full space-y-4 border border-green-900">
397
- <h2 className="text-2xl font-bold text-white text-center">{isAdmin ? "Panel Admin" : "Subir Video"}</h2>
398
- <input type="url" placeholder="URL de YouTube" className="w-full p-3 bg-gray-700 text-white rounded border border-gray-600" value={url} onChange={e => setUrl(e.target.value)} required />
399
- <input type="text" placeholder="Título" className="w-full p-3 bg-gray-700 text-white rounded border border-gray-600" value={title} onChange={e => setTitle(e.target.value)} required maxLength={50} />
400
- <button disabled={loading} className="w-full py-3 bg-green-600 text-white font-bold rounded">
401
- {loading ? 'Subiendo...' : 'Publicar'}
402
- </button>
403
- </form>
 
 
 
 
 
 
 
 
 
404
  );
405
  }
 
45
 
46
  const EMAILJS_SERVICE_ID = "service_v18p188";
47
  const EMAILJS_TEMPLATE_ID = "template_wexg1ww";
48
+ const EMAILJS_PUBLIC_KEY = "XK6EoFoAqInBoDjIe";
49
 
50
  export default function App() {
51
  const [videos, setVideos] = useState([]);
 
80
  }, []);
81
 
82
  useEffect(() => {
83
+ if (!userId || !user) return;
84
  const profileRef = doc(db, 'users', userId);
85
  const unsubscribeProfile = onSnapshot(profileRef, (docSnap) => {
86
  if (docSnap.exists()) {
 
101
  useEffect(() => {
102
  if (user?.email !== ADMIN_EMAIL) return;
103
  const checkReset = async () => {
104
+ try {
105
+ const settingsRef = doc(db, 'settings', 'stats');
106
+ const settingsSnap = await getDocs(query(collection(db, 'settings')));
107
+ if (settingsSnap.empty) return;
108
+
109
+ const statsData = settingsSnap.docs[0].data();
110
+ const lastResetDate = statsData.lastReset.toDate();
111
+ const now = new Date();
112
+ const diffMs = now - lastResetDate;
113
+
114
+ if (diffMs >= 432000000) {
115
+ const batch = writeBatch(db);
116
+ const allUsers = await getDocs(collection(db, 'users'));
117
+
118
+ allUsers.forEach((uDoc) => {
119
+ if (uDoc.data().email !== ADMIN_EMAIL) {
120
+ batch.update(uDoc.ref, { coins: 0 });
121
+ }
122
+ });
123
+
124
+ batch.update(settingsRef, { lastReset: serverTimestamp() });
125
+ await batch.commit();
126
+ alert("SISTEMA: Ciclo de 5 días detectado. Las monedas han sido reseteadas.");
127
+ }
128
+ } catch (err) { console.error("Error Reset:", err); }
129
  };
130
  checkReset();
131
  }, [user]);
 
139
  fetchedVideos = fetchedVideos.sort(() => Math.random() - 0.5);
140
  setVideos(fetchedVideos);
141
  if (initialLoadRef.current || !currentVideoId) {
142
+ const firstVideo = fetchedVideos[0];
143
+ setCurrentVideoId(firstVideo.videoId);
144
+ setCurrentVideoTitle(firstVideo.title);
145
  initialLoadRef.current = false;
146
  }
147
  } else {
148
+ setVideos([{ id: '1', title: DEFAULT_VIDEO_TITLE, videoId: DEFAULT_VIDEO_ID }]);
 
149
  if (initialLoadRef.current || !currentVideoId) {
150
  setCurrentVideoId(DEFAULT_VIDEO_ID);
151
  setCurrentVideoTitle(DEFAULT_VIDEO_TITLE);
 
221
  useEffect(() => {
222
  if (ytApiReady && userId && !playerRef.current && currentVideoId) {
223
  playerRef.current = new window.YT.Player('player', {
224
+ height: '100%',
225
+ width: '100%',
226
+ videoId: currentVideoId,
227
  playerVars: { 'playsinline': 1, 'autoplay': 1, 'controls': 1, 'modestbranding': 1, 'rel': 0 },
228
  events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange }
229
  });
 
243
 
244
  const handleDeleteVideo = async (e, vId) => {
245
  e.stopPropagation();
246
+ if (!confirm("¿Deseas eliminar este video permanentemente?")) return;
247
+ try { await deleteDoc(doc(db, "videos", vId)); } catch (err) { console.error(err); }
248
  };
249
 
250
  const handleGiveCoins = async (tId, cC) => {
251
+ const amtStr = prompt("Cantidad de monedas para este usuario (puedes usar números negativos):", "1000");
252
  if (!amtStr) return;
253
  const amt = parseInt(amtStr);
254
  if (isNaN(amt)) return;
255
+ try { await updateDoc(doc(db, 'users', tId), { coins: increment(amt) }); alert("Monedas actualizadas"); } catch (e) { console.error(e); }
256
  };
257
 
258
  const handleGoogleLogin = async () => {
259
+ const provider = new GoogleAuthProvider();
260
+ try { await signInWithPopup(auth, provider); } catch (e) { console.error("Login Error:", e); }
261
  };
262
 
263
  const handleLogout = async () => {
 
265
  await signOut(auth);
266
  };
267
 
268
+ const sendMassNotification = async () => {
269
+ if (!window.emailjs) return alert("EmailJS no está inicializado.");
270
+ if (!confirm("Esto enviará un correo a todos los usuarios registrados. ¿Continuar?")) return;
271
+
272
  const snap = await getDocs(collection(db, 'users'));
273
+ let count = 0;
274
  snap.forEach((uDoc) => {
275
+ const d = uDoc.data();
276
+ if (d.email) {
277
  window.emailjs.send(EMAILJS_SERVICE_ID, EMAILJS_TEMPLATE_ID, {
278
+ to_email: d.email,
279
+ subject: "¡Reseteo de Monedas en CoinTube!",
280
+ message: "Hola, te informamos que los puntos se han reseteado para un nuevo ciclo de premios. ¡Vuelve a CoinTube para subir al ranking!"
281
+ }, EMAILJS_PUBLIC_KEY);
282
+ count++;
283
  }
284
  });
285
+ alert(`Se han enviado ${count} notificaciones.`);
286
  };
287
 
288
+ if (!authReady) {
289
+ return (
290
+ <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900">
291
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mb-4"></div>
292
+ <p className="text-lg text-gray-400">Sincronizando con CoinTube...</p>
 
 
293
  </div>
294
+ );
295
+ }
296
+
297
+ if (!user) {
298
+ return (
299
+ <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
300
+ <div className="max-w-md w-full bg-gray-800 rounded-3xl shadow-2xl p-10 text-center border border-gray-700">
301
+ <div className="flex justify-center mb-8">
302
+ <div className="bg-blue-600 p-5 rounded-full shadow-lg animate-bounce">
303
+ <svg className="w-14 h-14 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
304
+ <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>
305
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
306
+ </svg>
307
+ </div>
308
+ </div>
309
+ <h1 className="text-4xl font-extrabold text-white mb-4">CoinTube</h1>
310
+ <p className="text-gray-400 mb-10 text-base">Gana monedas por ver contenido y promociona tus videos con nosotros.</p>
311
+ <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 shadow-xl">
312
+ <img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/action/google.svg" alt="G" className="w-6 h-6"/>
313
+ Empezar con Google
314
+ </button>
315
+ </div>
316
+ </div>
317
+ );
318
+ }
319
 
320
  return (
321
+ <div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-8 bg-gray-900">
322
+ <header className="flex flex-col md:flex-row justify-between items-center pb-6 mb-6 border-b border-gray-800 gap-4">
323
+ <div className="flex items-center gap-2">
324
+ <h1 className="text-3xl font-black text-white tracking-tighter">COIN<span className="text-blue-500">TUBE</span></h1>
325
+ <span className="bg-blue-500/10 text-blue-500 text-[10px] px-2 py-0.5 rounded-full font-bold border border-blue-500/20">BETA v2</span>
326
+ </div>
327
+
328
+ <div className="flex items-center gap-5">
329
+ <div className="flex flex-col items-end">
330
+ <span className="text-[10px] text-gray-500 font-mono">{user.email}</span>
331
+ <span className="text-2xl font-black text-yellow-400 drop-shadow-md">
332
+ {Math.floor(userProfile?.coins || 0)} <span className="text-sm">🪙</span>
333
+ </span>
334
  </div>
335
+ <button onClick={handleLogout} className="p-2 bg-gray-800 hover:bg-red-900/30 hover:text-red-400 rounded-lg transition-colors">
336
+ <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>
337
+ </button>
338
  </div>
339
  </header>
340
 
341
+ <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">
342
+ <button onClick={() => setCurrentTab('ganar')} className={`flex items-center gap-2 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'}`}>
343
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
344
+ Ver Videos
345
+ </button>
346
+ <button onClick={() => setCurrentTab('promocionar')} className={`flex items-center gap-2 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'}`}>
347
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/></svg>
348
+ Subir Video
349
+ </button>
350
  {user.email === ADMIN_EMAIL && (
351
+ <button onClick={sendMassNotification} className="flex items-center gap-2 px-8 py-3 rounded-xl font-bold bg-indigo-600 hover:bg-indigo-500 text-white shadow-lg shadow-indigo-600/20 transition-all">
352
+ 📧 Notificar
353
+ </button>
354
  )}
355
  </nav>
356
 
357
+ <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-8`}>
358
  <div className="lg:w-2/3">
359
+ <div className="aspect-video bg-black rounded-3xl overflow-hidden shadow-2xl border border-gray-800">
360
  <div id="player" className="w-full h-full"></div>
361
  </div>
362
+ <div className="mt-5 p-5 bg-gray-800/80 backdrop-blur-sm rounded-2xl border border-gray-700">
363
+ <h3 className="text-xl font-bold text-white mb-1">{currentVideoTitle}</h3>
364
+ <p className="text-xs text-blue-400 font-bold uppercase tracking-widest">En reproducción</p>
365
  </div>
366
  </div>
367
+
368
+ <aside className="lg:w-1/3 flex flex-col gap-6">
369
+ <div className="bg-gray-800 rounded-3xl p-6 flex-1 flex flex-col min-h-[300px] border border-gray-700 shadow-xl">
370
+ <div className="flex justify-between items-center mb-4">
371
+ <h3 className="text-gray-400 text-xs font-black uppercase tracking-tighter">Playlist Aleatoria</h3>
372
+ <span className="text-[10px] bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded font-mono">SHUFFLE ON</span>
373
+ </div>
374
+ <div className="overflow-y-auto flex-1 space-y-3 pr-2 custom-scrollbar">
375
  {videos.map((v) => (
376
+ <button key={v.id} onClick={() => loadVideo(v)} className={`w-full text-left p-3 rounded-xl flex justify-between items-center group transition-all border ${v.videoId === currentVideoId ? 'bg-blue-600/20 border-blue-500/50' : 'bg-gray-700/50 border-transparent hover:border-gray-600'}`}>
377
+ <div className={`text-sm truncate flex-1 font-medium ${v.videoId === currentVideoId ? 'text-blue-400' : 'text-gray-300'}`}>{v.title}</div>
378
  {user?.email === ADMIN_EMAIL && (
379
+ <div onClick={(e) => handleDeleteVideo(e, v.id)} className="ml-2 opacity-0 group-hover:opacity-100 text-red-500 hover:text-red-300 p-1">
380
+ <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fillRule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clipRule="evenodd"/></svg>
381
+ </div>
382
  )}
383
  </button>
384
  ))}
385
  </div>
386
  </div>
387
+
388
+ <div className="bg-gray-800 rounded-3xl p-6 flex-1 flex flex-col min-h-[300px] border border-gray-700 shadow-xl">
389
+ <h3 className="text-yellow-500 text-xs font-black uppercase tracking-tighter mb-4">Top Usuarios (Ranking)</h3>
390
+ <div className="overflow-y-auto flex-1 space-y-2 pr-2 custom-scrollbar">
391
  {leaderboard.map((u, i) => (
392
+ <div key={u.id} className="flex justify-between items-center p-3 bg-gray-700/30 rounded-xl border border-gray-600/30">
393
+ <div className="flex items-center gap-3">
394
+ <span className="text-[10px] font-bold text-gray-500 font-mono">#{i+1}</span>
395
+ <span className="text-sm text-gray-200 font-medium truncate w-32">{u.email?.split('@')[0]}</span>
396
+ </div>
397
+ <div className="flex items-center gap-3">
398
+ <span className="text-yellow-400 font-bold text-sm font-mono">{Math.floor(u.coins || 0)}</span>
399
  {user?.email === ADMIN_EMAIL && (
400
+ <button onClick={() => handleGiveCoins(u.id, u.coins)} className="bg-green-600/20 text-green-500 hover:bg-green-600 hover:text-white p-1 rounded-md transition-all">
401
+ <svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="3" d="M12 4v16m8-8H4"/></svg>
402
+ </button>
403
  )}
404
  </div>
405
  </div>
 
421
  const coins = userProfile?.coins || 0;
422
  const hasEnoughCoins = coins >= videoCost;
423
  if (isAdmin || hasEnoughCoins) return <PromoUnlocked userId={userId} isAdmin={isAdmin} videoCost={videoCost} db={db} />;
424
+
425
  return (
426
+ <div className="text-center p-12 bg-gray-800 rounded-3xl shadow-2xl max-w-xl border border-gray-700">
427
+ <h2 className="text-3xl font-black text-white mb-2">Puntos Insuficientes</h2>
428
+ <p className="text-gray-400 mb-8">Necesitas acumular {videoCost.toLocaleString()} monedas para subir tu video.</p>
429
+ <div className="w-full bg-gray-700 rounded-full h-4 mb-3 overflow-hidden p-1">
430
+ <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>
431
+ </div>
432
+ <p className="text-sm font-bold text-yellow-500 mb-8 font-mono">{Math.floor(coins).toLocaleString()} / {videoCost.toLocaleString()}</p>
433
+ <div className="bg-gray-900/50 p-6 rounded-2xl border border-dashed border-gray-600">
434
+ <p className="text-sm text-gray-400 mb-4 italic">Sigue viendo videos para completar tu meta automáticamente.</p>
435
+ <div className="flex justify-center gap-4 text-[10px] font-bold text-gray-500 uppercase">
436
+ <span>1 minuto ≈ 60 monedas</span>
437
+ </div>
438
  </div>
 
439
  </div>
440
  );
441
  }
 
448
  const handleSubmit = async (e) => {
449
  e.preventDefault();
450
  const vId = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/)?.[1];
451
+ if (!vId) return alert("Error: No parece ser un enlace de YouTube válido.");
452
+ if (!title.trim()) return alert("Error: Por favor escribe un título.");
453
+
454
  setLoading(true);
455
  try {
456
+ if (!isAdmin) {
457
+ const userRef = doc(db, 'users', userId);
458
+ await updateDoc(userRef, { coins: increment(-videoCost) });
459
+ }
460
  await addDoc(collection(db, 'videos'), {
461
+ title: title,
462
+ videoId: vId,
463
+ addedBy: userId,
464
+ createdAt: serverTimestamp(),
465
+ isPromoted: !isAdmin
466
  });
467
+ alert("¡ÉXITO! Tu video ha sido publicado en la red.");
468
  setUrl(''); setTitle('');
469
+ } catch (err) { console.error(err); alert("Error al subir."); } finally { setLoading(false); }
470
  };
471
 
472
  return (
473
+ <div className="bg-gray-800 p-10 rounded-3xl shadow-2xl max-w-md w-full border-2 border-blue-500/20">
474
+ <h2 className="text-3xl font-black text-white mb-2 text-center">{isAdmin ? "MODO ADMIN" : "PROMOCIONAR"}</h2>
475
+ <p className="text-center text-gray-400 text-sm mb-8">{isAdmin ? "Sube contenido sin costo administrativo" : "Tu video aparecerá en la playlist de todos los usuarios"}</p>
476
+ <form onSubmit={handleSubmit} className="space-y-5">
477
+ <div>
478
+ <label className="text-xs font-black text-gray-500 uppercase ml-1 mb-2 block">Link de YouTube</label>
479
+ <input type="url" placeholder="https://..." 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 />
480
+ </div>
481
+ <div>
482
+ <label className="text-xs font-black text-gray-500 uppercase ml-1 mb-2 block">Título del Video</label>
483
+ <input type="text" placeholder="Mi Video Épico" 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} />
484
+ </div>
485
+ <button disabled={loading} className="w-full py-4 bg-blue-600 hover:bg-blue-500 text-white font-black rounded-xl shadow-lg shadow-blue-600/20 transition-all disabled:opacity-50">
486
+ {loading ? 'PUBLICANDO...' : (isAdmin ? 'SUBIR AHORA' : `CANJEAR ${videoCost.toLocaleString()} MONEDAS`)}
487
+ </button>
488
+ </form>
489
+ </div>
490
  );
491
  }