se crea el carousel dinámico

This commit is contained in:
David Itehua Xalamihua 2025-05-04 18:31:18 -06:00
parent fc7e7f2a00
commit 21852c1fff
12 changed files with 564 additions and 197 deletions

View File

@ -74,5 +74,29 @@ CREATE TABLE visited_from (
); );
-- cargar a la base de datos de producción
CREATE TABLE carousel (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
img_name VARCHAR(150) NOT NULL,
bg_color VARCHAR(40) NOT NULL,
txt_color VARCHAR(40) NOT NULL,
txt VARCHAR(400) NOT NULL
);
ALTER DATABASE formha SET timezone TO 'America/Mexico_City'; ALTER DATABASE formha SET timezone TO 'America/Mexico_City';
SELECT
CASE source_name
WHEN 'li' THEN 'LinkedIn'
WHEN 'wa' THEN 'WhatsApp'
WHEN 'fb' THEN 'Facebook'
WHEN 'x' THEN 'X'
ELSE 'Otro'
END AS nombre_legible,
COUNT(source_name) AS total
FROM visited_from
GROUP BY source_name;
-- select * from visited_from;

Binary file not shown.

View File

@ -0,0 +1,33 @@
from flask_wtf import FlaskForm
from wtforms import StringField, FileField
from wtforms.fields import HiddenField
from wtforms.validators import DataRequired
from wtforms.widgets import ColorInput
from flask_wtf.file import FileAllowed
import re
from wtforms import ValidationError
# def hex_color_only(form, field):
# if not re.match(r'^#(?:[0-9a-fA-F]{3}){1,2}$', field.data):
# raise ValidationError('Debe ser un color hexadecimal válido (#RRGGBB o #RGB).')
class Carousel(FlaskForm):
# id = HiddenField() # Descomenta si lo necesitas
img = FileField(
'Imagen de fondo: ',
validators=[
DataRequired(),
FileAllowed(['jpg', 'jpeg', 'png', 'mp4'], 'Solo se permiten archivos .jpg, .png o .mp4')
]
)
bg_color = StringField('Color de fondo del texto: ', widget=ColorInput(), validators=[DataRequired()])
txt_color = StringField('Color del texto: ', widget=ColorInput(), validators=[DataRequired()])
txt = StringField('Texto: ', validators=[DataRequired()])

View File

@ -32,7 +32,8 @@ v = {
'edit_post': 'h_tmp_usr/e_edit_post.html', 'edit_post': 'h_tmp_usr/e_edit_post.html',
'change_pswd': 'h_tmp_usr/f_change_pswd.html', 'change_pswd': 'h_tmp_usr/f_change_pswd.html',
'metrics': 'h_tmp_usr/g_metrics.html', 'metrics': 'h_tmp_usr/g_metrics.html',
'manage_profiles': 'h_tmp_usr/h_manage_profiles.html' 'manage_profiles': 'h_tmp_usr/h_manage_profiles.html',
'carousel': 'h_tmp_usr/i_carousel_form.html'
} }
} }
@ -137,3 +138,20 @@ def get_date_n_time(timestamp: datetime) -> dict:
} }
return obj return obj
def hex_to_rgb(hex_color):
# Elimina el símbolo '#' si está presente
hex_color = hex_color.lstrip('#')
# Convierte los pares de caracteres hexadecimales a enteros
if len(hex_color) == 6:
r = int(hex_color[0:2], 16)
g = int(hex_color[2:4], 16)
b = int(hex_color[4:6], 16)
return (r, g, b)
else:
raise ValueError("El color HEX debe tener 6 dígitos.")
def rgba_to_string(rgba):
r, g, b, a = rgba
return f"rgba({r}, {g}, {b}, {a})"

79
main.py
View File

@ -1,14 +1,15 @@
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity, get_jwt, decode_token, verify_jwt_in_request, set_access_cookies, get_csrf_token from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity, get_jwt, decode_token, verify_jwt_in_request, set_access_cookies, get_csrf_token
from flask_mail import Message, Mail from flask_mail import Message, Mail
from threading import Thread from threading import Thread
from flask import Flask, render_template, redirect, url_for, flash, current_app, request, jsonify, make_response from flask import Flask, render_template, redirect, url_for, flash, current_app, request, jsonify, make_response, send_from_directory, abort
from forms_py.cls_form_contact import ContactForm from forms_py.cls_form_contact import ContactForm
from forms_py.cls_form_add_usr import AddUser from forms_py.cls_form_add_usr import AddUser
from forms_py.cls_form_login import LogIn from forms_py.cls_form_login import LogIn
from forms_py.cls_db import DBContact from forms_py.cls_db import DBContact
from forms_py.cls_db_usr import DBForma from forms_py.cls_db_usr import DBForma
from forms_py.cls_change_pswd import ChangePwsd from forms_py.cls_change_pswd import ChangePwsd
from forms_py.functions import db_conf_obj, generar_contrasena, hash_password, v, l_flash_msj, saludo_hr, cur_date, min_read_pst, get_date_n_time, getRandomId from forms_py.cls_form_carousel import Carousel
from forms_py.functions import db_conf_obj, generar_contrasena, hash_password, v, l_flash_msj, saludo_hr, cur_date, min_read_pst, get_date_n_time, getRandomId, hex_to_rgb, rgba_to_string
from forms_py.cls_recover_pswd import RecoverPswd from forms_py.cls_recover_pswd import RecoverPswd
import os import os
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@ -21,6 +22,7 @@ from bs4 import BeautifulSoup
from functools import wraps from functools import wraps
from flask_caching import Cache from flask_caching import Cache
app = Flask(__name__) app = Flask(__name__)
cache = Cache(app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': '/tmp/flask-formha'}) cache = Cache(app, config={'CACHE_TYPE': 'filesystem', 'CACHE_DIR': '/tmp/flask-formha'})
@ -126,9 +128,18 @@ def validate_user_exists(f):
# ################################################################## # ##################################################################
@app.route('/') @app.route('/')
@cache.cached(timeout=3600) # @cache.cached(timeout=3600)
def home(): def home():
return render_template(v['home'], active_page='home') q = """
SELECT
id, img_name, bg_color, txt_color, txt, TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada
FROM
carousel
ORDER BY
id DESC;
"""
data = dbUsers.get_all_data(q)
return render_template(v['home'], active_page='home', data=data)
@app.route('/about-us') @app.route('/about-us')
@cache.cached(timeout=43200) @cache.cached(timeout=43200)
@ -893,11 +904,69 @@ def update_user():
dbUsers.update_data(q, t) dbUsers.update_data(q, t)
res = {'ok': True, 'message': 'El elemento se actualizó correctamente', "id": "id_usr"} res = {'ok': True, 'message': 'El elemento se actualizó correctamente', "id": "id_usr"}
return jsonify(res), 200 return jsonify(res), 200
@app.route('/user/carousel', methods=['GET', 'POST'])
@jwt_required()
@validate_user_exists
@admin_required
def carousel():
form = Carousel()
mnsj_flash = None
q_all_slides = """
SELECT
id, img_name, bg_color, txt_color, txt, TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada
FROM
carousel
ORDER BY
id DESC;
"""
data = dbUsers.get_all_data(q_all_slides)
if request.method == 'POST' and form.validate_on_submit():
# img
image_file = form.img.data
filename = secure_filename(image_file.filename)
prev_img = dbUsers.get_all_data("SELECT img_name FROM carousel;")
lst_img = [ele[0] for ele in prev_img]
# validar que el archivo no se haya cargado previamente
if filename in lst_img:
mnsj_flash = l_flash_msj('Error Archivo', 'La imagen ya ha sido cargada previamente.', 'error')
else:
image_file.save(f'./static/uploads/{filename}')
bg_color = rgba_to_string((*hex_to_rgb(form.bg_color.data), 0.5))
txt_color = rgba_to_string((*hex_to_rgb(form.txt_color.data), 1))
txt = form.txt.data
q = "INSERT INTO carousel (img_name, bg_color, txt_color, txt) VALUES (%s, %s, %s, %s);"
t = (filename, bg_color, txt_color, txt)
dbUsers.update_data(q, t)
mnsj_flash = l_flash_msj('Datos Slide', f'Datos agregados para imagen: {filename}', 'success')
flash(mnsj_flash)
return redirect(url_for('carousel'))
return render_template(v['tmp_user']['carousel'], form=form, data=data, active_page='metrics')
@app.route('/user/carousel/download/<path:filename>')
@jwt_required()
@validate_user_exists
@admin_required
def download_file(filename):
uploads_dir = os.path.join(app.root_path, 'static', 'uploads')
try:
return send_from_directory(uploads_dir, filename, as_attachment=True)
except FileNotFoundError:
abort(404)
# ------------------------------------------------------------- # -------------------------------------------------------------
# MANEJO DE ERRORES DE JWT # MANEJO DE ERRORES DE JWT

View File

@ -1,99 +1,5 @@
/* Variables para consistencia */ /* Variables para consistencia */
:root {
--primary-color: #00acc1;
--shadow-light: 0 3px 4px rgba(0, 0, 0, 0.4);
--shadow-hover: 0 15px 26px rgba(0, 0, 0, 0.5);
--card-radius: 5px;
--image-height: 250px;
--banner-text-color: #ffff;
--banner-overlay: rgba(116, 118, 119, 0.8);
}
/* Estructura principal */
/* main {
width: 95vw;
min-height: 100vh;
} */
/* ---------------------------- */
/* Fila 1 - Banner con efecto de zoom */
.r-i {
width: 100%;
}
.r-i-ii {
background-image: url('/static/y_img/home/reunion.avif');
background-size: 100%;
background-repeat: no-repeat;
background-position: center;
border-radius: 10px;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
transition: background-size 2s ease-in-out;
height: 35vh; /* Valor por defecto para móviles */
}
.r-i-ii:hover {
background-size: 110%; /* Efecto de zoom mantenido */
}
.r-i-ii spam {
background-color: var(--banner-overlay);
padding: 20px;
border-radius: 10px;
text-align: center;
font-size: clamp(1rem, 3vw, 25px); /* Texto responsivo */
color: var(--banner-text-color);
max-width: 80%;
}
/* ---------------------------- */
/* Fila 2 - Tarjetas con efectos */
.r-ii {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 1em;
}
.card-flyer {
min-height: 100%;
background: #fff;
border-radius: var(--card-radius);
box-shadow: var(--shadow-light);
transition: transform 0.3s, box-shadow 0.3s;
overflow: hidden;
}
.image-box img {
width: 100%;
height: var(--image-height);
object-fit: cover;
transition: transform 0.9s;
}
.text-container {
padding: 20px;
text-align: center;
}
.text-container h6 {
margin: 0;
font-size: 18px;
color: var(--primary-color);
}
/* Efectos hover (completos) */
.card-flyer:hover {
transform: translateY(-10px);
box-shadow: var(--shadow-hover);
}
.card-flyer:hover img {
transform: scale(1.15); /* Efecto de escala mantenido */
}
/* ---------------------------- */ /* ---------------------------- */
/* Media Queries optimizadas */ /* Media Queries optimizadas */
@ -101,9 +7,7 @@
main { main {
width: 90vw; width: 90vw;
} }
.r-i-ii {
height: 35vh;
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@ -111,9 +15,7 @@
width: 85vw; width: 85vw;
margin: 20px auto; margin: 20px auto;
} }
.r-i-ii {
height: 45vh;
}
} }
@media (min-width: 1440px) { @media (min-width: 1440px) {
@ -121,20 +23,14 @@
width: 80vw; width: 80vw;
margin: 2em auto; margin: 2em auto;
} }
.r-i-ii {
height: 50vh;
}
} }
@media (min-width: 1920px) { @media (min-width: 1920px) {
.r-i-ii {
height: 60vh;
}
} }
/* Ajuste específico para texto en móviles */ /* Ajuste específico para texto en móviles */
@media (max-width: 767px) { @media (max-width: 767px) {
.r-i-ii spam {
font-size: 1em;
}
} }

View File

@ -0,0 +1,18 @@
import { simpleNotification } from '../../z_comps/notify.js';
let form = document.querySelector("form");
let img = document.getElementById("img");
form.addEventListener("submit", function (e) {
let allowedExtensions = ['jpg', 'jpeg', 'png', 'mp4'];
let filePath = img.value;
let extension = filePath.split('.').pop().toLowerCase();
if (!allowedExtensions.includes(extension)) {
e.preventDefault();
simpleNotification("Error Extensión", "Solo se permiten archivos con extensión: .jpg, .jpeg, .png, .mp4", "error")
return;
}
});

View File

@ -10,96 +10,69 @@
{% block body %} {% block body %}
<div class="r-i"> <style>
<div class="r-i-ii"> @media screen and (orientation: landscape) {
<spam> .carousel img {
Potenciamos el talento con soluciones personalizadas que alinean personas, información y propósito. Diseñamos height: 80vh;
estrategias basadas en información, ordenada, confiable, disponible y segura, dando FORMHä a tus decisiones. object-fit: cover;
</spam> }
</div> }
</div>
<div class="r-ii">
<!-- Tarjeta 1 -->
<div class="card-flyer">
<div class="image-box">
<img src="{{url_for('static', filename='y_img/home/software.avif')}}" alt="Software">
</div>
<div class="text-container">
<h6>
Actualización del proceso de reclutamiento y selección.
</h6>
<p>
Actualmente desde la AI hasta las técnicas de entrevista, han abierto un nuevo horizonte en el marco del
reclutamiento y selección de personal…
</p>
</div>
</div>
<!-- Tarjeta 2 -->
<div class="card-flyer">
<div class="image-box">
<img src="{{url_for('static', filename='y_img/home/work.avif')}}" alt="Productividad">
</div>
<div class="text-container">
<h6>
Beneficios e impacto en la productividad.
</h6>
<p>
En la medida que adoptemos la NOM 035, factores de riesgos psicosocial en el trabajo, identificación y
prevención; como una guía para optimizar el clima y…
</p>
</div>
</div>
<!-- Tarjeta 3 -->
<div class="card-flyer">
<div class="image-box">
<img src="{{url_for('static', filename='y_img/home/chair.avif')}}" alt="Ley Silla">
</div>
<div class="text-container">
<h6>
Alcances y objetivos de la Ley Silla.
</h6>
<p>
El pasado 19 de diciembre de 2024, se publico en el Diario Oficial de la Federación el decreto por el cual
se reforman y adicionan diversas…
</p>
</div>
</div>
</div>
.carousel-caption.centered {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none; /* clics pasan a la imagen debajo */
}
.carousel-caption.centered .caption-content {
padding: 1rem 2rem;
border-radius: 10px;
color: #fff;
text-align: center;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
pointer-events: auto; /* permite clics aquí */
width: max-content;
max-width: 90%;
}
.carousel-caption.centered .caption-content h2 {
font-size: clamp(1.5rem, 5vw, 3rem);
margin: 0;
text-shadow: 1px 1px 4px rgba(0,0,0,0.6);
}
</style>
<div id="carouselExampleCaptions" class="carousel slide" data-bs-ride="carousel"> <div id="carouselExampleCaptions" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-indicators"> <div class="carousel-indicators">
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button> {% for s in data %}
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="1" aria-label="Slide 2"></button> <button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="{{ loop.index0 }}"
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="2" aria-label="Slide 3"></button> class="{% if loop.first %}active{% endif %}" aria-current="{{ 'true' if loop.first else 'false' }}"
aria-label="Slide {{ loop.index }}"></button>
{% endfor %}
</div> </div>
<div class="carousel-inner"> <div class="carousel-inner">
<div class="carousel-item active">
<img src="{{url_for('static', filename='y_img/home/software.avif')}}" class="d-block w-100" alt="..."> {% for s in data %}
<div class="carousel-caption d-none d-md-block"> <div class="carousel-item {% if loop.first %}active{% endif %}">
<h5>First slide label</h5> {% if s[1].endswith('.mp4') %}
<p>Some representative placeholder content for the first slide.</p> <video class="d-block w-100" autoplay muted loop playsinline>
</div> <source src="{{ url_for('static', filename='uploads/' + s[1]) }}" type="video/mp4">
</div> Tu navegador no soporta el video.
<div class="carousel-item"> </video>
<img src="{{url_for('static', filename='y_img/home/work.avif')}}" class="d-block w-100" alt="..."> {% else %}
<div class="carousel-caption d-none d-md-block"> <img src="{{ url_for('static', filename='uploads/' + s[1]) }}" class="d-block w-100" alt="...">
<h5>Second slide label</h5> {% endif %}
<p>Some representative placeholder content for the second slide.</p> <div class="carousel-caption centered">
</div> <div class="caption-content" style="background-color: {{ s[2] }}; color: {{ s[3] }};">
</div> <h2>{{ s[4] }}</h2>
<div class="carousel-item"> </div>
<img src="{{url_for('static', filename='y_img/home/chair.avif')}}" class="d-block w-100" alt="...">
<div class="carousel-caption d-none d-md-block">
<h5>Third slide label</h5>
<p>Some representative placeholder content for the third slide.</p>
</div> </div>
</div> </div>
{% endfor %}
</div> </div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide="prev"> <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span> <span class="carousel-control-prev-icon" aria-hidden="true"></span>
@ -111,6 +84,5 @@
</button> </button>
</div> </div>
{% endblock body %} {% endblock body %}

View File

@ -11,6 +11,7 @@
{% block body %} {% block body %}
<div class="expanding-panels" data-aos="zoom-in" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out"> <div class="expanding-panels" data-aos="zoom-in" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
<!-- Quiénes Somos --> <!-- Quiénes Somos -->
<div class="panel p1" > <div class="panel p1" >

View File

@ -0,0 +1,334 @@
{% extends 'h_tmp_usr/z_tmp.html' %}
{% block css %}
<link rel="stylesheet" href="{{ url_for('static', filename='f_contact/form.css') }}">
<!-- {# datatables #} -->
<link rel="stylesheet" href="https://cdn.datatables.net/2.2.2/css/dataTables.bootstrap5.css">
<!-- {# i jconfirm #} -->
<link rel="stylesheet" href="https://htmlguyllc.github.io/jConfirm/jConfirm.min.css">
<!-- {# f jconfirm #} -->
{% endblock css %}
{% block body %}
<style>
/* estilos del carousel */
@media screen and (orientation: landscape) {
.carousel img {
height: 80vh;
object-fit: cover;
}
}
.carousel-item video {
width: 100%;
height: 80vh;
object-fit: cover;
display: block;
}
.carousel-item {
position: relative;
overflow: hidden;
/* height: 100vh; */
}
.carousel-inner {
overflow: hidden;
}
.carousel-caption.centered {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.carousel-caption.centered .caption-content {
padding: 1rem 2rem;
border-radius: 10px;
text-align: center;
box-shadow: 0 0 10px rgba(0,0,0,0.3);
pointer-events: auto;
width: max-content;
max-width: 90%;
}
.carousel-caption.centered .caption-content h2 {
font-size: clamp(1.5rem, 5vw, 3rem);
margin: 0;
text-shadow: 1px 1px 4px rgba(0,0,0,0.6);
}
/* ------------------ */
/* Aplica a ambas: imágenes y videos */
.carousel-item img,
.carousel-item video {
/* width: 100%; */
/* height: 100%; */
object-fit: cover;
display: block;
}
</style>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#home" type="button" role="tab" aria-controls="home" aria-selected="true"><i class="bi bi-file-earmark-slides-fill"></i> Slides</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="false"><i class="bi bi-database-fill-up"></i> Añadir Slide</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact" type="button" role="tab" aria-controls="contact" aria-selected="false"><i class="bi bi-play-circle-fill"></i> Carousel</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
<!-- {# --------------------------------------------------------------- #} -->
<!-- {# i tabla slides #} -->
<table id="tblCarousel" class="table table-striped" style="width:100%" >
<thead>
<th>ID</th>
<th>Creado</th>
<th>Archivo</th>
<th>Texto</th>
<th>Acciones</th>
</thead>
<tbody>
{% for ele in data %}
<tr id="{{ ele[0] }}">
<td>{{ loop.index }}</td>
<td>{{ ele[5] }}</td>
<td>{{ ele[1] }}</td>
<td style="background-color: {{ ele[2] }}; color: {{ ele[3] }};">{{ ele[4] }}</td>
<td >
<div class="field_btns">
<a href="{{ url_for('download_file', filename = ele[1] ) }}" class="btn btn-dark" style="color: white;">
<i class="bi bi-download"></i>
</a>
<a href="#" class="btn btn-danger delete-btn" data-id="{{ ele[0] }}">
<i class="bi bi-trash"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- {# f tabla slides #} -->
<!-- {# --------------------------------------------------------------- #} -->
</div>
<div class="tab-pane fade" id="profile" role="tabpanel" aria-labelledby="profile-tab">
<!-- {# --------------------------------------------------------------- #} -->
<!-- {# i form add slide #} -->
<!-- {# inicio form #} -->
<div class="form-container" data-aos="fade-down" data-aos-delay="0" data-aos-duration="800"
data-aos-easing="ease-in-out">
<div class="form-header">
<h2>Agregar Slide</h2>
</div>
<form method="POST" action="{{ url_for('carousel') }}" enctype="multipart/form-data" class="login-form">
{{ form.hidden_tag() }}
<!-- {# input imagen #} -->
<div class="form-row">
{{ form.img.label() }}
{{ form.img( class_="form-control" ) }}
</div>
<!-- {# bg color picker #} -->
<div class="form-row">
{{ form.bg_color.label() }}
{{ form.bg_color( type="color", class_="form-control form-control-color", value="") }}
</div>
<!-- {# txt color picker #} -->
<div class="form-row">
{{ form.txt_color.label() }}
{{ form.txt_color( type="color", class_="form-control form-control-color", value="" ) }}
</div>
<!-- {# txt #} -->
<div class="form-row">
{{ form.txt.label() }}
{{ form.txt( class_="form-control" ) }}
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-database-fill-up"></i> Enviar Datos
</button>
</form>
</div>
<!-- {# final form #} -->
<!-- {# f form add slide #} -->
<!-- {# --------------------------------------------------------------- #} -->
</div>
<div class="tab-pane fade" id="contact" role="tabpanel" aria-labelledby="contact-tab">
<!-- {# --------------------------------------------------------------- #} -->
<!-- {# i carousel #} -->
<div id="carouselExampleCaptions" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-indicators">
{% for s in data %}
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="{{ loop.index0 }}"
class="{% if loop.first %}active{% endif %}" aria-current="{{ 'true' if loop.first else 'false' }}"
aria-label="Slide {{ loop.index }}"></button>
{% endfor %}
</div>
<div class="carousel-inner">
<!-- {# inicio card item slide #} -->
{% for s in data %}
<div class="carousel-item {% if loop.first %}active{% endif %}">
{% if s[1].endswith('.mp4') %}
<video class="d-block w-100" autoplay muted loop playsinline>
<source src="{{ url_for('static', filename='uploads/' + s[1]) }}" type="video/mp4">
Tu navegador no soporta el video.
</video>
{% else %}
<img src="{{ url_for('static', filename='uploads/' + s[1]) }}" class="d-block w-100" alt="...">
{% endif %}
<div class="carousel-caption centered">
<div class="caption-content" style="background-color: {{ s[2] }}; color: {{ s[3] }};">
<h2>{{ s[4] }}</h2>
</div>
</div>
</div>
{% endfor %}
<!-- {# fin card item slide #} -->
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
<!-- {# f carousel #} -->
<!-- {# --------------------------------------------------------------- #} -->
</div>
</div>
{% endblock body %}
{% block js %}
<!-- {# js databases #} -->
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
<!-- <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script> -->
<script src="https://cdn.datatables.net/2.2.2/js/dataTables.js"></script>
<script src="https://cdn.datatables.net/2.2.2/js/dataTables.bootstrap5.js"></script>
<!-- jQuery y jConfirm -->
<!-- <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> -->
<script src="https://htmlguyllc.github.io/jConfirm/jConfirm.min.js"></script>
<script>
function delete_file(e){
let btn = $(this); // `this` es el botón que lanzó el jConfirm
let postId = btn.data('id'); // lee el data-id
let data = { 'id': postId };
console.log(data)
}
// https://htmlguyllc.github.io/jConfirm/
$(function(){
$('.delete-btn').jConfirm({
//false|array: if provided, this will override the default confirm/deny buttons (see below for an example)
btns: false,
//string: question displayed to the user
question: '¿Deseas continuar con la eliminación?',
//string: confirm button text
confirm_text: 'Sí',
//string: deny button text
deny_text: 'No',
//boolean: if true, when the confirm button is clicked the user will be redirected to the button's href location
// follow_href: true,
//boolean: if true and follow_href is true, the href will be opened in a new window
open_new_tab: false,
//boolean: if true, the tooltip will be hidden if you click outside of it
hide_on_click: true,
//string ('auto','top','bottom','left','right'): preferred location of the tooltip (defaults to auto if no space)
position: 'auto',
//boolean: if true, the deny button will be shown
show_deny_btn: true,
//string ('black', 'white', 'bootstrap-4', 'bootstrap-4-white')
theme: 'bootstrap-4',
//string ('tiny', 'small', 'medium', 'large')
size: 'medium',
//boolean: show the tooltip immediately on instantiation
show_now: false,
//string: class(es) to add to the tooltip
'class': ''
}).on('confirm', delete_file)
.on('deny', function(e){
var btn = $(this);
//do something on deny
}).on('jc-show', function(e, tooltip){
// console.log("el tooltip es visible");
//do something when tooltip is shown
//tooltip dom element is passed as the second parameter
}).on('jc-hide', function(e){
//do something when tooltip is hidden
});
});
</script>
<script>
new DataTable('#tblCarousel', {
initComplete: function() {
// Agrega campos de filtro para cada columna
this.api().columns().every(function() {
let column = this;
let header = $(column.header());
let title = header.text().trim();
// Excluir la columna "Estatus" del filtro
if (title !== 'Acciones') {
// Crea input de filtro
header.append('<div class="filter"><input type="text" class="form-control" placeholder="'+title+'" /></div>');
// Aplica el filtro al escribir
$('input', header)
.on('keyup change', function() {
if (column.search() !== this.value) {
column.search(this.value).draw();
}
});
}
});
}
});
</script>
<!-- {# if flash #} -->
{% include 'z_comps/if_flash.html' %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
<!-- {# validador archivos extensión #} -->
<script type="module" src="{{ url_for('static', filename='h_tmp_user/i_carousel_form/carousel_form.js') }}"></script>
{% endblock js %}

View File

@ -110,6 +110,8 @@
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<!-- <li><hr class="dropdown-divider"></li> --> <!-- <li><hr class="dropdown-divider"></li> -->
<li><a class="dropdown-item" href="{{ url_for('carousel') }}"><i class="bi bi-easel-fill"></i> Carousel</a></li>
<li><a class="dropdown-item" href="{{ url_for('metrics') }}"><i class="bi bi-bar-chart-line-fill"></i> Métricas</a></li> <li><a class="dropdown-item" href="{{ url_for('metrics') }}"><i class="bi bi-bar-chart-line-fill"></i> Métricas</a></li>
<li><a class="dropdown-item" href="{{ url_for('manage_profiles') }}"><i class="bi bi-person-fill-gear"></i> Administrar Perfiles</a></li> <li><a class="dropdown-item" href="{{ url_for('manage_profiles') }}"><i class="bi bi-person-fill-gear"></i> Administrar Perfiles</a></li>
<!-- <li><hr class="dropdown-divider"></li> --> <!-- <li><hr class="dropdown-divider"></li> -->