salomonsky commited on
Commit
a3af96b
·
verified ·
1 Parent(s): 3fc951b

Update cointube.jsx

Browse files
Files changed (1) hide show
  1. cointube.jsx +77 -50
cointube.jsx CHANGED
@@ -22,11 +22,8 @@ import {
22
  limit
23
  } from 'firebase/firestore';
24
 
25
- // --- Variables Globales ---
26
- // Se definen fuera para que mantengan su valor, pero se inicializan dentro
27
  let app, auth, db;
28
 
29
- // --- Constantes del Sistema ---
30
  const ADMIN_EMAIL = "photonicsupernova@gmail.com";
31
  const VIDEO_COST = 10000;
32
  const DEFAULT_VIDEO_ID = "UtEhewStfMA";
@@ -34,11 +31,9 @@ const DEFAULT_VIDEO_TITLE = "Pelicula Completa";
34
 
35
  export default function App() {
36
 
37
- // --- Estados de Inicialización ---
38
  const [firebaseReady, setFirebaseReady] = useState(false);
39
  const [configError, setConfigError] = useState(null);
40
 
41
- // --- Estados de la App ---
42
  const [videos, setVideos] = useState([]);
43
  const [leaderboard, setLeaderboard] = useState([]);
44
  const [user, setUser] = useState(null);
@@ -54,8 +49,6 @@ export default function App() {
54
  const coinIntervalRef = useRef(null);
55
  const localCoinBufferRef = useRef(0);
56
 
57
- // --- 1. OBTENER CONFIGURACIÓN DEL SERVIDOR ---
58
- // Esto reemplaza a las credenciales hardcodeadas
59
  useEffect(() => {
60
  const initFirebase = async () => {
61
  try {
@@ -68,19 +61,17 @@ export default function App() {
68
  app = initializeApp(config);
69
  auth = getAuth(app);
70
  db = getFirestore(app);
71
- console.log("Firebase conectado con éxito.");
72
  }
73
  setFirebaseReady(true);
74
 
75
  } catch (error) {
76
- console.error("Error inicializando:", error);
77
  setConfigError("Error de conexión con el servidor.");
78
  }
79
  };
80
  initFirebase();
81
  }, []);
82
 
83
- // --- 2. Auth ---
84
  useEffect(() => {
85
  if (!firebaseReady || !auth) return;
86
  const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
@@ -97,7 +88,6 @@ export default function App() {
97
  return () => unsubscribeAuth();
98
  }, [firebaseReady]);
99
 
100
- // --- 3. Perfil de Usuario ---
101
  useEffect(() => {
102
  if (!firebaseReady || !userId || !db) return;
103
  const profileRef = doc(db, 'users', userId);
@@ -110,14 +100,13 @@ export default function App() {
110
  coins: 0,
111
  watchedDefaultVideo: false
112
  };
113
- setDoc(profileRef, newProfile).catch(e => console.error("Error creating profile:", e));
114
  setUserProfile(newProfile);
115
  }
116
- }, (error) => console.error("Error listening to profile:", error));
117
  return () => unsubscribeProfile();
118
  }, [userId, user, firebaseReady]);
119
 
120
- // --- 4. Cargar Videos ---
121
  useEffect(() => {
122
  if (!firebaseReady || !db) return;
123
  const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(20));
@@ -132,7 +121,6 @@ export default function App() {
132
  return () => unsubscribe();
133
  }, [firebaseReady]);
134
 
135
- // --- 5. Leaderboard ---
136
  useEffect(() => {
137
  if (!firebaseReady || !db) return;
138
  const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(20));
@@ -143,7 +131,6 @@ export default function App() {
143
  return () => unsubscribe();
144
  }, [firebaseReady]);
145
 
146
- // --- 6. YouTube API ---
147
  useEffect(() => {
148
  if (!window.YT) {
149
  const tag = document.createElement('script');
@@ -156,7 +143,6 @@ export default function App() {
156
  }
157
  }, []);
158
 
159
- // --- Lógica de Monedas ---
160
  const saveCoinBuffer = useCallback(() => {
161
  if (localCoinBufferRef.current > 0 && userId && db) {
162
  const coinsToSave = localCoinBufferRef.current;
@@ -183,7 +169,6 @@ export default function App() {
183
  }
184
  }, [saveCoinBuffer]);
185
 
186
- // --- Player Events ---
187
  const onPlayerReady = useCallback((event) => {
188
  playerRef.current = event.target;
189
  playerRef.current.playVideo();
@@ -225,7 +210,6 @@ export default function App() {
225
  if (playerRef.current) playerRef.current.loadVideoById(video.videoId);
226
  };
227
 
228
- // --- Handlers ---
229
  const handleGoogleLogin = async () => {
230
  if (!auth) return;
231
  const provider = new GoogleAuthProvider();
@@ -242,27 +226,55 @@ export default function App() {
242
  await signOut(auth);
243
  };
244
 
245
- // --- Render Views ---
246
  if (configError) {
247
  return <div className="flex justify-center items-center h-full text-red-500">{configError}</div>;
248
  }
249
 
250
  if (!firebaseReady || !authReady) {
251
  return (
252
- <div className="flex justify-center items-center h-full">
253
  <div className="spinner mb-4"></div>
 
 
 
254
  </div>
255
  );
256
  }
257
 
258
  if (authReady && !user) {
259
  return (
260
- <div className="flex flex-col items-center justify-center h-full bg-gray-900 p-4">
261
- <div className="bg-gray-800 p-8 rounded-2xl shadow-xl text-center max-w-md w-full border border-gray-700">
262
- <h1 className="text-3xl font-bold text-white mb-4">CoinTube</h1>
263
- <button onClick={handleGoogleLogin} className="w-full py-3 bg-white text-gray-900 font-bold rounded-lg hover:bg-gray-100">
264
- Entrar con Google
265
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  </div>
267
  </div>
268
  );
@@ -286,7 +298,6 @@ export default function App() {
286
  <button onClick={() => setCurrentTab('promocionar')} className={`flex-1 py-2 rounded ${currentTab === 'promocionar' ? 'bg-blue-600 text-white' : 'text-gray-400'}`}>Subir Video</button>
287
  </nav>
288
 
289
- {/* TAB: GANAR */}
290
  <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6`}>
291
  <div className="lg:w-2/3">
292
  <div className="aspect-video bg-black rounded-lg overflow-hidden shadow-xl">
@@ -308,12 +319,14 @@ export default function App() {
308
  </div>
309
  </div>
310
  <div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
311
- <h3 className="text-yellow-500 text-sm uppercase font-bold mb-2">Ranking</h3>
312
  <div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
313
  {leaderboard.map((u, i) => (
314
  <div key={u.id || i} className="flex justify-between p-2 bg-gray-700 rounded text-sm">
315
- <span className="text-white">#{i+1} {u.email?.split('@')[0]}</span>
316
- <span className="text-yellow-400">{Math.floor(u.coins)}</span>
 
 
317
  </div>
318
  ))}
319
  </div>
@@ -321,8 +334,6 @@ export default function App() {
321
  </aside>
322
  </main>
323
 
324
- {/* TAB: PROMOCIONAR */}
325
- {/* Aquí pasamos 'db' como prop, que es lo que fallaba antes */}
326
  <main className={`${currentTab === 'promocionar' ? 'flex' : 'hidden'} flex-1 items-center justify-center`}>
327
  <PromoCheck
328
  user={user}
@@ -337,8 +348,6 @@ export default function App() {
337
  );
338
  }
339
 
340
- // --- Componentes Hijos ---
341
-
342
  function PromoCheck({ user, userProfile, userId, adminEmail, videoCost, db }) {
343
  const isAdmin = user?.email === adminEmail;
344
  const coins = userProfile?.coins || 0;
@@ -361,6 +370,7 @@ function PromoCheck({ user, userProfile, userId, adminEmail, videoCost, db }) {
361
 
362
  function PromoUnlocked({ userId, isAdmin, videoCost, db }) {
363
  const [url, setUrl] = useState('');
 
364
  const [message, setMessage] = useState({ text: '', type: '' });
365
  const [loading, setLoading] = useState(false);
366
 
@@ -375,27 +385,28 @@ function PromoUnlocked({ userId, isAdmin, videoCost, db }) {
375
  const videoId = extractYouTubeID(url);
376
 
377
  if (!videoId) {
378
- setMessage({ text: "Enlace no válido", type: 'error' });
 
 
 
 
 
379
  return;
380
  }
381
 
382
- // Validación extra para asegurar que db existe
383
  if (!db) {
384
  setMessage({ text: "Error: Base de datos no conectada", type: 'error' });
385
  return;
386
  }
387
 
388
  setLoading(true);
389
- const title = isAdmin ? `Recomendado (Admin)` : `Video de Usuario`;
390
 
391
  try {
392
- // 1. Descontar monedas (si no es admin)
393
  if (!isAdmin) {
394
  const userRef = doc(db, 'users', userId);
395
  await updateDoc(userRef, { coins: increment(-videoCost) });
396
  }
397
 
398
- // 2. Guardar video en Firestore
399
  const videosRef = collection(db, 'videos');
400
  await addDoc(videosRef, {
401
  title: title,
@@ -405,8 +416,9 @@ function PromoUnlocked({ userId, isAdmin, videoCost, db }) {
405
  isPromoted: !isAdmin
406
  });
407
 
408
- setMessage({ text: "¡Video agregado!", type: 'success' });
409
  setUrl('');
 
410
  } catch (error) {
411
  console.error("Error al subir:", error);
412
  if (error.code === 'permission-denied') {
@@ -421,16 +433,31 @@ function PromoUnlocked({ userId, isAdmin, videoCost, db }) {
421
 
422
  return (
423
  <div className="bg-gray-800 p-8 rounded-lg shadow-xl max-w-md w-full border border-green-900">
424
- <h2 className="text-2xl font-bold text-white mb-4 text-center">{isAdmin ? "Admin Panel" : "¡Meta Alcanzada!"}</h2>
425
  <form onSubmit={handleSubmit} className="space-y-4">
426
- <input
427
- type="url"
428
- placeholder="Enlace de YouTube..."
429
- className="w-full p-3 bg-gray-700 text-white rounded border border-gray-600 focus:border-blue-500 outline-none"
430
- value={url}
431
- onChange={(e) => setUrl(e.target.value)}
432
- required
433
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
434
  <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">
435
  {loading ? 'Procesando...' : (isAdmin ? 'Subir Gratis' : `Pagar ${videoCost} y Subir`)}
436
  </button>
 
22
  limit
23
  } from 'firebase/firestore';
24
 
 
 
25
  let app, auth, db;
26
 
 
27
  const ADMIN_EMAIL = "photonicsupernova@gmail.com";
28
  const VIDEO_COST = 10000;
29
  const DEFAULT_VIDEO_ID = "UtEhewStfMA";
 
31
 
32
  export default function App() {
33
 
 
34
  const [firebaseReady, setFirebaseReady] = useState(false);
35
  const [configError, setConfigError] = useState(null);
36
 
 
37
  const [videos, setVideos] = useState([]);
38
  const [leaderboard, setLeaderboard] = useState([]);
39
  const [user, setUser] = useState(null);
 
49
  const coinIntervalRef = useRef(null);
50
  const localCoinBufferRef = useRef(0);
51
 
 
 
52
  useEffect(() => {
53
  const initFirebase = async () => {
54
  try {
 
61
  app = initializeApp(config);
62
  auth = getAuth(app);
63
  db = getFirestore(app);
 
64
  }
65
  setFirebaseReady(true);
66
 
67
  } catch (error) {
68
+ console.error(error);
69
  setConfigError("Error de conexión con el servidor.");
70
  }
71
  };
72
  initFirebase();
73
  }, []);
74
 
 
75
  useEffect(() => {
76
  if (!firebaseReady || !auth) return;
77
  const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
 
88
  return () => unsubscribeAuth();
89
  }, [firebaseReady]);
90
 
 
91
  useEffect(() => {
92
  if (!firebaseReady || !userId || !db) return;
93
  const profileRef = doc(db, 'users', userId);
 
100
  coins: 0,
101
  watchedDefaultVideo: false
102
  };
103
+ setDoc(profileRef, newProfile).catch(e => console.error(e));
104
  setUserProfile(newProfile);
105
  }
106
+ }, (error) => console.error(error));
107
  return () => unsubscribeProfile();
108
  }, [userId, user, firebaseReady]);
109
 
 
110
  useEffect(() => {
111
  if (!firebaseReady || !db) return;
112
  const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(20));
 
121
  return () => unsubscribe();
122
  }, [firebaseReady]);
123
 
 
124
  useEffect(() => {
125
  if (!firebaseReady || !db) return;
126
  const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(20));
 
131
  return () => unsubscribe();
132
  }, [firebaseReady]);
133
 
 
134
  useEffect(() => {
135
  if (!window.YT) {
136
  const tag = document.createElement('script');
 
143
  }
144
  }, []);
145
 
 
146
  const saveCoinBuffer = useCallback(() => {
147
  if (localCoinBufferRef.current > 0 && userId && db) {
148
  const coinsToSave = localCoinBufferRef.current;
 
169
  }
170
  }, [saveCoinBuffer]);
171
 
 
172
  const onPlayerReady = useCallback((event) => {
173
  playerRef.current = event.target;
174
  playerRef.current.playVideo();
 
210
  if (playerRef.current) playerRef.current.loadVideoById(video.videoId);
211
  };
212
 
 
213
  const handleGoogleLogin = async () => {
214
  if (!auth) return;
215
  const provider = new GoogleAuthProvider();
 
226
  await signOut(auth);
227
  };
228
 
 
229
  if (configError) {
230
  return <div className="flex justify-center items-center h-full text-red-500">{configError}</div>;
231
  }
232
 
233
  if (!firebaseReady || !authReady) {
234
  return (
235
+ <div className="flex flex-col items-center justify-center h-full w-full">
236
  <div className="spinner mb-4"></div>
237
+ <p className="text-lg text-gray-400">
238
+ {!firebaseReady ? "Cargando sistema..." : "Conectando..."}
239
+ </p>
240
  </div>
241
  );
242
  }
243
 
244
  if (authReady && !user) {
245
  return (
246
+ <div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
247
+ <div className="max-w-md w-full bg-gray-800 rounded-2xl shadow-2xl p-8 text-center border border-gray-700">
248
+ <div className="flex justify-center mb-6">
249
+ <div className="bg-blue-600 p-4 rounded-full shadow-lg">
250
+ <svg className="w-12 h-12 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
251
+ <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>
252
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
253
+ </svg>
254
+ </div>
255
+ </div>
256
+ <h1 className="text-3xl font-bold text-white mb-2 tracking-tight">Bienvenido a CoinTube</h1>
257
+ <p className="text-gray-400 mb-8 text-sm leading-relaxed">
258
+ Descubre contenido increíble, gana monedas virtuales por cada minuto que ves y promociona tus propios videos.<br/><br/>
259
+ <span className="text-yellow-400 font-bold text-base">¡Junta 10,000 monedas para subir tu video!</span>
260
+ </p>
261
+ <div className="space-y-3">
262
+ <button
263
+ onClick={handleGoogleLogin}
264
+ 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"
265
+ >
266
+ <svg className="w-5 h-5 mr-2" viewBox="0 0 24 24">
267
+ <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"/>
268
+ <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"/>
269
+ <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"/>
270
+ <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"/>
271
+ </svg>
272
+ Continuar con Google
273
+ </button>
274
+ </div>
275
+ <p className="mt-6 text-xs text-gray-500">
276
+ Al continuar, aceptas nuestros términos de servicio.
277
+ </p>
278
  </div>
279
  </div>
280
  );
 
298
  <button onClick={() => setCurrentTab('promocionar')} className={`flex-1 py-2 rounded ${currentTab === 'promocionar' ? 'bg-blue-600 text-white' : 'text-gray-400'}`}>Subir Video</button>
299
  </nav>
300
 
 
301
  <main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6`}>
302
  <div className="lg:w-2/3">
303
  <div className="aspect-video bg-black rounded-lg overflow-hidden shadow-xl">
 
319
  </div>
320
  </div>
321
  <div className="bg-gray-800 rounded p-4 flex-1 flex flex-col min-h-[200px]">
322
+ <h3 className="text-yellow-500 text-sm uppercase font-bold mb-2">Ranking Global</h3>
323
  <div className="overflow-y-auto flex-1 pr-2 custom-scrollbar space-y-2">
324
  {leaderboard.map((u, i) => (
325
  <div key={u.id || i} className="flex justify-between p-2 bg-gray-700 rounded text-sm">
326
+ <span className="text-white text-xs md:text-sm truncate mr-2" title={u.email}>
327
+ #{i+1} {u.email}
328
+ </span>
329
+ <span className="text-yellow-400 font-mono whitespace-nowrap">{Math.floor(u.coins)}</span>
330
  </div>
331
  ))}
332
  </div>
 
334
  </aside>
335
  </main>
336
 
 
 
337
  <main className={`${currentTab === 'promocionar' ? 'flex' : 'hidden'} flex-1 items-center justify-center`}>
338
  <PromoCheck
339
  user={user}
 
348
  );
349
  }
350
 
 
 
351
  function PromoCheck({ user, userProfile, userId, adminEmail, videoCost, db }) {
352
  const isAdmin = user?.email === adminEmail;
353
  const coins = userProfile?.coins || 0;
 
370
 
371
  function PromoUnlocked({ userId, isAdmin, videoCost, db }) {
372
  const [url, setUrl] = useState('');
373
+ const [title, setTitle] = useState('');
374
  const [message, setMessage] = useState({ text: '', type: '' });
375
  const [loading, setLoading] = useState(false);
376
 
 
385
  const videoId = extractYouTubeID(url);
386
 
387
  if (!videoId) {
388
+ setMessage({ text: "Enlace de YouTube no válido", type: 'error' });
389
+ return;
390
+ }
391
+
392
+ if (!title.trim()) {
393
+ setMessage({ text: "Por favor escribe un título", type: 'error' });
394
  return;
395
  }
396
 
 
397
  if (!db) {
398
  setMessage({ text: "Error: Base de datos no conectada", type: 'error' });
399
  return;
400
  }
401
 
402
  setLoading(true);
 
403
 
404
  try {
 
405
  if (!isAdmin) {
406
  const userRef = doc(db, 'users', userId);
407
  await updateDoc(userRef, { coins: increment(-videoCost) });
408
  }
409
 
 
410
  const videosRef = collection(db, 'videos');
411
  await addDoc(videosRef, {
412
  title: title,
 
416
  isPromoted: !isAdmin
417
  });
418
 
419
+ setMessage({ text: "¡Video agregado con éxito!", type: 'success' });
420
  setUrl('');
421
+ setTitle('');
422
  } catch (error) {
423
  console.error("Error al subir:", error);
424
  if (error.code === 'permission-denied') {
 
433
 
434
  return (
435
  <div className="bg-gray-800 p-8 rounded-lg shadow-xl max-w-md w-full border border-green-900">
436
+ <h2 className="text-2xl font-bold text-white mb-4 text-center">{isAdmin ? "Panel Admin" : "¡Meta Alcanzada!"}</h2>
437
  <form onSubmit={handleSubmit} className="space-y-4">
438
+ <div>
439
+ <label className="block text-sm text-gray-400 mb-1">Enlace del Video</label>
440
+ <input
441
+ type="url"
442
+ placeholder="https://youtube.com/watch?v=..."
443
+ className="w-full p-3 bg-gray-700 text-white rounded border border-gray-600 focus:border-blue-500 outline-none"
444
+ value={url}
445
+ onChange={(e) => setUrl(e.target.value)}
446
+ required
447
+ />
448
+ </div>
449
+ <div>
450
+ <label className="block text-sm text-gray-400 mb-1">Título del Video</label>
451
+ <input
452
+ type="text"
453
+ placeholder="Ej: Mi Gameplay Épico"
454
+ className="w-full p-3 bg-gray-700 text-white rounded border border-gray-600 focus:border-blue-500 outline-none"
455
+ value={title}
456
+ onChange={(e) => setTitle(e.target.value)}
457
+ required
458
+ maxLength={50}
459
+ />
460
+ </div>
461
  <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">
462
  {loading ? 'Procesando...' : (isAdmin ? 'Subir Gratis' : `Pagar ${videoCost} y Subir`)}
463
  </button>