versión casi final

This commit is contained in:
David Itehua Xalamihua 2025-05-24 17:16:44 -06:00
parent ff235ca40b
commit e43e6ed2b2
9 changed files with 360 additions and 247 deletions

View File

@ -87,19 +87,3 @@ CREATE TABLE carousel (
);
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
ORDER BY total DESC;
-- select * from visited_from;

View File

@ -50,7 +50,7 @@ class Carousel(FlaskForm):
'Imagen de fondo: ',
validators=[
DataRequired(),
FileAllowed(['jpg', 'jpeg', 'png', 'mp4', 'avif', 'webp'], 'Solo se permiten archivos .jpg, .png o .mp4')
FileAllowed(['jpg', 'jpeg', 'png', 'mp4', 'avif', 'webp'], 'Solo se permiten archivos: .jpg, jpeg, mp4, avif o webp')
]
)

103
main.py
View File

@ -968,6 +968,9 @@ def manage_profiles():
t = None
subject = None
html_content = None
r_str_isAdmin = '' if f_isAdmin == 'TRUE' else 'No'
r_str_isContactNoti = '' if f_isContactNoti == 'TRUE' else 'No'
# si el f_id no es igual a '' entonces es una actualización de datos
if f_id != '':
@ -976,13 +979,46 @@ def manage_profiles():
f_mnsj = l_flash_msj('Éxito', f'Usuario actualizado: {f_nombre}', 'success')
subject = "Usuario actualizado"
html_content = """
<h1>¡Hola!</h1>
<p>Tu cuenta ha sido actualizada con éxito.</p>
<p><strong>Nombre:</strong> {}</p>
<p><strong>Apellido:</strong> {}</p>
<p><strong>Género:</strong> {}</p>
<p><strong>Email:</strong> {}</p>
""".format(f_nombre, f_apellido, f_genero, f_email)
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: Arial, sans-serif; background-color: #f4f4f4; padding: 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; padding: 40px;">
<tr>
<td align="center" style="padding-bottom: 20px;">
<img src="https://formha.temporal.work/static/y_img/logos/formha_blanco_vertical.png" alt="Logo Formha" style="width: 150px; height: auto;">
<p style="color: #4a5568; font-size: 16px; margin-top: 10px;"><strong>Tu cuenta ha sido actualizada con éxito.</strong></p>
</td>
</tr>
<tr>
<td style="color: #2d3748; font-size: 15px; line-height: 1.6;">
<p>
<strong>Nombre:</strong> {} <br>
<strong>Apellido:</strong> {}<br>
<strong>Género:</strong> {} <br>
<strong>Email:</strong> {} <br>
<strong>Permisos de Admin:</strong> {}<br>
<strong>Notificaciones por email:</strong> {}
</p>
</td>
</tr>
<tr>
<td align="center" style="padding-top: 30px;">
<a href="https://formha.temporal.work/login" style="background-color: #4299e1; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 5px; font-weight: bold; display: inline-block;">
Ir al sitio Formha
</a>
</td>
</tr>
<tr>
<td align="center" style="padding-top: 30px; font-size: 12px; color: #a0aec0;">
Si no realizaste esta actualización, por favor contacta con el soporte técnico.
</td>
</tr>
</table>
</td>
</tr>
</table>
""".format(f_nombre, f_apellido, f_genero, f_email, r_str_isAdmin, r_str_isContactNoti)
# en caso de que el f_id = '' quiere decir que es un nuevo registro y debo checar que no sea un registro preexiste a partir del email
else:
@ -999,20 +1035,49 @@ def manage_profiles():
random_id = getRandomId()
tmp_pswd = generar_contrasena()
hashed_pswd = hash_password(tmp_pswd)
q = "INSERT INTO users (id, nombre, apellido, genero, email, pswd, is_admin, is_pswd_reseted ) values (%s, %s, %s, %s, %s, %s, %s, %s);"
t = (random_id, f_nombre, f_apellido, f_genero, f_email, hashed_pswd, f_isAdmin, True)
q = "INSERT INTO users (id, nombre, apellido, genero, email, pswd, is_admin, is_pswd_reseted, is_contact_noti ) values (%s, %s, %s, %s, %s, %s, %s, %s, %s);"
t = (random_id, f_nombre, f_apellido, f_genero, f_email, hashed_pswd, f_isAdmin, True, f_isContactNoti)
f_mnsj = l_flash_msj('Éxito', f'Usuario creado: {f_nombre}', 'success')
subject = "Nueva cuenta creada"
html_content = """
<h1>¡Bienvenido a Forma!</h1>
<p>Tu cuenta ha sido creada con éxito.</p>
<p><strong>Nombre:</strong> {}</p>
<p><strong>Apellido:</strong> {}</p>
<p><strong>Género:</strong> {}</p>
<p><strong>Email:</strong> {}</p>
<p><strong>Contraseña temporal:</strong> {}</p>
<p><strong>Una vez inicies sesión debes de cambiar la contraseña a una nueva que puedas recordar</strong></p>
""".format(f_nombre, f_apellido, f_genero, f_email, tmp_pswd)
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: Arial, sans-serif; background-color: #f4f4f4; padding: 20px;">
<tr>
<td align="center">
<table width="600" cellpadding="0" cellspacing="0" style="background-color: #ffffff; border-radius: 8px; padding: 40px;">
<tr>
<td align="center" style="padding-bottom: 20px;">
<img src="https://formha.temporal.work/static/y_img/logos/formha_blanco_vertical.png" alt="Logo Formha" style="width: 150px; height: auto;">
<h1 style="color: #2c5282; margin: 0;">¡Bienvenido a <span style="color:#4299e1;">Formha</span>!</h1>
<p style="color: #4a5568; font-size: 16px; margin-top: 10px;">Tu cuenta ha sido creada con éxito.</p>
</td>
</tr>
<tr>
<td style="color: #2d3748; font-size: 15px; line-height: 1.6;">
<p>
<strong>Nombre:</strong> {}<br>
<strong>Apellido:</strong> {}<br>
<strong>Género:</strong> {}<br>
<strong>Email:</strong> {}<br>
<strong>Contraseña temporal:</strong> <code style="background-color:#edf2f7; padding: 4px 8px; border-radius: 4px;">{}</code><br>
<strong>Permisos de Admin:</strong> {}<br>
<strong>Notificaciones por email:</strong> {}
</p>
<p style="margin-top: 20px;"><strong style="color: #e53e3e;">Importante:</strong> Una vez inicies sesión, debes cambiar la contraseña por una nueva que recuerdes.</p>
</td>
</tr>
<tr>
<td align="center" style="padding-top: 30px;">
<a href="https://formha.temporal.work/login" style="background-color: #4299e1; color: #ffffff; text-decoration: none; padding: 12px 24px; border-radius: 5px; font-weight: bold; display: inline-block;">
Ir al sitio Formha
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
""".format(f_nombre, f_apellido, f_genero, f_email, tmp_pswd, r_str_isAdmin, r_str_isContactNoti)
# Crear el mensaje de correo con todo el contenido
msg = Message(
@ -1143,7 +1208,7 @@ def carousel():
flash(mnsj_flash)
return redirect(url_for('carousel'))
return render_template(v['tmp_user']['carousel'], form=form, data=data, active_page='metrics')
return render_template(v['tmp_user']['carousel'], form=form, data=data, active_page='carousel')
@app.route('/user/carousel/delete-slide/<int:id>', methods=["POST", "DELETE"])

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@ -8,19 +8,120 @@
<link rel="stylesheet" href="https://htmlguyllc.github.io/jConfirm/jConfirm.min.css">
<!-- {# f jconfirm #} -->
<!-- <link rel="stylesheet" href="{{ url_for('static', filename='f_contact/form.css') }}"> -->
{% endblock css %}
{% block body %}
<style>
/* Smartphones (hasta 767px) */
@media (max-width: 767px) {
/* main{ background-color: black; } */
main { min-height: 80dvh; }
}
/* Tablets (768px - 1023px) */
@media (min-width: 768px) and (max-width: 1023px) {
/* main{ background-color: pink; } */
main { min-height: 80dvh; }
}
/* Laptops (1024px - 1439px) monitores resulición baja */
@media (min-width: 1024px) and (max-width: 1439px) {
/* main{ background-color: purple; } */
main { min-height: 80dvh; }
}
/* PCs de escritorio (1440px - 1919px) macbook */
@media (min-width: 1440px) and (max-width: 1919px) {
/* main{ background-color: greenyellow; } */
main { min-height: 80dvh; }
}
/* Pantallas Ultrawide (1920px en adelante) */
@media (min-width: 1920px) {
/* main{ background-color: red; } */
main { min-height: 80dvh; }
}
#dt-length-0 {
width: 6em;
}
</style>
<style>
/* Estilos generales de la tabla (Bootstrap ya maneja gran parte de esto) */
#tblUsers {
/* Asegúrate de que no haya un min-width fijo que rompa la responsividad */
width: 100% !important; /* !important para asegurar que sobreescribe estilos de DataTables/Bootstrap si es necesario */
}
/* Ocultar las cabeceras de la tabla en pantallas pequeñas */
@media screen and (max-width: 768px) {
#tblUsers thead {
display: none; /* Oculta las cabeceras en móviles */
}
#tblUsers,
#tblUsers tbody,
#tblUsers tr,
#tblUsers td {
display: block; /* Hace que todos los elementos de la tabla se comporten como bloques */
width: 100%; /* Ocupan todo el ancho disponible */
}
#tblUsers tr {
margin-bottom: 1rem; /* Espacio entre las "tarjetas" (filas) */
border: 1px solid #dee2e6; /* Borde para simular la tarjeta */
border-radius: 0.5rem; /* Bordes redondeados */
background-color: #fff;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); /* Sombra suave */
padding: 1rem; /* Espacio interno de la tarjeta */
box-sizing: border-box; /* Incluir padding y borde en el ancho total */
}
#tblUsers td {
border: none; /* Elimina los bordes de las celdas individuales */
position: relative; /* Necesario para posicionar el data-label */
padding-left: 50% !important; /* Espacio para el data-label */
text-align: left !important; /* Asegura que el texto se alinee a la izquierda */
white-space: normal; /* Permite que el texto se ajuste y no esté en una sola línea */
}
#tblUsers td::before {
/* Muestra el contenido del data-label como pseudo-elemento */
content: attr(data-label);
position: absolute;
left: 0.5rem; /* Posición del label */
width: 45%; /* Ancho del label */
padding-right: 1rem;
white-space: nowrap; /* Evita que el label se rompa */
font-weight: bold;
text-align: right; /* Alinea el label a la derecha dentro de su espacio */
color: #495057; /* Color para el label */
}
}
</style>
<div>
<h1>Administrar Perfiles</h1>
<!-- Botón para abrir el modal -->
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalFormAddUsr">
<i class="bi bi-person-fill-add"></i> Añadir Usuario
</button>
<table id="tblUsers" class="table table-striped" style="width:100%" data-aos="fade-up" data-aos-delay="0"
<table id="tblUsers" class="table table-striped" style="width:100%" data-aos="fade-up" data-aos-delay="0"
data-aos-duration="800">
<thead>
<tr>
@ -30,21 +131,21 @@
<th>Última Conexión</th>
<th>Admin</th>
<th>Posts</th>
<th>Notificiones Contactos</th>
<th>Notificaciones Email</th>
<th>Acciones</th>
</tr>
</thead>
<tbody>
{% for ele in data_all_users %}
<tr data-id="{{ele[0]}}">
<td>{{ ele[1] }}</td>
<td>{{ ele[2] }}</td>
<td>{{ ele[3] }}</td>
<td>{{ ele[4] }}</td>
<td>{{ ele[5] }}</td>
<td>{{ ele[6] }}</td>
<td>{{ ele[7] }}</td>
<td>
<td data-label="Nombre">{{ ele[1] }}</td>
<td data-label="Apellidos">{{ ele[2] }}</td>
<td data-label="Email">{{ ele[3] }}</td>
<td data-label="Últ. Conexión">{{ ele[4] }}</td>
<td data-label="Admin">{{ ele[5] }}</td>
<td data-label="Posts">{{ ele[6] }}</td>
<td data-label="Notif. Email">{{ ele[7] }}</td>
<td data-label="Acciones"> {# Mantén data-label para Acciones también #}
<a href="" class="btn btn-primary" data-bs-toggle="modal" data-id="{{ele[0]}}"
data-bs-target="#modalFormAddUsr">
<i class="bi bi-pencil-square"></i>
@ -59,11 +160,12 @@
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Modal Bootstrap 5 -->
<div class="modal fade" id="modalFormAddUsr" tabindex="-1" aria-labelledby="modalFormAddUsrLabel" aria-hidden="true" >
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
@ -74,7 +176,7 @@
</div>
<div class="modal-body">
<!-- {# i form add user #} -->
<form method="POST" action="{{ url_for('manage_profiles') }}" class="login-form">
<form method="POST" action="{{ url_for('manage_profiles') }}" class="form-container">
{{ form.hidden_tag() }}
{{ form.id }}
@ -125,6 +227,14 @@
</div>
{% endblock body %}
{% block js %}
@ -307,32 +417,16 @@ document.getElementById('tblUsers').addEventListener('click', (event) => {
</script>
<script>
// new DataTable('#tblUsers');
new DataTable('#tblUsers', {
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();
}
});
}
});
}
$(document).ready(function() {
$('#tblUsers').DataTable({
// Puedes añadir tus opciones de DataTables aquí
"language": {
url: "https://cdn.datatables.net/plug-ins/1.13.6/i18n/es-ES.json" // Idioma español
},
});
});
</script>
@ -341,9 +435,7 @@ document.getElementById('tblUsers').addEventListener('click', (event) => {
{% include 'z_comps/if_flash.html' %}
<script>
</script>

View File

@ -216,7 +216,7 @@
/* Manejo específico de columnas con contenido largo */
#tblCarousel td[data-label="Archivo"],
#tblCarousel td[data-label="Texto"] {
#tblCarousel td[data-label="Metadatos"] {
white-space: normal;
max-height: none;
overflow: visible;
@ -229,59 +229,20 @@
}
}
</style>
<style>
/* formulario */
/* Ajustes para móviles */
@media (max-width: 576px) {
.form-control-color {
width: 3rem;
height: 3rem;
}
.input-group-text {
font-size: 0.8rem;
padding: 0.375rem 0.5rem;
}
.btn-lg {
padding: 0.5rem 1rem;
font-size: 1rem;
}
}
/* Mejora para los contadores de caracteres */
.form-text {
text-align: right;
font-size: 0.8rem;
}
/* Estilo para los inputs de color */
.input-group .form-control-color {
flex: 0 0 auto;
width: 3.5rem;
}
#dt-length-0 {
width: 6em;
margin-top: 1em;
}
</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"
@ -304,10 +265,7 @@
<thead>
<tr>
<th>ID</th>
<th>Creado</th>
<th>Archivo</th>
<th>New Tab</th>
<th>Texto</th>
<th>Metadatos</th>
<th>Acciones</th>
</tr>
</thead>
@ -315,12 +273,12 @@
{% for ele in data %}
<tr data-id="{{ ele[0] }}">
<td data-label="ID">{{ loop.index }}</td>
<td data-label="Creado">{{ ele[5] }}</td>
<td data-label="Archivo">{{ ele[1] }}</td>
<td data-label="New Tab">{% if ele[7] %}✔️{% else %}❌{% endif %}</td>
<td data-label="Texto" style="background-color: {{ ele[2] }}; color: {{ ele[3] }};">
{{ ele[4] }}
<td data-label="Metadatos" style="background-color: {{ ele[2] }}; color: {{ ele[3] }};">
Texto {{ ele[4][:60] }}... <br>
Fecha creado: {{ ele[5] }} <br>
Archivo: {{ ele[1] }} <br>
New Tab: {% if ele[7] %}✔️{% else %}❌{% endif %}
</td>
<td data-label="Acciones">
<div class="field_btns">
@ -350,116 +308,117 @@
<h2>Agregar Slide</h2>
</div>
<form method="POST" action="{{ url_for('carousel') }}" enctype="multipart/form-data" class="needs-validation" novalidate>
{{ form.hidden_tag() }}
<!-- Fila 1: Campos de imagen y colores -->
<div class="row">
<!-- Input imagen - Ocupa todo el ancho en móviles, mitad en pantallas medianas/grandes -->
<div class="col-12 col-md-6 mb-3">
{{ form.img.label(class="form-label fw-bold") }}
{{ form.img(class="form-control") }}
<div class="invalid-feedback">
Por favor selecciona un archivo válido.
</div>
<small class="form-text text-muted">
Formatos soportados: JPG, PNG, GIF, MP4 (max 5MB)
</small>
</div>
<!-- bg color picker - Apila en móviles, 2 por fila en pantallas medianas/grandes -->
<div class="col-6 col-md-3 mb-3">
{{ form.bg_color.label(class="form-label fw-bold") }}
<div class="input-group">
{{ form.bg_color(type="color", class="form-control form-control-color", value="#ffffff") }}
<span class="input-group-text">{{ form.bg_color.data }}</span>
</div>
</div>
<!-- txt color picker -->
<div class="col-6 col-md-3 mb-3">
{{ form.txt_color.label(class="form-label fw-bold") }}
<div class="input-group">
{{ form.txt_color(type="color", class="form-control form-control-color", value="#000000") }}
<span class="input-group-text">{{ form.txt_color.data }}</span>
</div>
</div>
</div>
<!-- Fila 2: Campos de texto y URL -->
<div class="row">
<!-- txt - Ocupa todo el ancho en móviles, 2/3 en pantallas medianas/grandes -->
<div class="col-12 col-md-8 mb-3">
{{ form.txt.label(class="form-label fw-bold") }}
{{ form.txt(class="form-control", maxlength="350", rows="3", placeholder="Texto que aparecerá en el slide") }}
<div class="form-text">
<span id="txt-counter">0</span>/350 caracteres
</div>
</div>
<!-- url - Ocupa todo el ancho en móviles, 1/3 en pantallas medianas/grandes -->
<div class="col-12 col-md-4 mb-3">
{{ form.url.label(class="form-label fw-bold") }}
{{ form.url(class="form-control", maxlength="250", placeholder="https://ejemplo.com") }}
<div class="form-text">
<span id="url-counter">0</span>/250 caracteres
</div>
</div>
</div>
<!-- Switch para new tab -->
<div class="row mb-4">
<div class="col-12">
<div class="form-check form-switch">
{{ form.isNewTab(class="form-check-input", type="checkbox", role="switch") }}
{{ form.isNewTab.label(class="form-check-label fw-bold") }}
</div>
</div>
</div>
<!-- Botón de submit -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-database-fill-up me-2"></i>Guardar Slide
</button>
</div>
</form>
<form method="POST" action="{{ url_for('carousel') }}" enctype="multipart/form-data" class="needs-validation"
novalidate>
{{ form.hidden_tag() }}
<!-- Fila 1: Campos de imagen y colores -->
<div class="row">
<!-- Input imagen - Ocupa todo el ancho en móviles, mitad en pantallas medianas/grandes -->
<div class="col-12 col-md-6 mb-3">
{{ form.img.label(class="form-label fw-bold") }}
{{ form.img(class="form-control") }}
<div class="invalid-feedback">
Por favor selecciona un archivo válido.
</div>
<small class="form-text text-muted">
Formatos soportados: JPG, JPEG, PNG, MP4, AVIF Y WEBP.
</small>
</div>
<!-- bg color picker - Apila en móviles, 2 por fila en pantallas medianas/grandes -->
<div class="col-6 col-md-3 mb-3">
{{ form.bg_color.label(class="form-label fw-bold") }}
<div class="input-group">
{{ form.bg_color(type="color", class="form-control form-control-color", value="#ffffff") }}
<span class="input-group-text">{{ form.bg_color.data }}</span>
</div>
</div>
<!-- txt color picker -->
<div class="col-6 col-md-3 mb-3">
{{ form.txt_color.label(class="form-label fw-bold") }}
<div class="input-group">
{{ form.txt_color(type="color", class="form-control form-control-color", value="#ffffff") }}
<span class="input-group-text">{{ form.txt_color.data }}</span>
</div>
</div>
</div>
<!-- Fila 2: Campos de texto y URL -->
<div class="row">
<!-- txt - Ocupa todo el ancho en móviles, 2/3 en pantallas medianas/grandes -->
<div class="col-12 col-md-8 mb-3">
{{ form.txt.label(class="form-label fw-bold") }}
{{ form.txt(class="form-control", maxlength="350", rows="3", placeholder="Texto que aparecerá en el slide") }}
<div class="form-text">
<span id="txt-counter">0</span>/350 caracteres
</div>
</div>
<!-- url - Ocupa todo el ancho en móviles, 1/3 en pantallas medianas/grandes -->
<div class="col-12 col-md-4 mb-3">
{{ form.url.label(class="form-label fw-bold") }}
{{ form.url(class="form-control", maxlength="250", placeholder="https://ejemplo.com") }}
<div class="form-text">
<span id="url-counter">0</span>/250 caracteres
</div>
</div>
</div>
<!-- Switch para new tab -->
<div class="row mb-4">
<div class="col-12">
<div class="form-check form-switch">
{{ form.isNewTab(class="form-check-input", type="checkbox", role="switch") }}
{{ form.isNewTab.label(class="form-check-label fw-bold") }}
</div>
</div>
</div>
<!-- Botón de submit -->
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-database-fill-up me-2"></i>Guardar Slide
</button>
</div>
</form>
<!-- JavaScript para contadores de caracteres -->
<script>
document.addEventListener('DOMContentLoaded', function() {
// Contador para el campo de texto
const txtField = document.querySelector('#{{ form.txt.id }}');
const txtCounter = document.querySelector('#txt-counter');
txtField.addEventListener('input', function() {
txtCounter.textContent = this.value.length;
});
// Contador para el campo URL
const urlField = document.querySelector('#{{ form.url.id }}');
const urlCounter = document.querySelector('#url-counter');
urlField.addEventListener('input', function() {
urlCounter.textContent = this.value.length;
});
// Validación del formulario
const forms = document.querySelectorAll('.needs-validation');
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
});
</script>
document.addEventListener('DOMContentLoaded', function () {
// Contador para el campo de texto
const txtField = document.querySelector('#{{ form.txt.id }}');
const txtCounter = document.querySelector('#txt-counter');
txtField.addEventListener('input', function () {
txtCounter.textContent = this.value.length;
});
// Contador para el campo URL
const urlField = document.querySelector('#{{ form.url.id }}');
const urlCounter = document.querySelector('#url-counter');
urlField.addEventListener('input', function () {
urlCounter.textContent = this.value.length;
});
// Validación del formulario
const forms = document.querySelectorAll('.needs-validation');
Array.from(forms).forEach(form => {
form.addEventListener('submit', event => {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated');
}, false);
});
});
</script>

View File

@ -104,16 +104,31 @@
<li class="nav-item"><a class="nav-link {% if active_page == 'change_pswd' %}active{% endif %}" href="{{ url_for('change_pswd') }}"><i class="bi bi-file-earmark-lock2"></i> Cambiar Contraseña</a></li>
{% if is_admin %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if active_page in ['metrics', 'manage_profiles'] %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear-wide"></i> Admin Opt.
<a class="nav-link dropdown-toggle {% if active_page in [ 'carousel', 'metrics', 'manage_profiles'] %}active{% endif %}" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear-wide"></i> Admin Opc.
</a>
<ul class="dropdown-menu">
<!-- <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('manage_profiles') }}"><i class="bi bi-person-fill-gear"></i> Administrar Perfiles</a></li>
<li>
<a class="dropdown-item" href="{{ url_for('carousel') }}">
{% if active_page == 'carousel' %}<i class="bi bi-arrow-return-right"></i>{% endif %}
<i class="bi bi-easel-fill"></i> Carousel
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('metrics') }}">
{% if active_page == 'metrics' %}<i class="bi bi-arrow-return-right"></i>{% endif %}
<i class="bi bi-bar-chart-line-fill"></i> Métricas
</a>
</li>
<li>
<a class="dropdown-item" href="{{ url_for('manage_profiles') }}">
{% if active_page == 'manage_profiles' %}<i class="bi bi-arrow-return-right"></i>{% endif %}
<i class="bi bi-person-fill-gear"></i> Perfiles
</a>
</li>
<!-- <li><hr class="dropdown-divider"></li> -->
</ul>

View File

@ -26,14 +26,10 @@
<div class="floating-btn border border-light shadow-lg" id="floatingBtn">
<a id="floatingBtnLink" target="_blank" href="https://chatgpt.com/g/g-6828126fba608191a2803ac89f54f504-formha-rh-para-pymes">
<img src="{{ url_for('static', filename='y_img/logos/chat_ia_formha.svg') }}"
alt="logo"
class="img-fluid rounded-circle rotating"
style="width: 100%; height: 100%;">
<img src="../../static/y_img/logos/chat_ia_formha.png" alt="logo" style="width: 100%; height: 100%;">
</a>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
const btn = document.getElementById('floatingBtn');
@ -125,6 +121,7 @@ document.addEventListener('DOMContentLoaded', function () {
function startInteraction(e) {
e.preventDefault();
btn.style.transition = 'none'; // Desactiva transición
const clientX = e.clientX;
const clientY = e.clientY;
@ -170,6 +167,7 @@ document.addEventListener('DOMContentLoaded', function () {
function endInteraction() {
startClientX = null;
startClientY = null;
btn.style.transition = ''; // Restaura transición CSS
if (isDragging) {
isDragging = false;