salomonsky commited on
Commit
6eb0bba
·
verified ·
1 Parent(s): c295fee

Update cointube.jsx

Browse files
Files changed (1) hide show
  1. cointube.jsx +196 -128
cointube.jsx CHANGED
@@ -5,8 +5,7 @@ import {
5
  onAuthStateChanged,
6
  GoogleAuthProvider,
7
  signInWithPopup,
8
- signOut,
9
- signInAnonymously
10
  } from 'firebase/auth';
11
  import {
12
  getFirestore,
@@ -19,43 +18,56 @@ import {
19
  increment,
20
  serverTimestamp,
21
  query,
22
- setLogLevel
 
23
  } from 'firebase/firestore';
24
 
25
- // --- Constantes de Firebase ---
26
- const firebaseConfig = {
27
- apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
28
- authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
29
- projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
30
- storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
31
- messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
32
- appId: import.meta.env.VITE_FIREBASE_APP_ID
33
- };
34
-
35
  let app, auth, db;
 
36
  try {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  app = initializeApp(firebaseConfig);
38
  auth = getAuth(app);
39
  db = getFirestore(app);
40
- setLogLevel('debug');
 
41
  } catch (error) {
42
- console.error("Error inicializando Firebase:", error);
43
  }
44
 
45
- // --- Constantes del Video (Agregadas) ---
 
 
46
  const DEFAULT_VIDEO_ID = "UtEhewStfMA";
47
- const DEFAULT_VIDEO_TITLE = "Pelicula Completa"; // Usando el título de tu lista
48
 
49
  export default function App() {
50
 
51
- // --- Estado de la Lista de Videos (CORREGIDO - Dentro del componente) ---
52
- const [videos, setVideos] = useState([
53
- { id: '1', title: 'Pelicula Completa', videoId: 'UtEhewStfMA' },
54
- { id: '2', title: 'Documental Comida', videoId: 'lwiNN7WUw50' },
55
- { id: '3', title: 'Cine de Arte', videoId: 'GxEx6Kgo6Es' },
56
- ]);
57
-
58
- // --- Resto de Estados ---
59
  const [user, setUser] = useState(null);
60
  const [userId, setUserId] = useState(null);
61
  const [authReady, setAuthReady] = useState(false);
@@ -73,7 +85,10 @@ export default function App() {
73
 
74
  // 1. Auth
75
  useEffect(() => {
76
- if (!auth) return;
 
 
 
77
  const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
78
  if (user) {
79
  setUser(user);
@@ -85,7 +100,6 @@ export default function App() {
85
  }
86
  setAuthReady(true);
87
  });
88
- setAuthReady(true);
89
  return () => unsubscribeAuth();
90
  }, []);
91
 
@@ -97,42 +111,48 @@ export default function App() {
97
  if (docSnap.exists()) {
98
  setUserProfile(docSnap.data());
99
  } else {
100
- const newProfile = { coins: 0, watchedDefaultVideo: false };
 
 
 
 
101
  setDoc(profileRef, newProfile).catch(e => console.error("Error creating profile:", e));
102
  setUserProfile(newProfile);
103
  }
104
  }, (error) => console.error("Error listening to profile:", error));
105
  return () => unsubscribeProfile();
106
- }, [userId]);
107
 
108
- // 3. Videos de Firebase (ELIMINADO / Comentado para usar lista fija)
109
- /*
110
  useEffect(() => {
111
  if (!db) return;
112
- const videosRef = collection(db, 'videos');
113
- const q = query(videosRef);
114
-
115
- const unsubscribeVideos = onSnapshot(q, (snapshot) => {
116
- if (snapshot.empty) {
117
- addDoc(videosRef, {
118
- title: DEFAULT_VIDEO_TITLE,
119
- videoId: DEFAULT_VIDEO_ID,
120
- addedBy: "system",
121
- createdAt: serverTimestamp()
122
- }).catch(e => console.error("Error adding default video:", e));
123
  } else {
124
- const videoList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
125
- // Si usas la lista fija, no hagas setVideos(videoList) aquí.
 
 
126
  }
127
- }, (error) => {
128
- console.error("Error listening to videos:", error);
129
  });
 
 
130
 
131
- return () => unsubscribeVideos();
 
 
 
 
 
 
 
 
132
  }, []);
133
- */
134
 
135
- // 4. API YouTube
136
  useEffect(() => {
137
  if (!window.YT) {
138
  const tag = document.createElement('script');
@@ -147,7 +167,7 @@ export default function App() {
147
 
148
  // --- Lógica de Monedas ---
149
  const saveCoinBuffer = useCallback(() => {
150
- if (localCoinBufferRef.current > 0 && userId) {
151
  const coinsToSave = localCoinBufferRef.current;
152
  localCoinBufferRef.current = 0;
153
  const profileRef = doc(db, 'users', userId);
@@ -184,14 +204,14 @@ export default function App() {
184
  else stopCoinInterval();
185
 
186
  if (state === window.YT.PlayerState.ENDED) {
187
- if (currentVideoId === DEFAULT_VIDEO_ID && userId) {
188
  const profileRef = doc(db, 'users', userId);
189
  updateDoc(profileRef, { watchedDefaultVideo: true });
190
  }
191
  }
192
  }, [startCoinInterval, stopCoinInterval, currentVideoId, userId]);
193
 
194
- // 5. Inicializar Player
195
  useEffect(() => {
196
  if (ytApiReady && userId && !playerRef.current) {
197
  playerRef.current = new window.YT.Player('player', {
@@ -217,6 +237,7 @@ export default function App() {
217
 
218
  // --- FUNCIONES DE LOGIN ---
219
  const handleGoogleLogin = async () => {
 
220
  const provider = new GoogleAuthProvider();
221
  try {
222
  await signInWithPopup(auth, provider);
@@ -225,15 +246,8 @@ export default function App() {
225
  }
226
  };
227
 
228
- const handleAnonymousLogin = async () => {
229
- try {
230
- await signInAnonymously(auth);
231
- } catch (error) {
232
- console.error("Error Anónimo:", error);
233
- }
234
- };
235
-
236
  const handleLogout = async () => {
 
237
  stopCoinInterval();
238
  await signOut(auth);
239
  };
@@ -253,26 +267,19 @@ export default function App() {
253
  return (
254
  <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
255
  <div className="max-w-md w-full bg-gray-800 rounded-2xl shadow-2xl p-8 text-center border border-gray-700">
256
-
257
- {/* ICONO */}
258
  <div className="flex justify-center mb-6">
259
  <div className="bg-blue-600 p-4 rounded-full shadow-lg">
260
- <svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
261
  <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>
262
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
263
  </svg>
264
  </div>
265
  </div>
266
-
267
- {/* TÍTULO */}
268
  <h1 className="text-3xl font-bold text-white mb-2 tracking-tight">Bienvenido a CoinTube</h1>
269
-
270
- {/* DESCRIPCIÓN */}
271
  <p className="text-gray-400 mb-8 text-sm leading-relaxed">
272
- Descubre contenido increíble, gana monedas virtuales por cada minuto que ves y promociona tus propios videos.
 
273
  </p>
274
-
275
- {/* BOTONES */}
276
  <div className="space-y-3">
277
  <button
278
  onClick={handleGoogleLogin}
@@ -286,18 +293,7 @@ export default function App() {
286
  </svg>
287
  Continuar con Google
288
  </button>
289
-
290
- <button
291
- onClick={handleAnonymousLogin}
292
- className="w-full px-4 py-3 bg-gray-700 text-gray-200 font-semibold rounded-lg hover:bg-gray-600 border border-gray-600 transition-all duration-200"
293
- >
294
- Entrar como Invitado
295
- </button>
296
  </div>
297
-
298
- <p className="mt-6 text-xs text-gray-500">
299
- Al continuar, aceptas nuestros términos de servicio ficticios.
300
- </p>
301
  </div>
302
  </div>
303
  );
@@ -310,10 +306,9 @@ export default function App() {
310
  <h1 className="text-3xl font-bold text-white">Coin<span className="text-blue-500">Tube</span></h1>
311
  <div className="flex items-center space-x-4 mt-4 md:mt-0">
312
  <div className="flex flex-col items-end">
313
- {/* Muestra 'Invitado' si no hay email */}
314
- <span className="text-sm text-gray-400">{user?.email || 'Invitado'}</span>
315
  <span className="text-lg font-bold text-yellow-400">
316
- {userProfile?.coins || 0} Monedas
317
  </span>
318
  </div>
319
  <button
@@ -340,6 +335,7 @@ export default function App() {
340
  </button>
341
  </nav>
342
 
 
343
  <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6 overflow-hidden`}>
344
  <div className="lg:w-2/3 flex flex-col">
345
  <div className="w-full bg-black rounded-lg shadow-2xl overflow-hidden">
@@ -351,51 +347,99 @@ export default function App() {
351
  <h3 className="text-lg font-semibold text-white">{currentVideoTitle}</h3>
352
  </div>
353
  </div>
354
- <aside className="lg:w-1/3 flex flex-col bg-gray-800 rounded-lg shadow-lg p-4">
355
- <h2 className="text-xl font-semibold mb-4 pb-2 border-b border-gray-700">Videos Disponibles</h2>
356
- <div className="flex-1 overflow-y-auto space-y-2 pr-2">
357
- {videos.map((video) => (
358
- <button
359
- key={video.id}
360
- onClick={() => loadVideo(video)}
361
- 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"
362
- disabled={video.videoId === currentVideoId}
363
- >
364
- {video.title || `Video ID: ${video.videoId}`}
365
- </button>
366
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  </div>
368
  </aside>
369
  </main>
370
 
 
371
  <main className={`${currentTab === 'promocionar' ? 'flex' : 'hidden'} flex-1 flex-col items-center justify-center`}>
372
- {userProfile?.watchedDefaultVideo ? (
373
- <PromoUnlocked userId={userId} />
374
- ) : (
375
- <PromoLocked />
376
- )}
 
 
377
  </main>
378
  </div>
379
  );
380
  }
381
 
382
- // --- Sub-Componentes ---
383
- function PromoLocked() {
 
 
 
 
 
 
 
 
 
 
 
384
  return (
385
- <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg">
386
- <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>
387
- <h2 className="text-2xl font-bold mb-2">Sección Bloqueada</h2>
388
- <p className="text-gray-300">
389
- Para poder promocionar tu propio video, primero debes ver el video predeterminado ("{DEFAULT_VIDEO_TITLE}") por completo.
 
 
 
390
  </p>
391
- <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>
 
 
 
 
 
 
 
 
 
 
 
392
  </div>
393
  );
394
  }
395
 
396
- function PromoUnlocked({ userId }) {
397
  const [url, setUrl] = useState('');
398
  const [message, setMessage] = useState({ text: '', type: 'success' });
 
399
 
400
  const extractYouTubeID = (url) => {
401
  const regex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
@@ -411,48 +455,72 @@ function PromoUnlocked({ userId }) {
411
  setMessage({ text: "URL de YouTube no válida.", type: 'error' });
412
  return;
413
  }
414
- const title = `Video de usuario (${videoId.substring(0, 5)}...)`;
 
 
415
 
416
  try {
417
- const videosRef = collection(db, 'videos');
418
- await addDoc(videosRef, {
419
- title: title,
420
- videoId: videoId,
421
- addedBy: userId,
422
- createdAt: serverTimestamp()
423
- });
 
 
 
 
 
 
 
 
 
424
  setMessage({ text: "¡Video agregado con éxito!", type: 'success' });
425
  setUrl('');
426
  } catch (error) {
427
  console.error("Error adding video:", error);
428
- setMessage({ text: "Error al agregar el video.", type: 'error' });
 
 
429
  }
430
  };
431
 
432
  return (
433
- <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg w-full">
434
- <svg className="w-16 h-16 text-green-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="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>
435
- <h2 className="text-2xl font-bold mb-2">¡Sección Desbloqueada!</h2>
 
 
 
 
 
 
436
  <p className="text-gray-300 mb-6">
437
- Pega el enlace de un video de YouTube para agregarlo a la lista pública.
 
 
438
  </p>
 
439
  <form onSubmit={handleSubmit} className="flex flex-col space-y-4">
440
  <input
441
  type="url"
442
  placeholder="https://www.youtube.com/watch?v=..."
443
- 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"
444
  required
445
  value={url}
446
  onChange={(e) => setUrl(e.target.value)}
 
447
  />
448
  <button
449
  type="submit"
450
- className="w-full 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"
 
451
  >
452
- Promocionar Video
453
  </button>
454
  {message.text && (
455
- <p className={`text-sm ${message.type === 'success' ? 'text-green-500' : 'text-red-500'}`}>
456
  {message.text}
457
  </p>
458
  )}
 
5
  onAuthStateChanged,
6
  GoogleAuthProvider,
7
  signInWithPopup,
8
+ signOut
 
9
  } from 'firebase/auth';
10
  import {
11
  getFirestore,
 
18
  increment,
19
  serverTimestamp,
20
  query,
21
+ orderBy,
22
+ limit
23
  } from 'firebase/firestore';
24
 
25
+ // --- CONFIGURACIÓN DE FIREBASE (JSON PARSER) ---
 
 
 
 
 
 
 
 
 
26
  let app, auth, db;
27
+
28
  try {
29
+ // 1. Intentamos leer la variable de entorno
30
+ const configRaw = import.meta.env.VITE_FIREBASE_CONFIG;
31
+
32
+ let firebaseConfig;
33
+
34
+ if (configRaw) {
35
+ // 2. Si existe, la parseamos de JSON a Objeto
36
+ firebaseConfig = JSON.parse(configRaw);
37
+ } else {
38
+ // 3. Fallback de seguridad o error visible en consola
39
+ console.error("FALTA CONFIGURACIÓN: La variable VITE_FIREBASE_CONFIG está vacía.");
40
+ // Opcional: Valores por defecto hardcodeados si todo falla
41
+ firebaseConfig = {
42
+ apiKey: "AIzaSyCwJjVJGuOmA_PKRbaTnQDrK-Q07NI_utc",
43
+ authDomain: "insights-5c2d6.firebaseapp.com",
44
+ projectId: "insights-5c2d6",
45
+ storageBucket: "insights-5c2d6.firebasestorage.app",
46
+ messagingSenderId: "61805724903",
47
+ appId: "1:61805724903:web:0f3771dc5cd44416600e42"
48
+ };
49
+ }
50
+
51
  app = initializeApp(firebaseConfig);
52
  auth = getAuth(app);
53
  db = getFirestore(app);
54
+ console.log("Firebase inicializado correctamente.");
55
+
56
  } catch (error) {
57
+ console.error("ERROR CRÍTICO AL INICIAR FIREBASE:", error);
58
  }
59
 
60
+ // --- Constantes del Sistema ---
61
+ const ADMIN_EMAIL = "photonicsupernova@gmail.com";
62
+ const VIDEO_COST = 10000;
63
  const DEFAULT_VIDEO_ID = "UtEhewStfMA";
64
+ const DEFAULT_VIDEO_TITLE = "Pelicula Completa";
65
 
66
  export default function App() {
67
 
68
+ // --- Estados ---
69
+ const [videos, setVideos] = useState([]);
70
+ const [leaderboard, setLeaderboard] = useState([]);
 
 
 
 
 
71
  const [user, setUser] = useState(null);
72
  const [userId, setUserId] = useState(null);
73
  const [authReady, setAuthReady] = useState(false);
 
85
 
86
  // 1. Auth
87
  useEffect(() => {
88
+ if (!auth) {
89
+ setAuthReady(true);
90
+ return;
91
+ }
92
  const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
93
  if (user) {
94
  setUser(user);
 
100
  }
101
  setAuthReady(true);
102
  });
 
103
  return () => unsubscribeAuth();
104
  }, []);
105
 
 
111
  if (docSnap.exists()) {
112
  setUserProfile(docSnap.data());
113
  } else {
114
+ const newProfile = {
115
+ email: user.email,
116
+ coins: 0,
117
+ watchedDefaultVideo: false
118
+ };
119
  setDoc(profileRef, newProfile).catch(e => console.error("Error creating profile:", e));
120
  setUserProfile(newProfile);
121
  }
122
  }, (error) => console.error("Error listening to profile:", error));
123
  return () => unsubscribeProfile();
124
+ }, [userId, user]);
125
 
126
+ // 3. Cargar Lista de Videos
 
127
  useEffect(() => {
128
  if (!db) return;
129
+ const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(20));
130
+ const unsubscribe = onSnapshot(q, (snapshot) => {
131
+ const fetchedVideos = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
132
+ if (fetchedVideos.length > 0) {
133
+ setVideos(fetchedVideos);
 
 
 
 
 
 
134
  } else {
135
+ setVideos([
136
+ { id: '1', title: 'Pelicula Completa', videoId: 'UtEhewStfMA' },
137
+ { id: '2', title: 'Documental Comida', videoId: 'lwiNN7WUw50' }
138
+ ]);
139
  }
 
 
140
  });
141
+ return () => unsubscribe();
142
+ }, []);
143
 
144
+ // 4. LEADERBOARD
145
+ useEffect(() => {
146
+ if (!db) return;
147
+ const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(20));
148
+ const unsubscribe = onSnapshot(q, (snapshot) => {
149
+ const usersList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
150
+ setLeaderboard(usersList);
151
+ });
152
+ return () => unsubscribe();
153
  }, []);
 
154
 
155
+ // 5. API YouTube
156
  useEffect(() => {
157
  if (!window.YT) {
158
  const tag = document.createElement('script');
 
167
 
168
  // --- Lógica de Monedas ---
169
  const saveCoinBuffer = useCallback(() => {
170
+ if (localCoinBufferRef.current > 0 && userId && db) {
171
  const coinsToSave = localCoinBufferRef.current;
172
  localCoinBufferRef.current = 0;
173
  const profileRef = doc(db, 'users', userId);
 
204
  else stopCoinInterval();
205
 
206
  if (state === window.YT.PlayerState.ENDED) {
207
+ if (currentVideoId === DEFAULT_VIDEO_ID && userId && db) {
208
  const profileRef = doc(db, 'users', userId);
209
  updateDoc(profileRef, { watchedDefaultVideo: true });
210
  }
211
  }
212
  }, [startCoinInterval, stopCoinInterval, currentVideoId, userId]);
213
 
214
+ // Inicializar Player
215
  useEffect(() => {
216
  if (ytApiReady && userId && !playerRef.current) {
217
  playerRef.current = new window.YT.Player('player', {
 
237
 
238
  // --- FUNCIONES DE LOGIN ---
239
  const handleGoogleLogin = async () => {
240
+ if (!auth) return;
241
  const provider = new GoogleAuthProvider();
242
  try {
243
  await signInWithPopup(auth, provider);
 
246
  }
247
  };
248
 
 
 
 
 
 
 
 
 
249
  const handleLogout = async () => {
250
+ if (!auth) return;
251
  stopCoinInterval();
252
  await signOut(auth);
253
  };
 
267
  return (
268
  <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
269
  <div className="max-w-md w-full bg-gray-800 rounded-2xl shadow-2xl p-8 text-center border border-gray-700">
 
 
270
  <div className="flex justify-center mb-6">
271
  <div className="bg-blue-600 p-4 rounded-full shadow-lg">
272
+ <svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
273
  <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>
274
  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
275
  </svg>
276
  </div>
277
  </div>
 
 
278
  <h1 className="text-3xl font-bold text-white mb-2 tracking-tight">Bienvenido a CoinTube</h1>
 
 
279
  <p className="text-gray-400 mb-8 text-sm leading-relaxed">
280
+ Gana monedas viendo videos y promociona tu contenido. <br/>
281
+ <span className="text-yellow-500 font-bold">¡Llega a 10,000 monedas para subir tu video!</span>
282
  </p>
 
 
283
  <div className="space-y-3">
284
  <button
285
  onClick={handleGoogleLogin}
 
293
  </svg>
294
  Continuar con Google
295
  </button>
 
 
 
 
 
 
 
296
  </div>
 
 
 
 
297
  </div>
298
  </div>
299
  );
 
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">{user.email}</span>
 
310
  <span className="text-lg font-bold text-yellow-400">
311
+ {Math.floor(userProfile?.coins || 0)} Monedas
312
  </span>
313
  </div>
314
  <button
 
335
  </button>
336
  </nav>
337
 
338
+ {/* TAB: GANAR MONEDAS */}
339
  <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6 overflow-hidden`}>
340
  <div className="lg:w-2/3 flex flex-col">
341
  <div className="w-full bg-black rounded-lg shadow-2xl overflow-hidden">
 
347
  <h3 className="text-lg font-semibold text-white">{currentVideoTitle}</h3>
348
  </div>
349
  </div>
350
+
351
+ <aside className="lg:w-1/3 flex flex-col gap-4 h-full overflow-hidden">
352
+ <div className="bg-gray-800 rounded-lg shadow-lg p-4 flex-1 flex flex-col min-h-[200px]">
353
+ <h2 className="text-xl font-semibold mb-4 pb-2 border-b border-gray-700">Videos Disponibles</h2>
354
+ <div className="flex-1 overflow-y-auto space-y-2 pr-2 custom-scrollbar">
355
+ {videos.map((video) => (
356
+ <button
357
+ key={video.id}
358
+ onClick={() => loadVideo(video)}
359
+ 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'}`}
360
+ >
361
+ <div className="text-sm font-medium truncate">{video.title || `Video`}</div>
362
+ </button>
363
+ ))}
364
+ </div>
365
+ </div>
366
+
367
+ <div className="bg-gray-800 rounded-lg shadow-lg p-4 flex-1 flex flex-col min-h-[200px]">
368
+ <h2 className="text-xl font-semibold mb-4 pb-2 border-b border-gray-700 text-yellow-400">Top Usuarios</h2>
369
+ <div className="flex-1 overflow-y-auto space-y-2 pr-2 custom-scrollbar">
370
+ {leaderboard.map((u, index) => (
371
+ <div key={u.id || index} className="flex justify-between items-center p-2 bg-gray-700 rounded hover:bg-gray-600">
372
+ <div className="flex items-center truncate">
373
+ <span className={`mr-2 font-bold ${index < 3 ? 'text-yellow-400' : 'text-gray-400'}`}>#{index + 1}</span>
374
+ <span className="text-xs md:text-sm truncate max-w-[120px]" title={u.email}>
375
+ {u.email ? u.email.split('@')[0] : 'Usuario'}
376
+ </span>
377
+ </div>
378
+ <span className="text-xs font-mono text-green-400">{Math.floor(u.coins || 0)} 🪙</span>
379
+ </div>
380
+ ))}
381
+ </div>
382
  </div>
383
  </aside>
384
  </main>
385
 
386
+ {/* TAB: PROMOCIONAR */}
387
  <main className={`${currentTab === 'promocionar' ? 'flex' : 'hidden'} flex-1 flex-col items-center justify-center`}>
388
+ <PromoCheck
389
+ user={user}
390
+ userProfile={userProfile}
391
+ userId={userId}
392
+ adminEmail={ADMIN_EMAIL}
393
+ videoCost={VIDEO_COST}
394
+ />
395
  </main>
396
  </div>
397
  );
398
  }
399
 
400
+ function PromoCheck({ user, userProfile, userId, adminEmail, videoCost }) {
401
+ const isAdmin = user?.email === adminEmail;
402
+ const coins = userProfile?.coins || 0;
403
+ const hasEnoughCoins = coins >= videoCost;
404
+
405
+ if (isAdmin) {
406
+ return <PromoUnlocked userId={userId} isAdmin={true} />;
407
+ }
408
+
409
+ if (hasEnoughCoins) {
410
+ return <PromoUnlocked userId={userId} isAdmin={false} videoCost={videoCost} />;
411
+ }
412
+
413
  return (
414
+ <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg border border-gray-700">
415
+ <div className="mb-4 relative inline-block">
416
+ <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>
417
+ <div className="absolute -top-2 -right-2 bg-red-500 text-white text-xs font-bold px-2 py-1 rounded-full">Locked</div>
418
+ </div>
419
+ <h2 className="text-2xl font-bold mb-2 text-white">Necesitas más monedas</h2>
420
+ <p className="text-gray-300 mb-6">
421
+ Para agregar tu video a la lista pública necesitas acumular <span className="text-yellow-400 font-bold">10,000 monedas</span>.
422
  </p>
423
+
424
+ <div className="bg-gray-900 p-4 rounded-lg mb-4">
425
+ <div className="flex justify-between text-sm mb-1 text-gray-400">
426
+ <span>Progreso</span>
427
+ <span>{Math.floor(coins)} / {videoCost}</span>
428
+ </div>
429
+ <div className="w-full bg-gray-700 rounded-full h-2.5">
430
+ <div className="bg-yellow-400 h-2.5 rounded-full transition-all duration-500" style={{ width: `${Math.min((coins / videoCost) * 100, 100)}%` }}></div>
431
+ </div>
432
+ </div>
433
+
434
+ <p className="text-gray-400 text-sm">¡Ve a "Ganar Monedas" y mira videos para sumar puntos!</p>
435
  </div>
436
  );
437
  }
438
 
439
+ function PromoUnlocked({ userId, isAdmin, videoCost }) {
440
  const [url, setUrl] = useState('');
441
  const [message, setMessage] = useState({ text: '', type: 'success' });
442
+ const [loading, setLoading] = useState(false);
443
 
444
  const extractYouTubeID = (url) => {
445
  const regex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
 
455
  setMessage({ text: "URL de YouTube no válida.", type: 'error' });
456
  return;
457
  }
458
+
459
+ setLoading(true);
460
+ const title = isAdmin ? `Recomendado (Admin)` : `Video de Usuario`;
461
 
462
  try {
463
+ if (!isAdmin && db) {
464
+ const userRef = doc(db, 'users', userId);
465
+ await updateDoc(userRef, { coins: increment(-videoCost) });
466
+ }
467
+
468
+ if (db) {
469
+ const videosRef = collection(db, 'videos');
470
+ await addDoc(videosRef, {
471
+ title: title,
472
+ videoId: videoId,
473
+ addedBy: userId,
474
+ createdAt: serverTimestamp(),
475
+ isPromoted: !isAdmin
476
+ });
477
+ }
478
+
479
  setMessage({ text: "¡Video agregado con éxito!", type: 'success' });
480
  setUrl('');
481
  } catch (error) {
482
  console.error("Error adding video:", error);
483
+ setMessage({ text: "Error al procesar. Intenta de nuevo.", type: 'error' });
484
+ } finally {
485
+ setLoading(false);
486
  }
487
  };
488
 
489
  return (
490
+ <div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg w-full border border-green-500/30">
491
+ <div className="flex justify-center mb-4">
492
+ <div className="bg-green-500/20 p-3 rounded-full">
493
+ <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>
494
+ </div>
495
+ </div>
496
+ <h2 className="text-2xl font-bold mb-2 text-white">
497
+ {isAdmin ? "Panel de Administrador" : "¡Objetivo Logrado!"}
498
+ </h2>
499
  <p className="text-gray-300 mb-6">
500
+ {isAdmin
501
+ ? "Como administrador, puedes agregar videos ilimitados."
502
+ : `Canjea ${videoCost} monedas para agregar tu video a la lista pública.`}
503
  </p>
504
+
505
  <form onSubmit={handleSubmit} className="flex flex-col space-y-4">
506
  <input
507
  type="url"
508
  placeholder="https://www.youtube.com/watch?v=..."
509
+ 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"
510
  required
511
  value={url}
512
  onChange={(e) => setUrl(e.target.value)}
513
+ disabled={loading}
514
  />
515
  <button
516
  type="submit"
517
+ disabled={loading}
518
+ 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'}`}
519
  >
520
+ {loading ? 'Procesando...' : (isAdmin ? 'Agregar Video Gratis' : `Pagar ${videoCost} Monedas y Agregar`)}
521
  </button>
522
  {message.text && (
523
+ <p className={`text-sm font-medium ${message.type === 'success' ? 'text-green-400' : 'text-red-400'}`}>
524
  {message.text}
525
  </p>
526
  )}