Skip to main content

Overview

Operapedia is a comprehensive knowledge base for managing gaming company credentials, payment methods, promotions, and operational information. It features real-time Firebase synchronization, partner categorization, and role-based edit controls. Access: /operapedia/ (requires authentication)

Architecture

Main Interface

8 Information Tabs

1. Credenciales (Credentials)

Game login credentials with real-time status toggles:
<div class="game-card">
  <div class="game-header">
    <div class="game-name">Golden Dragon</div>
    <div class="status-toggle active" 
         data-company-id="1" 
         data-game-id="1"></div>
  </div>
  <div class="game-details">
    <div class="detail-row">
      <span class="detail-label">Username:</span>
      <span class="detail-value">Dragons Cashier</span>
      <button class="copy-btn" data-copy="Dragons Cashier">Copiar</button>
    </div>
    <div class="detail-row">
      <span class="detail-label">Link:</span>
      <span class="detail-value">https://pos.goldendragoncity.com/pos/8093768</span>
      <button class="link-btn" data-link="...">🔗</button>
    </div>
  </div>
  <div class="last-modified">Última mod: 2025-12-12</div>
</div>
<div class="game-card" data-edit-card="1">
  <div class="game-header">
    <div class="game-name">Golden Dragon</div>
    <div class="status-toggle active"></div>
  </div>
  <div class="game-details">
    <div class="detail-row">
      <span class="detail-label">Username:</span>
      <input class="edit-username-input" value="Dragons Cashier">
    </div>
    <div class="detail-row">
      <span class="detail-label">Link:</span>
      <input class="edit-link-input" value="https://...">
    </div>
  </div>
  <div class="edit-actions">
    <button class="save-edit-btn">Guardar</button>
    <button class="delete-game-btn">Eliminar</button>
  </div>
</div>
Firebase Sync (app.js:447-477):
const applyRemoteSettings = () => {
  window.firebaseOnValue(window.gamesRef, snapshot => {
    const data = snapshot.val();
    Object.values(data).forEach(s => {
      const company = companies.find(c => String(c.id) === String(s.companyId));
      const game = company.games.find(g => String(g.id) === String(s.gameId));
      game.active = s.active; // Real-time toggle sync
      if (s.lastModified) game.lastModified = s.lastModified;
    });
    renderCompanies();
  });
};

2. Depósito (Deposit Methods)

Payment methods for deposits:
// View Mode (app.js:949-993)
const renderDeposito = (company) => {
  const metodos = company.metodosDeposito || [];
  
  metodos.forEach((metodo) => {
    html += `
      <div class="metodo-deposito-item">
        <div class="metodo-titulo">${metodo.metodo || 'Método de depósito'}</div>
        <div class="metodo-detalles">
          <div class="metodo-row">
            <span class="metodo-label">Proveedor:</span>
            <span class="metodo-value">${metodo.proveedor || 'N/A'}</span>
          </div>
          <div class="metodo-row">
            <span class="metodo-label">Monto mínimo:</span>
            <span class="metodo-value">${metodo.montoMinimo || 'N/A'}</span>
          </div>
          <div class="metodo-row">
            <span class="metodo-label">Monto máximo:</span>
            <span class="metodo-value">${metodo.montoMaximo || 'N/A'}</span>
          </div>
        </div>
      </div>
    `;
  });
};

3. Cashout (Withdrawal Methods)

Same structure as Depósito but for withdrawals (app.js:1052-1150):
const renderCashout = (company) => {
  const metodos = company.metodosCashout || [];
  // Renders identical structure to Depósito
};

4. Consideraciones (Considerations)

Free-form text area for cashout rules and special considerations:
<div class="tab-info-card">
  <h3>Consideraciones para cashouts</h3>
  <p style="white-space: pre-wrap;">${consideraciones}</p>
</div>

5. Promociones (Promotions)

List of active promotions:
// app.js:1193-1263
const renderPromociones = (company) => {
  const promociones = company.promociones || [];
  
  promociones.forEach((promo, index) => {
    html += `
      <div class="promocion-simple-item">
        <div class="promocion-simple-title">${promo.titulo || 'Promoción ' + (index + 1)}</div>
        <div class="promocion-simple-desc">${promo.descripcion || 'Sin descripción'}</div>
      </div>
    `;
  });
};

6. Términos (Terms & Conditions)

Link or text for T&C:
// app.js:1267-1316
const renderTerminos = (company) => {
  const link = company.terminosLink || company.terminosCondiciones || '';
  
  if (link.startsWith('http://') || link.startsWith('https://')) {
    container.innerHTML = `
      <a href="${link}" target="_blank" class="terminos-link-btn">
        Ver términos y condiciones completos →
      </a>
    `;
  } else {
    container.innerHTML = `<p style="white-space: pre-wrap;">${link}</p>`;
  }
};

7. Canales (Support Channels)

Contact channels for customer support:
// app.js:1320-1390
const renderCanales = (company) => {
  const canales = company.canales || company.canalesAtencion || [];
  
  canales.forEach((canal) => {
    const nombre = canal.nombre || canal.tipo || 'Canal de atención';
    const contacto = canal.contacto || canal.valor || canal.link || '';
    
    html += `
      <div class="canal-item">
        <div class="canal-nombre">${nombre}</div>
        ${contacto ? `<div class="canal-contacto">${contacto}</div>` : ''}
      </div>
    `;
  });
};

8. Notas (Notes Timeline)

Chronological notes with timestamps:
// app.js:1393-1500
const renderNotas = (company) => {
  const notas = Array.isArray(company.notas) ? company.notas : [];
  
  // View mode shows timeline
  notas.forEach((nota, index) => {
    const timestamp = nota.timestamp || 'Sin fecha';
    const author = nota.author || 'Anónimo';
    const text = nota.text || nota.content || '';
    
    html += `
      <div class="nota-timeline-item">
        <div class="nota-timeline-marker"></div>
        <div class="nota-timeline-content">
          <div class="nota-timeline-header">
            <span class="nota-timeline-author">${author}</span>
            <span class="nota-timeline-date">${timestamp}</span>
          </div>
          <div class="nota-timeline-text">${text}</div>
        </div>
      </div>
    `;
  });
};

Partner System

Companies can be grouped by partner (app.js:345-385): Available Partners:
  • 🐲 Dragon
  • 🔒 Tierlock
  • 🎮 Taparcadia
  • Wysaro

Partner Filtering

// Filter chips in navbar
<button class="filter-chip partner-chip" data-partner="Dragon">🐲 Dragon</button>
<button class="filter-chip partner-chip" data-partner="Tierlock">🔒 Tierlock</button>
<button class="filter-chip partner-chip" data-partner="Taparcadia">🎮 Taparcadia</button>
<button class="filter-chip partner-chip" data-partner="Wysaro">⭐ Wysaro</button>

Partner Badge Assignment

In edit mode, supervisors can assign/change partners (app.js:560-611):
partnerBadge.onclick = async () => {
  if (!isEditMode || !currentCompany) return;
  
  const sel = document.createElement('select');
  sel.innerHTML = `<option value="">Sin partner</option>` +
    PARTNERS.map(p => 
      `<option value="${p}" ${company.partner === p ? 'selected' : ''}>
        ${PARTNER_ICONS[p]} ${p}
      </option>`
    ).join('');
  
  partnerBadge.replaceWith(sel);
  sel.focus();
  
  sel.addEventListener('change', async () => {
    company.partner = sel.value || '';
    await window.firebaseSet(
      window.firebaseRef(window.db, `companies/${company.id}/partner`),
      company.partner
    );
  });
};

Search System

Searches across all companies and tabs (operapedia/index.html:24-27):
<input type="text" class="omnibar" id="globalSearch" 
       placeholder="Buscar en todas las compañías...">

Search Filters Panel

Expands on focus with category filters (operapedia/index.html:28-47):
<div class="search-filters" id="searchFilters">
  <div class="filter-group">
    <span class="filter-group-label">Buscar en:</span>
    <button class="filter-chip" data-category="credenciales">🎮 Credenciales</button>
    <button class="filter-chip" data-category="deposito">💰 Depósito</button>
    <button class="filter-chip" data-category="cashout">💸 Cashout</button>
    <button class="filter-chip" data-category="consideraciones">📋 Consideraciones</button>
    <button class="filter-chip" data-category="promociones">🎁 Promociones</button>
    <button class="filter-chip" data-category="terminos">📜 Términos</button>
    <button class="filter-chip" data-category="canales">📞 Canales</button>
    <button class="filter-chip" data-category="notas">📝 Notas</button>
  </div>
</div>
Filter Persistence (app.js:294-309):
let activeCategoryFilters = [];
const savedCatFilters = localStorage.getItem('operapediaCategoryFilters');
if (savedCatFilters) {
  try { 
    activeCategoryFilters = JSON.parse(savedCatFilters); 
  } catch { 
    activeCategoryFilters = []; 
  }
}

const saveCategoryFilters = () => {
  localStorage.setItem('operapediaCategoryFilters', JSON.stringify(activeCategoryFilters));
};

Game Catalog

Master list of all available games (app.js:96-252):

Catalog Modal

<div class="modal-content">
  <h2>📋 Catálogo de Juegos</h2>
  <p>Gestiona la lista maestra de juegos. Estos nombres aparecerán como opciones al agregar juegos a compañías.</p>
  
  <div id="catalogList">
    <!-- Game entries with name and link -->
  </div>
  
  <button id="addCatalogGameBtn">+ Agregar juego al catálogo</button>
  <button id="saveCatalogBtn">Guardar catálogo</button>
</div>

Catalog Entry Structure

let gameCatalog = []; // [{id, name, link}]

// Load from Firebase
window.firebaseOnValue(window.gameCatalogRef, snapshot => {
  const data = snapshot.val() || {};
  gameCatalog = Object.entries(data).map(([key, val]) => ({
    id: val.id ?? key,
    name: val.name || '',
    link: val.link || ''
  }));
});

Using Catalog in Edit Mode

<select class="new-game-select">
  <option value="" disabled selected>Selecciona del catálogo...</option>
  <option value="1" data-link="https://...">Golden Dragon</option>
  <option value="2" data-link="https://...">Orion Stars</option>
  <!-- ... -->
  <option value="__custom__">✏️ Otro (escribir nombre)</option>
</select>
When a catalog game is selected, the link is auto-filled:
gameSelect.addEventListener('change', () => {
  const val = gameSelect.value;
  if (val === '__custom__') {
    customRow.style.display = 'flex'; // Show name input
  } else {
    const selectedOption = gameSelect.options[gameSelect.selectedIndex];
    const catalogLink = selectedOption.getAttribute('data-link') || '';
    linkInput.value = catalogLink; // Auto-fill link
  }
});

Edit Mode

Available to: Supervisors only

Activation

// operapedia/index.html:222-229
if (user.role === 'supervisor') {
  localStorage.setItem('credentialsAdminLoggedIn', 'true');
} else {
  localStorage.removeItem('credentialsAdminLoggedIn');
  document.getElementById('editModeBtn').style.display = 'none';
  document.getElementById('catalogBtn').style.display = 'none';
}

Edit Controls

Edit Button

Toggles edit mode on current company:
editModeBtn.addEventListener('click', () => {
  isEditMode = !isEditMode;
  updateUI();
});

Save Changes

Each tab has specific save logic:
const saveCurrentTab = () => {
  switch (currentTab) {
    case 'credenciales': saveGames(); break;
    case 'deposito': saveDeposito(); break;
    // ...
  }
};

Add Company

Button appears in edit mode:
<button id="addCompanyBtn" style="display: none;">
  ➕ Nueva compañía
</button>

Delete Company

Requires supervisor password:
const pwd = await showPasswordModal(
  'Contraseña de supervisor',
  'Confirma la eliminación.'
);
if (pwd !== ADMIN_PASSWORD) return;

Theme Switcher

// app.js:254-275
let currentTheme = localStorage.getItem('operapediaTheme') || 'dark';

const applyTheme = (theme) => {
  document.documentElement.setAttribute('data-theme', theme);
  themeToggleInput.checked = theme === 'light';
};

const toggleTheme = () => {
  currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
  applyTheme(currentTheme);
  localStorage.setItem('operapediaTheme', currentTheme);
};

themeToggleInput.addEventListener('change', toggleTheme);
applyTheme(currentTheme);
Toggle UI:
<label class="theme-switch">
  <input type="checkbox" id="themeToggle">
  <span class="theme-slider">
    <span class="theme-icon-light">☀️</span>
    <span class="theme-icon-dark">🌙</span>
  </span>
</label>

Firebase Integration

Configuration

// operapedia/index.html:181-207
const firebaseConfig = {
  apiKey: "AIzaSyBvfknEWRrS_0As8sDcQVGfH0RKydzw93A",
  authDomain: "credentials-toggles.firebaseapp.com",
  databaseURL: "https://credentials-toggles-default-rtdb.firebaseio.com",
  projectId: "credentials-toggles",
  storageBucket: "credentials-toggles.firebasestorage.app",
  messagingSenderId: "274936685002",
  appId: "1:274936685002:web:435009cb0b36c65a9def33",
  measurementId: "G-CDQM9CG3LF"
};

const app = initializeApp(firebaseConfig);
const db = getDatabase(app);

// Expose globally
window.db = db;
window.gamesRef = ref(db, "gamesConfig");
window.companiesRef = ref(db, "companies");
window.gameCatalogRef = ref(db, "gameCatalog");
window.firebaseOnValue = onValue;
window.firebaseSet = set;
window.firebaseRef = ref;

Real-Time Updates

// Listen to companies changes
window.firebaseOnValue(window.companiesRef, snapshot => {
  const data = snapshot.val() || {};
  companies = Object.entries(data).map(([id, company]) => ({
    id,
    ...company
  }));
  renderCompanies();
});

Company Data Structure

{
  id: 1,
  name: "Play Play Play",
  color: "#3B82F6",
  partner: "Dragon", // Optional: Dragon, Tierlock, Taparcadia, Wysaro
  games: [
    {
      id: 1,
      name: "Golden Dragon",
      username: "Dragons Cashier",
      link: "https://pos.goldendragoncity.com/pos/8093768",
      active: true,
      lastModified: "2025-12-12"
    }
  ],
  metodosDeposito: [
    {
      metodo: "Tarjeta de crédito",
      proveedor: "Visa",
      montoMinimo: "$10",
      montoMaximo: "$5000"
    }
  ],
  metodosCashout: [...],
  consideracionesCashout: "Texto libre...",
  promociones: [
    {
      titulo: "Bono de bienvenida",
      descripcion: "100% hasta $500"
    }
  ],
  terminosLink: "https://example.com/terms",
  canales: [
    {
      nombre: "WhatsApp",
      contacto: "+1234567890"
    }
  ],
  notas: [
    {
      timestamp: "2025-01-15 14:30",
      author: "John Doe",
      text: "Updated credentials"
    }
  ]
}

Best Practices

Assign companies to partners (Dragon, Tierlock, etc.) to group related brands and enable filtered views.
Maintain the master game catalog to ensure consistent game names and auto-filled links across companies.
Use the Notes timeline to track important changes, outages, or special instructions with timestamps.
Use the active/inactive toggle to disable games that are temporarily unavailable instead of deleting credentials.
Use the sidebar and omnibar search to check if a company already exists before creating a duplicate.
Complete all 8 tabs for comprehensive company documentation, especially Consideraciones and Canales.

Build docs developers (and LLMs) love