salomonsky commited on
Commit
197ca8d
·
verified ·
1 Parent(s): 6452ace

Update cointube.jsx

Browse files
Files changed (1) hide show
  1. cointube.jsx +89 -234
cointube.jsx CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import React, { useState, useEffect, useRef, useCallback } from 'react';
2
  import { initializeApp } from 'firebase/app';
3
  import {
@@ -5,7 +7,9 @@ import {
5
  onAuthStateChanged,
6
  GoogleAuthProvider,
7
  signInWithPopup,
8
- signOut
 
 
9
  } from 'firebase/auth';
10
  import {
11
  getFirestore,
@@ -18,12 +22,9 @@ import {
18
  increment,
19
  serverTimestamp,
20
  query,
21
- orderBy,
22
- limit,
23
  setLogLevel
24
  } from 'firebase/firestore';
25
 
26
- // --- Constantes de Firebase ---
27
  const firebaseConfig = {
28
  apiKey: "AIzaSyCwJjVJGuOmA_PKRbaTnQDrK-Q07NI_utc",
29
  authDomain: "insights-5c2d6.firebaseapp.com",
@@ -38,27 +39,21 @@ try {
38
  app = initializeApp(firebaseConfig);
39
  auth = getAuth(app);
40
  db = getFirestore(app);
 
41
  } catch (error) {
42
  console.error("Error inicializando Firebase:", error);
43
  }
44
 
45
  const DEFAULT_VIDEO_ID = "UtEhewStfMA";
46
- const DEFAULT_VIDEO_TITLE = "Pelicula Completa";
47
- const COIN_GOAL = 10000;
48
 
49
  export default function App() {
50
 
51
- // Lista de videos inicializada con datos fijos para el primer render
52
- const [videos, setVideos] = useState([
53
- { id: '1', title: DEFAULT_VIDEO_TITLE, videoId: DEFAULT_VIDEO_ID },
54
- { id: '2', title: 'Documental Comida', videoId: 'lwiNN7WUw50' },
55
- { id: '3', title: 'Cine de Arte', videoId: 'GxEx6Kgo6Es' },
56
- ]);
57
-
58
  const [user, setUser] = useState(null);
59
  const [userId, setUserId] = useState(null);
60
  const [authReady, setAuthReady] = useState(false);
61
  const [userProfile, setUserProfile] = useState(null);
 
62
  const [currentTab, setCurrentTab] = useState('ganar');
63
  const [currentVideoId, setCurrentVideoId] = useState(DEFAULT_VIDEO_ID);
64
  const [currentVideoTitle, setCurrentVideoTitle] = useState(DEFAULT_VIDEO_TITLE);
@@ -68,12 +63,9 @@ export default function App() {
68
  const coinIntervalRef = useRef(null);
69
  const localCoinBufferRef = useRef(0);
70
 
71
- // 1. Auth
72
  useEffect(() => {
73
- if (!auth) {
74
- setAuthReady(true);
75
- return;
76
- }
77
  const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
78
  if (user) {
79
  setUser(user);
@@ -85,45 +77,46 @@ export default function App() {
85
  }
86
  setAuthReady(true);
87
  });
 
 
 
88
  return () => unsubscribeAuth();
89
  }, []);
90
 
91
- // 2. Perfil de Usuario
92
  useEffect(() => {
93
  if (!userId || !db) return;
 
94
  const profileRef = doc(db, 'users', userId);
 
95
  const unsubscribeProfile = onSnapshot(profileRef, (docSnap) => {
96
  if (docSnap.exists()) {
97
  setUserProfile(docSnap.data());
98
  } else {
99
- const name = user?.displayName || `Usuario ${userId.substring(0, 4)}`;
100
- const newProfile = {
101
- coins: 0,
102
- watchedDefaultVideo: false,
103
- name: name
104
- };
105
- setDoc(profileRef, newProfile).catch(e => console.error(e));
106
  setUserProfile(newProfile);
107
  }
108
- }, (error) => console.error("Error perfil:", error));
 
 
 
109
  return () => unsubscribeProfile();
110
- }, [userId, user]);
111
 
112
- // 3. Videos de Firebase (RESTAURADO: Carga dinámica)
113
  useEffect(() => {
114
  if (!db) return;
 
115
  const videosRef = collection(db, 'videos');
116
- const q = query(videosRef, orderBy('createdAt', 'desc'), limit(100)); // Límite para evitar sobrecarga
117
 
118
  const unsubscribeVideos = onSnapshot(q, (snapshot) => {
119
  if (snapshot.empty) {
120
- // Si no hay videos, se añade el por defecto solo en la base de datos
121
  addDoc(videosRef, {
122
  title: DEFAULT_VIDEO_TITLE,
123
  videoId: DEFAULT_VIDEO_ID,
124
  addedBy: "system",
125
  createdAt: serverTimestamp()
126
- }).catch(e => console.error(e));
127
  } else {
128
  const videoList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
129
  setVideos(videoList);
@@ -134,28 +127,31 @@ export default function App() {
134
 
135
  return () => unsubscribeVideos();
136
  }, []);
137
-
138
- // 4. API YouTube
139
  useEffect(() => {
140
  if (!window.YT) {
141
  const tag = document.createElement('script');
142
  tag.src = "https://www.youtube.com/iframe_api";
143
  const firstScriptTag = document.getElementsByTagName('script')[0];
144
  firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
145
- window.onYouTubeIframeAPIReady = () => setYtApiReady(true);
 
 
 
146
  } else {
147
  setYtApiReady(true);
148
  }
149
  }, []);
150
 
151
- // --- Lógica de Monedas ---
152
  const saveCoinBuffer = useCallback(() => {
153
  if (localCoinBufferRef.current > 0 && userId) {
154
  const coinsToSave = localCoinBufferRef.current;
155
  localCoinBufferRef.current = 0;
156
  const profileRef = doc(db, 'users', userId);
157
  updateDoc(profileRef, { coins: increment(coinsToSave) })
158
- .catch(e => { localCoinBufferRef.current += coinsToSave; });
 
 
159
  }
160
  }, [userId]);
161
 
@@ -163,7 +159,9 @@ export default function App() {
163
  if (coinIntervalRef.current) return;
164
  coinIntervalRef.current = setInterval(() => {
165
  localCoinBufferRef.current++;
166
- if (localCoinBufferRef.current >= 5) saveCoinBuffer();
 
 
167
  }, 1000);
168
  }, [saveCoinBuffer]);
169
 
@@ -175,7 +173,6 @@ export default function App() {
175
  }
176
  }, [saveCoinBuffer]);
177
 
178
- // --- Player Events ---
179
  const onPlayerReady = useCallback((event) => {
180
  playerRef.current = event.target;
181
  playerRef.current.playVideo();
@@ -183,9 +180,11 @@ export default function App() {
183
 
184
  const onPlayerStateChange = useCallback((event) => {
185
  const state = event.data;
186
- if (state === window.YT.PlayerState.PLAYING) startCoinInterval();
187
- else stopCoinInterval();
188
-
 
 
189
  if (state === window.YT.PlayerState.ENDED) {
190
  if (currentVideoId === DEFAULT_VIDEO_ID && userId) {
191
  const profileRef = doc(db, 'users', userId);
@@ -194,20 +193,21 @@ export default function App() {
194
  }
195
  }, [startCoinInterval, stopCoinInterval, currentVideoId, userId]);
196
 
197
- // 5. Inicializar Player
198
  useEffect(() => {
199
  if (ytApiReady && userId && !playerRef.current) {
200
- try {
201
- playerRef.current = new window.YT.Player('player', {
202
- height: '100%',
203
- width: '100%',
204
- videoId: currentVideoId,
205
- playerVars: { 'playsinline': 1, 'autoplay': 1, 'controls': 1, 'modestbranding': 1, 'rel': 0 },
206
- events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange }
207
- });
208
- } catch (e) {
209
- console.error("Error iniciando player", e);
210
- }
 
 
211
  }
212
  if (!userId && playerRef.current) {
213
  stopCoinInterval();
@@ -219,19 +219,17 @@ export default function App() {
219
  stopCoinInterval();
220
  setCurrentVideoId(video.videoId);
221
  setCurrentVideoTitle(video.title);
222
- if (playerRef.current && typeof playerRef.current.loadVideoById === 'function') {
223
  playerRef.current.loadVideoById(video.videoId);
224
  }
225
  };
226
 
227
- // --- FUNCIONES DE LOGIN (SIN ANÓNIMO) ---
228
  const handleGoogleLogin = async () => {
229
  const provider = new GoogleAuthProvider();
230
  try {
231
  await signInWithPopup(auth, provider);
232
  } catch (error) {
233
- console.error("Error login:", error);
234
- alert("Error al conectar con Google. Verifica tu conexión o configuración.");
235
  }
236
  };
237
 
@@ -249,66 +247,29 @@ export default function App() {
249
  );
250
  }
251
 
252
- // --- Renderizado de Login (SIN BOTÓN ANÓNIMO) ---
253
  if (authReady && !user) {
254
  return (
255
- <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
256
- <div className="max-w-md w-full bg-gray-800 rounded-2xl shadow-2xl p-8 text-center border border-gray-700">
257
- <div className="flex justify-center mb-6">
258
- <div className="bg-blue-600 p-4 rounded-full shadow-lg">
259
- <svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
260
- <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>
261
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
262
- </svg>
263
- </div>
264
- </div>
265
- <h1 className="text-3xl font-bold text-white mb-2 tracking-tight">Bienvenido a CoinTube</h1>
266
- <p className="text-gray-400 mb-8 text-sm leading-relaxed">
267
- Descubre contenido increíble, gana monedas virtuales por cada minuto que ves y promociona tus propios videos.
268
- </p>
269
- <button
270
- onClick={handleGoogleLogin}
271
- 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"
272
- >
273
- <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
274
- <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"/>
275
- <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"/>
276
- <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"/>
277
- <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"/>
278
- </svg>
279
- Continuar con Google
280
- </button>
281
-
282
- <p className="mt-6 text-xs text-gray-500">
283
- Al continuar, aceptas nuestros términos de servicio.
284
- </p>
285
- </div>
286
  </div>
287
  );
288
  }
289
 
290
- const isWatched = userProfile?.watchedDefaultVideo;
291
- const hasEnoughCoins = (userProfile?.coins || 0) >= COIN_GOAL;
292
-
293
- let PromoComponent;
294
- if (!isWatched) {
295
- PromoComponent = <PromoLocked defaultTitle={DEFAULT_VIDEO_TITLE} />;
296
- } else if (!hasEnoughCoins) {
297
- PromoComponent = <GoalLocked coinGoal={COIN_GOAL} currentCoins={userProfile?.coins || 0} />;
298
- } else {
299
- PromoComponent = <PromoUnlocked userId={userId} />;
300
- }
301
-
302
- // --- App Principal ---
303
  return (
304
  <div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-6">
305
  <header className="flex flex-col md:flex-row justify-between items-center pb-4 mb-4 border-b border-gray-700">
306
  <h1 className="text-3xl font-bold text-white">Coin<span className="text-blue-500">Tube</span></h1>
307
  <div className="flex items-center space-x-4 mt-4 md:mt-0">
308
  <div className="flex flex-col items-end">
309
- <span className="text-sm text-gray-400">{userProfile?.name || user?.email || 'Usuario'}</span>
310
  <span className="text-lg font-bold text-yellow-400">
311
- {(userProfile?.coins || 0).toLocaleString()} Monedas
312
  </span>
313
  </div>
314
  <button
@@ -320,7 +281,7 @@ export default function App() {
320
  </div>
321
  </header>
322
 
323
- <nav className="flex space-x-1 mb-6 rounded-lg bg-gray-800 p-1 max-w-lg mx-auto">
324
  <button
325
  onClick={() => setCurrentTab('ganar')}
326
  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'}`}
@@ -333,15 +294,8 @@ export default function App() {
333
  >
334
  Promocionar Video
335
  </button>
336
- <button
337
- onClick={() => setCurrentTab('ranking')}
338
- className={`flex-1 py-2 px-4 rounded-lg text-center font-medium text-sm transition-colors ${currentTab === 'ranking' ? 'bg-blue-600 text-white' : 'text-gray-300 hover:bg-gray-700'}`}
339
- >
340
- Ranking
341
- </button>
342
  </nav>
343
 
344
- {/* SECCIÓN GANAR MONEDAS */}
345
  <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6 overflow-hidden`}>
346
  <div className="lg:w-2/3 flex flex-col">
347
  <div className="w-full bg-black rounded-lg shadow-2xl overflow-hidden">
@@ -356,141 +310,42 @@ export default function App() {
356
  <aside className="lg:w-1/3 flex flex-col bg-gray-800 rounded-lg shadow-lg p-4">
357
  <h2 className="text-xl font-semibold mb-4 pb-2 border-b border-gray-700">Videos Disponibles</h2>
358
  <div className="flex-1 overflow-y-auto space-y-2 pr-2">
359
- {videos.map((video) => (
360
- <button
361
- key={video.id}
362
- onClick={() => loadVideo(video)}
363
- className="w-full text-left px-4 py-3 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
364
- disabled={video.videoId === currentVideoId}
365
- >
366
- {video.title || `Video ID: ${video.videoId}`}
367
- </button>
368
- ))}
 
 
 
 
369
  </div>
370
  </aside>
371
  </main>
372
 
373
- {/* SECCIÓN PROMOCIONAR VIDEO */}
374
  <main className={`${currentTab === 'promocionar' ? 'flex' : 'hidden'} flex-1 flex-col items-center justify-center`}>
375
- {PromoComponent}
376
- </main>
377
-
378
- {/* SECCIÓN RANKING */}
379
- <main className={`${currentTab === 'ranking' ? 'flex' : 'hidden'} flex-1 flex-col items-center justify-start py-4 overflow-auto`}>
380
- <Leaderboard db={db} currentUserId={userId} />
381
  </main>
382
  </div>
383
  );
384
  }
385
 
386
- // --- Sub-Componentes ---
387
-
388
- function Leaderboard({ db, currentUserId }) {
389
- const [ranking, setRanking] = useState([]);
390
- const [loading, setLoading] = useState(true);
391
- const [error, setError] = useState(null);
392
-
393
- useEffect(() => {
394
- if (!db) return;
395
- setLoading(true);
396
-
397
- try {
398
- // Consulta: top 10 usuarios ordenados por monedas
399
- const q = query(
400
- collection(db, 'users'),
401
- orderBy('coins', 'desc'),
402
- limit(10)
403
- );
404
-
405
- const unsubscribe = onSnapshot(q, (snapshot) => {
406
- const users = [];
407
- snapshot.forEach((doc) => {
408
- const data = doc.data();
409
- users.push({
410
- id: doc.id,
411
- coins: data.coins || 0,
412
- name: data.name || `Usuario`
413
- });
414
- });
415
- setRanking(users);
416
- setLoading(false);
417
- setError(null);
418
- }, (err) => {
419
- console.error("Error ranking:", err);
420
- setError("No se pudo cargar el ranking. (Revisar consola y reglas de Firebase)");
421
- setLoading(false);
422
- });
423
- return () => unsubscribe();
424
- } catch (e) {
425
- console.error("Error al iniciar consulta de ranking:", e);
426
- setError("Error interno al iniciar el ranking.");
427
- setLoading(false);
428
- }
429
- }, [db]);
430
-
431
- if (loading) return <div className="text-center p-8 text-gray-400">Cargando ranking...</div>;
432
- if (error) return <div className="text-center p-8 text-red-400">{error}</div>;
433
-
434
- return (
435
- <div className="w-full max-w-lg bg-gray-800 rounded-xl shadow-2xl p-6">
436
- <h2 className="text-3xl font-bold text-white mb-6 pb-2 border-b border-gray-700 text-center">🏆 Ranking de Monedas 🏆</h2>
437
-
438
- {ranking.length === 0 ? (
439
- <div className="text-center p-8 text-gray-400">Aún no hay usuarios en el ranking.</div>
440
- ) : (
441
- <ul className="space-y-3">
442
- {ranking.map((user, index) => (
443
- <li
444
- key={user.id}
445
- className={`flex items-center justify-between p-4 rounded-lg transition-all duration-200 ${user.id === currentUserId
446
- ? 'bg-blue-600 shadow-lg ring-2 ring-blue-400'
447
- : 'bg-gray-700 hover:bg-gray-600'
448
- }`}
449
- >
450
- <div className="flex items-center space-x-4">
451
- <span className={`text-xl font-extrabold w-6 text-center ${index < 3 ? 'text-yellow-400' : 'text-gray-400'}`}>
452
- {index + 1}.
453
- </span>
454
- <span className={`text-lg font-medium truncate ${user.id === currentUserId ? 'text-white' : 'text-gray-200'}`}>
455
- {user.id === currentUserId ? `${user.name} (Tú)` : user.name}
456
- </span>
457
- </div>
458
- <span className={`text-xl font-bold ${user.id === currentUserId ? 'text-yellow-200' : 'text-yellow-400'}`}>
459
- {(user.coins || 0).toLocaleString()} 💰
460
- </span>
461
- </li>
462
- ))}
463
- </ul>
464
- )}
465
- </div>
466
- );
467
- }
468
-
469
- function GoalLocked({ coinGoal, currentCoins }) {
470
- return (
471
- <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg">
472
- <svg className="w-16 h-16 text-yellow-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8c1.657 0 3 1.343 3 3s-1.343 3-3 3-1.343-3-3-3 3 1.343 3 3v4m-6 0h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2z"></path></svg>
473
- <h2 className="text-2xl font-bold mb-2 text-yellow-400">¡Meta de Monedas!</h2>
474
- <p className="text-gray-300">
475
- ¡Felicidades! Desbloqueaste la promoción por ver el video, pero el servicio requiere un mínimo de monedas.
476
- </p>
477
- <p className="text-gray-100 text-lg mt-4 font-semibold">
478
- Acumulado: {(currentCoins || 0).toLocaleString()} / {coinGoal.toLocaleString()} Monedas
479
- </p>
480
- <p className="text-blue-400 text-sm mt-2 font-bold">
481
- ¡Debes seguir viendo videos hasta juntar {coinGoal.toLocaleString()} monedas!
482
- </p>
483
- </div>
484
- );
485
- }
486
-
487
- function PromoLocked({ defaultTitle }) {
488
  return (
489
  <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg">
490
  <svg className="w-16 h-16 text-yellow-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><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>
491
  <h2 className="text-2xl font-bold mb-2">Sección Bloqueada</h2>
492
  <p className="text-gray-300">
493
- Para poder promocionar tu propio video, primero debes ver el video predeterminado ("{defaultTitle}") por completo.
494
  </p>
495
  <p className="text-gray-400 text-sm mt-4">¡Ve a "Ganar Monedas", búscalo en la lista y míralo hasta el final!</p>
496
  </div>
@@ -528,7 +383,7 @@ function PromoUnlocked({ userId }) {
528
  setMessage({ text: "¡Video agregado con éxito!", type: 'success' });
529
  setUrl('');
530
  } catch (error) {
531
- console.error(error);
532
  setMessage({ text: "Error al agregar el video.", type: 'error' });
533
  }
534
  };
 
1
+ JavaScript
2
+
3
  import React, { useState, useEffect, useRef, useCallback } from 'react';
4
  import { initializeApp } from 'firebase/app';
5
  import {
 
7
  onAuthStateChanged,
8
  GoogleAuthProvider,
9
  signInWithPopup,
10
+ signOut,
11
+ signInWithCustomToken,
12
+ signInAnonymously
13
  } from 'firebase/auth';
14
  import {
15
  getFirestore,
 
22
  increment,
23
  serverTimestamp,
24
  query,
 
 
25
  setLogLevel
26
  } from 'firebase/firestore';
27
 
 
28
  const firebaseConfig = {
29
  apiKey: "AIzaSyCwJjVJGuOmA_PKRbaTnQDrK-Q07NI_utc",
30
  authDomain: "insights-5c2d6.firebaseapp.com",
 
39
  app = initializeApp(firebaseConfig);
40
  auth = getAuth(app);
41
  db = getFirestore(app);
42
+ setLogLevel('debug');
43
  } catch (error) {
44
  console.error("Error inicializando Firebase:", error);
45
  }
46
 
47
  const DEFAULT_VIDEO_ID = "UtEhewStfMA";
48
+ const DEFAULT_VIDEO_TITLE = "Cómo Programar un Cohete";
 
49
 
50
  export default function App() {
51
 
 
 
 
 
 
 
 
52
  const [user, setUser] = useState(null);
53
  const [userId, setUserId] = useState(null);
54
  const [authReady, setAuthReady] = useState(false);
55
  const [userProfile, setUserProfile] = useState(null);
56
+ const [videos, setVideos] = useState([]);
57
  const [currentTab, setCurrentTab] = useState('ganar');
58
  const [currentVideoId, setCurrentVideoId] = useState(DEFAULT_VIDEO_ID);
59
  const [currentVideoTitle, setCurrentVideoTitle] = useState(DEFAULT_VIDEO_TITLE);
 
63
  const coinIntervalRef = useRef(null);
64
  const localCoinBufferRef = useRef(0);
65
 
 
66
  useEffect(() => {
67
+ if (!auth) return;
68
+
 
 
69
  const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
70
  if (user) {
71
  setUser(user);
 
77
  }
78
  setAuthReady(true);
79
  });
80
+
81
+ setAuthReady(true);
82
+
83
  return () => unsubscribeAuth();
84
  }, []);
85
 
 
86
  useEffect(() => {
87
  if (!userId || !db) return;
88
+
89
  const profileRef = doc(db, 'users', userId);
90
+
91
  const unsubscribeProfile = onSnapshot(profileRef, (docSnap) => {
92
  if (docSnap.exists()) {
93
  setUserProfile(docSnap.data());
94
  } else {
95
+ const newProfile = { coins: 0, watchedDefaultVideo: false };
96
+ setDoc(profileRef, newProfile).catch(e => console.error("Error creating profile:", e));
 
 
 
 
 
97
  setUserProfile(newProfile);
98
  }
99
+ }, (error) => {
100
+ console.error("Error listening to profile:", error);
101
+ });
102
+
103
  return () => unsubscribeProfile();
104
+ }, [userId]);
105
 
 
106
  useEffect(() => {
107
  if (!db) return;
108
+
109
  const videosRef = collection(db, 'videos');
110
+ const q = query(videosRef);
111
 
112
  const unsubscribeVideos = onSnapshot(q, (snapshot) => {
113
  if (snapshot.empty) {
 
114
  addDoc(videosRef, {
115
  title: DEFAULT_VIDEO_TITLE,
116
  videoId: DEFAULT_VIDEO_ID,
117
  addedBy: "system",
118
  createdAt: serverTimestamp()
119
+ }).catch(e => console.error("Error adding default video:", e));
120
  } else {
121
  const videoList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
122
  setVideos(videoList);
 
127
 
128
  return () => unsubscribeVideos();
129
  }, []);
130
+
 
131
  useEffect(() => {
132
  if (!window.YT) {
133
  const tag = document.createElement('script');
134
  tag.src = "https://www.youtube.com/iframe_api";
135
  const firstScriptTag = document.getElementsByTagName('script')[0];
136
  firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
137
+
138
+ window.onYouTubeIframeAPIReady = () => {
139
+ setYtApiReady(true);
140
+ };
141
  } else {
142
  setYtApiReady(true);
143
  }
144
  }, []);
145
 
 
146
  const saveCoinBuffer = useCallback(() => {
147
  if (localCoinBufferRef.current > 0 && userId) {
148
  const coinsToSave = localCoinBufferRef.current;
149
  localCoinBufferRef.current = 0;
150
  const profileRef = doc(db, 'users', userId);
151
  updateDoc(profileRef, { coins: increment(coinsToSave) })
152
+ .catch(e => {
153
+ localCoinBufferRef.current += coinsToSave;
154
+ });
155
  }
156
  }, [userId]);
157
 
 
159
  if (coinIntervalRef.current) return;
160
  coinIntervalRef.current = setInterval(() => {
161
  localCoinBufferRef.current++;
162
+ if (localCoinBufferRef.current >= 5) {
163
+ saveCoinBuffer();
164
+ }
165
  }, 1000);
166
  }, [saveCoinBuffer]);
167
 
 
173
  }
174
  }, [saveCoinBuffer]);
175
 
 
176
  const onPlayerReady = useCallback((event) => {
177
  playerRef.current = event.target;
178
  playerRef.current.playVideo();
 
180
 
181
  const onPlayerStateChange = useCallback((event) => {
182
  const state = event.data;
183
+ if (state === window.YT.PlayerState.PLAYING) {
184
+ startCoinInterval();
185
+ } else {
186
+ stopCoinInterval();
187
+ }
188
  if (state === window.YT.PlayerState.ENDED) {
189
  if (currentVideoId === DEFAULT_VIDEO_ID && userId) {
190
  const profileRef = doc(db, 'users', userId);
 
193
  }
194
  }, [startCoinInterval, stopCoinInterval, currentVideoId, userId]);
195
 
 
196
  useEffect(() => {
197
  if (ytApiReady && userId && !playerRef.current) {
198
+ playerRef.current = new window.YT.Player('player', {
199
+ height: '100%',
200
+ width: '100%',
201
+ videoId: currentVideoId,
202
+ playerVars: {
203
+ 'playsinline': 1, 'autoplay': 1, 'controls': 1,
204
+ 'modestbranding': 1, 'rel': 0
205
+ },
206
+ events: {
207
+ 'onReady': onPlayerReady,
208
+ 'onStateChange': onPlayerStateChange
209
+ }
210
+ });
211
  }
212
  if (!userId && playerRef.current) {
213
  stopCoinInterval();
 
219
  stopCoinInterval();
220
  setCurrentVideoId(video.videoId);
221
  setCurrentVideoTitle(video.title);
222
+ if (playerRef.current) {
223
  playerRef.current.loadVideoById(video.videoId);
224
  }
225
  };
226
 
 
227
  const handleGoogleLogin = async () => {
228
  const provider = new GoogleAuthProvider();
229
  try {
230
  await signInWithPopup(auth, provider);
231
  } catch (error) {
232
+ console.error("Error en el inicio de sesión con Google:", error);
 
233
  }
234
  };
235
 
 
247
  );
248
  }
249
 
 
250
  if (authReady && !user) {
251
  return (
252
+ <div className="flex flex-col items-center justify-center h-full w-full">
253
+ <p className="text-lg text-gray-400 mb-6">Por favor, inicia sesión para continuar.</p>
254
+ <button
255
+ onClick={handleGoogleLogin}
256
+ className="px-6 py-2 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 transition-colors"
257
+ >
258
+ Iniciar sesión con Google
259
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
260
  </div>
261
  );
262
  }
263
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  return (
265
  <div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-6">
266
  <header className="flex flex-col md:flex-row justify-between items-center pb-4 mb-4 border-b border-gray-700">
267
  <h1 className="text-3xl font-bold text-white">Coin<span className="text-blue-500">Tube</span></h1>
268
  <div className="flex items-center space-x-4 mt-4 md:mt-0">
269
  <div className="flex flex-col items-end">
270
+ <span className="text-sm text-gray-400">{user?.email}</span>
271
  <span className="text-lg font-bold text-yellow-400">
272
+ {userProfile?.coins || 0} Monedas
273
  </span>
274
  </div>
275
  <button
 
281
  </div>
282
  </header>
283
 
284
+ <nav className="flex space-x-1 mb-6 rounded-lg bg-gray-800 p-1 max-w-md mx-auto">
285
  <button
286
  onClick={() => setCurrentTab('ganar')}
287
  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'}`}
 
294
  >
295
  Promocionar Video
296
  </button>
 
 
 
 
 
 
297
  </nav>
298
 
 
299
  <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6 overflow-hidden`}>
300
  <div className="lg:w-2/3 flex flex-col">
301
  <div className="w-full bg-black rounded-lg shadow-2xl overflow-hidden">
 
310
  <aside className="lg:w-1/3 flex flex-col bg-gray-800 rounded-lg shadow-lg p-4">
311
  <h2 className="text-xl font-semibold mb-4 pb-2 border-b border-gray-700">Videos Disponibles</h2>
312
  <div className="flex-1 overflow-y-auto space-y-2 pr-2">
313
+ {videos.length === 0 ? (
314
+ <p className="text-gray-400">Cargando videos...</p>
315
+ ) : (
316
+ videos.map((video) => (
317
+ <button
318
+ key={video.id}
319
+ onClick={() => loadVideo(video)}
320
+ className="w-full text-left px-4 py-3 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50"
321
+ disabled={video.videoId === currentVideoId}
322
+ >
323
+ {video.title || `Video ID: ${video.videoId}`}
324
+ </button>
325
+ ))
326
+ )}
327
  </div>
328
  </aside>
329
  </main>
330
 
 
331
  <main className={`${currentTab === 'promocionar' ? 'flex' : 'hidden'} flex-1 flex-col items-center justify-center`}>
332
+ {userProfile?.watchedDefaultVideo ? (
333
+ <PromoUnlocked userId={userId} />
334
+ ) : (
335
+ <PromoLocked />
336
+ )}
 
337
  </main>
338
  </div>
339
  );
340
  }
341
 
342
+ function PromoLocked() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  return (
344
  <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg">
345
  <svg className="w-16 h-16 text-yellow-500 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><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>
346
  <h2 className="text-2xl font-bold mb-2">Sección Bloqueada</h2>
347
  <p className="text-gray-300">
348
+ Para poder promocionar tu propio video, primero debes ver el video predeterminado ("{DEFAULT_VIDEO_TITLE}") por completo.
349
  </p>
350
  <p className="text-gray-400 text-sm mt-4">¡Ve a "Ganar Monedas", búscalo en la lista y míralo hasta el final!</p>
351
  </div>
 
383
  setMessage({ text: "¡Video agregado con éxito!", type: 'success' });
384
  setUrl('');
385
  } catch (error) {
386
+ console.error("Error adding video:", error);
387
  setMessage({ text: "Error al agregar el video.", type: 'error' });
388
  }
389
  };