Spaces:
Running
Running
Update cointube.jsx
Browse files- cointube.jsx +35 -45
cointube.jsx
CHANGED
|
@@ -23,7 +23,22 @@ import {
|
|
| 23 |
limit
|
| 24 |
} from 'firebase/firestore';
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
const ADMIN_EMAIL = "dimensionalpulsar@gmail.com";
|
| 29 |
const VIDEO_COST = 10000;
|
|
@@ -32,8 +47,10 @@ const DEFAULT_VIDEO_TITLE = "Cuentos de Terror";
|
|
| 32 |
|
| 33 |
export default function App() {
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
| 37 |
|
| 38 |
const [videos, setVideos] = useState([]);
|
| 39 |
const [leaderboard, setLeaderboard] = useState([]);
|
|
@@ -43,7 +60,6 @@ export default function App() {
|
|
| 43 |
const [userProfile, setUserProfile] = useState(null);
|
| 44 |
const [currentTab, setCurrentTab] = useState('ganar');
|
| 45 |
|
| 46 |
-
// CAMBIO 1: Iniciamos en null para no mostrar el default mientras carga
|
| 47 |
const [currentVideoId, setCurrentVideoId] = useState(null);
|
| 48 |
const [currentVideoTitle, setCurrentVideoTitle] = useState(null);
|
| 49 |
|
|
@@ -54,31 +70,10 @@ export default function App() {
|
|
| 54 |
const localCoinBufferRef = useRef(0);
|
| 55 |
const initialLoadRef = useRef(true);
|
| 56 |
|
| 57 |
-
useEffect
|
| 58 |
-
const initFirebase = async () => {
|
| 59 |
-
try {
|
| 60 |
-
const response = await fetch('/api/config');
|
| 61 |
-
if (!response.ok) throw new Error("Error de red al cargar config");
|
| 62 |
-
|
| 63 |
-
const config = await response.json();
|
| 64 |
-
|
| 65 |
-
if (!app) {
|
| 66 |
-
app = initializeApp(config);
|
| 67 |
-
auth = getAuth(app);
|
| 68 |
-
db = getFirestore(app);
|
| 69 |
-
}
|
| 70 |
-
setFirebaseReady(true);
|
| 71 |
-
|
| 72 |
-
} catch (error) {
|
| 73 |
-
console.error(error);
|
| 74 |
-
setConfigError("Error de conexión con el servidor.");
|
| 75 |
-
}
|
| 76 |
-
};
|
| 77 |
-
initFirebase();
|
| 78 |
-
}, []);
|
| 79 |
|
| 80 |
useEffect(() => {
|
| 81 |
-
if (!
|
| 82 |
const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
|
| 83 |
if (user) {
|
| 84 |
setUser(user);
|
|
@@ -91,10 +86,10 @@ export default function App() {
|
|
| 91 |
setAuthReady(true);
|
| 92 |
});
|
| 93 |
return () => unsubscribeAuth();
|
| 94 |
-
}, [
|
| 95 |
|
| 96 |
useEffect(() => {
|
| 97 |
-
if (!
|
| 98 |
const profileRef = doc(db, 'users', userId);
|
| 99 |
const unsubscribeProfile = onSnapshot(profileRef, (docSnap) => {
|
| 100 |
if (docSnap.exists()) {
|
|
@@ -110,18 +105,16 @@ export default function App() {
|
|
| 110 |
}
|
| 111 |
}, (error) => console.error(error));
|
| 112 |
return () => unsubscribeProfile();
|
| 113 |
-
}, [userId, user
|
| 114 |
|
| 115 |
useEffect(() => {
|
| 116 |
-
if (!
|
| 117 |
-
// La consulta ya ordena por fecha descendente (el más nuevo primero)
|
| 118 |
const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(50));
|
| 119 |
const unsubscribe = onSnapshot(q, (snapshot) => {
|
| 120 |
const fetchedVideos = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
| 121 |
|
| 122 |
if (fetchedVideos.length > 0) {
|
| 123 |
setVideos(fetchedVideos);
|
| 124 |
-
// CAMBIO 2: Si es la carga inicial O si no hay video seleccionado aún
|
| 125 |
if (initialLoadRef.current || !currentVideoId) {
|
| 126 |
const latestVideo = fetchedVideos[0];
|
| 127 |
setCurrentVideoId(latestVideo.videoId);
|
|
@@ -129,7 +122,6 @@ export default function App() {
|
|
| 129 |
initialLoadRef.current = false;
|
| 130 |
}
|
| 131 |
} else {
|
| 132 |
-
// Solo usamos el default si NO hay videos en la base de datos
|
| 133 |
setVideos([{ id: '1', title: DEFAULT_VIDEO_TITLE, videoId: DEFAULT_VIDEO_ID }]);
|
| 134 |
if (initialLoadRef.current || !currentVideoId) {
|
| 135 |
setCurrentVideoId(DEFAULT_VIDEO_ID);
|
|
@@ -139,10 +131,10 @@ export default function App() {
|
|
| 139 |
}
|
| 140 |
});
|
| 141 |
return () => unsubscribe();
|
| 142 |
-
}, [
|
| 143 |
|
| 144 |
useEffect(() => {
|
| 145 |
-
if (!
|
| 146 |
const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(100));
|
| 147 |
const unsubscribe = onSnapshot(q, (snapshot) => {
|
| 148 |
let usersList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
|
@@ -155,7 +147,7 @@ export default function App() {
|
|
| 155 |
setLeaderboard(usersList.slice(0, 20));
|
| 156 |
});
|
| 157 |
return () => unsubscribe();
|
| 158 |
-
}, [
|
| 159 |
|
| 160 |
useEffect(() => {
|
| 161 |
if (!window.YT) {
|
|
@@ -214,7 +206,6 @@ export default function App() {
|
|
| 214 |
}, [startCoinInterval, stopCoinInterval, currentVideoId, userId]);
|
| 215 |
|
| 216 |
useEffect(() => {
|
| 217 |
-
// CAMBIO 3: Aseguramos que currentVideoId exista antes de inicializar el player
|
| 218 |
if (ytApiReady && userId && !playerRef.current && currentVideoId) {
|
| 219 |
playerRef.current = new window.YT.Player('player', {
|
| 220 |
height: '100%',
|
|
@@ -275,20 +266,17 @@ export default function App() {
|
|
| 275 |
await signOut(auth);
|
| 276 |
};
|
| 277 |
|
| 278 |
-
if (
|
| 279 |
-
return <div className="flex justify-center items-center h-full text-red-500">{configError}</div>;
|
| 280 |
-
}
|
| 281 |
-
|
| 282 |
-
if (!firebaseReady || !authReady) {
|
| 283 |
return (
|
| 284 |
<div className="flex flex-col items-center justify-center h-full w-full">
|
| 285 |
<div className="spinner mb-4"></div>
|
| 286 |
-
<p className="text-lg text-gray-400">
|
| 287 |
</div>
|
| 288 |
);
|
| 289 |
}
|
| 290 |
|
| 291 |
if (authReady && !user) {
|
|
|
|
| 292 |
return (
|
| 293 |
<div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
|
| 294 |
<div className="max-w-md w-full bg-gray-800 rounded-2xl shadow-2xl p-8 text-center border border-gray-700">
|
|
@@ -322,6 +310,7 @@ export default function App() {
|
|
| 322 |
);
|
| 323 |
}
|
| 324 |
|
|
|
|
| 325 |
return (
|
| 326 |
<div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-6">
|
| 327 |
|
|
@@ -384,7 +373,6 @@ export default function App() {
|
|
| 384 |
<main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6`}>
|
| 385 |
<div className="lg:w-2/3">
|
| 386 |
<div className="aspect-video bg-black rounded-lg overflow-hidden shadow-xl flex items-center justify-center">
|
| 387 |
-
{/* CAMBIO 4: Renderizado condicional del player */}
|
| 388 |
{currentVideoId ? (
|
| 389 |
<div id="player" className="w-full h-full"></div>
|
| 390 |
) : (
|
|
@@ -451,6 +439,8 @@ export default function App() {
|
|
| 451 |
);
|
| 452 |
}
|
| 453 |
|
|
|
|
|
|
|
| 454 |
function PromoCheck({ user, userProfile, userId, adminEmail, videoCost, db }) {
|
| 455 |
const isAdmin = user?.email === adminEmail;
|
| 456 |
const coins = userProfile?.coins || 0;
|
|
|
|
| 23 |
limit
|
| 24 |
} from 'firebase/firestore';
|
| 25 |
|
| 26 |
+
// --- CAMBIO IMPORTANTE: Inicialización Global ---
|
| 27 |
+
// Definimos la configuración usando las variables de entorno de Hugging Face (Vite)
|
| 28 |
+
const firebaseConfig = {
|
| 29 |
+
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
|
| 30 |
+
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
|
| 31 |
+
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
|
| 32 |
+
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
|
| 33 |
+
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
|
| 34 |
+
appId: import.meta.env.VITE_FIREBASE_APP_ID
|
| 35 |
+
};
|
| 36 |
+
|
| 37 |
+
// Inicializamos Firebase inmediatamente
|
| 38 |
+
const app = initializeApp(firebaseConfig);
|
| 39 |
+
const auth = getAuth(app);
|
| 40 |
+
const db = getFirestore(app);
|
| 41 |
+
// ------------------------------------------------
|
| 42 |
|
| 43 |
const ADMIN_EMAIL = "dimensionalpulsar@gmail.com";
|
| 44 |
const VIDEO_COST = 10000;
|
|
|
|
| 47 |
|
| 48 |
export default function App() {
|
| 49 |
|
| 50 |
+
// Ya no necesitamos 'configError' porque cargamos la config directa
|
| 51 |
+
// Pero mantenemos 'firebaseReady' para no romper tu lógica visual,
|
| 52 |
+
// aunque ahora siempre será true desde el inicio.
|
| 53 |
+
const [firebaseReady, setFirebaseReady] = useState(true);
|
| 54 |
|
| 55 |
const [videos, setVideos] = useState([]);
|
| 56 |
const [leaderboard, setLeaderboard] = useState([]);
|
|
|
|
| 60 |
const [userProfile, setUserProfile] = useState(null);
|
| 61 |
const [currentTab, setCurrentTab] = useState('ganar');
|
| 62 |
|
|
|
|
| 63 |
const [currentVideoId, setCurrentVideoId] = useState(null);
|
| 64 |
const [currentVideoTitle, setCurrentVideoTitle] = useState(null);
|
| 65 |
|
|
|
|
| 70 |
const localCoinBufferRef = useRef(0);
|
| 71 |
const initialLoadRef = useRef(true);
|
| 72 |
|
| 73 |
+
// --- NOTA: Eliminé el useEffect del 'fetch' porque ya inicializamos arriba ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
useEffect(() => {
|
| 76 |
+
if (!auth) return;
|
| 77 |
const unsubscribeAuth = onAuthStateChanged(auth, (user) => {
|
| 78 |
if (user) {
|
| 79 |
setUser(user);
|
|
|
|
| 86 |
setAuthReady(true);
|
| 87 |
});
|
| 88 |
return () => unsubscribeAuth();
|
| 89 |
+
}, []); // Array vacío porque 'auth' ya es estable
|
| 90 |
|
| 91 |
useEffect(() => {
|
| 92 |
+
if (!userId || !db) return;
|
| 93 |
const profileRef = doc(db, 'users', userId);
|
| 94 |
const unsubscribeProfile = onSnapshot(profileRef, (docSnap) => {
|
| 95 |
if (docSnap.exists()) {
|
|
|
|
| 105 |
}
|
| 106 |
}, (error) => console.error(error));
|
| 107 |
return () => unsubscribeProfile();
|
| 108 |
+
}, [userId, user]); // Quitamos firebaseReady de dependencias
|
| 109 |
|
| 110 |
useEffect(() => {
|
| 111 |
+
if (!db) return;
|
|
|
|
| 112 |
const q = query(collection(db, 'videos'), orderBy('createdAt', 'desc'), limit(50));
|
| 113 |
const unsubscribe = onSnapshot(q, (snapshot) => {
|
| 114 |
const fetchedVideos = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
| 115 |
|
| 116 |
if (fetchedVideos.length > 0) {
|
| 117 |
setVideos(fetchedVideos);
|
|
|
|
| 118 |
if (initialLoadRef.current || !currentVideoId) {
|
| 119 |
const latestVideo = fetchedVideos[0];
|
| 120 |
setCurrentVideoId(latestVideo.videoId);
|
|
|
|
| 122 |
initialLoadRef.current = false;
|
| 123 |
}
|
| 124 |
} else {
|
|
|
|
| 125 |
setVideos([{ id: '1', title: DEFAULT_VIDEO_TITLE, videoId: DEFAULT_VIDEO_ID }]);
|
| 126 |
if (initialLoadRef.current || !currentVideoId) {
|
| 127 |
setCurrentVideoId(DEFAULT_VIDEO_ID);
|
|
|
|
| 131 |
}
|
| 132 |
});
|
| 133 |
return () => unsubscribe();
|
| 134 |
+
}, [currentVideoId]);
|
| 135 |
|
| 136 |
useEffect(() => {
|
| 137 |
+
if (!db) return;
|
| 138 |
const q = query(collection(db, 'users'), orderBy('coins', 'desc'), limit(100));
|
| 139 |
const unsubscribe = onSnapshot(q, (snapshot) => {
|
| 140 |
let usersList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
|
|
|
|
| 147 |
setLeaderboard(usersList.slice(0, 20));
|
| 148 |
});
|
| 149 |
return () => unsubscribe();
|
| 150 |
+
}, []);
|
| 151 |
|
| 152 |
useEffect(() => {
|
| 153 |
if (!window.YT) {
|
|
|
|
| 206 |
}, [startCoinInterval, stopCoinInterval, currentVideoId, userId]);
|
| 207 |
|
| 208 |
useEffect(() => {
|
|
|
|
| 209 |
if (ytApiReady && userId && !playerRef.current && currentVideoId) {
|
| 210 |
playerRef.current = new window.YT.Player('player', {
|
| 211 |
height: '100%',
|
|
|
|
| 266 |
await signOut(auth);
|
| 267 |
};
|
| 268 |
|
| 269 |
+
if (!authReady) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
return (
|
| 271 |
<div className="flex flex-col items-center justify-center h-full w-full">
|
| 272 |
<div className="spinner mb-4"></div>
|
| 273 |
+
<p className="text-lg text-gray-400">Conectando...</p>
|
| 274 |
</div>
|
| 275 |
);
|
| 276 |
}
|
| 277 |
|
| 278 |
if (authReady && !user) {
|
| 279 |
+
// ... (TU UI DE LOGIN SE MANTIENE IGUAL)
|
| 280 |
return (
|
| 281 |
<div className="flex flex-col items-center justify-center h-full w-full bg-gray-900 p-4">
|
| 282 |
<div className="max-w-md w-full bg-gray-800 rounded-2xl shadow-2xl p-8 text-center border border-gray-700">
|
|
|
|
| 310 |
);
|
| 311 |
}
|
| 312 |
|
| 313 |
+
// El resto del return (header, main, etc) sigue igual que en tu código original...
|
| 314 |
return (
|
| 315 |
<div className="h-full w-full max-w-7xl mx-auto flex flex-col p-4 md:p-6">
|
| 316 |
|
|
|
|
| 373 |
<main className={`${currentTab === 'ganar' ? 'flex' : 'hidden'} flex-1 flex-col lg:flex-row gap-6`}>
|
| 374 |
<div className="lg:w-2/3">
|
| 375 |
<div className="aspect-video bg-black rounded-lg overflow-hidden shadow-xl flex items-center justify-center">
|
|
|
|
| 376 |
{currentVideoId ? (
|
| 377 |
<div id="player" className="w-full h-full"></div>
|
| 378 |
) : (
|
|
|
|
| 439 |
);
|
| 440 |
}
|
| 441 |
|
| 442 |
+
// Los componentes PromoCheck y PromoUnlocked se mantienen igual,
|
| 443 |
+
// solo asegúrate de que PromoUnlocked reciba 'db' como prop, lo cual ya haces.
|
| 444 |
function PromoCheck({ user, userProfile, userId, adminEmail, videoCost, db }) {
|
| 445 |
const isAdmin = user?.email === adminEmail;
|
| 446 |
const coins = userProfile?.coins || 0;
|