muchos cambios
This commit is contained in:
parent
6d30f68376
commit
cf405ba012
@ -81,7 +81,9 @@ CREATE TABLE carousel (
|
||||
img_name VARCHAR(150) NOT NULL,
|
||||
bg_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';
|
||||
|
Binary file not shown.
Binary file not shown.
@ -80,11 +80,11 @@ class DBForma:
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error inesperado: {e}")
|
||||
|
||||
def get_all_data(self, query):
|
||||
def get_all_data(self, query: str, data_tuple: tuple = ()):
|
||||
try:
|
||||
with self._get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(query)
|
||||
cursor.execute(query, data_tuple)
|
||||
result = cursor.fetchall()
|
||||
return result
|
||||
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 wtforms import StringField, FileField
|
||||
from wtforms import StringField, FileField, BooleanField # Añade BooleanField aquí
|
||||
from wtforms.fields import HiddenField
|
||||
from wtforms.validators import DataRequired
|
||||
from wtforms.widgets import ColorInput
|
||||
@ -8,10 +43,6 @@ 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
|
||||
|
||||
@ -19,15 +50,17 @@ class Carousel(FlaskForm):
|
||||
'Imagen de fondo: ',
|
||||
validators=[
|
||||
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()])
|
||||
|
||||
txt_color = StringField('Color del texto: ', widget=ColorInput(), validators=[DataRequired()])
|
||||
|
||||
txt = StringField('Texto: ', validators=[DataRequired()])
|
||||
|
||||
|
||||
url = StringField('URL (opt): ')
|
||||
|
||||
# Añade el checkbox aquí
|
||||
isNewTab = BooleanField('¿En nueva ventana?', default=False)
|
289
main.py
289
main.py
@ -31,6 +31,7 @@ bcrypt = Bcrypt(app)
|
||||
|
||||
url_login = os.getenv("login_url")
|
||||
|
||||
|
||||
email_sender = os.getenv("email_sender")
|
||||
email_pswd = os.getenv("pswd_formha")
|
||||
# lst_email_to = ["davidix1991@gmail.com", "davicho1991@live.com"]
|
||||
@ -91,9 +92,9 @@ dbUsers = DBForma(jsonDbContact)
|
||||
|
||||
|
||||
def lst_email_to() -> list:
|
||||
data = dbUsers.get_all_data('SELECT email FROM users WHERE is_contact_noti = true;')
|
||||
re = [ele[0] for ele in data]
|
||||
return re
|
||||
data = dbUsers.get_all_data('SELECT email FROM users WHERE is_contact_noti = true;', ())
|
||||
respuesta = [ele[0] for ele in data]
|
||||
return respuesta
|
||||
|
||||
# decorador para rutas protegidas en caso de que borres al vuelo a un usuario.
|
||||
def validate_user_exists(f):
|
||||
@ -126,19 +127,34 @@ def validate_user_exists(f):
|
||||
#
|
||||
# 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('/')
|
||||
# @cache.cached(timeout=3600)
|
||||
def home():
|
||||
q = """
|
||||
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
|
||||
carousel
|
||||
ORDER BY
|
||||
id DESC;
|
||||
"""
|
||||
data = dbUsers.get_all_data(q)
|
||||
data = dbUsers.get_all_data(q, ())
|
||||
return render_template(v['home'], active_page='home', data=data)
|
||||
|
||||
@app.route('/about-us')
|
||||
@ -181,8 +197,6 @@ def api_posts():
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
|
||||
|
||||
@app.route('/blog/<int:post_id>')
|
||||
@app.route('/blog/<int:post_id>/src/<string:source_name>')
|
||||
# @cache.cached(timeout=43200)
|
||||
@ -201,8 +215,8 @@ def blog_post(post_id, source_name=None):
|
||||
SELECT
|
||||
u.nombre,
|
||||
u.apellido,
|
||||
TO_CHAR(p.created_at, 'DD/MM/YYYY HH24:MI') AS fecha_creada,
|
||||
TO_CHAR(p.updated_at, 'DD/MM/YYYY HH24:MI') AS fecha_updated,
|
||||
TO_CHAR(p.created_at, 'DD/MM/YYYY') AS fecha_creada,
|
||||
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,
|
||||
p.title,
|
||||
p.body,
|
||||
@ -220,9 +234,39 @@ def blog_post(post_id, source_name=None):
|
||||
"""
|
||||
t = (post_id,)
|
||||
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)
|
||||
|
||||
@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'])
|
||||
def contact():
|
||||
@ -232,39 +276,69 @@ def contact():
|
||||
|
||||
if form.validate_on_submit():
|
||||
# Procesar datos del formulario
|
||||
f_mnsj = l_flash_msj('Contacto', '¡Gracias por contactarnos! Te responderemos pronto. ☺️', 'success' )
|
||||
flash(f_mnsj)
|
||||
|
||||
|
||||
|
||||
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()
|
||||
|
||||
obj_datetime = get_date_n_time(c_date)
|
||||
hora = obj_datetime['hour']
|
||||
fecha = obj_datetime['date']
|
||||
# validar si ya se recibio info de ese email
|
||||
q_val_mail = "SELECT COUNT(email) FROM contact WHERE email = %s;"
|
||||
|
||||
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 )
|
||||
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)
|
||||
hora = obj_datetime['hour']
|
||||
fecha = obj_datetime['date']
|
||||
data = ( c_date, nombre, apellido, email, estado, num_tel, size_co, rol_contacto, industry_type, tipo_req )
|
||||
|
||||
# Guardar datos en la base de datos
|
||||
dbContact.carga_contact(data)
|
||||
# Guardar datos en la base de datos
|
||||
dbContact.carga_contact(data)
|
||||
|
||||
# Configurar y enviar email asíncrono
|
||||
msg = Message( "Subject, una persona busca asesoria", sender=email_sender, recipients=lst_email_to() )
|
||||
# Configurar y enviar email asíncrono
|
||||
msg = Message( "Subject, una persona busca asesoria", sender=email_sender, recipients=lst_email_to() )
|
||||
url_login = "https://formha.temporal.work/login"
|
||||
|
||||
msg.html =f"""
|
||||
<h1>Nuevo contacto recibido</h1>
|
||||
<p><trong>Fecha: </strong> {fecha}</p>
|
||||
<p><strong>Hora:</strong> {hora}</p>
|
||||
<p><strong>Nombre:</strong> {form.nombre.data} {form.apellido.data}</p>
|
||||
<p><strong>Email:</strong> {form.email.data}</p>
|
||||
<p><strong>Teléfono:</strong> {form.num_tel.data}</p>
|
||||
<p><strong>Mensaje:</strong> {form.tipo_req.data}</p>
|
||||
<p><a href="{url_login}" target='_blank'>Iniciar Sesión</a></p>
|
||||
"""
|
||||
|
||||
# Enviar en segundo plano
|
||||
thr = Thread( target=send_async_email, args=(current_app._get_current_object(), msg) )
|
||||
thr.start()
|
||||
msg.html = f"""
|
||||
<div style="font-family: Arial, sans-serif;">
|
||||
<img src="https://formha.temporal.work/static/y_img/logos/formha_blanco_vertical.png" alt="Escudo FORMHä" height="150" style="margin-bottom: 10px;"><br>
|
||||
<h2 style="color: #333;">📩 Nuevo contacto recibido</h2>
|
||||
<ul>
|
||||
<li><strong>Fecha:</strong> {fecha}</li>
|
||||
<li><strong>Hora:</strong> {hora}</li>
|
||||
<li><strong>Nombre:</strong> {form.nombre.data} {form.apellido.data}</li>
|
||||
<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
|
||||
thr = Thread( target=send_async_email, args=(current_app._get_current_object(), msg) )
|
||||
thr.start()
|
||||
|
||||
flash(f_mnsj)
|
||||
return redirect(url_for('contact'))
|
||||
|
||||
return render_template(v['contact'], form=form, active_page='contact')
|
||||
@ -423,14 +497,11 @@ def user_home():
|
||||
f_mnsj = None
|
||||
|
||||
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'
|
||||
f_mnsj = l_flash_msj(f'{saludo_hr()}', f'{strGreet} {nombre}', 'success')
|
||||
# dbUsers.update_data(q, d)
|
||||
flash(f_mnsj)
|
||||
|
||||
|
||||
flash(update_pswd)
|
||||
|
||||
q = "UPDATE users SET lst_conn = %s WHERE id = %s;"
|
||||
@ -440,7 +511,15 @@ def user_home():
|
||||
|
||||
q_contact = """
|
||||
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
|
||||
CONTACT
|
||||
WHERE
|
||||
@ -449,9 +528,8 @@ def user_home():
|
||||
ORDER BY
|
||||
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')
|
||||
|
||||
@app.route('/user/manage-record', methods=['POST'])
|
||||
@ -491,11 +569,23 @@ def get_contact_data():
|
||||
def download_db():
|
||||
q = """
|
||||
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
|
||||
contact;
|
||||
"""
|
||||
dbData = dbUsers.get_all_data(q)
|
||||
dbData = dbUsers.get_all_data(q, ())
|
||||
return jsonify({"data": dbData})
|
||||
|
||||
@app.route('/user/txt-editor')
|
||||
@ -554,7 +644,7 @@ def my_posts():
|
||||
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)
|
||||
q_all = fr"""
|
||||
q_all = r"""
|
||||
SELECT
|
||||
id,
|
||||
TO_CHAR(created_at, 'DD/MM/YYYY HH24:MI') as fecha_creada,
|
||||
@ -566,13 +656,13 @@ def my_posts():
|
||||
FROM
|
||||
posts
|
||||
WHERE
|
||||
id_usr = '{id_usr}'
|
||||
id_usr = %s
|
||||
ORDER BY
|
||||
COALESCE(updated_at, 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_pages = (total_posts + per_page - 1) // per_page
|
||||
|
||||
@ -606,7 +696,17 @@ def del_post():
|
||||
def post(post_id):
|
||||
# Obtener el post
|
||||
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 )
|
||||
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;"
|
||||
@ -673,6 +773,11 @@ def data_metrics():
|
||||
# INNER JOIN posts p ON p.id = pv.id_post
|
||||
# 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"""
|
||||
SELECT
|
||||
CASE EXTRACT(MONTH FROM full_date_time AT TIME ZONE 'America/Mexico_City')
|
||||
@ -736,13 +841,34 @@ def data_metrics():
|
||||
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 = {
|
||||
"count_monthly": dbUsers.get_all_data(q_contact),
|
||||
"count_state": dbUsers.get_all_data(q_count_state),
|
||||
"size_co": dbUsers.get_all_data(q_size_co),
|
||||
"rol_contact": dbUsers.get_all_data(q_rol_contact),
|
||||
"industry_type": dbUsers.get_all_data(q_industry_type),
|
||||
"group_status": dbUsers.get_all_data(q_group_status)
|
||||
"count_monthly": dbUsers.get_all_data(q_contact, ()),
|
||||
"count_state": dbUsers.get_all_data(q_count_state, ()),
|
||||
"size_co": dbUsers.get_all_data(q_size_co, ()),
|
||||
"rol_contact": dbUsers.get_all_data(q_rol_contact, ()),
|
||||
"industry_type": dbUsers.get_all_data(q_industry_type, ()),
|
||||
"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)
|
||||
|
||||
@ -776,7 +902,7 @@ def manage_profiles():
|
||||
) p ON u.id = p.id_usr
|
||||
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():
|
||||
f_nombre = f'{form.nombre.data}'.title().strip()
|
||||
@ -919,22 +1045,33 @@ def carousel():
|
||||
|
||||
q_all_slides = """
|
||||
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
|
||||
carousel
|
||||
ORDER BY
|
||||
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():
|
||||
# img
|
||||
image_file = form.img.data
|
||||
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]
|
||||
|
||||
url = None if form.url.data == '' else form.url.data
|
||||
|
||||
isNewTab = form.isNewTab.data
|
||||
|
||||
# validar que el archivo no se haya cargado previamente
|
||||
if filename in lst_img:
|
||||
@ -945,8 +1082,8 @@ def carousel():
|
||||
txt_color = rgba_to_string((*hex_to_rgb(form.txt_color.data), 1))
|
||||
txt = form.txt.data
|
||||
|
||||
q = "INSERT INTO carousel (img_name, bg_color, txt_color, txt) VALUES (%s, %s, %s, %s);"
|
||||
t = (filename, bg_color, txt_color, txt)
|
||||
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, url, isNewTab)
|
||||
dbUsers.update_data(q, t)
|
||||
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')
|
||||
|
||||
|
||||
@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>')
|
||||
@jwt_required()
|
||||
@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%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Altura del carousel para ajustarse al viewport */
|
||||
.carousel-item {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
/* ---------------------------- */
|
||||
/* Media Queries optimizadas */
|
||||
@media (min-width: 768px) {
|
||||
main {
|
||||
width: 90vw;
|
||||
/* Contenedor de los ítems del carrusel */
|
||||
.carousel-inner {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Centrar captions */
|
||||
.carousel-caption.centered {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Contenido dentro del caption */
|
||||
.carousel-caption.centered .caption-content {
|
||||
padding: 1rem 2rem;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
box-shadow: 0 0 10px rgba(0,0,0,0.3);
|
||||
pointer-events: auto;
|
||||
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);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
main {
|
||||
width: 85vw;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
}
|
||||
/* --------- Responsivo (ajustes si necesitas) ---------- */
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
main {
|
||||
width: 80vw;
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (min-width: 1920px) {
|
||||
|
||||
}
|
||||
|
||||
/* Ajuste específico para texto en móviles */
|
||||
/* Smartphones */
|
||||
@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 {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
@ -22,4 +13,29 @@
|
||||
|
||||
.card-footer {
|
||||
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;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #eee;
|
||||
border-radius: 2rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
overflow: hidden;
|
||||
transition: width 0.3s ease;
|
||||
display: inline-block; /* Para que solo ocupe el ancho necesario */
|
||||
}
|
||||
|
||||
.share:hover {
|
||||
width: 18rem;
|
||||
}
|
||||
|
||||
.share__wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.share__toggle {
|
||||
background: #549c67;
|
||||
color: white;
|
||||
.share-toggle {
|
||||
background-color: #eee;
|
||||
color: #333;
|
||||
border-radius: 50%;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
margin: 0.25rem;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.share__button {
|
||||
background: #555;
|
||||
color: white;
|
||||
.share-toggle:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
|
||||
.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%;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Mostrar botones solo cuando se hace hover */
|
||||
.share:hover .share__button {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
.share-btn:hover {
|
||||
color: #007bff; /* Un color de ejemplo al hacer hover */
|
||||
}
|
||||
|
||||
.fb { background: #1877f2; }
|
||||
.tw { background: #000000; }
|
||||
.in { background: #0077b5; }
|
||||
.copy { background: #444; }
|
||||
.wa { background-color: #25D366; }
|
||||
/* 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;
|
||||
left: auto;
|
||||
right: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
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,
|
||||
|
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 {
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
background-color: #fff;
|
||||
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;
|
||||
color: #333;
|
||||
|
||||
& h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #2c3e50;
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
& span {
|
||||
display: block;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
|
||||
& span i {
|
||||
margin-right: 0.3rem;
|
||||
}
|
||||
|
||||
& img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
overflow: hidden; /* Para contener floats */
|
||||
}
|
||||
|
||||
.pst-cont > div:first-of-type {
|
||||
margin-bottom: 2rem;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
/* Encabezado */
|
||||
.pst-cont h1 {
|
||||
font-size: clamp(1.8rem, 5vw, 2.5rem);
|
||||
margin-bottom: 1.5rem;
|
||||
color: #2c3e50;
|
||||
line-height: 1.3;
|
||||
font-weight: 700;
|
||||
border-bottom: 2px solid #f0f0f0;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
/* Estilos para el contenido del post */
|
||||
/* Metadatos */
|
||||
.pst-cont > span {
|
||||
display: block;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #6c757d;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.pst-cont > div:last-of-type {
|
||||
.pst-cont span i {
|
||||
margin-right: 0.4rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Botón de regreso */
|
||||
.pst-cont .btn-secondary {
|
||||
margin-bottom: 1rem;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Contenido principal */
|
||||
.bd_post {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
hyphens: auto;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.pst-cont p {
|
||||
/* Párrafos */
|
||||
.bd_post p {
|
||||
text-align: justify;
|
||||
text-justify: inter-word;
|
||||
margin-bottom: 1.5rem;
|
||||
margin: 0 0 1.5rem 0;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.pst-cont .note-float-left,
|
||||
.pst-cont .note-float-right {
|
||||
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 {
|
||||
/* Imágenes */
|
||||
.bd_post img {
|
||||
max-width: 100%;
|
||||
margin-left: auto !important;
|
||||
margin-right: auto !important;
|
||||
border-radius: 4px;
|
||||
margin: 1.5rem 0;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.1);
|
||||
margin: 1.5rem auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.pst-cont::after {
|
||||
content: "";
|
||||
display: table;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -------------------------- */
|
||||
ul, ol {
|
||||
/* Asegura que el espacio no afecte la alineación de viñetas/números */
|
||||
padding-left: 1em; /* Ajusta según tu diseño */
|
||||
/* Elementos flotantes */
|
||||
.bd_post .note-float-left,
|
||||
.bd_post .note-float-right {
|
||||
margin: 1rem 0;
|
||||
max-width: 100%;
|
||||
float: none;
|
||||
}
|
||||
|
||||
li {
|
||||
text-align: justify; /* Justifica el texto */
|
||||
margin-bottom: 0.5em; /* Espaciado entre elementos (opcional) */
|
||||
/* Iframes/videos */
|
||||
.bd_post iframe {
|
||||
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;
|
||||
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");
|
||||
|
||||
form.addEventListener("submit", function (e) {
|
||||
let allowedExtensions = ['jpg', 'jpeg', 'png', 'mp4'];
|
||||
let allowedExtensions = ['jpg', 'jpeg', 'png', 'mp4', 'avif', 'webp'];
|
||||
let filePath = img.value;
|
||||
let extension = filePath.split('.').pop().toLowerCase();
|
||||
|
||||
|
@ -1,42 +1,72 @@
|
||||
.form-control{
|
||||
width: 50% !important;
|
||||
margin-bottom: 1em !important;
|
||||
margin-top: 1em !important;
|
||||
/* Input responsivo */
|
||||
.form-control {
|
||||
width: 90% !important;
|
||||
max-width: 800px;
|
||||
margin: 1em auto !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Editor Summernote responsivo */
|
||||
.note-editor {
|
||||
width: 50% !important;
|
||||
min-height: 70vh !important;
|
||||
width: 90% !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 {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.8;
|
||||
color: #333;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 2rem;
|
||||
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;
|
||||
padding: 1em !important;
|
||||
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 999; /* para que esté por encima del contenido */
|
||||
padding: 1em !important;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* Fondo del área de edición */
|
||||
div.note-editing-area {
|
||||
background-color: white !important;
|
||||
|
||||
}
|
||||
|
||||
/* aplica en la sección de edición del post */
|
||||
#btn-cancel{
|
||||
/* Botón cancelar */
|
||||
#btn-cancel {
|
||||
margin-left: 1em !important;
|
||||
}
|
||||
|
||||
/* IMPORTANTE, UNA ANIMACIÓN DE AOS INTEFIERE CON SUMMERNOTE */
|
||||
/* AOS interfiere con modales de Summernote */
|
||||
div.note-modal-backdrop {
|
||||
z-index: 1 !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,69 +10,38 @@
|
||||
|
||||
{% block body %}
|
||||
|
||||
<style>
|
||||
@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 id="carouselExampleCaptions" class="carousel slide" data-bs-ride="carousel" data-aos="fade-left" data-aos-delay="300" data-aos-duration="800">
|
||||
<div class="carousel-indicators">
|
||||
{% for s in data %}
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="{{ loop.index0 }}"
|
||||
class="{% if loop.first %}active{% endif %}" aria-current="{{ 'true' if loop.first else 'false' }}"
|
||||
aria-label="Slide {{ loop.index }}"></button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="carousel-inner">
|
||||
|
||||
{% for s in data %}
|
||||
<div class="carousel-item {% if loop.first %}active{% endif %}">
|
||||
{% if s[1].endswith('.mp4') %}
|
||||
<video class="d-block w-100" autoplay muted loop playsinline>
|
||||
<source src="{{ url_for('static', filename='uploads/' + s[1]) }}" type="video/mp4">
|
||||
Tu navegador no soporta el video.
|
||||
</video>
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='uploads/' + s[1]) }}" class="d-block w-100" alt="...">
|
||||
{% endif %}
|
||||
<div class="carousel-caption centered">
|
||||
<div class="caption-content" style="background-color: {{ s[2] }}; color: {{ s[3] }};">
|
||||
<h2>{{ s[4] }}</h2>
|
||||
</div>
|
||||
<!-- {# inicio card item slide #} -->
|
||||
{% for s in data %}
|
||||
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="{{ loop.index0 }}"
|
||||
class="{% if loop.first %}active{% endif %}" aria-current="{{ 'true' if loop.first else 'false' }}"
|
||||
aria-label="Slide {{ loop.index }}"></button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<div class="carousel-inner">
|
||||
<!-- {# inicio card item slide #} -->
|
||||
{% for s in data %}
|
||||
<div class="carousel-item {% if loop.first %}active{% endif %}">
|
||||
{% if s[1].endswith('.mp4') %}
|
||||
<video class="d-block w-100" autoplay muted loop playsinline>
|
||||
<source src="{{ url_for('static', filename='uploads/' + s[1]) }}" type="video/mp4">
|
||||
Tu navegador no soporta el video.
|
||||
</video>
|
||||
{% else %}
|
||||
<img src="{{ url_for('static', filename='uploads/' + s[1]) }}" class="d-block w-100" alt="...">
|
||||
{% endif %}
|
||||
<div class="carousel-caption centered">
|
||||
<div class="caption-content" style="background-color: {{ s[2] }}; color: {{ s[3] }};">
|
||||
<h2>{{ s[4] }}</h2>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
<!-- {# fin card item slide #} -->
|
||||
</div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
@ -86,3 +55,13 @@
|
||||
|
||||
{% 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://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js"></script>
|
||||
|
||||
|
||||
|
||||
<!-- {# aos script #} -->
|
||||
{% 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 %}
|
||||
<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 %}
|
||||
|
||||
{% block navbar %}
|
||||
@ -10,36 +11,6 @@
|
||||
|
||||
{% 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="input-group mb-3">
|
||||
<!-- <input id="searchInput" type="text" autocomplete="off" /> -->
|
||||
@ -55,16 +26,6 @@
|
||||
</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 class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">Cargando...</span>
|
||||
@ -78,279 +39,17 @@
|
||||
</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 %}
|
||||
|
||||
{% block js %}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% include 'z_comps/if_flash.html' %}
|
||||
|
||||
<!-- {# aos script #} -->
|
||||
{% 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 %}
|
@ -12,59 +12,56 @@
|
||||
{% block body %}
|
||||
|
||||
|
||||
|
||||
<div class="pst-cont">
|
||||
<div class="pst-cont" data-aos="fade-left" data-aos-delay="300" data-aos-duration="800">
|
||||
<h1>{{data[5]}}</h1>
|
||||
|
||||
<spam>
|
||||
<a type="button" class="btn btn-secondary" href="{{ url_for( 'blog' ) }}"><i class="bi bi-arrow-left" ></i> Publicaciones</a> <br>
|
||||
<i class="bi bi-person-circle"></i> {{data[0]}} {{data[1]}} |
|
||||
<i class="bi bi-pencil-square"></i> {{data[2]}} |
|
||||
{% if data[3] is not none %}
|
||||
<i class="bi bi-arrow-repeat"></i> {{data[3]}} |
|
||||
{% endif %}
|
||||
<i class="bi bi-clock-history"></i> {{ data[4] }} Minutos |
|
||||
<i class="bi bi-eye"></i> {{ data[7] }}
|
||||
</spam>
|
||||
<!-- {# volver, contenedor datos autor, creación publicación, fecha actualización, tiempo lectura, vistas, btn_share_all_rrss #} -->
|
||||
|
||||
<div class="share">
|
||||
<div class="share__wrapper">
|
||||
<div class="share__toggle"><i class="bi bi-share-fill"></i></div>
|
||||
<button href="#" class="share__button fb"><i class="bi bi-facebook"></i></button>
|
||||
<button href="#" class="share__button tw"><i class="bi bi-twitter-x"></i></button>
|
||||
<button class="share__button in"><i class="bi bi-linkedin"></i></button>
|
||||
<button class="share__button copy"><i class="bi bi-link-45deg"></i></button>
|
||||
<button class="share__button wa"><i class="bi bi-whatsapp"></i></button>
|
||||
<div class="meta-bar">
|
||||
<a type="button" class="btn btn-light" href="{{ url_for('blog') }}">
|
||||
<i class="bi bi-arrow-left"></i> Volver
|
||||
</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 %}
|
||||
<i class="bi bi-chevron-compact-right"></i> <span class="meta-item"><i class="bi bi-arrow-repeat"></i> {{data[3]}}</span>
|
||||
{% endif %} <br>
|
||||
<span class="meta-item"><i class="bi bi-clock-history"></i> {{data[4]}} Min(s) lectura.</span> <br>
|
||||
<span class="meta-item"><i class="bi bi-eye"></i> <span id="viewed">{{data[7]}}</span></span>
|
||||
</div>
|
||||
|
||||
<div class="share-container">
|
||||
<div class="share-toggle" aria-label="Compartir">
|
||||
<i class="bi bi-share-fill"></i>
|
||||
</div>
|
||||
<div class="share-buttons">
|
||||
<button class="share-btn mail" aria-label="Compartir por email"><i class="bi bi-envelope-at-fill"></i></button>
|
||||
<button class="share-btn in" aria-label="Compartir en LinkedIn"><i class="bi bi-linkedin"></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 class="bd_post">
|
||||
|
||||
{{data[6] | safe}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.share__toggle').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
btn.closest('.share').classList.toggle('open');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock body %}
|
||||
{% 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 src="{{ url_for('static', filename='e_blog/b_posts.js') }}"></script>
|
||||
<!-- {# flecha ir hasta arriba #} -->
|
||||
{% include 'z_comps/arrow_to_up.html' %}
|
||||
|
||||
{% 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/contact_modal.css') }}">
|
||||
|
||||
<!-- {# Librería de aos.js [animaciones] #} -->
|
||||
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
|
||||
|
||||
@ -74,66 +76,6 @@
|
||||
</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 -->
|
||||
<div class="modal fade" id="modalContactData" tabindex="-1" aria-labelledby="modalContactDataLabel" aria-hidden="true">
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
{% block body %}
|
||||
<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">
|
||||
<input type="text" name="title" class="form-control" placeholder="Título de la publicación" maxlength="100">
|
||||
|
@ -23,8 +23,8 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<h2 class="display-5 mb-4">Mis Publicaciones</h2>
|
||||
|
||||
|
||||
<div class="container">
|
||||
|
||||
<!-- {# i pagination #} -->
|
||||
@ -119,7 +119,7 @@
|
||||
<!-- {# f pagination #} -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% endblock body %}
|
||||
|
@ -1,68 +1,50 @@
|
||||
{% extends 'h_tmp_usr/z_tmp.html' %}
|
||||
|
||||
{% 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='e_blog/b_share_btn.css' ) }}">
|
||||
{% endblock css %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
<div class="pst-cont">
|
||||
<h1>{{data[2]}}</h1>
|
||||
<span>
|
||||
<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}}
|
||||
</span>
|
||||
<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('edit_post', id_post= post_id ) }}"><i class="bi bi-vector-pen"></i> Editar.</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
<div class="pst-cont" data-aos="fade-left" data-aos-delay="300" data-aos-duration="800">
|
||||
<h1>{{data[2]}}</h1>
|
||||
<!-- {# volver, contenedor datos autor, creación publicación, fecha actualización, tiempo lectura, vistas, btn_share_all_rrss #} -->
|
||||
|
||||
<div class="meta-bar">
|
||||
<div>
|
||||
<a type="button" class="btn btn-secondary" href="{{ url_for('my_posts') }}">
|
||||
<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 class="bd-post">
|
||||
</div>
|
||||
|
||||
<div class="bd_post">
|
||||
{{data[3] | safe}}
|
||||
</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 %}
|
||||
@ -72,8 +54,9 @@
|
||||
{% include 'z_comps/arrow_to_up.html' %}
|
||||
|
||||
|
||||
<!-- {# aos script #} -->
|
||||
{% include 'z_comps/aos_script.html' %}
|
||||
|
||||
|
||||
|
||||
{% include "z_comps/read_progress.html" %}
|
||||
|
||||
{% 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">
|
||||
<input type="text" name="title" class="form-control" placeholder="Título de la publicación">
|
||||
|
@ -3,20 +3,12 @@
|
||||
{% block 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='h_tmp_user/f_change_pswd/change_pswd.css' ) }}">
|
||||
{% endblock css %}
|
||||
|
||||
{% 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-header">
|
||||
<h2>Cambio de Contraseña</h2>
|
||||
|
@ -19,7 +19,6 @@
|
||||
</style>
|
||||
|
||||
<div>
|
||||
<h1>Métricas del Sitio</h1>
|
||||
|
||||
<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">
|
||||
@ -53,6 +52,16 @@
|
||||
<canvas id="dbStatus"></canvas>
|
||||
</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>
|
||||
@ -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">
|
||||
<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>
|
||||
@ -93,23 +98,27 @@
|
||||
<div class="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="home-tab">
|
||||
<!-- {# --------------------------------------------------------------- #} -->
|
||||
<!-- {# i tabla slides #} -->
|
||||
<table id="tblCarousel" class="table table-striped" style="width:100%" >
|
||||
<table id="tblCarousel" class="table table-striped" style="width:100%">
|
||||
<thead>
|
||||
<th>ID</th>
|
||||
<th>Creado</th>
|
||||
<th>Archivo</th>
|
||||
<th>New Tab</th>
|
||||
<th>Texto</th>
|
||||
<th>Acciones</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ele in data %}
|
||||
<tr id="{{ ele[0] }}">
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ ele[5] }}</td>
|
||||
<td>{{ ele[1] }}</td>
|
||||
|
||||
<td style="background-color: {{ ele[2] }}; color: {{ ele[3] }};">{{ ele[4] }}</td>
|
||||
<td >
|
||||
<tr data-id="{{ ele[0] }}">
|
||||
<td data-label="ID">{{ loop.index }}</td>
|
||||
<td data-label="Creado">{{ ele[5] }}</td>
|
||||
<td data-label="Archivo">{{ ele[1] }}</td>
|
||||
<td data-label="New Tab">{% if ele[7] %}✔️{% else %}❌{% endif %}</td>
|
||||
<td data-label="Texto" style="background-color: {{ ele[2] }}; color: {{ ele[3] }};">
|
||||
{{ ele[4] }}
|
||||
|
||||
</td>
|
||||
<td data-label="Acciones">
|
||||
<div class="field_btns">
|
||||
<a href="{{ url_for('download_file', filename = ele[1] ) }}" class="btn btn-dark" style="color: white;">
|
||||
<i class="bi bi-download"></i>
|
||||
@ -122,8 +131,10 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
</table>
|
||||
|
||||
|
||||
<!-- {# f tabla slides #} -->
|
||||
<!-- {# --------------------------------------------------------------- #} -->
|
||||
</div>
|
||||
@ -162,6 +173,21 @@
|
||||
{{ form.txt.label() }}
|
||||
{{ form.txt( class_="form-control" ) }}
|
||||
</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">
|
||||
<i class="bi bi-database-fill-up"></i> Enviar Datos
|
||||
</button>
|
||||
@ -198,6 +224,9 @@
|
||||
<div class="carousel-caption centered">
|
||||
<div class="caption-content" style="background-color: {{ s[2] }}; color: {{ s[3] }};">
|
||||
<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>
|
||||
@ -221,6 +250,8 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% endblock body %}
|
||||
|
||||
{% block js %}
|
||||
@ -237,13 +268,40 @@
|
||||
|
||||
|
||||
<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/
|
||||
$(function(){
|
||||
$('.delete-btn').jConfirm({
|
||||
@ -277,10 +335,11 @@
|
||||
.on('deny', function(e){
|
||||
var btn = $(this);
|
||||
//do something on deny
|
||||
}).on('jc-show', function(e, tooltip){
|
||||
}).on('jc-show', function(e, tooltip){
|
||||
// console.log("el tooltip es visible");
|
||||
//do something when tooltip is shown
|
||||
//tooltip dom element is passed as the second parameter
|
||||
|
||||
}).on('jc-hide', function(e){
|
||||
//do something when tooltip is hidden
|
||||
});
|
||||
@ -299,7 +358,9 @@
|
||||
let title = header.text().trim();
|
||||
|
||||
// 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
|
||||
header.append('<div class="filter"><input type="text" class="form-control" placeholder="'+title+'" /></div>');
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user