Spaces:
Running
Running
Update cointube.jsx
Browse files- 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 |
-
|
|
|
|
| 23 |
} from 'firebase/firestore';
|
| 24 |
|
| 25 |
-
// ---
|
| 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 |
-
|
|
|
|
| 41 |
} catch (error) {
|
| 42 |
-
console.error("
|
| 43 |
}
|
| 44 |
|
| 45 |
-
// --- Constantes del
|
|
|
|
|
|
|
| 46 |
const DEFAULT_VIDEO_ID = "UtEhewStfMA";
|
| 47 |
-
const DEFAULT_VIDEO_TITLE = "Pelicula Completa";
|
| 48 |
|
| 49 |
export default function App() {
|
| 50 |
|
| 51 |
-
// ---
|
| 52 |
-
const [videos, setVideos] = useState([
|
| 53 |
-
|
| 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)
|
|
|
|
|
|
|
|
|
|
| 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 = {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 109 |
-
/*
|
| 110 |
useEffect(() => {
|
| 111 |
if (!db) return;
|
| 112 |
-
const
|
| 113 |
-
const
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 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 |
-
|
| 125 |
-
|
|
|
|
|
|
|
| 126 |
}
|
| 127 |
-
}, (error) => {
|
| 128 |
-
console.error("Error listening to videos:", error);
|
| 129 |
});
|
|
|
|
|
|
|
| 130 |
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
}, []);
|
| 133 |
-
*/
|
| 134 |
|
| 135 |
-
//
|
| 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 |
-
//
|
| 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"
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 355 |
-
|
| 356 |
-
<div className="
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
</div>
|
| 368 |
</aside>
|
| 369 |
</main>
|
| 370 |
|
|
|
|
| 371 |
<main className={`${currentTab === 'promocionar' ? 'flex' : 'hidden'} flex-1 flex-col items-center justify-center`}>
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
|
|
|
|
|
|
| 377 |
</main>
|
| 378 |
</div>
|
| 379 |
);
|
| 380 |
}
|
| 381 |
|
| 382 |
-
|
| 383 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
return (
|
| 385 |
-
<div className="text-center p-8 bg-gray-800 rounded-lg shadow-xl max-w-lg">
|
| 386 |
-
<
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
|
|
|
|
|
|
|
|
|
| 390 |
</p>
|
| 391 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 415 |
|
| 416 |
try {
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 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
|
|
|
|
|
|
|
| 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 |
-
<
|
| 435 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
<p className="text-gray-300 mb-6">
|
| 437 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 451 |
>
|
| 452 |
-
|
| 453 |
</button>
|
| 454 |
{message.text && (
|
| 455 |
-
<p className={`text-sm ${message.type === 'success' ? 'text-green-
|
| 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 |
)}
|