IMPLEMENTACION DE LOS VIDEOS EN EL CATALOGO PUBLICO
This commit is contained in:
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
|
Before Width: | Height: | Size: 68 B After Width: | Height: | Size: 68 B |
@@ -23,6 +23,7 @@ const DATA_DIR = path.join(__dirname, 'data');
|
||||
const FRONTEND_DIR = path.join(__dirname, 'frontend');
|
||||
const IMG_DIR = path.join(__dirname, 'Imagenes');
|
||||
const PEDIDOS_DIR = path.join(__dirname, 'pedidos');
|
||||
const VIDEOS_DIR = path.join(__dirname, 'videos');
|
||||
|
||||
// ════════════════════════════════
|
||||
// MIDDLEWARES
|
||||
@@ -32,6 +33,7 @@ app.use(cookieParser(SECRET));
|
||||
app.use(express.static(FRONTEND_DIR));
|
||||
app.use('/Imagenes', express.static(IMG_DIR, { maxAge: '7d' }));
|
||||
app.use('/pedidos', express.static(PEDIDOS_DIR));
|
||||
app.use('/videos', express.static(VIDEOS_DIR));
|
||||
|
||||
// Evita el 404 de favicon.ico
|
||||
app.get('/favicon.ico', (req, res) => res.status(204).end());
|
||||
@@ -2162,6 +2164,169 @@ app.post('/api/pedidos/generar-pdf', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ════════════════════════════════
|
||||
// REDES SOCIALES — gestión de publicaciones embebidas
|
||||
// ════════════════════════════════
|
||||
const REDES_FILE = 'redes.json';
|
||||
|
||||
async function getRedes() {
|
||||
try { return await readJson(REDES_FILE); }
|
||||
catch { return []; }
|
||||
}
|
||||
|
||||
function extraerPlataforma(url) {
|
||||
if (/instagram\.com/i.test(url)) return 'instagram';
|
||||
if (/facebook\.com/i.test(url)) return 'facebook';
|
||||
return null;
|
||||
}
|
||||
|
||||
// Público: solo activas
|
||||
app.get('/api/redes', async (req, res) => {
|
||||
const todas = await getRedes();
|
||||
res.json(todas.filter(r => r.activo).sort((a, b) => a.orden - b.orden));
|
||||
});
|
||||
|
||||
// Admin: todas
|
||||
app.get('/api/redes/admin', requireAdmin, async (req, res) => {
|
||||
const todas = await getRedes();
|
||||
res.json(todas.sort((a, b) => a.orden - b.orden));
|
||||
});
|
||||
|
||||
app.post('/api/redes', requireAdmin, async (req, res) => {
|
||||
const { url } = req.body || {};
|
||||
if (!url || !url.trim()) {
|
||||
return res.status(400).json({ error: 'La URL es obligatoria' });
|
||||
}
|
||||
const plataforma = extraerPlataforma(url);
|
||||
if (!plataforma) {
|
||||
return res.status(400).json({ error: 'La URL debe ser de Instagram o Facebook' });
|
||||
}
|
||||
const todas = await getRedes();
|
||||
const nueva = {
|
||||
id: String(Date.now()),
|
||||
url: url.trim(),
|
||||
plataforma,
|
||||
activo: true,
|
||||
orden: todas.length
|
||||
};
|
||||
todas.push(nueva);
|
||||
await writeJson(REDES_FILE, todas);
|
||||
res.json(todas);
|
||||
});
|
||||
|
||||
app.delete('/api/redes/:id', requireAdmin, async (req, res) => {
|
||||
let todas = await getRedes();
|
||||
todas = todas.filter(r => r.id !== req.params.id);
|
||||
await writeJson(REDES_FILE, todas);
|
||||
res.json(todas);
|
||||
});
|
||||
|
||||
app.put('/api/redes/:id', requireAdmin, async (req, res) => {
|
||||
const todas = await getRedes();
|
||||
const idx = todas.findIndex(r => r.id === req.params.id);
|
||||
if (idx === -1) return res.status(404).json({ error: 'No encontrada' });
|
||||
const { activo } = req.body || {};
|
||||
if (typeof activo === 'boolean') todas[idx].activo = activo;
|
||||
await writeJson(REDES_FILE, todas);
|
||||
res.json(todas);
|
||||
});
|
||||
|
||||
// ════════════════════════════════
|
||||
// VIDEOS — gestión de videos subidos
|
||||
// ════════════════════════════════
|
||||
const MAX_VIDEOS = 10;
|
||||
|
||||
const videoUpload = multer({
|
||||
storage: multer.diskStorage({
|
||||
destination: VIDEOS_DIR,
|
||||
filename: (req, file, cb) => {
|
||||
const ext = path.extname(file.originalname) || '.mp4';
|
||||
cb(null, `video-${Date.now()}${ext}`);
|
||||
}
|
||||
}),
|
||||
limits: { fileSize: 200 * 1024 * 1024 } // 200MB
|
||||
});
|
||||
|
||||
app.get('/api/videos', async (req, res) => {
|
||||
try {
|
||||
await fs.mkdir(VIDEOS_DIR, { recursive: true });
|
||||
const files = await fs.readdir(VIDEOS_DIR);
|
||||
const videos = files
|
||||
.filter(f => f.startsWith('video-'))
|
||||
.sort()
|
||||
.reverse()
|
||||
.map(f => ({ filename: f, url: `/videos/${f}` }));
|
||||
res.json(videos);
|
||||
} catch (err) {
|
||||
console.error('Error listing videos:', err);
|
||||
res.status(500).json({ error: 'Error al listar videos' });
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/videos', requireAdmin, videoUpload.single('video'), async (req, res) => {
|
||||
try {
|
||||
await fs.mkdir(VIDEOS_DIR, { recursive: true });
|
||||
const files = await fs.readdir(VIDEOS_DIR);
|
||||
const videoFiles = files.filter(f => f.startsWith('video-'));
|
||||
if (videoFiles.length > MAX_VIDEOS) {
|
||||
// Eliminar el que se subió justo ahora si excede
|
||||
if (req.file) await fs.unlink(req.file.path).catch(() => {});
|
||||
return res.status(400).json({ error: `Máximo ${MAX_VIDEOS} videos. Elimina uno antes de subir otro.` });
|
||||
}
|
||||
const videos = videoFiles.sort().reverse().map(f => ({ filename: f, url: `/videos/${f}` }));
|
||||
res.json(videos);
|
||||
} catch (err) {
|
||||
console.error('Error uploading video:', err);
|
||||
if (req.file) await fs.unlink(req.file.path).catch(() => {});
|
||||
res.status(500).json({ error: 'Error al subir video' });
|
||||
}
|
||||
});
|
||||
|
||||
app.delete('/api/videos/:filename', requireAdmin, async (req, res) => {
|
||||
const { filename } = req.params;
|
||||
if (!filename.startsWith('video-')) {
|
||||
return res.status(400).json({ error: 'Nombre de archivo inválido' });
|
||||
}
|
||||
try {
|
||||
await fs.unlink(path.join(VIDEOS_DIR, filename));
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') return res.status(404).json({ error: 'Video no encontrado' });
|
||||
console.error('Error deleting video:', err);
|
||||
res.status(500).json({ error: 'Error al eliminar video' });
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy para embeds de redes sociales (evita bloqueo de terceros en móviles)
|
||||
app.get('/api/redes/proxy', async (req, res) => {
|
||||
const { url } = req.query;
|
||||
if (!url) return res.status(400).json({ error: 'Falta url' });
|
||||
|
||||
let embedUrl;
|
||||
if (url.includes('instagram.com/p/')) {
|
||||
const match = url.match(/instagram\.com\/p\/([^/?]+)/);
|
||||
if (!match) return res.status(400).json({ error: 'URL de Instagram inválida' });
|
||||
embedUrl = `https://www.instagram.com/p/${match[1]}/embed`;
|
||||
} else if (url.includes('facebook.com')) {
|
||||
embedUrl = `https://www.facebook.com/plugins/post.php?href=${encodeURIComponent(url)}&show_text=true`;
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Solo se admiten URLs de Instagram y Facebook' });
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(embedUrl, {
|
||||
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; InfinityBot)' }
|
||||
});
|
||||
const html = await response.text();
|
||||
res.set('Content-Type', 'text/html; charset=utf-8');
|
||||
res.set('X-Frame-Options', '');
|
||||
res.send(html);
|
||||
} catch (err) {
|
||||
console.error('Redes proxy error:', err);
|
||||
res.status(502).send('Error al cargar el contenido');
|
||||
}
|
||||
});
|
||||
|
||||
// ════════════════════════════════
|
||||
// START
|
||||
// ════════════════════════════════
|
||||
|
||||
+1
-28
@@ -1,28 +1 @@
|
||||
[
|
||||
{
|
||||
"filename": "pedido-1781199786075.pdf",
|
||||
"fecha": "2026-06-11T17:43:06.095Z",
|
||||
"cliente": {
|
||||
"nombre": "juan",
|
||||
"ciudad": "medellin",
|
||||
"telefono": "4444",
|
||||
"direccion": "44444"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"ref": "AF100-198",
|
||||
"nombre": "BOLSO",
|
||||
"precio": 10000,
|
||||
"cantidad": 1
|
||||
},
|
||||
{
|
||||
"ref": "AF100-197",
|
||||
"nombre": "BOLSO",
|
||||
"precio": 10000,
|
||||
"cantidad": 1
|
||||
}
|
||||
],
|
||||
"vendedor": "JUAN PABLO",
|
||||
"total": 20000
|
||||
}
|
||||
]
|
||||
[]
|
||||
|
||||
+3
-3
@@ -205,7 +205,7 @@
|
||||
"dama",
|
||||
"aaa"
|
||||
],
|
||||
"img": "bolsos/dama/aaa/TEST-A.webp"
|
||||
"img": "agotados/TEST-A.webp"
|
||||
},
|
||||
{
|
||||
"id": "TEST-B",
|
||||
@@ -215,7 +215,7 @@
|
||||
"dama",
|
||||
"aaa"
|
||||
],
|
||||
"img": "bolsos/dama/aaa/TEST-B.webp"
|
||||
"img": "agotados/TEST-B.webp"
|
||||
},
|
||||
{
|
||||
"id": "TEST-C",
|
||||
@@ -235,7 +235,7 @@
|
||||
"dama",
|
||||
"aaa"
|
||||
],
|
||||
"img": "bolsos/dama/aaa/TEST-D.webp"
|
||||
"img": "agotados/TEST-D.webp"
|
||||
},
|
||||
{
|
||||
"id": "AF2296",
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
[
|
||||
{
|
||||
"id": "1781231563437",
|
||||
"url": "https://www.instagram.com/reel/DZdHX9QJoNS/?igsh=NndzcGRvcjRzbzZ4",
|
||||
"plataforma": "instagram",
|
||||
"activo": true,
|
||||
"orden": 0
|
||||
}
|
||||
]
|
||||
+6
-96
@@ -1,110 +1,20 @@
|
||||
[
|
||||
{
|
||||
"token": "079fdd39b3668e3b9e2fddd3226282aa3a5f45f0162db16b95746203dbe8af74",
|
||||
"token": "dc141a69e7bb5c9f66d1f1131c0cb6902e6a7082289febb54077b685e2f1449e",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781197392273
|
||||
"createdAt": 1781357350132
|
||||
},
|
||||
{
|
||||
"token": "b34f81ffe57e3870875439696e6150eb5a1ea900ad610a25da409dc6b576e9ed",
|
||||
"token": "13beb84b5e8f84e499bd00950852ac0a247eb4627e277296ba9e1e65a30d02e8",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781197414777
|
||||
"createdAt": 1781357357508
|
||||
},
|
||||
{
|
||||
"token": "db6d37e43fe99bb6adb3f824425df3c3c5ef1cb638695578e1807659ad35e102",
|
||||
"token": "268cfe4078b47c62418fbe26aef0472d0029680b86f6c44252a6cc8faba3f7b6",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781197734902
|
||||
},
|
||||
{
|
||||
"token": "33bd4d4276521f5f800455a1a29ca5b4b9108a746ac602399aba623dc2c55d9d",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781197806825
|
||||
},
|
||||
{
|
||||
"token": "63d52b8966cbf5df5449d8b0ae33efbbb4c75f8e1542e1e1ddde46d5c3e9cae1",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781198005875
|
||||
},
|
||||
{
|
||||
"token": "2784a09fe817c7654283e0ad7dfb68f6d80737b95f38fa863d76dcc7c8b7eb05",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781198034642
|
||||
},
|
||||
{
|
||||
"token": "c3c5b6b92ad256ea628da9c18ba298b2f95c637443dab0c64f3586eba400fb62",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781198193732
|
||||
},
|
||||
{
|
||||
"token": "fc8fcf45847ebdc70f89667a2cf462a2c8ed5a456e80afba7dacf9dfc6b8e151",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781198211090
|
||||
},
|
||||
{
|
||||
"token": "58217cadc5e85afb045d9c0d3dbfa40504916c2a4029b8598b0f1596036e3662",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781198211127
|
||||
},
|
||||
{
|
||||
"token": "012f430c03a3e605ef51f4c58dde55bf0b914c37c53e2f5bf123a79c0cd68eb7",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781198383754
|
||||
},
|
||||
{
|
||||
"token": "85e084066b269d9b5601f9ee721b45decbafebfc13ea083f27e3161d72777700",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781198491124
|
||||
},
|
||||
{
|
||||
"token": "25a5413011b12ba2508f960f6f743ff7c18868544a83479d6b1ecccf20d838a9",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781198723225
|
||||
},
|
||||
{
|
||||
"token": "1d0ebe5a9496bd1df9cfc8e7d49f10048b317a4833441bd0effc57c3225efa69",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781198967207
|
||||
},
|
||||
{
|
||||
"token": "269e3c40159d2faeb0767216feae6e3e0d88c9d1c8ca9ab9d2497986ff1621b7",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781199074685
|
||||
},
|
||||
{
|
||||
"token": "1bc1e7c6c2a505208e3165753de8abd2609c34590375e48ef1abfa587b58e64a",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781199372671
|
||||
},
|
||||
{
|
||||
"token": "4dfe860e82bf21f1216ac252fa4ae378dee65914827df3c326b5812c4f69ff93",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781199472733
|
||||
},
|
||||
{
|
||||
"token": "f8255e39ecef32f69c94e723ff4c7a06976ebfa4cbe92c8600f9644de97c4d0a",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781199500791
|
||||
},
|
||||
{
|
||||
"token": "88000048fc7e0116dcf7101416ff75e119513a2e18ab9e7f87273b070ebbe0e2",
|
||||
"usuario": "admin",
|
||||
"rol": "admin",
|
||||
"createdAt": 1781199793144
|
||||
"createdAt": 1781358702492
|
||||
}
|
||||
]
|
||||
|
||||
@@ -12,6 +12,7 @@ services:
|
||||
- ./Imagenes:/app/Imagenes
|
||||
- ./pedidos:/app/pedidos
|
||||
- ./frontend:/app/frontend
|
||||
- ./videos:/app/videos
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
|
||||
@@ -2687,6 +2687,42 @@ select {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.video-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 10px 14px;
|
||||
background: #fafaff;
|
||||
border: 1.5px solid #e8e6f0;
|
||||
border-radius: 10px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.video-filename {
|
||||
flex: 1;
|
||||
color: #4a4a68;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.video-delete {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #b91c1c;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.2s;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.video-delete:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.vendedores-add {
|
||||
flex-direction: column;
|
||||
|
||||
@@ -186,6 +186,7 @@ async function cargarEstadisticas() {
|
||||
|
||||
document.getElementById('statProductos').textContent = productos.length;
|
||||
document.getElementById('statCategorias').textContent = categorias.length;
|
||||
cargarRedes(); // carga la sección de videos
|
||||
} catch (err) {
|
||||
console.error('Error cargando estadísticas:', err);
|
||||
}
|
||||
@@ -1858,12 +1859,15 @@ function setupProductosSearch() {
|
||||
// EXCEL/CSV — asignar nombre + precio por referencia
|
||||
// ════════════════════════════════
|
||||
function setupExcelUpload() {
|
||||
const input = document.getElementById('excelArchivo');
|
||||
const hint = document.getElementById('excelArchivoHint');
|
||||
const form = document.getElementById('formExcelUpload');
|
||||
const btn = document.getElementById('btnProcesarExcel');
|
||||
const input = document.getElementById('excelArchivo');
|
||||
const hint = document.getElementById('excelArchivoHint');
|
||||
const form = document.getElementById('formExcelUpload');
|
||||
const btn = document.getElementById('btnProcesarExcel');
|
||||
const selBtn = document.getElementById('btnSeleccionarExcel');
|
||||
if (!input || !form) return;
|
||||
|
||||
selBtn.addEventListener('click', () => input.click());
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
const f = input.files && input.files[0];
|
||||
if (f) {
|
||||
@@ -2744,3 +2748,106 @@ function initGestores() {
|
||||
|
||||
// cargarDescargas() ahora usa los gestores
|
||||
// (definido arriba en la sección DESCARGAS)
|
||||
|
||||
// ════════════════════════════════
|
||||
// VIDEOS — gestión desde Dashboard
|
||||
// ════════════════════════════════
|
||||
async function cargarRedes() {
|
||||
await cargarListadoVideos();
|
||||
setupVideosForm();
|
||||
}
|
||||
|
||||
async function cargarListadoVideos() {
|
||||
const listEl = document.getElementById('videosList');
|
||||
const errEl = document.getElementById('videosError');
|
||||
errEl.classList.remove('show');
|
||||
try {
|
||||
const res = await fetch('/api/videos');
|
||||
const videos = await res.json();
|
||||
if (videos.length === 0) {
|
||||
listEl.innerHTML = '<p class="pedidos-empty" style="margin:0">No hay videos. Sube el primero.</p>';
|
||||
return;
|
||||
}
|
||||
listEl.innerHTML = videos.map(v => `
|
||||
<div class="video-item">
|
||||
<video src="${v.url}" controls preload="metadata" style="width:200px;border-radius:8px;max-height:120px;background:#000"></video>
|
||||
<span class="video-filename" title="${escapeHtml(v.filename)}">${escapeHtml(v.filename)}</span>
|
||||
<button type="button" class="video-delete" data-filename="${v.filename}" title="Eliminar">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
listEl.querySelectorAll('.video-delete').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
if (!confirm('¿Eliminar este video?')) return;
|
||||
try {
|
||||
const res = await fetch(`/api/videos/${btn.dataset.filename}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || 'Error al eliminar');
|
||||
}
|
||||
await cargarListadoVideos();
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.classList.add('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
listEl.innerHTML = `<p class="pedidos-empty" style="margin:0">Error: ${err.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function setupVideosForm() {
|
||||
const btn = document.getElementById('btnSubirVideo');
|
||||
const input = document.getElementById('videoFileInput');
|
||||
const errEl = document.getElementById('videosError');
|
||||
const selBtn = document.getElementById('btnSeleccionarVideo');
|
||||
const nameEl = document.getElementById('videoFileName');
|
||||
if (!btn || btn.dataset.vidWired) return;
|
||||
btn.dataset.vidWired = '1';
|
||||
|
||||
selBtn.addEventListener('click', () => input.click());
|
||||
|
||||
input.addEventListener('change', () => {
|
||||
nameEl.textContent = input.files[0] ? input.files[0].name : 'Ningún archivo seleccionado';
|
||||
});
|
||||
|
||||
async function subir() {
|
||||
const file = input.files[0];
|
||||
if (!file) {
|
||||
errEl.textContent = 'Selecciona un archivo de video';
|
||||
errEl.classList.add('show');
|
||||
return;
|
||||
}
|
||||
errEl.classList.remove('show');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ Subiendo…';
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('video', file);
|
||||
const res = await fetch('/api/videos', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: fd
|
||||
});
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || 'Error al subir');
|
||||
}
|
||||
input.value = '';
|
||||
nameEl.textContent = 'Ningún archivo seleccionado';
|
||||
await cargarListadoVideos();
|
||||
} catch (err) {
|
||||
errEl.textContent = err.message;
|
||||
errEl.classList.add('show');
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '+ Subir video';
|
||||
}
|
||||
}
|
||||
|
||||
btn.addEventListener('click', subir);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="admin.css?v=22">
|
||||
<link rel="stylesheet" href="admin.css?v=24">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -113,6 +113,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Videos -->
|
||||
<div class="view-content-card" style="margin-top:28px">
|
||||
<h2 class="view-section-title">🎬 Videos</h2>
|
||||
<p class="view-section-desc">Sube hasta 10 videos para que aparezcan en el catálogo público, debajo del botón "Ver catálogo".</p>
|
||||
|
||||
<div class="videos-form" style="display:flex;gap:10px;margin-bottom:16px;flex-wrap:wrap;align-items:center">
|
||||
<div class="form-group" style="flex:1;min-width:200px;margin-bottom:0">
|
||||
<input type="file" id="videoFileInput" accept="video/*" style="display:none">
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<button type="button" class="btn btn-ghost" id="btnSeleccionarVideo" style="white-space:nowrap">📁 Seleccionar archivo</button>
|
||||
<span id="videoFileName" style="font-size:0.85rem;color:#8b8ba7;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">Ningún archivo seleccionado</span>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-primary" id="btnSubirVideo">+ Subir video</button>
|
||||
</div>
|
||||
<div class="form-error" id="videosError" role="alert"></div>
|
||||
<div class="videos-list" id="videosList" style="display:grid;gap:12px"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- PRODUCTOS VIEW -->
|
||||
@@ -206,8 +225,11 @@
|
||||
<form id="formExcelUpload" class="excel-form" autocomplete="off">
|
||||
<div class="form-group">
|
||||
<label for="excelArchivo">Archivo Excel/CSV</label>
|
||||
<input type="file" id="excelArchivo" accept=".xlsx,.xls,.csv" required>
|
||||
<span class="form-hint" id="excelArchivoHint">Ningún archivo seleccionado</span>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<input type="file" id="excelArchivo" accept=".xlsx,.xls,.csv" required style="display:none">
|
||||
<button type="button" class="btn btn-ghost" id="btnSeleccionarExcel">📁 Seleccionar archivo</button>
|
||||
<span class="form-hint" id="excelArchivoHint" style="margin:0">Ningún archivo seleccionado</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="excel-summary" id="excelSummary" style="display:none"></div>
|
||||
@@ -741,6 +763,6 @@
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<script src="admin.js?v=23"></script>
|
||||
<script src="admin.js?v=26"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -25,6 +25,23 @@ let cargando = true;
|
||||
// ════════════════════════════════
|
||||
// CARGA DE DATOS
|
||||
// ════════════════════════════════
|
||||
async function cargarRedes() {
|
||||
try {
|
||||
const res = await fetch('/api/videos');
|
||||
const videos = await res.json();
|
||||
const grid = document.getElementById('redesGrid');
|
||||
if (videos.length === 0) { grid.innerHTML = ''; return; }
|
||||
|
||||
grid.innerHTML = videos.map(v => `
|
||||
<div class="video-card">
|
||||
<video src="${v.url}" controls preload="metadata" playsinline></video>
|
||||
</div>
|
||||
`).join('');
|
||||
} catch (err) {
|
||||
console.error('Error cargando videos:', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function cargarDatos() {
|
||||
try {
|
||||
const [resCats, resProds] = await Promise.all([
|
||||
@@ -608,6 +625,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setupNav();
|
||||
setupCartModals();
|
||||
cargarDatos();
|
||||
cargarRedes();
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -539,6 +539,64 @@ section {
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* ════════════════════════════════
|
||||
REDES SOCIALES
|
||||
════════════════════════════════ */
|
||||
.redes-section {
|
||||
padding: 20px 20px 10px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.redes-grid {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
padding-bottom: 12px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d4d0e0 transparent;
|
||||
}
|
||||
|
||||
.redes-grid::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.redes-grid::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.redes-grid::-webkit-scrollbar-thumb {
|
||||
background: #d4d0e0;
|
||||
border-radius: 99px;
|
||||
}
|
||||
|
||||
.redes-grid .video-card {
|
||||
flex: 0 0 auto;
|
||||
width: 360px;
|
||||
max-width: 85vw;
|
||||
scroll-snap-align: start;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.redes-grid .video-card video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
max-height: 600px;
|
||||
object-fit: contain;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.redes-grid .red-embed .instagram-media {
|
||||
min-width: 326px !important;
|
||||
max-width: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* ════════════════════════════════
|
||||
CONTADOR + PRODUCTOS
|
||||
════════════════════════════════ */
|
||||
|
||||
+7
-2
@@ -9,7 +9,7 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Plus+Jakarta+Sans:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="estilos.css?v=7">
|
||||
<link rel="stylesheet" href="estilos.css?v=13">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -55,6 +55,11 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- VIDEOS -->
|
||||
<section class="redes-section" id="redes">
|
||||
<div class="redes-grid" id="redesGrid"></div>
|
||||
</section>
|
||||
|
||||
<!-- PRODUCTOS -->
|
||||
<section class="productos-section" id="catalogo">
|
||||
<div class="section-header">
|
||||
@@ -184,6 +189,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=8"></script>
|
||||
<script src="app.js?v=13"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
%PDF-1.3
|
||||
%ÿÿÿÿ
|
||||
7 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Parent 1 0 R
|
||||
/MediaBox [0 0 595.28 841.89]
|
||||
/Contents 5 0 R
|
||||
/Resources 6 0 R
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<<
|
||||
/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
|
||||
/Font <<
|
||||
/F2 8 0 R
|
||||
/F1 9 0 R
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
5 0 obj
|
||||
<<
|
||||
/Length 782
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
stream
|
||||
xœÍX]këF}ׯ˜?ÐÍ|Ï.=$·-ô¡ÐÆo¥F‘ psáhÿ~YÉqìDrãׯØHãetæÌÇ-ÂY)åÝcsó¥ÿû¯®ÿýç[èžL¦†%£&óì˜Ä1²
|
||||
I°iÁj*®�–3‹qQbxê¾5ßš{Âízk'0„pO¨ëÇææ'qXÍ+fêµxïîõ·„Fiÿ„õ/Í�ëæ·ÆSKœÇ›¯ûøïî?Žÿþî׆’Á?ÍýJ©¸€ÛÞcC�N =aÑgR˜'R[„•›«WyGêfL^9Øÿ~”ž5M '�¾ s›€†·ÀV‡´@+î…„]ÝÇÕ½ž-ÂR¿>®D1ÙˆŠ0F
|
||||
¾zè[p„Õý¡^ŸFÀ§Ö®»ïàæmý³f©¯!Õ<˦EX1êÆlZ¡¤=cÍjÝzËÚéñ¤ž¿ø\JÊ’waé–ªSÔPdÊ”wŒ.-p
|
||||
µóRÃ
|
||||
™?‚žJ�fMz�#*~xòT
|
||||
¶à{üÁ9ØfÂÆL0^’ê)t ðØä‡õý.¬äYÄKÔZ$׫a@’/!ö‡±wÌ»±¼>BEÆ9Ǧ-OÚõÅ};Uï}¸nJа¥D§Ï8qž-õR›ÃÅÅË Þ_„šÔi ø3¯¸0Áò"êÍ[qP¶‡Ðº"·jR¢xÙ$d¸ νéiRÞMç²-iVS?±HÝaðv²®a.ñZlü·{‰Ê<-<B¥Ž·8µ‚Õ"…ÇÜc
|
||||
—“ÑkÒR–ky¤#êí.ª»´ðDÚJêB‚‚ü $EòE»Vq²Ú™\VçHæ‘1îÒ-tA£Æ)Lèã깯‰–ãƒù¼ò—fPòç üÜn |O·ÄÅ»eçuuË>Æ+ë–Yú®®[f <‚²ê˜l{:&Ûÿ£cÎþ¦¦¢ÇÅùv«oþ/U•^*‰Dœ8T()èùÈ
|
||||
YëñŹçÞ¿,.¥
|
||||
endstream
|
||||
endobj
|
||||
12 0 obj
|
||||
<<
|
||||
/Type /Page
|
||||
/Parent 1 0 R
|
||||
/MediaBox [0 0 595.28 841.89]
|
||||
/Contents 10 0 R
|
||||
/Resources 11 0 R
|
||||
>>
|
||||
endobj
|
||||
11 0 obj
|
||||
<<
|
||||
/ProcSet [/PDF /Text /ImageB /ImageC /ImageI]
|
||||
/Font <<
|
||||
/F1 9 0 R
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
10 0 obj
|
||||
<<
|
||||
/Length 131
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
stream
|
||||
xœmÍ»
|
||||
Â@Ð~¾âþ€›™}Ì΀X`§l'VK$à÷ÛX¦=Í0;òsô…†ãô}õé~ÑWâPra7N¥¨iÄ&iÉÎâÊÕr’T±ö7}H¶†±ý]̓E5
|
||||
’m¡á,0´™û$‘ã9ÉüD»Ò©Ñ�~c&€
|
||||
endstream
|
||||
endobj
|
||||
14 0 obj
|
||||
(PDFKit)
|
||||
endobj
|
||||
15 0 obj
|
||||
(PDFKit)
|
||||
endobj
|
||||
16 0 obj
|
||||
(D:20260611174306Z)
|
||||
endobj
|
||||
17 0 obj
|
||||
(Pedido - juan)
|
||||
endobj
|
||||
18 0 obj
|
||||
(Infinity)
|
||||
endobj
|
||||
19 0 obj
|
||||
(Pedido de productos)
|
||||
endobj
|
||||
13 0 obj
|
||||
<<
|
||||
/Producer 14 0 R
|
||||
/Creator 15 0 R
|
||||
/CreationDate 16 0 R
|
||||
/Title 17 0 R
|
||||
/Author 18 0 R
|
||||
/Subject 19 0 R
|
||||
>>
|
||||
endobj
|
||||
9 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/BaseFont /Helvetica
|
||||
/Subtype /Type1
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
8 0 obj
|
||||
<<
|
||||
/Type /Font
|
||||
/BaseFont /Helvetica-Bold
|
||||
/Subtype /Type1
|
||||
/Encoding /WinAnsiEncoding
|
||||
>>
|
||||
endobj
|
||||
4 0 obj
|
||||
<<
|
||||
>>
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/Type /Catalog
|
||||
/Pages 1 0 R
|
||||
/Names 2 0 R
|
||||
>>
|
||||
endobj
|
||||
1 0 obj
|
||||
<<
|
||||
/Type /Pages
|
||||
/Count 2
|
||||
/Kids [7 0 R 12 0 R]
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<<
|
||||
/Dests <<
|
||||
/Names [
|
||||
]
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 20
|
||||
0000000000 65535 f
|
||||
0000002071 00000 n
|
||||
0000002135 00000 n
|
||||
0000002009 00000 n
|
||||
0000001988 00000 n
|
||||
0000000224 00000 n
|
||||
0000000125 00000 n
|
||||
0000000015 00000 n
|
||||
0000001886 00000 n
|
||||
0000001789 00000 n
|
||||
0000001281 00000 n
|
||||
0000001191 00000 n
|
||||
0000001078 00000 n
|
||||
0000001668 00000 n
|
||||
0000001485 00000 n
|
||||
0000001510 00000 n
|
||||
0000001535 00000 n
|
||||
0000001571 00000 n
|
||||
0000001603 00000 n
|
||||
0000001630 00000 n
|
||||
trailer
|
||||
<<
|
||||
/Size 20
|
||||
/Root 3 0 R
|
||||
/Info 13 0 R
|
||||
Binary file not shown.
Reference in New Issue
Block a user