diff --git a/Documents_ref/conf.sql b/Documents_ref/conf.sql index e26f73b..9379459 100644 --- a/Documents_ref/conf.sql +++ b/Documents_ref/conf.sql @@ -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'; diff --git a/forms_py/__pycache__/cls_db_usr.cpython-312.pyc b/forms_py/__pycache__/cls_db_usr.cpython-312.pyc index 52cfa4e..7b1f1c9 100644 Binary files a/forms_py/__pycache__/cls_db_usr.cpython-312.pyc and b/forms_py/__pycache__/cls_db_usr.cpython-312.pyc differ diff --git a/forms_py/__pycache__/cls_form_carousel.cpython-312.pyc b/forms_py/__pycache__/cls_form_carousel.cpython-312.pyc index f00ea98..4e9f409 100644 Binary files a/forms_py/__pycache__/cls_form_carousel.cpython-312.pyc and b/forms_py/__pycache__/cls_form_carousel.cpython-312.pyc differ diff --git a/forms_py/cls_db_usr.py b/forms_py/cls_db_usr.py index d73604b..16ee8e9 100644 --- a/forms_py/cls_db_usr.py +++ b/forms_py/cls_db_usr.py @@ -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: diff --git a/forms_py/cls_form_carousel.py b/forms_py/cls_form_carousel.py index 07e699b..f715361 100644 --- a/forms_py/cls_form_carousel.py +++ b/forms_py/cls_form_carousel.py @@ -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) \ No newline at end of file diff --git a/main.py b/main.py index 4bea124..1506eaf 100644 --- a/main.py +++ b/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/') @app.route('/blog//src/') # @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""" -

Nuevo contacto recibido

-

Fecha: {fecha}

-

Hora: {hora}

-

Nombre: {form.nombre.data} {form.apellido.data}

-

Email: {form.email.data}

-

Teléfono: {form.num_tel.data}

-

Mensaje: {form.tipo_req.data}

-

Iniciar Sesión

- """ - - # Enviar en segundo plano - thr = Thread( target=send_async_email, args=(current_app._get_current_object(), msg) ) - thr.start() + msg.html = f""" +
+ Escudo FORMHä
+

📩 Nuevo contacto recibido

+
    +
  • Fecha: {fecha}
  • +
  • Hora: {hora}
  • +
  • Nombre: {form.nombre.data} {form.apellido.data}
  • +
  • Email: {form.email.data}
  • +
  • Teléfono: {form.num_tel.data}
  • +
  • Mensaje: {form.tipo_req.data}
  • +
+

👉 Iniciar Sesión

+
+ """ + 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/', 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/') @jwt_required() @validate_user_exists diff --git a/static/a_home/home.css b/static/a_home/home.css index 53a71e3..a8a696c 100644 --- a/static/a_home/home.css +++ b/static/a_home/home.css @@ -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) { - -} \ No newline at end of file + .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 */ +} diff --git a/static/e_blog/0_all_posts_main.css b/static/e_blog/0_all_posts_main.css new file mode 100644 index 0000000..60d9217 --- /dev/null +++ b/static/e_blog/0_all_posts_main.css @@ -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; + } +} + diff --git a/static/e_blog/a_all_posts.css b/static/e_blog/a_all_posts.css index 1e81e0b..73fd9a8 100644 --- a/static/e_blog/a_all_posts.css +++ b/static/e_blog/a_all_posts.css @@ -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; } \ No newline at end of file diff --git a/static/e_blog/a_all_posts_visit.js b/static/e_blog/a_all_posts_visit.js new file mode 100644 index 0000000..a969c26 --- /dev/null +++ b/static/e_blog/a_all_posts_visit.js @@ -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 = ` +
+

No se encontraron resultados

+

Intenta con otros términos de búsqueda

+
+ `; + 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 ? ` ${post.author_updated}
` : ""; + + const card = document.createElement("div"); + card.className = "col card-wrapper"; + card.innerHTML = ` +
+ + ${post.title} + +
+
+
${post.title}
+ + ${post.autor}
+ ${post.author_creation}
+ ${dateUpdate} + ${post.read_time_min} min(s) de lectura. +
+

${post.preview}...

+
+
+
+ `; + 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 = `...`; + 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 = ` +
+

Error al cargar los posts

+

Por favor intenta recargar la página

+
+ `; + }) + .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 + }); + \ No newline at end of file diff --git a/static/e_blog/b_posts.js b/static/e_blog/b_posts.js new file mode 100644 index 0000000..3d55f3d --- /dev/null +++ b/static/e_blog/b_posts.js @@ -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); diff --git a/static/e_blog/b_share_btn.css b/static/e_blog/b_share_btn.css index b18d167..4dbb3d5 100644 --- a/static/e_blog/b_share_btn.css +++ b/static/e_blog/b_share_btn.css @@ -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; } \ No newline at end of file +/* 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; + } +} \ No newline at end of file diff --git a/static/e_blog/b_share_btn.js b/static/e_blog/b_share_btn.js new file mode 100644 index 0000000..e8375c3 --- /dev/null +++ b/static/e_blog/b_share_btn.js @@ -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); + }); + }); + } +}); \ No newline at end of file diff --git a/static/e_blog/copy_url.js b/static/e_blog/copy_url.js index 9229779..fcd4356 100644 --- a/static/e_blog/copy_url.js +++ b/static/e_blog/copy_url.js @@ -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; + }); +} diff --git a/static/h_tmp_user/a_home/a_home.css b/static/h_tmp_user/a_home/a_home.css index cae3e10..b5bd0ff 100644 --- a/static/h_tmp_user/a_home/a_home.css +++ b/static/h_tmp_user/a_home/a_home.css @@ -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, diff --git a/static/h_tmp_user/a_home/contact_modal.css b/static/h_tmp_user/a_home/contact_modal.css new file mode 100644 index 0000000..1c81778 --- /dev/null +++ b/static/h_tmp_user/a_home/contact_modal.css @@ -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); +} \ No newline at end of file diff --git a/static/h_tmp_user/d_read_post/read_post.css b/static/h_tmp_user/d_read_post/read_post.css index 4005e77..d8bf8e7 100644 --- a/static/h_tmp_user/d_read_post/read_post.css +++ b/static/h_tmp_user/d_read_post/read_post.css @@ -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; + } + +} \ No newline at end of file diff --git a/static/h_tmp_user/f_change_pswd/change_pswd.css b/static/h_tmp_user/f_change_pswd/change_pswd.css new file mode 100644 index 0000000..9ce5546 --- /dev/null +++ b/static/h_tmp_user/f_change_pswd/change_pswd.css @@ -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; } +} \ No newline at end of file diff --git a/static/h_tmp_user/i_carousel_form/carousel_form.js b/static/h_tmp_user/i_carousel_form/carousel_form.js index 8fe7a91..7866bb6 100644 --- a/static/h_tmp_user/i_carousel_form/carousel_form.js +++ b/static/h_tmp_user/i_carousel_form/carousel_form.js @@ -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(); diff --git a/static/h_tmp_user/text_editor.css b/static/h_tmp_user/text_editor.css index facaaad..3cdb852 100644 --- a/static/h_tmp_user/text_editor.css +++ b/static/h_tmp_user/text_editor.css @@ -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; + } +} diff --git a/templates/a_home/home.html b/templates/a_home/home.html index d0ddb9e..df24298 100644 --- a/templates/a_home/home.html +++ b/templates/a_home/home.html @@ -10,69 +10,38 @@ {% block body %} - - -