muchos cambios
This commit is contained in:
parent
6d30f68376
commit
cf405ba012
@ -81,7 +81,9 @@ CREATE TABLE carousel (
|
|||||||
img_name VARCHAR(150) NOT NULL,
|
img_name VARCHAR(150) NOT NULL,
|
||||||
bg_color VARCHAR(40) NOT NULL,
|
bg_color VARCHAR(40) NOT NULL,
|
||||||
txt_color VARCHAR(40) NOT NULL,
|
txt_color VARCHAR(40) NOT NULL,
|
||||||
txt VARCHAR(400) NOT NULL
|
txt VARCHAR(400) NOT NULL,
|
||||||
|
url VARCHAR(250),
|
||||||
|
is_new_tab boolean
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER DATABASE formha SET timezone TO 'America/Mexico_City';
|
ALTER DATABASE formha SET timezone TO 'America/Mexico_City';
|
||||||
|
Binary file not shown.
Binary file not shown.
@ -80,11 +80,11 @@ class DBForma:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Error inesperado: {e}")
|
raise RuntimeError(f"Error inesperado: {e}")
|
||||||
|
|
||||||
def get_all_data(self, query):
|
def get_all_data(self, query: str, data_tuple: tuple = ()):
|
||||||
try:
|
try:
|
||||||
with self._get_connection() as conn:
|
with self._get_connection() as conn:
|
||||||
with conn.cursor() as cursor:
|
with conn.cursor() as cursor:
|
||||||
cursor.execute(query)
|
cursor.execute(query, data_tuple)
|
||||||
result = cursor.fetchall()
|
result = cursor.fetchall()
|
||||||
return result
|
return result
|
||||||
except psycopg2.DatabaseError as e:
|
except psycopg2.DatabaseError as e:
|
||||||
|
@ -1,5 +1,40 @@
|
|||||||
|
# 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()])
|
||||||
|
|
||||||
|
# url = StringField('URL (opt): ')
|
||||||
|
|
||||||
|
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import StringField, FileField
|
from wtforms import StringField, FileField, BooleanField # Añade BooleanField aquí
|
||||||
from wtforms.fields import HiddenField
|
from wtforms.fields import HiddenField
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
from wtforms.widgets import ColorInput
|
from wtforms.widgets import ColorInput
|
||||||
@ -8,10 +43,6 @@ from flask_wtf.file import FileAllowed
|
|||||||
import re
|
import re
|
||||||
from wtforms import ValidationError
|
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):
|
class Carousel(FlaskForm):
|
||||||
# id = HiddenField() # Descomenta si lo necesitas
|
# id = HiddenField() # Descomenta si lo necesitas
|
||||||
|
|
||||||
@ -19,15 +50,17 @@ class Carousel(FlaskForm):
|
|||||||
'Imagen de fondo: ',
|
'Imagen de fondo: ',
|
||||||
validators=[
|
validators=[
|
||||||
DataRequired(),
|
DataRequired(),
|
||||||
FileAllowed(['jpg', 'jpeg', 'png', 'mp4'], 'Solo se permiten archivos .jpg, .png o .mp4')
|
FileAllowed(['jpg', 'jpeg', 'png', 'mp4', 'avif', 'webp'], 'Solo se permiten archivos .jpg, .png o .mp4')
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
bg_color = StringField('Color de fondo del texto: ', widget=ColorInput(), validators=[DataRequired()])
|
bg_color = StringField('Color de fondo del texto: ', widget=ColorInput(), validators=[DataRequired()])
|
||||||
|
|
||||||
txt_color = StringField('Color del texto: ', widget=ColorInput(), validators=[DataRequired()])
|
txt_color = StringField('Color del texto: ', widget=ColorInput(), validators=[DataRequired()])
|
||||||
|
|
||||||
txt = StringField('Texto: ', validators=[DataRequired()])
|
txt = StringField('Texto: ', validators=[DataRequired()])
|
||||||
|
|
||||||
|
url = StringField('URL (opt): ')
|
||||||
|
|
||||||
|
# Añade el checkbox aquí
|
||||||
|
isNewTab = BooleanField('¿En nueva ventana?', default=False)
|
261
main.py
261
main.py
@ -31,6 +31,7 @@ bcrypt = Bcrypt(app)
|
|||||||
|
|
||||||
url_login = os.getenv("login_url")
|
url_login = os.getenv("login_url")
|
||||||
|
|
||||||
|
|
||||||
email_sender = os.getenv("email_sender")
|
email_sender = os.getenv("email_sender")
|
||||||
email_pswd = os.getenv("pswd_formha")
|
email_pswd = os.getenv("pswd_formha")
|
||||||
# lst_email_to = ["davidix1991@gmail.com", "davicho1991@live.com"]
|
# lst_email_to = ["davidix1991@gmail.com", "davicho1991@live.com"]
|
||||||
@ -91,9 +92,9 @@ dbUsers = DBForma(jsonDbContact)
|
|||||||
|
|
||||||
|
|
||||||
def lst_email_to() -> list:
|
def lst_email_to() -> list:
|
||||||
data = dbUsers.get_all_data('SELECT email FROM users WHERE is_contact_noti = true;')
|
data = dbUsers.get_all_data('SELECT email FROM users WHERE is_contact_noti = true;', ())
|
||||||
re = [ele[0] for ele in data]
|
respuesta = [ele[0] for ele in data]
|
||||||
return re
|
return respuesta
|
||||||
|
|
||||||
# decorador para rutas protegidas en caso de que borres al vuelo a un usuario.
|
# decorador para rutas protegidas en caso de que borres al vuelo a un usuario.
|
||||||
def validate_user_exists(f):
|
def validate_user_exists(f):
|
||||||
@ -127,18 +128,33 @@ def validate_user_exists(f):
|
|||||||
# Font Name: Big Money-ne | https://patorjk.com/software/taag/#p=testall&f=Graffiti&t=USER
|
# Font Name: Big Money-ne | https://patorjk.com/software/taag/#p=testall&f=Graffiti&t=USER
|
||||||
# ##################################################################
|
# ##################################################################
|
||||||
|
|
||||||
|
@app.errorhandler(404)
|
||||||
|
def page_not_found(e):
|
||||||
|
# puedes usar una plantilla HTML personalizada si quieres
|
||||||
|
# return render_template('404.html'), 404
|
||||||
|
mnsj = l_flash_msj("Error Sección", "La sección a la que quieres entrar no existe", "error")
|
||||||
|
flash(mnsj)
|
||||||
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
# @cache.cached(timeout=3600)
|
# @cache.cached(timeout=3600)
|
||||||
def home():
|
def home():
|
||||||
q = """
|
q = """
|
||||||
SELECT
|
SELECT
|
||||||
id, img_name, bg_color, txt_color, txt, TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada
|
id,
|
||||||
|
img_name,
|
||||||
|
bg_color,
|
||||||
|
txt_color,
|
||||||
|
txt,
|
||||||
|
TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada,
|
||||||
|
url,
|
||||||
|
is_new_tab
|
||||||
FROM
|
FROM
|
||||||
carousel
|
carousel
|
||||||
ORDER BY
|
ORDER BY
|
||||||
id DESC;
|
id DESC;
|
||||||
"""
|
"""
|
||||||
data = dbUsers.get_all_data(q)
|
data = dbUsers.get_all_data(q, ())
|
||||||
return render_template(v['home'], active_page='home', data=data)
|
return render_template(v['home'], active_page='home', data=data)
|
||||||
|
|
||||||
@app.route('/about-us')
|
@app.route('/about-us')
|
||||||
@ -181,8 +197,6 @@ def api_posts():
|
|||||||
return jsonify(data)
|
return jsonify(data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/blog/<int:post_id>')
|
@app.route('/blog/<int:post_id>')
|
||||||
@app.route('/blog/<int:post_id>/src/<string:source_name>')
|
@app.route('/blog/<int:post_id>/src/<string:source_name>')
|
||||||
# @cache.cached(timeout=43200)
|
# @cache.cached(timeout=43200)
|
||||||
@ -201,8 +215,8 @@ def blog_post(post_id, source_name=None):
|
|||||||
SELECT
|
SELECT
|
||||||
u.nombre,
|
u.nombre,
|
||||||
u.apellido,
|
u.apellido,
|
||||||
TO_CHAR(p.created_at, 'DD/MM/YYYY HH24:MI') AS fecha_creada,
|
TO_CHAR(p.created_at, 'DD/MM/YYYY') AS fecha_creada,
|
||||||
TO_CHAR(p.updated_at, 'DD/MM/YYYY HH24:MI') AS fecha_updated,
|
TO_CHAR(p.updated_at, 'DD/MM/YYYY') AS fecha_updated,
|
||||||
GREATEST(1, CEIL(length(regexp_replace(p.body_no_img, '\s+', ' ', 'g')) / 5.0 / 375)) AS read_time_min,
|
GREATEST(1, CEIL(length(regexp_replace(p.body_no_img, '\s+', ' ', 'g')) / 5.0 / 375)) AS read_time_min,
|
||||||
p.title,
|
p.title,
|
||||||
p.body,
|
p.body,
|
||||||
@ -221,8 +235,38 @@ def blog_post(post_id, source_name=None):
|
|||||||
t = (post_id,)
|
t = (post_id,)
|
||||||
data = dbUsers.get_data(q, t)
|
data = dbUsers.get_data(q, t)
|
||||||
|
|
||||||
|
|
||||||
|
if data is None:
|
||||||
|
# Si no se encuentra el post, redirigir a la página de error 404
|
||||||
|
# return render_template('404.html'), 404
|
||||||
|
msg = l_flash_msj('Error Publicación','La publicación que intentas ver fue eliminada o no existe. 😅', 'warning')
|
||||||
|
flash(msg)
|
||||||
|
return redirect(url_for('blog'))
|
||||||
|
|
||||||
|
|
||||||
return render_template(v['blog']['post'], data=data)
|
return render_template(v['blog']['post'], data=data)
|
||||||
|
|
||||||
|
@app.route('/blog/api/count-post-viewed')
|
||||||
|
def count_posts_viewed():
|
||||||
|
q = "SELECT id_post, COUNT(id_post) AS count FROM posts_visited GROUP BY id_post ORDER BY id_post;"
|
||||||
|
data = dbUsers.get_all_data_dict(q)
|
||||||
|
return jsonify(data)
|
||||||
|
|
||||||
|
@app.route('/blog/get-data', methods=['POST'])
|
||||||
|
def get_data():
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
return jsonify({"success": False, "message": "No data received"}), 400
|
||||||
|
# Ejemplo: extraer y mostrar los campos esperados
|
||||||
|
post_id = data.get("post_id")
|
||||||
|
|
||||||
|
# actualizar la base de datos
|
||||||
|
q_update = "INSERT INTO posts_visited (id_post, viewed) VALUES (%s, %s);"
|
||||||
|
d_tuple = (post_id, cur_date())
|
||||||
|
dbUsers.update_data(q_update, d_tuple)
|
||||||
|
|
||||||
|
return jsonify({"success": True}), 200
|
||||||
|
|
||||||
@app.route("/contact", methods=['GET', 'POST'])
|
@app.route("/contact", methods=['GET', 'POST'])
|
||||||
def contact():
|
def contact():
|
||||||
@ -232,39 +276,69 @@ def contact():
|
|||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
# Procesar datos del formulario
|
# Procesar datos del formulario
|
||||||
f_mnsj = l_flash_msj('Contacto', '¡Gracias por contactarnos! Te responderemos pronto. ☺️', 'success' )
|
|
||||||
flash(f_mnsj)
|
|
||||||
|
|
||||||
|
|
||||||
c_date = cur_date()
|
c_date = cur_date()
|
||||||
|
nombre = form.nombre.data.strip()
|
||||||
|
apellido = form.apellido.data.strip()
|
||||||
|
email = form.email.data.strip()
|
||||||
|
estado = form.estado.data.strip()
|
||||||
|
num_tel = form.num_tel.data.strip()
|
||||||
|
size_co = form.size_co.data.strip()
|
||||||
|
rol_contacto = form.rol_contacto.data.strip()
|
||||||
|
industry_type = form.industry_type.data.strip()
|
||||||
|
tipo_req = form.tipo_req.data.strip()
|
||||||
|
|
||||||
|
# validar si ya se recibio info de ese email
|
||||||
|
q_val_mail = "SELECT COUNT(email) FROM contact WHERE email = %s;"
|
||||||
|
|
||||||
|
q_val_tel = "SELECT COUNT(num_tel) FROM contact WHERE num_tel = %s;"
|
||||||
|
|
||||||
|
res_mail = dbUsers.get_all_data(q_val_mail, (email,))
|
||||||
|
is_dup_mail = res_mail[0][0] if res_mail else 0
|
||||||
|
|
||||||
|
res_phone = dbUsers.get_all_data(q_val_tel, (num_tel,))
|
||||||
|
is_dup_phone = res_phone[0][0] if res_phone else 0
|
||||||
|
|
||||||
|
f_mnsj = None
|
||||||
|
|
||||||
|
if is_dup_mail > 0 or is_dup_phone > 0:
|
||||||
|
f_mnsj = l_flash_msj("Información Precargada", "Ya contamos con tu información, pronto nos pondremos en contacto", "success")
|
||||||
|
else:
|
||||||
|
|
||||||
obj_datetime = get_date_n_time(c_date)
|
obj_datetime = get_date_n_time(c_date)
|
||||||
hora = obj_datetime['hour']
|
hora = obj_datetime['hour']
|
||||||
fecha = obj_datetime['date']
|
fecha = obj_datetime['date']
|
||||||
|
data = ( c_date, nombre, apellido, email, estado, num_tel, size_co, rol_contacto, industry_type, tipo_req )
|
||||||
data = ( c_date, form.nombre.data, form.apellido.data, form.email.data, form.estado.data, form.num_tel.data, form.size_co.data, form.rol_contacto.data, form.industry_type.data, form.tipo_req.data )
|
|
||||||
|
|
||||||
# Guardar datos en la base de datos
|
# Guardar datos en la base de datos
|
||||||
dbContact.carga_contact(data)
|
dbContact.carga_contact(data)
|
||||||
|
|
||||||
# Configurar y enviar email asíncrono
|
# Configurar y enviar email asíncrono
|
||||||
msg = Message( "Subject, una persona busca asesoria", sender=email_sender, recipients=lst_email_to() )
|
msg = Message( "Subject, una persona busca asesoria", sender=email_sender, recipients=lst_email_to() )
|
||||||
|
url_login = "https://formha.temporal.work/login"
|
||||||
|
|
||||||
msg.html = f"""
|
msg.html = f"""
|
||||||
<h1>Nuevo contacto recibido</h1>
|
<div style="font-family: Arial, sans-serif;">
|
||||||
<p><trong>Fecha: </strong> {fecha}</p>
|
<img src="https://formha.temporal.work/static/y_img/logos/formha_blanco_vertical.png" alt="Escudo FORMHä" height="150" style="margin-bottom: 10px;"><br>
|
||||||
<p><strong>Hora:</strong> {hora}</p>
|
<h2 style="color: #333;">📩 Nuevo contacto recibido</h2>
|
||||||
<p><strong>Nombre:</strong> {form.nombre.data} {form.apellido.data}</p>
|
<ul>
|
||||||
<p><strong>Email:</strong> {form.email.data}</p>
|
<li><strong>Fecha:</strong> {fecha}</li>
|
||||||
<p><strong>Teléfono:</strong> {form.num_tel.data}</p>
|
<li><strong>Hora:</strong> {hora}</li>
|
||||||
<p><strong>Mensaje:</strong> {form.tipo_req.data}</p>
|
<li><strong>Nombre:</strong> {form.nombre.data} {form.apellido.data}</li>
|
||||||
<p><a href="{url_login}" target='_blank'>Iniciar Sesión</a></p>
|
<li><strong>Email:</strong> {form.email.data}</li>
|
||||||
|
<li><strong>Teléfono:</strong> {form.num_tel.data}</li>
|
||||||
|
<li><strong>Mensaje:</strong> {form.tipo_req.data}</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="{url_login}" target="_blank">👉 Iniciar Sesión</a></p>
|
||||||
|
</div>
|
||||||
"""
|
"""
|
||||||
|
f_mnsj = l_flash_msj('Contacto', '¡Gracias por contactarnos! Te responderemos pronto. ☺️', 'success' )
|
||||||
|
|
||||||
# Enviar en segundo plano
|
# Enviar en segundo plano
|
||||||
thr = Thread( target=send_async_email, args=(current_app._get_current_object(), msg) )
|
thr = Thread( target=send_async_email, args=(current_app._get_current_object(), msg) )
|
||||||
thr.start()
|
thr.start()
|
||||||
|
|
||||||
|
flash(f_mnsj)
|
||||||
return redirect(url_for('contact'))
|
return redirect(url_for('contact'))
|
||||||
|
|
||||||
return render_template(v['contact'], form=form, active_page='contact')
|
return render_template(v['contact'], form=form, active_page='contact')
|
||||||
@ -423,14 +497,11 @@ def user_home():
|
|||||||
f_mnsj = None
|
f_mnsj = None
|
||||||
|
|
||||||
if lst_conn != current_date:
|
if lst_conn != current_date:
|
||||||
# q = "UPDATE users SET lst_conn = %s WHERE id = %s;"
|
|
||||||
# d = (cur_date(), usr_id)
|
|
||||||
strGreet = 'Bienvenido' if gender == 'M' else 'Bienvenida'
|
strGreet = 'Bienvenido' if gender == 'M' else 'Bienvenida'
|
||||||
f_mnsj = l_flash_msj(f'{saludo_hr()}', f'{strGreet} {nombre}', 'success')
|
f_mnsj = l_flash_msj(f'{saludo_hr()}', f'{strGreet} {nombre}', 'success')
|
||||||
# dbUsers.update_data(q, d)
|
# dbUsers.update_data(q, d)
|
||||||
flash(f_mnsj)
|
flash(f_mnsj)
|
||||||
|
|
||||||
|
|
||||||
flash(update_pswd)
|
flash(update_pswd)
|
||||||
|
|
||||||
q = "UPDATE users SET lst_conn = %s WHERE id = %s;"
|
q = "UPDATE users SET lst_conn = %s WHERE id = %s;"
|
||||||
@ -440,7 +511,15 @@ def user_home():
|
|||||||
|
|
||||||
q_contact = """
|
q_contact = """
|
||||||
SELECT
|
SELECT
|
||||||
ID, TO_CHAR(FULL_DATE_TIME, 'DD/MM/YYYY') AS FECHA_FORMATEADA, NOMBRE, APELLIDO, ESTADO, SIZE_CO, ROL_CONTACTO, INDUSTRY_TYPE, STATUS
|
ID,
|
||||||
|
TO_CHAR(FULL_DATE_TIME, 'DD/MM/YYYY') AS FECHA_FORMATEADA,
|
||||||
|
NOMBRE,
|
||||||
|
APELLIDO,
|
||||||
|
ESTADO,
|
||||||
|
SIZE_CO,
|
||||||
|
ROL_CONTACTO,
|
||||||
|
INDUSTRY_TYPE,
|
||||||
|
STATUS
|
||||||
FROM
|
FROM
|
||||||
CONTACT
|
CONTACT
|
||||||
WHERE
|
WHERE
|
||||||
@ -449,9 +528,8 @@ def user_home():
|
|||||||
ORDER BY
|
ORDER BY
|
||||||
FULL_DATE_TIME DESC;
|
FULL_DATE_TIME DESC;
|
||||||
"""
|
"""
|
||||||
data_contact = dbUsers.get_all_data(q_contact)
|
data_contact = dbUsers.get_all_data(q_contact, ())
|
||||||
|
|
||||||
# return render_template(v['tmp_user']['home'], token_data=token_data, f_mnsj=f_mnsj, nombre=nombre, exp=exp, data_contact=data_contact)
|
|
||||||
return render_template(v['tmp_user']['home'], f_mnsj=f_mnsj, nombre=nombre, exp=exp, data_contact=data_contact, active_page='user_home')
|
return render_template(v['tmp_user']['home'], f_mnsj=f_mnsj, nombre=nombre, exp=exp, data_contact=data_contact, active_page='user_home')
|
||||||
|
|
||||||
@app.route('/user/manage-record', methods=['POST'])
|
@app.route('/user/manage-record', methods=['POST'])
|
||||||
@ -491,11 +569,23 @@ def get_contact_data():
|
|||||||
def download_db():
|
def download_db():
|
||||||
q = """
|
q = """
|
||||||
SELECT
|
SELECT
|
||||||
id, TO_CHAR(FULL_DATE_TIME, 'DD/MM/YYYY') as fecha, TO_CHAR(FULL_DATE_TIME, 'HH24:MI') as hora, nombre, apellido, email, estado, num_tel, size_co, rol_contacto, industry_type, tipo_req, status
|
id,
|
||||||
|
TO_CHAR(FULL_DATE_TIME, 'DD/MM/YYYY') as fecha,
|
||||||
|
TO_CHAR(FULL_DATE_TIME, 'HH24:MI') as hora,
|
||||||
|
nombre,
|
||||||
|
apellido,
|
||||||
|
email,
|
||||||
|
estado,
|
||||||
|
num_tel,
|
||||||
|
size_co,
|
||||||
|
rol_contacto,
|
||||||
|
industry_type,
|
||||||
|
tipo_req,
|
||||||
|
status
|
||||||
FROM
|
FROM
|
||||||
contact;
|
contact;
|
||||||
"""
|
"""
|
||||||
dbData = dbUsers.get_all_data(q)
|
dbData = dbUsers.get_all_data(q, ())
|
||||||
return jsonify({"data": dbData})
|
return jsonify({"data": dbData})
|
||||||
|
|
||||||
@app.route('/user/txt-editor')
|
@app.route('/user/txt-editor')
|
||||||
@ -554,7 +644,7 @@ def my_posts():
|
|||||||
per_page = 8 # Número de tarjetas por página
|
per_page = 8 # Número de tarjetas por página
|
||||||
|
|
||||||
# Obtener todos los posts del usuario (puedes optimizar esto con paginación SQL real después)
|
# Obtener todos los posts del usuario (puedes optimizar esto con paginación SQL real después)
|
||||||
q_all = fr"""
|
q_all = r"""
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada,
|
TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada,
|
||||||
@ -566,13 +656,13 @@ def my_posts():
|
|||||||
FROM
|
FROM
|
||||||
posts
|
posts
|
||||||
WHERE
|
WHERE
|
||||||
id_usr = '{id_usr}'
|
id_usr = %s
|
||||||
ORDER BY
|
ORDER BY
|
||||||
COALESCE(updated_at, created_at) DESC,
|
COALESCE(updated_at, created_at) DESC,
|
||||||
created_at DESC;
|
created_at DESC;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data_all = dbUsers.get_all_data(q_all)
|
data_all = dbUsers.get_all_data(q_all, (id_usr,))
|
||||||
total_posts = len(data_all)
|
total_posts = len(data_all)
|
||||||
total_pages = (total_posts + per_page - 1) // per_page
|
total_pages = (total_posts + per_page - 1) // per_page
|
||||||
|
|
||||||
@ -606,7 +696,17 @@ def del_post():
|
|||||||
def post(post_id):
|
def post(post_id):
|
||||||
# Obtener el post
|
# Obtener el post
|
||||||
usr_id = get_jwt()['sub']
|
usr_id = get_jwt()['sub']
|
||||||
q = "select TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada, TO_CHAR(updated_at, 'DD/MM/YYYY HH24:MI') as fecha_updated, title, body from posts where id_usr = %s AND id = %s;"
|
q = """
|
||||||
|
SELECT
|
||||||
|
TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada,
|
||||||
|
TO_CHAR(updated_at, 'DD/MM/YYYY HH24:MI') as fecha_updated,
|
||||||
|
title,
|
||||||
|
body
|
||||||
|
FROM
|
||||||
|
posts
|
||||||
|
WHERE
|
||||||
|
id_usr = %s AND id = %s;
|
||||||
|
"""
|
||||||
t = (usr_id, post_id )
|
t = (usr_id, post_id )
|
||||||
data = dbUsers.get_data(q, t)
|
data = dbUsers.get_data(q, t)
|
||||||
q_nw = r"SELECT array_length(regexp_split_to_array(TRIM(body_no_img), '\s+'), 1) FROM posts WHERE id_usr = %s AND id = %s;"
|
q_nw = r"SELECT array_length(regexp_split_to_array(TRIM(body_no_img), '\s+'), 1) FROM posts WHERE id_usr = %s AND id = %s;"
|
||||||
@ -673,6 +773,11 @@ def data_metrics():
|
|||||||
# INNER JOIN posts p ON p.id = pv.id_post
|
# INNER JOIN posts p ON p.id = pv.id_post
|
||||||
# INNER JOIN users u ON u.id = p.id_usr;
|
# INNER JOIN users u ON u.id = p.id_usr;
|
||||||
|
|
||||||
|
# -- SELECT pv.id_post, pv.viewed, u.nombre, u.apellido
|
||||||
|
# -- FROM posts_visited pv
|
||||||
|
# -- INNER JOIN posts p ON p.id = pv.id_post
|
||||||
|
# -- INNER JOIN users u ON u.id = p.id_usr;
|
||||||
|
|
||||||
q_contact = r"""
|
q_contact = r"""
|
||||||
SELECT
|
SELECT
|
||||||
CASE EXTRACT(MONTH FROM full_date_time AT TIME ZONE 'America/Mexico_City')
|
CASE EXTRACT(MONTH FROM full_date_time AT TIME ZONE 'America/Mexico_City')
|
||||||
@ -736,13 +841,34 @@ def data_metrics():
|
|||||||
conteo;
|
conteo;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
q_posts_mas_vistos = "SELECT id_post, COUNT(id_post) AS conteo FROM posts_visited GROUP BY id_post ORDER BY conteo DESC LIMIT 10;"
|
||||||
|
|
||||||
|
q_top_three_authors = """
|
||||||
|
SELECT
|
||||||
|
CONCAT(u.nombre, ' ', u.apellido) AS autor,
|
||||||
|
COUNT(*) AS total_posts
|
||||||
|
FROM
|
||||||
|
posts_visited pv
|
||||||
|
INNER JOIN
|
||||||
|
posts p ON p.id = pv.id_post
|
||||||
|
INNER JOIN
|
||||||
|
users u ON u.id = p.id_usr
|
||||||
|
GROUP BY
|
||||||
|
u.id, u.nombre, u.apellido
|
||||||
|
ORDER BY
|
||||||
|
total_posts DESC
|
||||||
|
LIMIT 3;
|
||||||
|
"""
|
||||||
|
|
||||||
data_contact = {
|
data_contact = {
|
||||||
"count_monthly": dbUsers.get_all_data(q_contact),
|
"count_monthly": dbUsers.get_all_data(q_contact, ()),
|
||||||
"count_state": dbUsers.get_all_data(q_count_state),
|
"count_state": dbUsers.get_all_data(q_count_state, ()),
|
||||||
"size_co": dbUsers.get_all_data(q_size_co),
|
"size_co": dbUsers.get_all_data(q_size_co, ()),
|
||||||
"rol_contact": dbUsers.get_all_data(q_rol_contact),
|
"rol_contact": dbUsers.get_all_data(q_rol_contact, ()),
|
||||||
"industry_type": dbUsers.get_all_data(q_industry_type),
|
"industry_type": dbUsers.get_all_data(q_industry_type, ()),
|
||||||
"group_status": dbUsers.get_all_data(q_group_status)
|
"group_status": dbUsers.get_all_data(q_group_status, ()),
|
||||||
|
"top_ten": dbUsers.get_all_data(q_posts_mas_vistos, ()),
|
||||||
|
"top_three_authors": dbUsers.get_all_data(q_top_three_authors, ()),
|
||||||
}
|
}
|
||||||
return jsonify(data_contact)
|
return jsonify(data_contact)
|
||||||
|
|
||||||
@ -776,7 +902,7 @@ def manage_profiles():
|
|||||||
) p ON u.id = p.id_usr
|
) p ON u.id = p.id_usr
|
||||||
ORDER BY conteo DESC;
|
ORDER BY conteo DESC;
|
||||||
"""
|
"""
|
||||||
data_all_users = dbUsers.get_all_data(q_all_users)
|
data_all_users = dbUsers.get_all_data(q_all_users, ())
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
f_nombre = f'{form.nombre.data}'.title().strip()
|
f_nombre = f'{form.nombre.data}'.title().strip()
|
||||||
@ -919,23 +1045,34 @@ def carousel():
|
|||||||
|
|
||||||
q_all_slides = """
|
q_all_slides = """
|
||||||
SELECT
|
SELECT
|
||||||
id, img_name, bg_color, txt_color, txt, TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada
|
id,
|
||||||
|
img_name,
|
||||||
|
bg_color,
|
||||||
|
txt_color,
|
||||||
|
txt,
|
||||||
|
TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada,
|
||||||
|
url,
|
||||||
|
is_new_tab
|
||||||
FROM
|
FROM
|
||||||
carousel
|
carousel
|
||||||
ORDER BY
|
ORDER BY
|
||||||
id DESC;
|
id DESC;
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = dbUsers.get_all_data(q_all_slides)
|
data = dbUsers.get_all_data(q_all_slides, ())
|
||||||
|
|
||||||
if request.method == 'POST' and form.validate_on_submit():
|
if request.method == 'POST' and form.validate_on_submit():
|
||||||
# img
|
# img
|
||||||
image_file = form.img.data
|
image_file = form.img.data
|
||||||
filename = secure_filename(image_file.filename)
|
filename = secure_filename(image_file.filename)
|
||||||
|
|
||||||
prev_img = dbUsers.get_all_data("SELECT img_name FROM carousel;")
|
prev_img = dbUsers.get_all_data("SELECT img_name FROM carousel;", ())
|
||||||
lst_img = [ele[0] for ele in prev_img]
|
lst_img = [ele[0] for ele in prev_img]
|
||||||
|
|
||||||
|
url = None if form.url.data == '' else form.url.data
|
||||||
|
|
||||||
|
isNewTab = form.isNewTab.data
|
||||||
|
|
||||||
# validar que el archivo no se haya cargado previamente
|
# validar que el archivo no se haya cargado previamente
|
||||||
if filename in lst_img:
|
if filename in lst_img:
|
||||||
mnsj_flash = l_flash_msj('Error Archivo', 'La imagen ya ha sido cargada previamente.', 'error')
|
mnsj_flash = l_flash_msj('Error Archivo', 'La imagen ya ha sido cargada previamente.', 'error')
|
||||||
@ -945,8 +1082,8 @@ def carousel():
|
|||||||
txt_color = rgba_to_string((*hex_to_rgb(form.txt_color.data), 1))
|
txt_color = rgba_to_string((*hex_to_rgb(form.txt_color.data), 1))
|
||||||
txt = form.txt.data
|
txt = form.txt.data
|
||||||
|
|
||||||
q = "INSERT INTO carousel (img_name, bg_color, txt_color, txt) VALUES (%s, %s, %s, %s);"
|
q = "INSERT INTO carousel (img_name, bg_color, txt_color, txt, url, is_new_tab) VALUES (%s, %s, %s, %s, %s, %s);"
|
||||||
t = (filename, bg_color, txt_color, txt)
|
t = (filename, bg_color, txt_color, txt, url, isNewTab)
|
||||||
dbUsers.update_data(q, t)
|
dbUsers.update_data(q, t)
|
||||||
mnsj_flash = l_flash_msj('Datos Slide', f'Datos agregados para imagen: {filename}', 'success')
|
mnsj_flash = l_flash_msj('Datos Slide', f'Datos agregados para imagen: {filename}', 'success')
|
||||||
|
|
||||||
@ -956,6 +1093,38 @@ def 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='metrics')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/user/carousel/delete-slide/<int:id>', methods=["POST", "DELETE"])
|
||||||
|
@jwt_required()
|
||||||
|
@validate_user_exists
|
||||||
|
@admin_required
|
||||||
|
def delete_slide(id):
|
||||||
|
try:
|
||||||
|
q_img_file = "SELECT img_name FROM carousel WHERE id = %s;"
|
||||||
|
t_img_file = (id,)
|
||||||
|
result = dbUsers.get_data(q_img_file, t_img_file)
|
||||||
|
if not result:
|
||||||
|
return jsonify({'error': 'No se encontró el registro'}), 404
|
||||||
|
|
||||||
|
data_img = result[0]
|
||||||
|
|
||||||
|
filepath = os.path.join(os.getcwd(), 'static', 'uploads', data_img)
|
||||||
|
|
||||||
|
q_del_record = "DELETE FROM carousel WHERE id = %s;"
|
||||||
|
dbUsers.update_data(q_del_record, t_img_file)
|
||||||
|
|
||||||
|
if os.path.isfile(filepath):
|
||||||
|
os.remove(filepath)
|
||||||
|
else:
|
||||||
|
print(f"[!] Archivo no encontrado: {filepath}")
|
||||||
|
|
||||||
|
return jsonify({'msg': 'Slide eliminado correctamente'}), 200
|
||||||
|
except Exception as e:
|
||||||
|
print(f'Error al eliminar el slide: {e}')
|
||||||
|
return jsonify({'error': 'Error interno del servidor'}), 500
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/user/carousel/download/<path:filename>')
|
@app.route('/user/carousel/download/<path:filename>')
|
||||||
@jwt_required()
|
@jwt_required()
|
||||||
@validate_user_exists
|
@validate_user_exists
|
||||||
|
@ -1,36 +1,88 @@
|
|||||||
/* Variables para consistencia */
|
/* Aplica a imágenes y videos dentro del carousel */
|
||||||
|
.carousel-item img,
|
||||||
|
.carousel-item video {
|
||||||
/* ---------------------------- */
|
width: 100%;
|
||||||
/* Media Queries optimizadas */
|
height: 100%;
|
||||||
@media (min-width: 768px) {
|
object-fit: cover;
|
||||||
main {
|
display: block;
|
||||||
width: 90vw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Altura del carousel para ajustarse al viewport */
|
||||||
|
.carousel-item {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 80vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
/* Contenedor de los ítems del carrusel */
|
||||||
main {
|
.carousel-inner {
|
||||||
width: 85vw;
|
overflow: hidden;
|
||||||
margin: 20px auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Centrar captions */
|
||||||
|
.carousel-caption.centered {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1440px) {
|
/* Contenido dentro del caption */
|
||||||
main {
|
.carousel-caption.centered .caption-content {
|
||||||
width: 80vw;
|
padding: 1rem 2rem;
|
||||||
margin: 2em auto;
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||||
|
pointer-events: auto;
|
||||||
|
max-width: 90%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5); /* opcional */
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Título dentro del caption */
|
||||||
|
.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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1920px) {
|
/* Evita que videos más anchos se corten mal en landscape */
|
||||||
|
@media screen and (orientation: landscape) {
|
||||||
|
.carousel-item img,
|
||||||
|
.carousel-item video {
|
||||||
|
height: 100vh;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ajuste específico para texto en móviles */
|
/* --------- Responsivo (ajustes si necesitas) ---------- */
|
||||||
|
|
||||||
|
/* Smartphones */
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
.carousel-caption.centered .caption-content {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-caption.centered .caption-content h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablets */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.carousel-caption.centered .caption-content h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Laptops */
|
||||||
|
@media (min-width: 1024px) and (max-width: 1439px) {
|
||||||
|
/* ajustes opcionales */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pantallas grandes */
|
||||||
|
@media (min-width: 1440px) {
|
||||||
|
/* ajustes opcionales */
|
||||||
}
|
}
|
28
static/e_blog/0_all_posts_main.css
Normal file
28
static/e_blog/0_all_posts_main.css
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/* Smartphones (hasta 767px) */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
/* body{ background-color: black; } */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablets (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
/* body{ background-color: pink; } */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Laptops (1024px - 1439px) monitores resulición baja */
|
||||||
|
@media (min-width: 1024px) and (max-width: 1439px) {
|
||||||
|
/* body{ background-color: purple; } */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PCs de escritorio (1440px - 1919px) macbook */
|
||||||
|
@media (min-width: 1440px) and (max-width: 1919px) {
|
||||||
|
/* body{ background-color: greenyellow; } */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pantallas Ultrawide (1920px en adelante) */
|
||||||
|
@media (min-width: 1920px) {
|
||||||
|
/* body{ background-color: red; } */
|
||||||
|
main {
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,3 @@
|
|||||||
.card-img {
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-bottom-left-radius: 0;
|
|
||||||
border-bottom-right-radius: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.card-title {
|
.card-title {
|
||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
}
|
}
|
||||||
@ -23,3 +14,28 @@
|
|||||||
.card-footer {
|
.card-footer {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-img-top {
|
||||||
|
max-height: 200px;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-img-top:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrapper {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-wrapper.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-indicator {
|
||||||
|
display: none;
|
||||||
|
}
|
283
static/e_blog/a_all_posts_visit.js
Normal file
283
static/e_blog/a_all_posts_visit.js
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
|
||||||
|
// DOM Elements
|
||||||
|
const DOM = {
|
||||||
|
container: document.getElementById("card-container"),
|
||||||
|
pagination: document.getElementById("pagination"),
|
||||||
|
searchInput: document.getElementById("search-input"),
|
||||||
|
loading: document.getElementById("loading-indicator"),
|
||||||
|
resultCount: document.getElementById("result-count")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const CONFIG = {
|
||||||
|
url: window.location.href.replace(/\/$/, "") + "/api/posts",
|
||||||
|
postsPerPage: 12,
|
||||||
|
maxVisiblePages: 5,
|
||||||
|
searchDelay: 300
|
||||||
|
};
|
||||||
|
|
||||||
|
// Application State
|
||||||
|
const state = {
|
||||||
|
allPosts: [],
|
||||||
|
filteredPosts: [],
|
||||||
|
currentPage: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
searchTerm: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Utility Functions
|
||||||
|
const utils = {
|
||||||
|
debounce: (func, delay) => {
|
||||||
|
let timeout;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), delay);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
normalizePost: post => ({
|
||||||
|
...post,
|
||||||
|
_autor: post.autor.toLowerCase(),
|
||||||
|
_title: post.title.toLowerCase(),
|
||||||
|
_preview: post.preview.toLowerCase()
|
||||||
|
}),
|
||||||
|
|
||||||
|
saveState: () => {
|
||||||
|
const searchState = {
|
||||||
|
page: state.currentPage,
|
||||||
|
term: state.searchTerm
|
||||||
|
};
|
||||||
|
localStorage.setItem("searchState", JSON.stringify(searchState));
|
||||||
|
},
|
||||||
|
|
||||||
|
loadState: () => {
|
||||||
|
const savedState = JSON.parse(localStorage.getItem("searchState")) || {};
|
||||||
|
state.currentPage = savedState.page || 1;
|
||||||
|
state.searchTerm = savedState.term || "";
|
||||||
|
DOM.searchInput.value = state.searchTerm;
|
||||||
|
},
|
||||||
|
|
||||||
|
animateCards: () => {
|
||||||
|
const cards = document.querySelectorAll('.card-wrapper');
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
const animate = () => {
|
||||||
|
if (index < cards.length) {
|
||||||
|
cards[index].classList.add('visible');
|
||||||
|
index++;
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rendering Functions
|
||||||
|
const render = {
|
||||||
|
showLoading: () => {
|
||||||
|
DOM.loading.style.display = 'block';
|
||||||
|
DOM.container.style.opacity = '0.5';
|
||||||
|
},
|
||||||
|
|
||||||
|
hideLoading: () => {
|
||||||
|
DOM.loading.style.display = 'none';
|
||||||
|
DOM.container.style.opacity = '1';
|
||||||
|
},
|
||||||
|
|
||||||
|
posts: () => {
|
||||||
|
DOM.container.innerHTML = "";
|
||||||
|
|
||||||
|
const start = (state.currentPage - 1) * CONFIG.postsPerPage;
|
||||||
|
const end = start + CONFIG.postsPerPage;
|
||||||
|
const postsToShow = state.filteredPosts.slice(start, end);
|
||||||
|
|
||||||
|
if (postsToShow.length === 0) {
|
||||||
|
DOM.container.innerHTML = `
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<h4>No se encontraron resultados</h4>
|
||||||
|
<p class="text-muted">Intenta con otros términos de búsqueda</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
postsToShow.forEach(post => {
|
||||||
|
const imgSrc = post.first_img || 'static/y_img/other/no_img.png';
|
||||||
|
const dateUpdate = post.author_updated ? `<i class="bi bi-arrow-repeat"></i> ${post.author_updated}<br>` : "";
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "col card-wrapper";
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="card h-100">
|
||||||
|
<a href="/blog/${post.id}" class="stretched-link" onclick="utils.saveState()">
|
||||||
|
<img src="${imgSrc}" class="card-img-top" alt="${post.title}" loading="lazy">
|
||||||
|
</a>
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<div class="mb-3">
|
||||||
|
<h5 class="card-title">${post.title}</h5>
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
<i class="bi bi-file-person-fill"></i> ${post.autor}<br>
|
||||||
|
<i class="bi bi-calendar-week"></i> ${post.author_creation}<br>
|
||||||
|
${dateUpdate}
|
||||||
|
<i class="bi bi-clock"></i> ${post.read_time_min} min(s) de lectura.
|
||||||
|
</small>
|
||||||
|
<p class="card-text mt-2">${post.preview}...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
fragment.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
DOM.container.appendChild(fragment);
|
||||||
|
utils.animateCards();
|
||||||
|
},
|
||||||
|
|
||||||
|
pagination: () => {
|
||||||
|
DOM.pagination.innerHTML = "";
|
||||||
|
state.totalPages = Math.ceil(state.filteredPosts.length / CONFIG.postsPerPage);
|
||||||
|
DOM.resultCount.textContent = state.filteredPosts.length;
|
||||||
|
|
||||||
|
const pageItem = (page, label = null, disabled = false, active = false) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = `page-item ${disabled ? 'disabled' : ''} ${active ? 'active' : ''}`;
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.className = "page-link";
|
||||||
|
a.href = "#";
|
||||||
|
a.textContent = label || page;
|
||||||
|
if (!disabled && !active) {
|
||||||
|
a.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
state.currentPage = page;
|
||||||
|
utils.saveState();
|
||||||
|
render.updateView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
li.appendChild(a);
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageDots = () => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "page-item disabled";
|
||||||
|
li.innerHTML = `<span class="page-link">...</span>`;
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Botón anterior
|
||||||
|
if (state.currentPage > 1) {
|
||||||
|
DOM.pagination.appendChild(pageItem(state.currentPage - 1, "«"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const startPage = Math.max(1, state.currentPage - Math.floor(CONFIG.maxVisiblePages / 2));
|
||||||
|
const endPage = Math.min(state.totalPages, startPage + CONFIG.maxVisiblePages - 1);
|
||||||
|
|
||||||
|
if (startPage > 1) {
|
||||||
|
DOM.pagination.appendChild(pageItem(1));
|
||||||
|
if (startPage > 2) DOM.pagination.appendChild(pageDots());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
DOM.pagination.appendChild(pageItem(i, null, false, i === state.currentPage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage < state.totalPages) {
|
||||||
|
if (endPage < state.totalPages - 1) DOM.pagination.appendChild(pageDots());
|
||||||
|
DOM.pagination.appendChild(pageItem(state.totalPages));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Botón siguiente
|
||||||
|
if (state.currentPage < state.totalPages) {
|
||||||
|
DOM.pagination.appendChild(pageItem(state.currentPage + 1, "»"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateView: () => {
|
||||||
|
render.posts();
|
||||||
|
render.pagination();
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search Functionality
|
||||||
|
const search = {
|
||||||
|
filterPostsByTerm: (posts, term) => {
|
||||||
|
if (!term) return [...posts];
|
||||||
|
const loweredTerm = term.toLowerCase();
|
||||||
|
return posts.filter(post =>
|
||||||
|
post._autor.includes(loweredTerm) ||
|
||||||
|
post._title.includes(loweredTerm) ||
|
||||||
|
post._preview.includes(loweredTerm)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
filterPosts: term => {
|
||||||
|
const newSearchTerm = term.toLowerCase();
|
||||||
|
// if (newSearchTerm === state.searchTerm) return;
|
||||||
|
if (newSearchTerm === state.searchTerm && newSearchTerm !== "") return;
|
||||||
|
|
||||||
|
state.searchTerm = newSearchTerm;
|
||||||
|
state.filteredPosts = search.filterPostsByTerm(state.allPosts, state.searchTerm);
|
||||||
|
state.currentPage = 1;
|
||||||
|
render.updateView();
|
||||||
|
},
|
||||||
|
|
||||||
|
init: () => {
|
||||||
|
DOM.searchInput.addEventListener("input", utils.debounce(() => {
|
||||||
|
search.filterPosts(DOM.searchInput.value);
|
||||||
|
}, CONFIG.searchDelay));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
const init = {
|
||||||
|
loadPosts: () => {
|
||||||
|
render.showLoading();
|
||||||
|
fetch(CONFIG.url)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('Error en la red');
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
state.allPosts = data.map(utils.normalizePost);
|
||||||
|
utils.loadState();
|
||||||
|
state.filteredPosts = search.filterPostsByTerm(state.allPosts, state.searchTerm);
|
||||||
|
state.totalPages = Math.ceil(state.filteredPosts.length / CONFIG.postsPerPage);
|
||||||
|
state.currentPage = Math.min(state.currentPage, state.totalPages || 1);
|
||||||
|
render.updateView();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
DOM.container.innerHTML = `
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<h4>Error al cargar los posts</h4>
|
||||||
|
<p class="text-muted">Por favor intenta recargar la página</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
render.hideLoading();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
init.loadPosts();
|
||||||
|
search.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// limpiar buscador
|
||||||
|
document.getElementById('clear-btn').addEventListener('click', function () {
|
||||||
|
const input = document.getElementById('search-input');
|
||||||
|
input.value = '';
|
||||||
|
state.searchTerm = ''; // resetear el estado
|
||||||
|
state.filteredPosts = [...state.allPosts]; // mostrar todo de nuevo
|
||||||
|
state.currentPage = 1;
|
||||||
|
render.updateView(); // actualizar la vista
|
||||||
|
localStorage.removeItem("searchState"); // opcional: limpiar localStorage
|
||||||
|
});
|
||||||
|
|
74
static/e_blog/b_posts.js
Normal file
74
static/e_blog/b_posts.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// conteo de visitas
|
||||||
|
|
||||||
|
// #######################################################
|
||||||
|
// REVISAR SI YA SE HA VISTO EL POST EL DÍA DE HOY
|
||||||
|
// #######################################################
|
||||||
|
const postId = window.location.pathname.split("/")[2]; // ID del post desde la URL
|
||||||
|
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
||||||
|
|
||||||
|
// Obtener historial de visitas del localStorage o un objeto vacío
|
||||||
|
let viewedPosts = JSON.parse(localStorage.getItem("url_viewed") || "{}");
|
||||||
|
|
||||||
|
// Revisar si ya fue visto hoy
|
||||||
|
const alreadyViewedToday = viewedPosts[postId] === today;
|
||||||
|
|
||||||
|
if (!alreadyViewedToday) {
|
||||||
|
// Actualizar localStorage
|
||||||
|
viewedPosts[postId] = today;
|
||||||
|
localStorage.setItem("url_viewed", JSON.stringify(viewedPosts));
|
||||||
|
|
||||||
|
// Enviar datos al backend
|
||||||
|
fetch("/blog/get-data", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ post_id: postId }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data.success) {
|
||||||
|
let viewed = document.getElementById("viewed");
|
||||||
|
let n_viewed = parseInt(viewed.innerText) + 1;
|
||||||
|
viewed.innerText = n_viewed;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => console.error("Error al enviar datos:", err));
|
||||||
|
}
|
||||||
|
// else {
|
||||||
|
// console.log("El post ya fue visto hoy");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #######################################################
|
||||||
|
// ACTUALIZAR CONTEO CADA MINUTO
|
||||||
|
// #######################################################
|
||||||
|
|
||||||
|
const cur_id = window.location.pathname.split("/")[2];
|
||||||
|
const viewed = document.getElementById("viewed");
|
||||||
|
|
||||||
|
async function fetchViewCount() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
window.location.origin + "/blog/api/count-post-viewed"
|
||||||
|
);
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
const dict = {};
|
||||||
|
data.forEach((item) => {
|
||||||
|
dict[item.id_post] = item.count;
|
||||||
|
});
|
||||||
|
|
||||||
|
const n_viewed = parseInt(viewed.innerText);
|
||||||
|
if (dict[cur_id] !== n_viewed) {
|
||||||
|
viewed.innerText = dict[cur_id] ?? 0;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error al obtener datos de visitas:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ejecutar de inmediato
|
||||||
|
// fetchViewCount();
|
||||||
|
|
||||||
|
// Luego cada minuto
|
||||||
|
setInterval(fetchViewCount, 30000);
|
@ -1,63 +1,88 @@
|
|||||||
.share {
|
.share-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: inline-block; /* Para que solo ocupe el ancho necesario */
|
||||||
align-items: center;
|
|
||||||
background: #eee;
|
|
||||||
border-radius: 2rem;
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.share:hover {
|
.share-toggle {
|
||||||
width: 18rem;
|
background-color: #eee;
|
||||||
}
|
color: #333;
|
||||||
|
|
||||||
.share__wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share__toggle {
|
|
||||||
background: #549c67;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 2.5rem;
|
width: 40px;
|
||||||
height: 2.5rem;
|
height: 40px;
|
||||||
margin: 0.25rem;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share__button {
|
.share-toggle:hover {
|
||||||
background: #555;
|
background-color: #ddd;
|
||||||
color: white;
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.share-buttons {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 100%; /* Mueve el bloque justo a la derecha del botón toggle */
|
||||||
|
transform: translateY(-50%); /* Centra verticalmente */
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
display: none;
|
||||||
|
flex-direction: row;
|
||||||
|
padding: 8px;
|
||||||
|
z-index: 10;
|
||||||
|
white-space: nowrap; /* Para evitar que se corte si es muy estrecho */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.share-container:hover .share-buttons {
|
||||||
|
display: flex; /* Mostrar al hacer hover en el contenedor */
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin: 0 5px;
|
||||||
|
color: #555;
|
||||||
|
transition: color 0.3s ease;
|
||||||
|
width: 30px; /* Ancho fijo para mantener la forma */
|
||||||
|
height: 30px; /* Alto fijo para mantener la forma */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 2.5rem;
|
}
|
||||||
height: 2.5rem;
|
|
||||||
margin-left: 0.5rem;
|
.share-btn:hover {
|
||||||
display: flex;
|
color: #007bff; /* Un color de ejemplo al hacer hover */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilos específicos para cada red social (opcional) */
|
||||||
|
.share-btn.mail:hover { color: #EA4335; } /* Rojo de Gmail */
|
||||||
|
.share-btn.in:hover { color: #0077B5; } /* Azul de LinkedIn */
|
||||||
|
.share-btn.fb:hover { color: #1877F2; } /* Azul de Facebook */
|
||||||
|
.share-btn.tw:hover { color: #000000; } /* Negro de X (Twitter) */
|
||||||
|
.share-btn.wa:hover { color: #25D366; } /* Verde de WhatsApp */
|
||||||
|
.share-btn.copy:hover { color: #6c757d; } /* Gris para copiar enlace */
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
.share-buttons {
|
||||||
|
flex-direction: column; /* Apilar los botones en pantallas pequeñas */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
left: auto;
|
||||||
opacity: 0;
|
right: 0;
|
||||||
transform: scale(0);
|
transform: translateX(0);
|
||||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mostrar botones solo cuando se hace hover */
|
.share-btn {
|
||||||
.share:hover .share__button {
|
margin: 5px 0;
|
||||||
opacity: 1;
|
}
|
||||||
transform: scale(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fb { background: #1877f2; }
|
|
||||||
.tw { background: #000000; }
|
|
||||||
.in { background: #0077b5; }
|
|
||||||
.copy { background: #444; }
|
|
||||||
.wa { background-color: #25D366; }
|
|
15
static/e_blog/b_share_btn.js
Normal file
15
static/e_blog/b_share_btn.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
// Opcional: Agregar funcionalidad para copiar al portapapeles
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
const copyButton = document.querySelector('.share-btn.copy');
|
||||||
|
if (copyButton) {
|
||||||
|
copyButton.addEventListener('click', function () {
|
||||||
|
const currentUrl = window.location.href;
|
||||||
|
navigator.clipboard.writeText(currentUrl).then(() => {
|
||||||
|
alert('Enlace copiado al portapapeles!');
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error al copiar al portapapeles: ', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
@ -67,5 +67,16 @@ if (btn_wa) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// compartir por email
|
||||||
|
// email
|
||||||
|
const btn_mail = document.querySelector("button.mail");
|
||||||
|
if (btn_mail) {
|
||||||
|
btn_mail.addEventListener("click", () => {
|
||||||
|
const subject = encodeURIComponent("¡Mira este post interesante!");
|
||||||
|
const body = encodeURIComponent(`Te comparto este enlace que creo que te gustará:\n\n${encode_url('mail')}`);
|
||||||
|
const mailUrl = `mailto:?subject=${subject}&body=${body}`;
|
||||||
|
window.location.href = mailUrl;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,38 +1,3 @@
|
|||||||
/* Contenedor general */
|
|
||||||
/* #dbContact_wrapper .row {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1em;
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 1em;
|
|
||||||
border-radius: 8px;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* Sección de "entries per page" */
|
|
||||||
/* #dbContact_wrapper .dt-layout-start,
|
|
||||||
#dbContact_wrapper .dt-layout-end {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* Ajuste a los selects */
|
|
||||||
/* #dbContact_wrapper .dt-length select,
|
|
||||||
#dbContact_wrapper .dt-search input[type="search"] {
|
|
||||||
min-width: 150px;
|
|
||||||
padding: 0.5em;
|
|
||||||
border-radius: 6px;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* Ajustes a los labels */
|
|
||||||
/* #dbContact_wrapper .dt-length label,
|
|
||||||
#dbContact_wrapper .dt-search label {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1em;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5em;
|
|
||||||
} */
|
|
||||||
|
|
||||||
/* ------------------------ */
|
/* ------------------------ */
|
||||||
.dt-length label,
|
.dt-length label,
|
||||||
|
57
static/h_tmp_user/a_home/contact_modal.css
Normal file
57
static/h_tmp_user/a_home/contact_modal.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/* Estilos personalizados para el modal */
|
||||||
|
.modal-contact-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-contact-icon {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #0d6efd; /* Color azul de Bootstrap */
|
||||||
|
min-width: 30px;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-contact-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-contact-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-contact-value {
|
||||||
|
color: #333;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Estilo para el campo de comentarios */
|
||||||
|
.comments-container {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 12px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-contact-item {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-contact-item:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-contact-icon {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-contact-item:hover .modal-contact-icon {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
@ -1,124 +1,144 @@
|
|||||||
|
|
||||||
.pst-cont{
|
|
||||||
width: 65%;
|
|
||||||
min-height: 80%;
|
|
||||||
margin: auto;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.pst-cont {
|
.pst-cont {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 800px;
|
||||||
margin: 2rem auto;
|
margin: 2rem auto;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
overflow: hidden; /* Para contener floats */
|
||||||
|
}
|
||||||
|
|
||||||
& h1 {
|
/* Encabezado */
|
||||||
font-size: 2.5rem;
|
.pst-cont h1 {
|
||||||
margin-bottom: 1rem;
|
font-size: clamp(1.8rem, 5vw, 2.5rem);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
line-height: 1.2;
|
line-height: 1.3;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
border-bottom: 2px solid #f0f0f0;
|
border-bottom: 2px solid #f0f0f0;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
& span {
|
/* Metadatos */
|
||||||
|
.pst-cont > span {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
color: #7f8c8d;
|
color: #6c757d;
|
||||||
font-size: 0.9rem;
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pst-cont span i {
|
||||||
& span i {
|
margin-right: 0.4rem;
|
||||||
margin-right: 0.3rem;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
& img {
|
/* Botón de regreso */
|
||||||
max-width: 100%;
|
.pst-cont .btn-secondary {
|
||||||
height: auto;
|
margin-bottom: 1rem;
|
||||||
border-radius: 4px;
|
display: inline-block;
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
/* Contenido principal */
|
||||||
|
.bd_post {
|
||||||
.pst-cont > div:first-of-type {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilos para el contenido del post */
|
|
||||||
|
|
||||||
.pst-cont > div:last-of-type {
|
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
line-height: 1.8;
|
line-height: 1.8;
|
||||||
|
hyphens: auto;
|
||||||
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pst-cont p {
|
/* Párrafos */
|
||||||
|
.bd_post p {
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
text-justify: inter-word;
|
text-justify: inter-word;
|
||||||
margin-bottom: 1.5rem;
|
margin: 0 0 1.5rem 0;
|
||||||
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pst-cont .note-float-left,
|
/* Imágenes */
|
||||||
.pst-cont .note-float-right {
|
.bd_post img {
|
||||||
margin-top: 0.5em;
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pst-cont .note-float-left {
|
|
||||||
float: left;
|
|
||||||
margin-right: 1.5em;
|
|
||||||
max-width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pst-cont .note-float-right {
|
|
||||||
float: right;
|
|
||||||
|
|
||||||
margin-left: 1.5em;
|
|
||||||
max-width: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pst-cont iframe {
|
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin-left: auto !important;
|
height: auto;
|
||||||
margin-right: auto !important;
|
border-radius: 6px;
|
||||||
border-radius: 4px;
|
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||||
margin: 1.5rem 0;
|
margin: 1.5rem auto;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
|
||||||
|
|
||||||
.pst-cont::after {
|
|
||||||
content: "";
|
|
||||||
display: table;
|
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------- */
|
/* Elementos flotantes */
|
||||||
ul, ol {
|
.bd_post .note-float-left,
|
||||||
/* Asegura que el espacio no afecte la alineación de viñetas/números */
|
.bd_post .note-float-right {
|
||||||
padding-left: 1em; /* Ajusta según tu diseño */
|
margin: 1rem 0;
|
||||||
|
max-width: 100%;
|
||||||
|
float: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
/* Iframes/videos */
|
||||||
text-align: justify; /* Justifica el texto */
|
.bd_post iframe {
|
||||||
margin-bottom: 0.5em; /* Espaciado entre elementos (opcional) */
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 2rem auto;
|
||||||
|
display: block;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
li p, li span {
|
/* Listas */
|
||||||
|
.bd_post ul,
|
||||||
|
.bd_post ol {
|
||||||
|
padding-left: 1.8rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd_post li {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
text-align: justify;
|
text-align: justify;
|
||||||
display: inline-block; /* Necesario para que funcione en elementos en línea */
|
|
||||||
width: 100%; /* Ocupa todo el ancho disponible */
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
text-align: justify;
|
|
||||||
hyphens: auto; /* Permite división de palabras con guiones */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Responsive para pantallas medianas/grandes */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.pst-cont {
|
||||||
|
padding: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd_post .note-float-left {
|
||||||
|
float: left;
|
||||||
|
margin: 1rem 1.5rem 1rem 0;
|
||||||
|
/* max-width: 50%; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.bd_post .note-float-right {
|
||||||
|
float: right;
|
||||||
|
margin: 1rem 0 1rem 1.5rem;
|
||||||
|
/* max-width: 50%; */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive para pantallas pequeñas */
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.pst-cont {
|
||||||
|
width: 95%;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share__wrapper {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.meta-bar {
|
||||||
|
& .btn{
|
||||||
|
font-size: 0.95rem;
|
||||||
|
padding: 0.4em 0.75em;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
28
static/h_tmp_user/f_change_pswd/change_pswd.css
Normal file
28
static/h_tmp_user/f_change_pswd/change_pswd.css
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
main {
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smartphones (hasta 767px) */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
main { min-height: 80dvh; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablets (768px - 1023px) */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
main { min-height: 80dvh; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Laptops (1024px - 1439px) monitores resulición baja */
|
||||||
|
@media (min-width: 1024px) and (max-width: 1439px) {
|
||||||
|
main { min-height: 80dvh; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PCs de escritorio (1440px - 1919px) macbook */
|
||||||
|
@media (min-width: 1440px) and (max-width: 1919px) {
|
||||||
|
main { min-height: 80dvh; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pantallas Ultrawide (1920px en adelante) */
|
||||||
|
@media (min-width: 1920px) {
|
||||||
|
main { min-height: 80dvh; }
|
||||||
|
}
|
@ -5,7 +5,7 @@ let form = document.querySelector("form");
|
|||||||
let img = document.getElementById("img");
|
let img = document.getElementById("img");
|
||||||
|
|
||||||
form.addEventListener("submit", function (e) {
|
form.addEventListener("submit", function (e) {
|
||||||
let allowedExtensions = ['jpg', 'jpeg', 'png', 'mp4'];
|
let allowedExtensions = ['jpg', 'jpeg', 'png', 'mp4', 'avif', 'webp'];
|
||||||
let filePath = img.value;
|
let filePath = img.value;
|
||||||
let extension = filePath.split('.').pop().toLowerCase();
|
let extension = filePath.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
@ -1,42 +1,72 @@
|
|||||||
|
/* Input responsivo */
|
||||||
.form-control {
|
.form-control {
|
||||||
width: 50% !important;
|
width: 90% !important;
|
||||||
margin-bottom: 1em !important;
|
max-width: 800px;
|
||||||
margin-top: 1em !important;
|
margin: 1em auto !important;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Editor Summernote responsivo */
|
||||||
.note-editor {
|
.note-editor {
|
||||||
width: 50% !important;
|
width: 90% !important;
|
||||||
min-height: 70vh !important;
|
max-width: 800px;
|
||||||
|
margin: 2rem auto !important;
|
||||||
|
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Área editable con estilos similares a .bd_post */
|
||||||
.note-editable {
|
.note-editable {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 2rem;
|
||||||
min-height: 65vh !important;
|
min-height: 65vh !important;
|
||||||
|
word-wrap: break-word;
|
||||||
|
hyphens: auto;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
div.note-toolbar{
|
/* Herramientas con fondo fijo */
|
||||||
|
.note-toolbar {
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
padding: 1em !important;
|
padding: 1em !important;
|
||||||
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 999; /* para que esté por encima del contenido */
|
z-index: 999;
|
||||||
padding: 1em !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Fondo del área de edición */
|
||||||
div.note-editing-area {
|
div.note-editing-area {
|
||||||
background-color: white !important;
|
background-color: white !important;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* aplica en la sección de edición del post */
|
/* Botón cancelar */
|
||||||
#btn-cancel {
|
#btn-cancel {
|
||||||
margin-left: 1em !important;
|
margin-left: 1em !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* IMPORTANTE, UNA ANIMACIÓN DE AOS INTEFIERE CON SUMMERNOTE */
|
/* AOS interfiere con modales de Summernote */
|
||||||
div.note-modal-backdrop {
|
div.note-modal-backdrop {
|
||||||
z-index: 1 !important;
|
z-index: 1 !important;
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Responsive editor */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.note-editable {
|
||||||
|
padding: 2.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.note-editor {
|
||||||
|
width: 95% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-editable {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -10,43 +10,9 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<style>
|
<div id="carouselExampleCaptions" class="carousel slide" data-bs-ride="carousel" data-aos="fade-left" data-aos-delay="300" data-aos-duration="800">
|
||||||
@media screen and (orientation: landscape) {
|
|
||||||
.carousel img {
|
|
||||||
height: 80vh;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 class="carousel-indicators">
|
<div class="carousel-indicators">
|
||||||
|
<!-- {# inicio card item slide #} -->
|
||||||
{% for s in data %}
|
{% for s in data %}
|
||||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="{{ loop.index0 }}"
|
<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' }}"
|
class="{% if loop.first %}active{% endif %}" aria-current="{{ 'true' if loop.first else 'false' }}"
|
||||||
@ -54,7 +20,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div class="carousel-inner">
|
<div class="carousel-inner">
|
||||||
|
<!-- {# inicio card item slide #} -->
|
||||||
{% for s in data %}
|
{% for s in data %}
|
||||||
<div class="carousel-item {% if loop.first %}active{% endif %}">
|
<div class="carousel-item {% if loop.first %}active{% endif %}">
|
||||||
{% if s[1].endswith('.mp4') %}
|
{% if s[1].endswith('.mp4') %}
|
||||||
@ -68,11 +34,14 @@
|
|||||||
<div class="carousel-caption centered">
|
<div class="carousel-caption centered">
|
||||||
<div class="caption-content" style="background-color: {{ s[2] }}; color: {{ s[3] }};">
|
<div class="caption-content" style="background-color: {{ s[2] }}; color: {{ s[3] }};">
|
||||||
<h2>{{ s[4] }}</h2>
|
<h2>{{ s[4] }}</h2>
|
||||||
|
{% if s[6] is not none %}
|
||||||
|
<a href="{{ s[6] }}" type="button" class="btn btn-success" target="{% if s[7] %}_blank{% endif %}">ver más</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<!-- {# fin card item slide #} -->
|
||||||
</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>
|
||||||
@ -86,3 +55,13 @@
|
|||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
|
||||||
|
|
||||||
|
{% include 'z_comps/if_flash.html' %}
|
||||||
|
|
||||||
|
<!-- {# aos script #} -->
|
||||||
|
{% include 'z_comps/aos_script.html' %}
|
||||||
|
|
||||||
|
{% endblock js %}
|
||||||
|
@ -210,8 +210,6 @@
|
|||||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js"></script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- {# aos script #} -->
|
<!-- {# aos script #} -->
|
||||||
{% include 'z_comps/aos_script.html' %}
|
{% include 'z_comps/aos_script.html' %}
|
||||||
|
|
||||||
|
@ -1,132 +0,0 @@
|
|||||||
{% extends 'template.html' %}
|
|
||||||
|
|
||||||
{% block css %}
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='e_blog/a_all_posts.css') }}">
|
|
||||||
{% endblock css %}
|
|
||||||
|
|
||||||
{% block navbar %}
|
|
||||||
{% include 'z_comps/navbar.html' %}
|
|
||||||
{% endblock navbar %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<form method="get" class="mb-4">
|
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" name="q" placeholder="Buscar título, autor o contenido..." value="{{ search }}">
|
|
||||||
<button class="btn btn-outline-primary" type="submit">
|
|
||||||
<i class="bi bi-search"></i> Buscar
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
<!-- {# i pagination #} -->
|
|
||||||
<nav aria-label="Page navigation">
|
|
||||||
<ul class="pagination justify-content-center mt-4">
|
|
||||||
|
|
||||||
<!-- Anterior -->
|
|
||||||
<li class="page-item {% if current_page <= 1 %}disabled{% endif %}">
|
|
||||||
<a class="page-link" href="?page={{ current_page - 1 }}{% if search %}&q={{ search }}{% endif %}">Anterior</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Páginas -->
|
|
||||||
{% for page_num in range(1, total_pages + 1) %}
|
|
||||||
<li class="page-item {% if page_num == current_page %}active{% endif %}">
|
|
||||||
<a class="page-link" href="?page={{ page_num }}{% if search %}&q={{ search }}{% endif %}">
|
|
||||||
{{ page_num }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Siguiente -->
|
|
||||||
<li class="page-item {% if current_page >= total_pages %}disabled{% endif %}">
|
|
||||||
<a class="page-link" href="?page={{ current_page + 1 }}{% if search %}&q={{ search }}{% endif %}">Siguiente</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<!-- {# f pagination #} -->
|
|
||||||
|
|
||||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4" id="card-container" data-aos="fade" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
|
|
||||||
|
|
||||||
<!-- {# adaptación #} -->
|
|
||||||
{% for post in data %}
|
|
||||||
<div class="col card-wrapper">
|
|
||||||
<div class="card h-100">
|
|
||||||
<!-- {# img #} -->
|
|
||||||
<img src="{{ post[7] if post[7] else url_for('static', filename='y_img/other/no_img.png') }}"
|
|
||||||
class="card-img-top" alt="card image">
|
|
||||||
|
|
||||||
<div class="card-body d-flex flex-column">
|
|
||||||
<div class="mb-3">
|
|
||||||
<!-- {# título #} -->
|
|
||||||
<!-- <h5 class="card-title">{{ post[5] }}</h5> -->
|
|
||||||
<a href="{{ url_for('blog_post', post_id = post[0] ) }}" class="btn btn-info"> <h5>{{post[5]}}</h5> </a> <br>
|
|
||||||
<small class="text-muted">
|
|
||||||
<!-- {# autor #} -->
|
|
||||||
<i class="bi bi-file-person-fill"></i> {{ post[1] }} {{ post[2] }} <br>
|
|
||||||
<!-- {# fecha creación #} -->
|
|
||||||
<i class="bi bi-calendar-week"></i> {{ post[3] }}<br>
|
|
||||||
<!-- {# if fecha actualización #} -->
|
|
||||||
{% if post[4] is not none %}
|
|
||||||
<i class="bi bi-arrow-repeat"></i> {{ post[4] }}<br>
|
|
||||||
{% endif %}
|
|
||||||
<!-- {# tiempo lectura #} -->
|
|
||||||
<i class="bi bi-clock"></i> {{ post[8] }} min. <br>
|
|
||||||
<i class="bi bi-eye"></i>
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<!-- {# breve resumen #} -->
|
|
||||||
<p class="card-text">{{ post[6] }}...</p>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- <div class="mt-auto"></div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- {# i pagination #} -->
|
|
||||||
<nav aria-label="Page navigation">
|
|
||||||
<ul class="pagination justify-content-center mt-4">
|
|
||||||
|
|
||||||
<!-- Anterior -->
|
|
||||||
<li class="page-item {% if current_page <= 1 %}disabled{% endif %}">
|
|
||||||
<a class="page-link" href="?page={{ current_page - 1 }}{% if search %}&q={{ search }}{% endif %}">Anterior</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<!-- Páginas -->
|
|
||||||
{% for page_num in range(1, total_pages + 1) %}
|
|
||||||
<li class="page-item {% if page_num == current_page %}active{% endif %}">
|
|
||||||
<a class="page-link" href="?page={{ page_num }}{% if search %}&q={{ search }}{% endif %}">
|
|
||||||
{{ page_num }}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Siguiente -->
|
|
||||||
<li class="page-item {% if current_page >= total_pages %}disabled{% endif %}">
|
|
||||||
<a class="page-link" href="?page={{ current_page + 1 }}{% if search %}&q={{ search }}{% endif %}">Siguiente</a>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
<!-- {# f pagination #} -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock body %}
|
|
||||||
|
|
||||||
{% block js %}
|
|
||||||
|
|
||||||
<!-- {# aos script #} -->
|
|
||||||
{% include 'z_comps/aos_script.html' %}
|
|
||||||
|
|
||||||
{% endblock js %}
|
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='e_blog/a_all_posts.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='e_blog/a_all_posts.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='e_blog/0_all_posts_main.css') }}">
|
||||||
{% endblock css %}
|
{% endblock css %}
|
||||||
|
|
||||||
{% block navbar %}
|
{% block navbar %}
|
||||||
@ -10,36 +11,6 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.card-img-top {
|
|
||||||
max-height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-img-top:hover {
|
|
||||||
transform: scale(1.02);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-wrapper {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-wrapper.visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading-indicator {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
<div class="container my-3">
|
<div class="container my-3">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<!-- <input id="searchInput" type="text" autocomplete="off" /> -->
|
<!-- <input id="searchInput" type="text" autocomplete="off" /> -->
|
||||||
@ -55,16 +26,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
// limpiar buscador
|
|
||||||
document.getElementById('clear-btn').addEventListener('click', function () {
|
|
||||||
const input = document.getElementById('search-input');
|
|
||||||
input.value = '';
|
|
||||||
input.dispatchEvent(new Event('input')); // Por si tienes un listener que filtra en tiempo real
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
<div id="loading-indicator" class="text-center my-5">
|
<div id="loading-indicator" class="text-center my-5">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
<span class="visually-hidden">Cargando...</span>
|
<span class="visually-hidden">Cargando...</span>
|
||||||
@ -78,279 +39,17 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const DOM = {
|
|
||||||
container: document.getElementById("card-container"),
|
|
||||||
pagination: document.getElementById("pagination"),
|
|
||||||
searchInput: document.getElementById("search-input"),
|
|
||||||
loading: document.getElementById("loading-indicator"),
|
|
||||||
resultCount: document.getElementById("result-count")
|
|
||||||
};
|
|
||||||
|
|
||||||
const CONFIG = {
|
|
||||||
url: window.location.href.replace(/\/$/, "") + "/api/posts",
|
|
||||||
postsPerPage: 12,
|
|
||||||
maxVisiblePages: 5,
|
|
||||||
searchDelay: 300
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
allPosts: [],
|
|
||||||
filteredPosts: [],
|
|
||||||
currentPage: 1,
|
|
||||||
totalPages: 1,
|
|
||||||
searchTerm: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
const utils = {
|
|
||||||
debounce: (func, delay) => {
|
|
||||||
let timeout;
|
|
||||||
return (...args) => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => func.apply(this, args), delay);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
normalizePost: post => ({
|
|
||||||
...post,
|
|
||||||
_autor: post.autor.toLowerCase(),
|
|
||||||
_title: post.title.toLowerCase(),
|
|
||||||
_preview: post.preview.toLowerCase()
|
|
||||||
}),
|
|
||||||
|
|
||||||
saveState: () => {
|
|
||||||
localStorage.setItem("lastPage", state.currentPage);
|
|
||||||
localStorage.setItem("lastSearch", state.searchTerm);
|
|
||||||
},
|
|
||||||
|
|
||||||
animateCards: () => {
|
|
||||||
const cards = document.querySelectorAll('.card-wrapper');
|
|
||||||
cards.forEach((card, index) => {
|
|
||||||
setTimeout(() => card.classList.add('visible'), index * 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const render = {
|
|
||||||
showLoading: () => {
|
|
||||||
DOM.loading.style.display = 'block';
|
|
||||||
DOM.container.style.opacity = '0.5';
|
|
||||||
},
|
|
||||||
|
|
||||||
hideLoading: () => {
|
|
||||||
DOM.loading.style.display = 'none';
|
|
||||||
DOM.container.style.opacity = '1';
|
|
||||||
},
|
|
||||||
|
|
||||||
posts: () => {
|
|
||||||
DOM.container.innerHTML = "";
|
|
||||||
|
|
||||||
const start = (state.currentPage - 1) * CONFIG.postsPerPage;
|
|
||||||
const end = start + CONFIG.postsPerPage;
|
|
||||||
const postsToShow = state.filteredPosts.slice(start, end);
|
|
||||||
|
|
||||||
if (postsToShow.length === 0) {
|
|
||||||
DOM.container.innerHTML = `
|
|
||||||
<div class="col-12 text-center py-5">
|
|
||||||
<h4>No se encontraron resultados</h4>
|
|
||||||
<p class="text-muted">Intenta con otros términos de búsqueda</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
postsToShow.forEach(post => {
|
|
||||||
const imgSrc = post.first_img || "{{ url_for('static', filename='y_img/other/no_img.png') }}";
|
|
||||||
const dateUpdate = post.author_updated ? `<i class="bi bi-arrow-repeat"></i> ${post.author_updated}<br>` : "";
|
|
||||||
|
|
||||||
const card = document.createElement("div");
|
|
||||||
card.className = "col card-wrapper";
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="card h-100">
|
|
||||||
<img src="${imgSrc}" class="card-img-top" alt="${post.title}" loading="lazy">
|
|
||||||
<div class="card-body d-flex flex-column">
|
|
||||||
<div class="mb-3">
|
|
||||||
<a href="/blog/${post.id}" class="btn btn-info stretched-link" onclick="utils.saveState()">
|
|
||||||
<h5 class="card-title">${post.title}</h5>
|
|
||||||
</a>
|
|
||||||
<small class="text-muted d-block mt-2">
|
|
||||||
<i class="bi bi-file-person-fill"></i> ${post.autor}<br>
|
|
||||||
<i class="bi bi-calendar-week"></i> ${post.author_creation}<br>
|
|
||||||
${dateUpdate}
|
|
||||||
<i class="bi bi-clock"></i> ${post.read_time_min} min.
|
|
||||||
</small>
|
|
||||||
<p class="card-text mt-2">${post.preview}...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
DOM.container.appendChild(card);
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.animateCards();
|
|
||||||
},
|
|
||||||
|
|
||||||
pagination: () => {
|
|
||||||
DOM.pagination.innerHTML = "";
|
|
||||||
state.totalPages = Math.ceil(state.filteredPosts.length / CONFIG.postsPerPage);
|
|
||||||
DOM.resultCount.textContent = state.filteredPosts.length;
|
|
||||||
|
|
||||||
const pageItem = (page, label = null, disabled = false, active = false) => {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.className = `page-item ${disabled ? 'disabled' : ''} ${active ? 'active' : ''}`;
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.className = "page-link";
|
|
||||||
a.href = "#";
|
|
||||||
a.textContent = label || page;
|
|
||||||
if (!disabled && !active) {
|
|
||||||
a.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
state.currentPage = page;
|
|
||||||
utils.saveState();
|
|
||||||
render.updateView();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
li.appendChild(a);
|
|
||||||
return li;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageDots = () => {
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.className = "page-item disabled";
|
|
||||||
li.innerHTML = `<span class="page-link">...</span>`;
|
|
||||||
return li;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Botón anterior
|
|
||||||
DOM.pagination.appendChild(pageItem(state.currentPage - 1, "«", state.currentPage === 1));
|
|
||||||
|
|
||||||
const startPage = Math.max(1, state.currentPage - Math.floor(CONFIG.maxVisiblePages / 2));
|
|
||||||
const endPage = Math.min(state.totalPages, startPage + CONFIG.maxVisiblePages - 1);
|
|
||||||
|
|
||||||
if (startPage > 1) {
|
|
||||||
DOM.pagination.appendChild(pageItem(1));
|
|
||||||
if (startPage > 2) DOM.pagination.appendChild(pageDots());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = startPage; i <= endPage; i++) {
|
|
||||||
DOM.pagination.appendChild(pageItem(i, null, false, i === state.currentPage));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endPage < state.totalPages) {
|
|
||||||
if (endPage < state.totalPages - 1) DOM.pagination.appendChild(pageDots());
|
|
||||||
DOM.pagination.appendChild(pageItem(state.totalPages));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Botón siguiente
|
|
||||||
DOM.pagination.appendChild(pageItem(state.currentPage + 1, "»", state.currentPage === state.totalPages));
|
|
||||||
},
|
|
||||||
|
|
||||||
updateView: () => {
|
|
||||||
render.posts();
|
|
||||||
render.pagination();
|
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const search = {
|
|
||||||
filterPosts: term => {
|
|
||||||
const newSearchTerm = term.toLowerCase(); // convertir el término a minúsculas
|
|
||||||
const isSameSearch = newSearchTerm === state.searchTerm;
|
|
||||||
state.searchTerm = newSearchTerm;
|
|
||||||
|
|
||||||
if (!state.searchTerm) {
|
|
||||||
state.filteredPosts = [...state.allPosts];
|
|
||||||
} else {
|
|
||||||
state.filteredPosts = state.allPosts.filter(post =>
|
|
||||||
post._autor.toLowerCase().includes(state.searchTerm) ||
|
|
||||||
post._title.toLowerCase().includes(state.searchTerm) ||
|
|
||||||
post._preview.toLowerCase().includes(state.searchTerm)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isSameSearch) {
|
|
||||||
state.currentPage = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
render.updateView();
|
|
||||||
},
|
|
||||||
|
|
||||||
init: () => {
|
|
||||||
DOM.searchInput.addEventListener("input", utils.debounce(() => {
|
|
||||||
search.filterPosts(DOM.searchInput.value);
|
|
||||||
}, CONFIG.searchDelay));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const init = {
|
|
||||||
loadPosts: () => {
|
|
||||||
render.showLoading();
|
|
||||||
fetch(CONFIG.url)
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) throw new Error('Error en la red');
|
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
state.allPosts = data.map(utils.normalizePost);
|
|
||||||
|
|
||||||
const lastSearch = localStorage.getItem("lastSearch") || "";
|
|
||||||
DOM.searchInput.value = lastSearch;
|
|
||||||
state.searchTerm = lastSearch.toLowerCase();
|
|
||||||
|
|
||||||
if (!state.searchTerm) {
|
|
||||||
state.filteredPosts = [...state.allPosts];
|
|
||||||
} else {
|
|
||||||
state.filteredPosts = state.allPosts.filter(post =>
|
|
||||||
post._autor.includes(state.searchTerm) ||
|
|
||||||
post._title.includes(state.searchTerm) ||
|
|
||||||
post._preview.includes(state.searchTerm)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const lastPage = parseInt(localStorage.getItem("lastPage")) || 1;
|
|
||||||
state.totalPages = Math.ceil(state.filteredPosts.length / CONFIG.postsPerPage);
|
|
||||||
state.currentPage = Math.min(lastPage, state.totalPages || 1);
|
|
||||||
|
|
||||||
render.updateView();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error:', error);
|
|
||||||
DOM.container.innerHTML = `
|
|
||||||
<div class="col-12 text-center py-5">
|
|
||||||
<h4>Error al cargar los posts</h4>
|
|
||||||
<p class="text-muted">Por favor intenta recargar la página</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
render.hideLoading();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
init.loadPosts();
|
|
||||||
search.init();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|
||||||
|
{% include 'z_comps/if_flash.html' %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- {# aos script #} -->
|
<!-- {# aos script #} -->
|
||||||
{% include 'z_comps/aos_script.html' %}
|
{% include 'z_comps/aos_script.html' %}
|
||||||
|
|
||||||
|
<!-- {# vaciar info api en tarjetas #} -->
|
||||||
|
<script src="{{url_for( 'static', filename='e_blog/a_all_posts_visit.js' )}}"></script>
|
||||||
|
|
||||||
|
|
||||||
{% endblock js %}
|
{% endblock js %}
|
@ -12,59 +12,56 @@
|
|||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="pst-cont" data-aos="fade-left" data-aos-delay="300" data-aos-duration="800">
|
||||||
<div class="pst-cont">
|
|
||||||
<h1>{{data[5]}}</h1>
|
<h1>{{data[5]}}</h1>
|
||||||
|
<!-- {# volver, contenedor datos autor, creación publicación, fecha actualización, tiempo lectura, vistas, btn_share_all_rrss #} -->
|
||||||
|
|
||||||
<spam>
|
<div class="meta-bar">
|
||||||
<a type="button" class="btn btn-secondary" href="{{ url_for( 'blog' ) }}"><i class="bi bi-arrow-left" ></i> Publicaciones</a> <br>
|
<a type="button" class="btn btn-light" href="{{ url_for('blog') }}">
|
||||||
<i class="bi bi-person-circle"></i> {{data[0]}} {{data[1]}} |
|
<i class="bi bi-arrow-left"></i> Volver
|
||||||
<i class="bi bi-pencil-square"></i> {{data[2]}} |
|
</a>
|
||||||
|
|
||||||
|
<div class="meta-info">
|
||||||
|
<span class="meta-item"><i class="bi bi-person-circle"></i> {{data[0]}} {{data[1]}}</span> <br>
|
||||||
|
<span class="meta-item"><i class="bi bi-pencil-square"></i> {{data[2]}}</span>
|
||||||
{% if data[3] is not none %}
|
{% if data[3] is not none %}
|
||||||
<i class="bi bi-arrow-repeat"></i> {{data[3]}} |
|
<i class="bi bi-chevron-compact-right"></i> <span class="meta-item"><i class="bi bi-arrow-repeat"></i> {{data[3]}}</span>
|
||||||
{% endif %}
|
{% endif %} <br>
|
||||||
<i class="bi bi-clock-history"></i> {{ data[4] }} Minutos |
|
<span class="meta-item"><i class="bi bi-clock-history"></i> {{data[4]}} Min(s) lectura.</span> <br>
|
||||||
<i class="bi bi-eye"></i> {{ data[7] }}
|
<span class="meta-item"><i class="bi bi-eye"></i> <span id="viewed">{{data[7]}}</span></span>
|
||||||
</spam>
|
</div>
|
||||||
|
|
||||||
<div class="share">
|
<div class="share-container">
|
||||||
<div class="share__wrapper">
|
<div class="share-toggle" aria-label="Compartir">
|
||||||
<div class="share__toggle"><i class="bi bi-share-fill"></i></div>
|
<i class="bi bi-share-fill"></i>
|
||||||
<button href="#" class="share__button fb"><i class="bi bi-facebook"></i></button>
|
</div>
|
||||||
<button href="#" class="share__button tw"><i class="bi bi-twitter-x"></i></button>
|
<div class="share-buttons">
|
||||||
<button class="share__button in"><i class="bi bi-linkedin"></i></button>
|
<button class="share-btn mail" aria-label="Compartir por email"><i class="bi bi-envelope-at-fill"></i></button>
|
||||||
<button class="share__button copy"><i class="bi bi-link-45deg"></i></button>
|
<button class="share-btn in" aria-label="Compartir en LinkedIn"><i class="bi bi-linkedin"></i></button>
|
||||||
<button class="share__button wa"><i class="bi bi-whatsapp"></i></button>
|
<button class="share-btn fb" aria-label="Compartir en Facebook"><i class="bi bi-facebook"></i></button>
|
||||||
|
<button class="share-btn tw" aria-label="Compartir en Twitter"><i class="bi bi-twitter-x"></i></button>
|
||||||
|
<button class="share-btn wa" aria-label="Compartir en WhatsApp"><i class="bi bi-whatsapp"></i></button>
|
||||||
|
<button class="share-btn copy" aria-label="Copiar enlace"><i class="bi bi-link-45deg"></i></button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bd_post">
|
<div class="bd_post">
|
||||||
|
|
||||||
{{data[6] | safe}}
|
{{data[6] | safe}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.querySelectorAll('.share__toggle').forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
btn.closest('.share').classList.toggle('open');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
{% include "z_comps/read_progress.html" %}
|
|
||||||
|
|
||||||
|
<!-- {# aos script #} -->
|
||||||
|
{% include 'z_comps/aos_script.html' %}
|
||||||
|
|
||||||
|
{% include "z_comps/read_progress.html" %}
|
||||||
<script type="module" src="{{ url_for('static', filename='e_blog/copy_url.js') }}"></script>
|
<script type="module" src="{{ url_for('static', filename='e_blog/copy_url.js') }}"></script>
|
||||||
|
|
||||||
|
<script src="{{ url_for('static', filename='e_blog/b_posts.js') }}"></script>
|
||||||
<!-- {# flecha ir hasta arriba #} -->
|
<!-- {# flecha ir hasta arriba #} -->
|
||||||
{% include 'z_comps/arrow_to_up.html' %}
|
{% include 'z_comps/arrow_to_up.html' %}
|
||||||
|
|
||||||
{% endblock js %}
|
{% endblock js %}
|
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='h_tmp_user/a_home/a_home.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='h_tmp_user/a_home/a_home.css') }}">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='h_tmp_user/a_home/contact_modal.css') }}">
|
||||||
|
|
||||||
<!-- {# Librería de aos.js [animaciones] #} -->
|
<!-- {# Librería de aos.js [animaciones] #} -->
|
||||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||||
|
|
||||||
@ -74,66 +76,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Estilos personalizados para el modal */
|
|
||||||
.modal-contact-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-contact-icon {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: #0d6efd; /* Color azul de Bootstrap */
|
|
||||||
min-width: 30px;
|
|
||||||
text-align: center;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-contact-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-contact-label {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #555;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-contact-value {
|
|
||||||
color: #333;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Estilo para el campo de comentarios */
|
|
||||||
.comments-container {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-top: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-contact-item {
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-contact-item:hover {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-contact-icon {
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-contact-item:hover .modal-contact-icon {
|
|
||||||
transform: scale(1.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div class="modal fade" id="modalContactData" tabindex="-1" aria-labelledby="modalContactDataLabel" aria-hidden="true">
|
<div class="modal fade" id="modalContactData" tabindex="-1" aria-labelledby="modalContactDataLabel" aria-hidden="true">
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div>
|
<div>
|
||||||
<h1>Crear una publicación</h1>
|
<!-- <h1>Crear una publicación</h1> -->
|
||||||
|
|
||||||
<form id="post-form" data-aos="fade-right" data-aos-delay="0" data-aos-duration="800">
|
<form id="post-form" data-aos="fade-right" data-aos-delay="0" data-aos-duration="800">
|
||||||
<input type="text" name="title" class="form-control" placeholder="Título de la publicación" maxlength="100">
|
<input type="text" name="title" class="form-control" placeholder="Título de la publicación" maxlength="100">
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 class="display-5 mb-4">Mis Publicaciones</h2>
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|
||||||
<!-- {# i pagination #} -->
|
<!-- {# i pagination #} -->
|
||||||
@ -119,7 +119,7 @@
|
|||||||
<!-- {# f pagination #} -->
|
<!-- {# f pagination #} -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
@ -1,68 +1,50 @@
|
|||||||
{% extends 'h_tmp_usr/z_tmp.html' %}
|
{% extends 'h_tmp_usr/z_tmp.html' %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
|
|
||||||
<link rel="stylesheet" href="{{ url_for( 'static', filename='h_tmp_user/d_read_post/read_post.css' ) }}">
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='h_tmp_user/d_read_post/read_post.css' ) }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='h_tmp_user/d_read_post/read_post.css' ) }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='e_blog/b_share_btn.css' ) }}">
|
||||||
{% endblock css %}
|
{% endblock css %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.video-responsive {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
padding-bottom: 56.25%; /* 16:9 ratio */
|
|
||||||
height: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-responsive iframe {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<div class="pst-cont" data-aos="fade-left" data-aos-delay="300" data-aos-duration="800">
|
||||||
|
|
||||||
<div class="pst-cont">
|
|
||||||
<h1>{{data[2]}}</h1>
|
<h1>{{data[2]}}</h1>
|
||||||
<span>
|
<!-- {# volver, contenedor datos autor, creación publicación, fecha actualización, tiempo lectura, vistas, btn_share_all_rrss #} -->
|
||||||
<i class="bi bi-calendar-week"></i> {{data[0]}}
|
|
||||||
{% if data[1] is not none %} | <i class="bi bi-arrow-repeat"></i> {{data[1]}}{% endif %}
|
<div class="meta-bar">
|
||||||
| <i class="bi bi-clock-fill"></i> {{time_read}}
|
|
||||||
</span>
|
|
||||||
<div>
|
<div>
|
||||||
<a type="button" class="btn btn-info" href="{{ url_for('my_posts') }}"><i class="bi bi-arrow-left"></i> Mis Publicaciones.</a>
|
<a type="button" class="btn btn-secondary" href="{{ url_for('my_posts') }}">
|
||||||
<a type="button" class="btn btn-secondary" href="{{ url_for('edit_post', id_post= post_id ) }}"><i class="bi bi-vector-pen"></i> Editar.</a>
|
<i class="bi bi-arrow-left"> Regresar</i>
|
||||||
|
</a>
|
||||||
|
<a type="button" class="btn btn-secondary" href="{{ url_for('edit_post', id_post= post_id ) }}">
|
||||||
|
<i class="bi bi-vector-pen"> Editar</i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="meta-info">
|
||||||
|
<i class="bi bi-calendar-week"></i> {{data[0]}} |
|
||||||
|
{% if data[1] is not none %}<i class="bi bi-arrow-repeat"></i> {{data[1]}} | {% endif %}
|
||||||
|
<i class="bi bi-clock-fill"></i> {{time_read}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bd-post">
|
</div>
|
||||||
|
|
||||||
|
<div class="bd_post">
|
||||||
{{data[3] | safe}}
|
{{data[3] | safe}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const iframes = document.querySelectorAll("iframe.note-video-clip");
|
|
||||||
iframes.forEach(iframe => {
|
|
||||||
const wrapper = document.createElement("div");
|
|
||||||
wrapper.className = "video-responsive";
|
|
||||||
iframe.parentNode.insertBefore(wrapper, iframe);
|
|
||||||
wrapper.appendChild(iframe);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
@ -72,8 +54,9 @@
|
|||||||
{% include 'z_comps/arrow_to_up.html' %}
|
{% include 'z_comps/arrow_to_up.html' %}
|
||||||
|
|
||||||
|
|
||||||
|
<!-- {# aos script #} -->
|
||||||
|
{% include 'z_comps/aos_script.html' %}
|
||||||
|
|
||||||
|
{% include "z_comps/read_progress.html" %}
|
||||||
|
|
||||||
|
|
||||||
{% endblock js %}
|
{% endblock js %}
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<h2 class="display-5 mb-4">Editar Publicación</h2>
|
<!-- <h2 class="display-5 mb-4">Editar Publicación</h2> -->
|
||||||
|
|
||||||
<form id="post-form" data-aos="fade-right" data-aos-delay="0" data-aos-duration="800">
|
<form id="post-form" data-aos="fade-right" data-aos-delay="0" data-aos-duration="800">
|
||||||
<input type="text" name="title" class="form-control" placeholder="Título de la publicación">
|
<input type="text" name="title" class="form-control" placeholder="Título de la publicación">
|
||||||
|
@ -3,20 +3,12 @@
|
|||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='f_contact/form.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='f_contact/form.css') }}">
|
||||||
<link rel="stylesheet" href="{{ url_for( 'static', filename='g_login/see_hide_pswd.css' ) }}">
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='g_login/see_hide_pswd.css' ) }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='h_tmp_user/f_change_pswd/change_pswd.css' ) }}">
|
||||||
{% endblock css %}
|
{% endblock css %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<style>
|
|
||||||
main{
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<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-container" data-aos="fade-down" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
|
||||||
<div class="form-header">
|
<div class="form-header">
|
||||||
<h2>Cambio de Contraseña</h2>
|
<h2>Cambio de Contraseña</h2>
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1>Métricas del Sitio</h1>
|
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row g-4" data-aos="fade-right" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
|
<div class="row g-4" data-aos="fade-right" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
|
||||||
@ -53,6 +52,16 @@
|
|||||||
<canvas id="dbStatus"></canvas>
|
<canvas id="dbStatus"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<div class="p-3 bg-light rounded shadow-sm">
|
||||||
|
<canvas id="dbTopTenPosts"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-sm-6">
|
||||||
|
<div class="p-3 bg-light rounded shadow-sm">
|
||||||
|
<canvas id="dbTop3Autores"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -397,6 +406,109 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// dbTopTenPosts
|
||||||
|
let top_ten = data.top_ten;
|
||||||
|
let lbl_top_ten = top_ten.map(e => `${e[0]}`);
|
||||||
|
let val_top_ten = top_ten.map(e => e[1]);
|
||||||
|
let colors_top_ten = top_ten.map((_, i) => getColor(i));
|
||||||
|
let ctx7 = document.getElementById("dbTopTenPosts");
|
||||||
|
|
||||||
|
new Chart(ctx7, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: lbl_top_ten,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Visto',
|
||||||
|
data: val_top_ten,
|
||||||
|
backgroundColor: colors_top_ten,
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'x',
|
||||||
|
elements: {
|
||||||
|
bar: {
|
||||||
|
borderWidth: 2,
|
||||||
|
cursor: 'pointer' // Cursor de pointer para indicar interactividad
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responsive: true,
|
||||||
|
onClick: (evt, elements) => { // Moved to root options
|
||||||
|
if (elements.length > 0) {
|
||||||
|
const index = elements[0].index;
|
||||||
|
const id = top_ten[index][0];
|
||||||
|
let dinamic_url = window.location.origin + `/blog/${id}`;
|
||||||
|
window.open(dinamic_url, '_blank');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function (tooltipItem) {
|
||||||
|
return tooltipItem.dataset.label + ': ' + tooltipItem.raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Top 10 de posts más vistos'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// dbTop3Autores dbTop3Autores
|
||||||
|
let top_3 = data.top_three_authors;
|
||||||
|
console.log(top_3);
|
||||||
|
let lbl_top_3 = top_3.map(e => `${e[0]}`);
|
||||||
|
let val_top_3 = top_3.map(e => e[1]);
|
||||||
|
let colors_top_3 = top_3.map((_, i) => getColor(i));
|
||||||
|
let ctx8 = document.getElementById("dbTop3Autores");
|
||||||
|
new Chart(ctx8, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: lbl_top_3,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Visto',
|
||||||
|
data: val_top_3,
|
||||||
|
backgroundColor: colors_top_3,
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'x',
|
||||||
|
elements: {
|
||||||
|
bar: {
|
||||||
|
borderWidth: 2,
|
||||||
|
cursor: 'pointer' // Cursor de pointer para indicar interactividad
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function (tooltipItem) {
|
||||||
|
return tooltipItem.dataset.label + ': ' + tooltipItem.raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Top 3 de autores más vistos'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
@ -78,6 +78,11 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
<ul class="nav nav-tabs" id="myTab" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<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>
|
<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>
|
||||||
@ -98,18 +103,22 @@
|
|||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
<th>Creado</th>
|
<th>Creado</th>
|
||||||
<th>Archivo</th>
|
<th>Archivo</th>
|
||||||
|
<th>New Tab</th>
|
||||||
<th>Texto</th>
|
<th>Texto</th>
|
||||||
<th>Acciones</th>
|
<th>Acciones</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for ele in data %}
|
{% for ele in data %}
|
||||||
<tr id="{{ ele[0] }}">
|
<tr data-id="{{ ele[0] }}">
|
||||||
<td>{{ loop.index }}</td>
|
<td data-label="ID">{{ loop.index }}</td>
|
||||||
<td>{{ ele[5] }}</td>
|
<td data-label="Creado">{{ ele[5] }}</td>
|
||||||
<td>{{ ele[1] }}</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 style="background-color: {{ ele[2] }}; color: {{ ele[3] }};">{{ ele[4] }}</td>
|
</td>
|
||||||
<td >
|
<td data-label="Acciones">
|
||||||
<div class="field_btns">
|
<div class="field_btns">
|
||||||
<a href="{{ url_for('download_file', filename = ele[1] ) }}" class="btn btn-dark" style="color: white;">
|
<a href="{{ url_for('download_file', filename = ele[1] ) }}" class="btn btn-dark" style="color: white;">
|
||||||
<i class="bi bi-download"></i>
|
<i class="bi bi-download"></i>
|
||||||
@ -122,8 +131,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
<!-- {# f tabla slides #} -->
|
<!-- {# f tabla slides #} -->
|
||||||
<!-- {# --------------------------------------------------------------- #} -->
|
<!-- {# --------------------------------------------------------------- #} -->
|
||||||
</div>
|
</div>
|
||||||
@ -162,6 +173,21 @@
|
|||||||
{{ form.txt.label() }}
|
{{ form.txt.label() }}
|
||||||
{{ form.txt( class_="form-control" ) }}
|
{{ form.txt( class_="form-control" ) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- {# url #} -->
|
||||||
|
<div class="form-row">
|
||||||
|
{{ form.url.label() }}
|
||||||
|
{{ form.url( class_="form-control" ) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- {# isNewTab #} -->
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<div class="form-row">
|
||||||
|
{{ form.isNewTab( class_="form-check-input", type="checkbox" ) }}
|
||||||
|
{{ form.isNewTab.label() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<i class="bi bi-database-fill-up"></i> Enviar Datos
|
<i class="bi bi-database-fill-up"></i> Enviar Datos
|
||||||
</button>
|
</button>
|
||||||
@ -198,6 +224,9 @@
|
|||||||
<div class="carousel-caption centered">
|
<div class="carousel-caption centered">
|
||||||
<div class="caption-content" style="background-color: {{ s[2] }}; color: {{ s[3] }};">
|
<div class="caption-content" style="background-color: {{ s[2] }}; color: {{ s[3] }};">
|
||||||
<h2>{{ s[4] }}</h2>
|
<h2>{{ s[4] }}</h2>
|
||||||
|
{% if s[6] is not none %}
|
||||||
|
<a href="{{ s[6] }}" type="button" class="btn btn-success" target="_blank">ver más</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -221,6 +250,8 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
@ -237,13 +268,40 @@
|
|||||||
|
|
||||||
|
|
||||||
<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)
|
|
||||||
|
|
||||||
|
|
||||||
|
function delete_file(e) {
|
||||||
|
let btn = $(this);
|
||||||
|
let postId = btn.data('id');
|
||||||
|
let url = `${window.location.origin}/user/carousel/delete-slide/${postId}`;
|
||||||
|
|
||||||
|
fetch(url, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer ' + localStorage.getItem('token_ai') // o tu token JWT
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.msg) {
|
||||||
|
console.log(data.msg);
|
||||||
|
let btn = document.querySelector(`[data-id="${postId}"]`);
|
||||||
|
let row = $(btn).closest('tr'); // usa jQuery para compatibilidad con DataTables
|
||||||
|
let table = $('#tblCarousel').DataTable(); // obtiene instancia
|
||||||
|
|
||||||
|
table.row(row).remove().draw(); // elimina la fila y redibuja la tabla
|
||||||
|
|
||||||
|
// eliminar el tr del slide si quieres: btn.closest('tr').remove();
|
||||||
|
} else {
|
||||||
|
console.error('Error:', data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error en fetch:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// https://htmlguyllc.github.io/jConfirm/
|
// https://htmlguyllc.github.io/jConfirm/
|
||||||
$(function(){
|
$(function(){
|
||||||
$('.delete-btn').jConfirm({
|
$('.delete-btn').jConfirm({
|
||||||
@ -281,6 +339,7 @@
|
|||||||
// console.log("el tooltip es visible");
|
// console.log("el tooltip es visible");
|
||||||
//do something when tooltip is shown
|
//do something when tooltip is shown
|
||||||
//tooltip dom element is passed as the second parameter
|
//tooltip dom element is passed as the second parameter
|
||||||
|
|
||||||
}).on('jc-hide', function(e){
|
}).on('jc-hide', function(e){
|
||||||
//do something when tooltip is hidden
|
//do something when tooltip is hidden
|
||||||
});
|
});
|
||||||
@ -299,7 +358,9 @@
|
|||||||
let title = header.text().trim();
|
let title = header.text().trim();
|
||||||
|
|
||||||
// Excluir la columna "Estatus" del filtro
|
// Excluir la columna "Estatus" del filtro
|
||||||
if (title !== 'Acciones') {
|
if (!(title.includes("ID") || title === 'New Tab' || title === 'Acciones')) {
|
||||||
|
|
||||||
|
console.log(title);
|
||||||
// Crea input de filtro
|
// Crea input de filtro
|
||||||
header.append('<div class="filter"><input type="text" class="form-control" placeholder="'+title+'" /></div>');
|
header.append('<div class="filter"><input type="text" class="form-control" placeholder="'+title+'" /></div>');
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user