salomonsky commited on
Commit
5edf1aa
·
verified ·
1 Parent(s): ed8b4af

Update cointube.jsx

Browse files
Files changed (1) hide show
  1. cointube.jsx +103 -200
cointube.jsx CHANGED
@@ -22,8 +22,8 @@ import {
22
  limit
23
  } from 'firebase/firestore';
24
 
25
- // --- Variables Globales (se llenarán después) ---
26
- let app, auth, db;
27
 
28
  // --- Constantes del Sistema ---
29
  const ADMIN_EMAIL = "photonicsupernova@gmail.com";
@@ -33,7 +33,7 @@ const DEFAULT_VIDEO_TITLE = "Pelicula Completa";
33
 
34
  export default function App() {
35
 
36
- // --- Estado de Inicialización ---
37
  const [firebaseReady, setFirebaseReady] = useState(false);
38
  const [configError, setConfigError] = useState(null);
39
 
@@ -53,40 +53,34 @@ export default function App() {
53
  const coinIntervalRef = useRef(null);
54
  const localCoinBufferRef = useRef(0);
55
 
56
- // --- EFECTO 0: OBTENER CONFIGURACIÓN DEL SERVIDOR (RUNTIME) ---
57
  useEffect(() => {
58
  const initFirebase = async () => {
59
  try {
60
- // 1. Pedimos la config al servidor
61
  const response = await fetch('/api/config');
62
- if (!response.ok) throw new Error("Falló la carga de configuración");
63
 
64
  const config = await response.json();
65
 
66
- // 2. Iniciamos Firebase con los datos recibidos
67
  if (!app) {
68
  app = initializeApp(config);
69
  auth = getAuth(app);
70
  db = getFirestore(app);
71
- console.log("Firebase conectado vía Secret Runtime");
72
  }
73
  setFirebaseReady(true);
74
 
75
  } catch (error) {
76
- console.error("Error crítico:", error);
77
- setConfigError("No se pudo conectar con el servidor de autenticación.");
78
  }
79
  };
80
-
81
  initFirebase();
82
  }, []);
83
 
84
- // --- Efectos (Solo corren si firebaseReady es true) ---
85
-
86
- // 1. Auth
87
  useEffect(() => {
88
  if (!firebaseReady || !auth) return;
89
-
90
  const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
91
  if (user) {
92
  setUser(user);
@@ -101,7 +95,7 @@ export default function App() {
101
  return () => unsubscribeAuth();
102
  }, [firebaseReady]);
103
 
104
- // 2. Perfil de Usuario
105
  useEffect(() => {
106
  if (!firebaseReady || !userId || !db) return;
107
  const profileRef = doc(db, 'users', userId);
@@ -121,7 +115,7 @@ export default function App() {
121
  return () => unsubscribeProfile();
122
  }, [userId, user, firebaseReady]);
123
 
124
- // 3. Cargar Lista de Videos
125
  useEffect(() => {
126
  if (!firebaseReady || !db) return;
127
  const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(20));
@@ -130,16 +124,13 @@ export default function App() {
130
  if (fetchedVideos.length > 0) {
131
  setVideos(fetchedVideos);
132
  } else {
133
- setVideos([
134
- { id: '1', title: 'Pelicula Completa', videoId: 'UtEhewStfMA' },
135
- { id: '2', title: 'Documental Comida', videoId: 'lwiNN7WUw50' }
136
- ]);
137
  }
138
  });
139
  return () => unsubscribe();
140
  }, [firebaseReady]);
141
 
142
- // 4. LEADERBOARD
143
  useEffect(() => {
144
  if (!firebaseReady || !db) return;
145
  const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(20));
@@ -150,7 +141,7 @@ export default function App() {
150
  return () => unsubscribe();
151
  }, [firebaseReady]);
152
 
153
- // 5. API YouTube
154
  useEffect(() => {
155
  if (!window.YT) {
156
  const tag = document.createElement('script');
@@ -209,7 +200,6 @@ export default function App() {
209
  }
210
  }, [startCoinInterval, stopCoinInterval, currentVideoId, userId]);
211
 
212
- // Inicializar Player
213
  useEffect(() => {
214
  if (ytApiReady && userId && !playerRef.current) {
215
  playerRef.current = new window.YT.Player('player', {
@@ -233,7 +223,7 @@ export default function App() {
233
  if (playerRef.current) playerRef.current.loadVideoById(video.videoId);
234
  };
235
 
236
- // --- FUNCIONES DE LOGIN ---
237
  const handleGoogleLogin = async () => {
238
  if (!auth) return;
239
  const provider = new GoogleAuthProvider();
@@ -250,140 +240,78 @@ export default function App() {
250
  await signOut(auth);
251
  };
252
 
253
- // --- Renderizado de Carga / Error ---
254
  if (configError) {
255
- return (
256
- <div className="flex flex-col items-center justify-center h-full w-full text-red-500">
257
- <p>⚠️ {configError}</p>
258
- </div>
259
- );
260
  }
261
 
262
  if (!firebaseReady || !authReady) {
263
  return (
264
- <div className="flex flex-col items-center justify-center h-full w-full">
265
  <div className="spinner mb-4"></div>
266
- <p className="text-lg text-gray-400">
267
- {!firebaseReady ? "Obteniendo configuración segura..." : "Conectando usuario..."}
268
- </p>
269
  </div>
270
  );
271
  }
272
 
273
- // --- Renderizado de Login ---
274
  if (authReady && !user) {
275
  return (
276
- <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
277
- <div className="max-w-md w-full bg-gray-800 rounded-2xl shadow-2xl p-8 text-center border border-gray-700">
278
- <div className="flex justify-center mb-6">
279
- <div className="bg-blue-600 p-4 rounded-full shadow-lg">
280
- <svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
281
- <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>
282
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
283
- </svg>
284
- </div>
285
- </div>
286
- <h1 className="text-3xl font-bold text-white mb-2 tracking-tight">Bienvenido a CoinTube</h1>
287
- <p className="text-gray-400 mb-8 text-sm leading-relaxed">
288
- Gana monedas viendo videos y promociona tu contenido. <br/>
289
- <span className="text-yellow-500 font-bold">¡Llega a 10,000 monedas para subir tu video!</span>
290
- </p>
291
- <div className="space-y-3">
292
- <button
293
- onClick={handleGoogleLogin}
294
- 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"
295
- >
296
- <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
297
- <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"/>
298
- <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"/>
299
- <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"/>
300
- <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"/>
301
- </svg>
302
- Continuar con Google
303
- </button>
304
- </div>
305
  </div>
306
  </div>
307
  );
308
  }
309
 
310
- // --- App Principal ---
311
  return (
312
  <div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-6">
313
- <header className="flex flex-col md:flex-row justify-between items-center pb-4 mb-4 border-b border-gray-700">
314
- <h1 className="text-3xl font-bold text-white">Coin<span className="text-blue-500">Tube</span></h1>
315
- <div className="flex items-center space-x-4 mt-4 md:mt-0">
316
- <div className="flex flex-col items-end">
317
- <span className="text-sm text-gray-400">{user.email}</span>
318
- <span className="text-lg font-bold text-yellow-400">
319
- {Math.floor(userProfile?.coins || 0)} Monedas
320
- </span>
321
- </div>
322
- <button
323
- onClick={handleLogout}
324
- className="px-4 py-2 bg-gray-700 text-white text-sm font-medium rounded-lg hover:bg-gray-600 transition-colors"
325
- >
326
- Salir
327
- </button>
328
  </div>
329
  </header>
330
 
331
- <nav className="flex space-x-1 mb-6 rounded-lg bg-gray-800 p-1 max-w-md mx-auto">
332
- <button
333
- onClick={() => setCurrentTab('ganar')}
334
- className={`flex-1 py-2 px-4 rounded-lg text-center font-medium text-sm transition-colors ${currentTab === 'ganar' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
335
- >
336
- Ganar Monedas
337
- </button>
338
- <button
339
- onClick={() => setCurrentTab('promocionar')}
340
- className={`flex-1 py-2 px-4 rounded-lg text-center font-medium text-sm transition-colors ${currentTab === 'promocionar' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
341
- >
342
- Promocionar Video
343
- </button>
344
  </nav>
345
 
346
- {/* TAB: GANAR MONEDAS */}
347
- <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6 overflow-hidden`}>
348
- <div className="lg:w-2/3 flex flex-col">
349
- <div className="w-full bg-black rounded-lg shadow-2xl overflow-hidden">
350
- <div className="aspect-video w-full">
351
- <div id="player" className="w-full h-full"></div>
352
- </div>
353
  </div>
354
- <div className="mt-4 p-4 bg-gray-800 rounded-lg">
355
- <h3 className="text-lg font-semibold text-white">{currentVideoTitle}</h3>
356
  </div>
357
  </div>
358
-
359
  <aside className="lg:w-1/3 flex flex-col gap-4 h-full overflow-hidden">
360
- <div className="bg-gray-800 rounded-lg shadow-lg p-4 flex-1 flex flex-col min-h-[200px]">
361
- <h2 className="text-xl font-semibold mb-4 pb-2 border-b border-gray-700">Videos Disponibles</h2>
362
- <div className="flex-1 overflow-y-auto space-y-2 pr-2 custom-scrollbar">
363
  {videos.map((video) => (
364
- <button
365
- key={video.id}
366
- onClick={() => loadVideo(video)}
367
- className={`w-full text-left px-4 py-3 rounded-lg transition-colors focus:outline-none ${video.videoId === currentVideoId ? 'bg-blue-900 border-l-4 border-blue-500' : 'bg-gray-700 hover:bg-gray-600'}`}
368
- >
369
- <div className="text-sm font-medium truncate">{video.title || `Video`}</div>
370
  </button>
371
  ))}
372
  </div>
373
  </div>
374
-
375
- <div className="bg-gray-800 rounded-lg shadow-lg p-4 flex-1 flex flex-col min-h-[200px]">
376
- <h2 className="text-xl font-semibold mb-4 pb-2 border-b border-gray-700 text-yellow-400">Top Usuarios</h2>
377
- <div className="flex-1 overflow-y-auto space-y-2 pr-2 custom-scrollbar">
378
- {leaderboard.map((u, index) => (
379
- <div key={u.id || index} className="flex justify-between items-center p-2 bg-gray-700 rounded hover:bg-gray-600">
380
- <div className="flex items-center truncate">
381
- <span className={`mr-2 font-bold ${index < 3 ? 'text-yellow-400' : 'text-gray-400'}`}>#{index + 1}</span>
382
- <span className="text-xs md:text-sm truncate max-w-[120px]" title={u.email}>
383
- {u.email ? u.email.split('@')[0] : 'Usuario'}
384
- </span>
385
- </div>
386
- <span className="text-xs font-mono text-green-400">{Math.floor(u.coins || 0)} 🪙</span>
387
  </div>
388
  ))}
389
  </div>
@@ -391,66 +319,50 @@ export default function App() {
391
  </aside>
392
  </main>
393
 
394
- {/* TAB: PROMOCIONAR */}
395
- <main className={`${currentTab === 'promocionar' ? 'flex' : 'hidden'} flex-1 flex-col items-center justify-center`}>
396
  <PromoCheck
397
  user={user}
398
  userProfile={userProfile}
399
  userId={userId}
400
  adminEmail={ADMIN_EMAIL}
401
  videoCost={VIDEO_COST}
 
402
  />
403
  </main>
404
  </div>
405
  );
406
  }
407
 
408
- function PromoCheck({ user, userProfile, userId, adminEmail, videoCost }) {
 
 
409
  const isAdmin = user?.email === adminEmail;
410
  const coins = userProfile?.coins || 0;
411
  const hasEnoughCoins = coins >= videoCost;
412
 
413
- if (isAdmin) {
414
- return <PromoUnlocked userId={userId} isAdmin={true} />;
415
- }
416
-
417
- if (hasEnoughCoins) {
418
- return <PromoUnlocked userId={userId} isAdmin={false} videoCost={videoCost} />;
419
- }
420
 
421
  return (
422
  <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg border border-gray-700">
423
- <div className="mb-4 relative inline-block">
424
- <svg className="w-20 h-20 text-gray-600 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
425
- <div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full">Locked</div>
 
426
  </div>
427
- <h2 className="text-2xl font-bold mb-2 text-white">Necesitas más monedas</h2>
428
- <p className="text-gray-300 mb-6">
429
- Para agregar tu video a la lista pública necesitas acumular <span className="text-yellow-400 font-bold">10,000 monedas</span>.
430
- </p>
431
-
432
- <div className="bg-gray-900 p-4 rounded-lg mb-4">
433
- <div className="flex justify-between text-sm mb-1 text-gray-400">
434
- <span>Progreso</span>
435
- <span>{Math.floor(coins)} / {videoCost}</span>
436
- </div>
437
- <div className="w-full bg-gray-700 rounded-full h-2.5">
438
- <div className="bg-yellow-400 h-2.5 rounded-full transition-all duration-500" style={{ width: `${Math.min((coins / videoCost) * 100, 100)}%` }}></div>
439
- </div>
440
- </div>
441
-
442
- <p className="text-gray-400 text-sm">¡Ve a "Ganar Monedas" y mira videos para sumar puntos!</p>
443
  </div>
444
  );
445
  }
446
 
447
- function PromoUnlocked({ userId, isAdmin, videoCost }) {
448
  const [url, setUrl] = useState('');
449
- const [message, setMessage] = useState({ text: '', type: 'success' });
450
  const [loading, setLoading] = useState(false);
451
 
452
  const extractYouTubeID = (url) => {
453
- const regex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
454
  const match = url.match(regex);
455
  return match ? match[1] : null;
456
  };
@@ -460,7 +372,12 @@ function PromoUnlocked({ userId, isAdmin, videoCost }) {
460
  const videoId = extractYouTubeID(url);
461
 
462
  if (!videoId) {
463
- setMessage({ text: "URL de YouTube no válida.", type: 'error' });
 
 
 
 
 
464
  return;
465
  }
466
 
@@ -468,67 +385,53 @@ function PromoUnlocked({ userId, isAdmin, videoCost }) {
468
  const title = isAdmin ? `Recomendado (Admin)` : `Video de Usuario`;
469
 
470
  try {
471
- if (!isAdmin && db) {
 
472
  const userRef = doc(db, 'users', userId);
473
  await updateDoc(userRef, { coins: increment(-videoCost) });
474
  }
475
 
476
- if (db) {
477
- const videosRef = collection(db, 'videos');
478
- await addDoc(videosRef, {
479
- title: title,
480
- videoId: videoId,
481
- addedBy: userId,
482
- createdAt: serverTimestamp(),
483
- isPromoted: !isAdmin
484
- });
485
- }
486
 
487
- setMessage({ text: "¡Video agregado con éxito!", type: 'success' });
488
  setUrl('');
489
  } catch (error) {
490
- console.error("Error adding video:", error);
491
- setMessage({ text: "Error al procesar. Intenta de nuevo.", type: 'error' });
 
 
 
 
 
492
  } finally {
493
  setLoading(false);
494
  }
495
  };
496
 
497
  return (
498
- <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg w-full border border-green-500/30">
499
- <div className="flex justify-center mb-4">
500
- <div className="bg-green-500/20 p-3 rounded-full">
501
- <svg className="w-10 h-10 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 11V7a4 4 0 118 0m-4 8v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"></path></svg>
502
- </div>
503
- </div>
504
- <h2 className="text-2xl font-bold mb-2 text-white">
505
- {isAdmin ? "Panel de Administrador" : "¡Objetivo Logrado!"}
506
- </h2>
507
- <p className="text-gray-300 mb-6">
508
- {isAdmin
509
- ? "Como administrador, puedes agregar videos ilimitados."
510
- : `Canjea ${videoCost} monedas para agregar tu video a la lista pública.`}
511
- </p>
512
-
513
- <form onSubmit={handleSubmit} className="flex flex-col space-y-4">
514
  <input
515
  type="url"
516
- placeholder="https://www.youtube.com/watch?v=..."
517
- className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-blue-500 placeholder-gray-500"
518
- required
519
  value={url}
520
  onChange={(e) => setUrl(e.target.value)}
521
- disabled={loading}
522
  />
523
- <button
524
- type="submit"
525
- disabled={loading}
526
- className={`w-full px-6 py-2 font-bold rounded-lg shadow-md transition-all ${loading ? 'bg-gray-600 cursor-not-allowed' : 'bg-green-600 hover:bg-green-700 text-white'}`}
527
- >
528
- {loading ? 'Procesando...' : (isAdmin ? 'Agregar Video Gratis' : `Pagar ${videoCost} Monedas y Agregar`)}
529
  </button>
530
  {message.text && (
531
- <p className={`text-sm font-medium ${message.type === 'success' ? 'text-green-400' : 'text-red-400'}`}>
532
  {message.text}
533
  </p>
534
  )}
 
22
  limit
23
  } from 'firebase/firestore';
24
 
25
+ // --- Variables Globales ---
26
+ let app, auth, db; // Se inicializan más tarde
27
 
28
  // --- Constantes del Sistema ---
29
  const ADMIN_EMAIL = "photonicsupernova@gmail.com";
 
33
 
34
  export default function App() {
35
 
36
+ // --- Estados de Inicialización ---
37
  const [firebaseReady, setFirebaseReady] = useState(false);
38
  const [configError, setConfigError] = useState(null);
39
 
 
53
  const coinIntervalRef = useRef(null);
54
  const localCoinBufferRef = useRef(0);
55
 
56
+ // --- 1. OBTENER CONFIGURACIÓN DEL SERVIDOR ---
57
  useEffect(() => {
58
  const initFirebase = async () => {
59
  try {
 
60
  const response = await fetch('/api/config');
61
+ if (!response.ok) throw new Error("Error de red al cargar config");
62
 
63
  const config = await response.json();
64
 
 
65
  if (!app) {
66
  app = initializeApp(config);
67
  auth = getAuth(app);
68
  db = getFirestore(app);
69
+ console.log("Firebase conectado con éxito.");
70
  }
71
  setFirebaseReady(true);
72
 
73
  } catch (error) {
74
+ console.error("Error inicializando:", error);
75
+ setConfigError("Error de conexión con el servidor.");
76
  }
77
  };
 
78
  initFirebase();
79
  }, []);
80
 
81
+ // --- 2. Auth ---
 
 
82
  useEffect(() => {
83
  if (!firebaseReady || !auth) return;
 
84
  const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
85
  if (user) {
86
  setUser(user);
 
95
  return () => unsubscribeAuth();
96
  }, [firebaseReady]);
97
 
98
+ // --- 3. Perfil de Usuario ---
99
  useEffect(() => {
100
  if (!firebaseReady || !userId || !db) return;
101
  const profileRef = doc(db, 'users', userId);
 
115
  return () => unsubscribeProfile();
116
  }, [userId, user, firebaseReady]);
117
 
118
+ // --- 4. Cargar Videos ---
119
  useEffect(() => {
120
  if (!firebaseReady || !db) return;
121
  const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(20));
 
124
  if (fetchedVideos.length > 0) {
125
  setVideos(fetchedVideos);
126
  } else {
127
+ setVideos([{ id: '1', title: 'Pelicula Default', videoId: DEFAULT_VIDEO_ID }]);
 
 
 
128
  }
129
  });
130
  return () => unsubscribe();
131
  }, [firebaseReady]);
132
 
133
+ // --- 5. Leaderboard ---
134
  useEffect(() => {
135
  if (!firebaseReady || !db) return;
136
  const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(20));
 
141
  return () => unsubscribe();
142
  }, [firebaseReady]);
143
 
144
+ // --- 6. YouTube API ---
145
  useEffect(() => {
146
  if (!window.YT) {
147
  const tag = document.createElement('script');
 
200
  }
201
  }, [startCoinInterval, stopCoinInterval, currentVideoId, userId]);
202
 
 
203
  useEffect(() => {
204
  if (ytApiReady && userId && !playerRef.current) {
205
  playerRef.current = new window.YT.Player('player', {
 
223
  if (playerRef.current) playerRef.current.loadVideoById(video.videoId);
224
  };
225
 
226
+ // --- Handlers ---
227
  const handleGoogleLogin = async () => {
228
  if (!auth) return;
229
  const provider = new GoogleAuthProvider();
 
240
  await signOut(auth);
241
  };
242
 
243
+ // --- Render Views ---
244
  if (configError) {
245
+ return <div className="flex justify-center items-center h-full text-red-500">{configError}</div>;
 
 
 
 
246
  }
247
 
248
  if (!firebaseReady || !authReady) {
249
  return (
250
+ <div className="flex justify-center items-center h-full">
251
  <div className="spinner mb-4"></div>
 
 
 
252
  </div>
253
  );
254
  }
255
 
 
256
  if (authReady && !user) {
257
  return (
258
+ <div className="flex flex-col items-center justify-center h-full bg-gray-900 p-4">
259
+ <div className="bg-gray-800 p-8 rounded-2xl shadow-xl text-center max-w-md w-full border border-gray-700">
260
+ <h1 className="text-3xl font-bold text-white mb-4">CoinTube</h1>
261
+ <button onClick={handleGoogleLogin} className="w-full py-3 bg-white text-gray-900 font-bold rounded-lg hover:bg-gray-100">
262
+ Entrar con Google
263
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  </div>
265
  </div>
266
  );
267
  }
268
 
 
269
  return (
270
  <div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-6">
271
+ <header className="flex justify-between items-center pb-4 mb-4 border-b border-gray-700">
272
+ <h1 className="text-2xl md:text-3xl font-bold text-white">Coin<span className="text-blue-500">Tube</span></h1>
273
+ <div className="flex items-center gap-4">
274
+ <div className="text-right">
275
+ <div className="text-xs text-gray-400">{user.email}</div>
276
+ <div className="text-lg font-bold text-yellow-400">{Math.floor(userProfile?.coins || 0)} 🪙</div>
277
+ </div>
278
+ <button onClick={handleLogout} className="px-3 py-1 bg-gray-700 text-white text-sm rounded hover:bg-gray-600">Salir</button>
 
 
 
 
 
 
 
279
  </div>
280
  </header>
281
 
282
+ <nav className="flex gap-2 mb-6 bg-gray-800 p-1 rounded-lg max-w-md mx-auto">
283
+ <button onClick={() => setCurrentTab('ganar')} className={`flex-1 py-2 rounded ${currentTab === 'ganar' ? 'bg-blue-600 text-white' : 'text-gray-400'}`}>Ver Videos</button>
284
+ <button onClick={() => setCurrentTab('promocionar')} className={`flex-1 py-2 rounded ${currentTab === 'promocionar' ? 'bg-blue-600 text-white' : 'text-gray-400'}`}>Subir Video</button>
 
 
 
 
 
 
 
 
 
 
285
  </nav>
286
 
287
+ {/* TAB: GANAR */}
288
+ <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6`}>
289
+ <div className="lg:w-2/3">
290
+ <div className="aspect-video bg-black rounded-lg overflow-hidden shadow-xl">
291
+ <div id="player" className="w-full h-full"></div>
 
 
292
  </div>
293
+ <div className="mt-3 p-3 bg-gray-800 rounded">
294
+ <h3 className="text-white font-medium">{currentVideoTitle}</h3>
295
  </div>
296
  </div>
 
297
  <aside className="lg:w-1/3 flex flex-col gap-4 h-full overflow-hidden">
298
+ <div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
299
+ <h3 className="text-gray-400 text-sm uppercase font-bold mb-2">Lista de Reproducción</h3>
300
+ <div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
301
  {videos.map((video) => (
302
+ <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'}`}>
303
+ <div className="text-sm text-white truncate">{video.title}</div>
 
 
 
 
304
  </button>
305
  ))}
306
  </div>
307
  </div>
308
+ <div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
309
+ <h3 className="text-yellow-500 text-sm uppercase font-bold mb-2">Ranking</h3>
310
+ <div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
311
+ {leaderboard.map((u, i) => (
312
+ <div key={u.id || i} className="flex justify-between p-2 bg-gray-700 rounded text-sm">
313
+ <span className="text-white">#{i+1} {u.email?.split('@')[0]}</span>
314
+ <span className="text-yellow-400">{Math.floor(u.coins)}</span>
 
 
 
 
 
 
315
  </div>
316
  ))}
317
  </div>
 
319
  </aside>
320
  </main>
321
 
322
+ {/* TAB: PROMOCIONAR (Pasamos db como prop para asegurar que llegue) */}
323
+ <main className={`${currentTab === 'promocionar' ? 'flex' : 'hidden'} flex-1 items-center justify-center`}>
324
  <PromoCheck
325
  user={user}
326
  userProfile={userProfile}
327
  userId={userId}
328
  adminEmail={ADMIN_EMAIL}
329
  videoCost={VIDEO_COST}
330
+ db={db}
331
  />
332
  </main>
333
  </div>
334
  );
335
  }
336
 
337
+ // --- Componentes Hijos ---
338
+
339
+ function PromoCheck({ user, userProfile, userId, adminEmail, videoCost, db }) {
340
  const isAdmin = user?.email === adminEmail;
341
  const coins = userProfile?.coins || 0;
342
  const hasEnoughCoins = coins >= videoCost;
343
 
344
+ if (isAdmin) return <PromoUnlocked userId={userId} isAdmin={true} db={db} />;
345
+ if (hasEnoughCoins) return <PromoUnlocked userId={userId} isAdmin={false} videoCost={videoCost} db={db} />;
 
 
 
 
 
346
 
347
  return (
348
  <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg border border-gray-700">
349
+ <h2 className="text-2xl font-bold text-white mb-2">Insuficientes Monedas</h2>
350
+ <p className="text-gray-400 mb-4">Necesitas {videoCost} monedas para subir un video.</p>
351
+ <div className="w-full bg-gray-700 rounded-full h-3 mb-2">
352
+ <div className="bg-yellow-400 h-3 rounded-full" style={{ width: `${Math.min((coins / videoCost) * 100, 100)}%` }}></div>
353
  </div>
354
+ <p className="text-sm text-gray-500">{Math.floor(coins)} / {videoCost}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  </div>
356
  );
357
  }
358
 
359
+ function PromoUnlocked({ userId, isAdmin, videoCost, db }) {
360
  const [url, setUrl] = useState('');
361
+ const [message, setMessage] = useState({ text: '', type: '' });
362
  const [loading, setLoading] = useState(false);
363
 
364
  const extractYouTubeID = (url) => {
365
+ const regex = /(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/;
366
  const match = url.match(regex);
367
  return match ? match[1] : null;
368
  };
 
372
  const videoId = extractYouTubeID(url);
373
 
374
  if (!videoId) {
375
+ setMessage({ text: "Enlace no válido", type: 'error' });
376
+ return;
377
+ }
378
+
379
+ if (!db) {
380
+ setMessage({ text: "Error: Base de datos no conectada", type: 'error' });
381
  return;
382
  }
383
 
 
385
  const title = isAdmin ? `Recomendado (Admin)` : `Video de Usuario`;
386
 
387
  try {
388
+ // Usamos el db que nos pasaron por PROPS
389
+ if (!isAdmin) {
390
  const userRef = doc(db, 'users', userId);
391
  await updateDoc(userRef, { coins: increment(-videoCost) });
392
  }
393
 
394
+ const videosRef = collection(db, 'videos');
395
+ await addDoc(videosRef, {
396
+ title: title,
397
+ videoId: videoId,
398
+ addedBy: userId,
399
+ createdAt: serverTimestamp(),
400
+ isPromoted: !isAdmin
401
+ });
 
 
402
 
403
+ setMessage({ text: "¡Video agregado!", type: 'success' });
404
  setUrl('');
405
  } catch (error) {
406
+ console.error("Error al subir:", error);
407
+ // Muestra el error real en pantalla si es de permisos
408
+ if (error.code === 'permission-denied') {
409
+ setMessage({ text: "Permiso denegado: Revisa las reglas de Firestore", type: 'error' });
410
+ } else {
411
+ setMessage({ text: "Error al procesar. Intenta de nuevo.", type: 'error' });
412
+ }
413
  } finally {
414
  setLoading(false);
415
  }
416
  };
417
 
418
  return (
419
+ <div className="bg-gray-800 p-8 rounded-lg shadow-xl max-w-md w-full border border-green-900">
420
+ <h2 className="text-2xl font-bold text-white mb-4 text-center">{isAdmin ? "Admin Panel" : "¡Meta Alcanzada!"}</h2>
421
+ <form onSubmit={handleSubmit} className="space-y-4">
 
 
 
 
 
 
 
 
 
 
 
 
 
422
  <input
423
  type="url"
424
+ placeholder="Enlace de YouTube..."
425
+ className="w-full p-3 bg-gray-700 text-white rounded border border-gray-600 focus:border-blue-500 outline-none"
 
426
  value={url}
427
  onChange={(e) => setUrl(e.target.value)}
428
+ required
429
  />
430
+ <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">
431
+ {loading ? 'Procesando...' : (isAdmin ? 'Subir Gratis' : `Pagar ${videoCost} y Subir`)}
 
 
 
 
432
  </button>
433
  {message.text && (
434
+ <p className={`text-center font-medium ${message.type === 'error' ? 'text-red-400' : 'text-green-400'}`}>
435
  {message.text}
436
  </p>
437
  )}