test
This commit is contained in:
parent
8273b48b6b
commit
396df4293f
@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
-- psql -h 127.0.0.1 -U postgres -a -f conf.sql
|
-- psql -h 127.0.0.1 -U postgres -a -f conf.sql
|
||||||
|
|
||||||
DROP DATABASE IF EXISTS forma;
|
DROP DATABASE IF EXISTS formha;
|
||||||
|
|
||||||
CREATE DATABASE forma;
|
CREATE DATABASE formha;
|
||||||
|
|
||||||
\c forma;
|
\c formha;
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE contact (
|
CREATE TABLE contact (
|
||||||
@ -63,4 +63,16 @@ CREATE TABLE posts (
|
|||||||
CREATE TABLE posts_visited(
|
CREATE TABLE posts_visited(
|
||||||
id_post INT,
|
id_post INT,
|
||||||
viewed TIMESTAMP WITH TIME ZONE
|
viewed TIMESTAMP WITH TIME ZONE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE visited_from (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
post_id INT NOT NULL,
|
||||||
|
source_name VARCHAR(50),
|
||||||
|
visit_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ALTER DATABASE formha SET timezone TO 'America/Mexico_City';
|
Binary file not shown.
@ -1,6 +1,8 @@
|
|||||||
import psycopg2
|
import psycopg2
|
||||||
from psycopg2 import sql
|
from psycopg2 import sql
|
||||||
from psycopg2.extras import execute_values
|
from psycopg2.extras import execute_values
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
|
|
||||||
class DBForma:
|
class DBForma:
|
||||||
def __init__(self, db_obj: dict):
|
def __init__(self, db_obj: dict):
|
||||||
@ -90,7 +92,18 @@ class DBForma:
|
|||||||
raise RuntimeError(f"Error al ejecutar la consulta: {e}")
|
raise RuntimeError(f"Error al ejecutar la consulta: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(f"Error inesperado: {e}")
|
raise RuntimeError(f"Error inesperado: {e}")
|
||||||
|
|
||||||
|
def get_all_data_dict(self, query):
|
||||||
|
try:
|
||||||
|
with self._get_connection() as conn:
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cursor:
|
||||||
|
cursor.execute(query)
|
||||||
|
return cursor.fetchall()
|
||||||
|
except psycopg2.DatabaseError as e:
|
||||||
|
raise RuntimeError(f"Error al ejecutar la consulta: {e}")
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(f"Error inesperado: {e}")
|
||||||
|
|
||||||
def update_data(self, query: str, data_tuple: tuple) -> bool:
|
def update_data(self, query: str, data_tuple: tuple) -> bool:
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
|
120
main.py
120
main.py
@ -145,102 +145,67 @@ def solutions():
|
|||||||
def methodology():
|
def methodology():
|
||||||
return render_template(v['methodology'], active_page='methodology')
|
return render_template(v['methodology'], active_page='methodology')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/blog')
|
@app.route('/blog')
|
||||||
def blog():
|
def blog():
|
||||||
|
return render_template( v['blog']['all_posts'], active_page='blog' )
|
||||||
|
|
||||||
# if any(x in search for x in ["'", '"', " OR ", "--", ";", "1=1"]):
|
@app.route('/blog/api/posts')
|
||||||
# app.logger.warning(f"Intento de SQL injection detectado: {search}")
|
def api_posts():
|
||||||
# 🛑 IMPORTANTE: Este bloque acepta input del usuario y necesita sanitización adecuada.
|
q = r"""
|
||||||
# TODO: Reemplazar concatenación de strings por parámetros SQL usando psycopg2.sql.SQL y placeholders (%s)
|
SELECT
|
||||||
# Ejemplo de ataque detectado: ' OR '1'='1
|
p.id,
|
||||||
# Aunque no ejecuta el ataque, sí lanza error → posible vector de DoS
|
u.nombre || ' ' || u.apellido AS autor,
|
||||||
|
TO_CHAR(p.created_at AT TIME ZONE 'America/Mexico_City', 'YYYY-MM-DD HH24:MI') AS author_creation,
|
||||||
|
TO_CHAR(p.updated_at AT TIME ZONE 'America/Mexico_City', 'YYYY-MM-DD HH24:MI') AS author_updated,
|
||||||
# Parámetros
|
p.title,
|
||||||
page = request.args.get("page", 1, type=int)
|
SUBSTRING(p.body_no_img FROM 1 FOR 180) AS preview,
|
||||||
search = request.args.get("q", "").strip()
|
(SELECT value FROM jsonb_array_elements_text(p.lista_imagenes) LIMIT 1) AS first_img,
|
||||||
per_page = 9
|
ROUND(length(regexp_replace(p.body_no_img, '\s+', ' ', 'g')) / 5.0 / 375, 1) AS read_time,
|
||||||
offset = (page - 1) * per_page
|
GREATEST(1, CEIL(length(regexp_replace(p.body_no_img, '\s+', ' ', 'g')) / 5.0 / 375)) AS read_time_min
|
||||||
|
FROM posts p
|
||||||
# Armado de condiciones SQL para búsqueda
|
JOIN users u ON p.id_usr = u.id
|
||||||
search_filter = ""
|
ORDER BY p.created_at DESC;
|
||||||
if search:
|
|
||||||
like = f"'%{search}%'"
|
|
||||||
search_filter = f"""
|
|
||||||
WHERE
|
|
||||||
LOWER(p.title) LIKE LOWER({like}) OR
|
|
||||||
LOWER(p.body_no_img) LIKE LOWER({like}) OR
|
|
||||||
LOWER(u.nombre || ' ' || u.apellido) LIKE LOWER({like})
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Conteo total
|
|
||||||
count_query = f"""
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM posts p
|
|
||||||
INNER JOIN users u ON u.id = p.id_usr
|
|
||||||
{search_filter};
|
|
||||||
"""
|
"""
|
||||||
total_posts = dbUsers.get_all_data(count_query)[0][0]
|
data = dbUsers.get_all_data_dict(q)
|
||||||
total_pages = (total_posts + per_page - 1) // per_page
|
return jsonify(data)
|
||||||
|
|
||||||
# Consulta con paginación
|
|
||||||
q = fr"""
|
|
||||||
SELECT
|
|
||||||
p.id,
|
|
||||||
u.nombre,
|
|
||||||
u.apellido,
|
|
||||||
TO_CHAR(p.created_at, 'DD/MM/YYYY HH24:MI'),
|
|
||||||
TO_CHAR(p.updated_at, 'DD/MM/YYYY HH24:MI'),
|
|
||||||
p.title,
|
|
||||||
LEFT(p.body_no_img, 180) AS preview,
|
|
||||||
p.lista_imagenes->0 AS primera_imagen,
|
|
||||||
array_length(regexp_split_to_array(TRIM(body_no_img), '\s+'), 1) / 375 as n_words
|
|
||||||
FROM
|
|
||||||
posts p
|
|
||||||
INNER JOIN
|
|
||||||
users u ON u.id = p.id_usr
|
|
||||||
{search_filter}
|
|
||||||
ORDER BY
|
|
||||||
p.created_at DESC
|
|
||||||
LIMIT {per_page} OFFSET {offset};
|
|
||||||
"""
|
|
||||||
data = dbUsers.get_all_data(q)
|
|
||||||
|
|
||||||
return render_template(
|
|
||||||
v['blog']['all_posts'],
|
|
||||||
active_page='blog',
|
|
||||||
data=data,
|
|
||||||
current_page=page,
|
|
||||||
total_pages=total_pages,
|
|
||||||
search=search # pasamos el término de búsqueda a la plantilla
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/blog/<int:post_id>')
|
@app.route('/blog/<int:post_id>')
|
||||||
|
@app.route('/blog/<int:post_id>/src/<string:source_name>')
|
||||||
# @cache.cached(timeout=43200)
|
# @cache.cached(timeout=43200)
|
||||||
def blog_post(post_id):
|
def blog_post(post_id, source_name=None):
|
||||||
# q_visited = "INSERT INTO posts_visited (id_post, viewed ) VALUES ( %s, %s);"
|
# source_name = source_name or 'direct'
|
||||||
# t_visited = (post_id, cur_date())
|
# source_name = source_name.lower()
|
||||||
# dbUsers.update_data(q_visited, t_visited)
|
# if source_name not in ['facebook', 'linkedin', 'x', 'li', 'fb', 'tw', 'direct']:
|
||||||
# Obtener el post
|
# source_name = 'None'
|
||||||
|
|
||||||
|
if source_name != None:
|
||||||
|
q_vf = "INSERT INTO visited_from (post_id, source_name) VALUES (%s, %s);"
|
||||||
|
t_vf = (post_id, source_name)
|
||||||
|
dbUsers.update_data(q_vf, t_vf)
|
||||||
|
|
||||||
q = fr"""
|
q = fr"""
|
||||||
SELECT
|
SELECT
|
||||||
u.nombre,
|
u.nombre,
|
||||||
u.apellido,
|
u.apellido,
|
||||||
TO_CHAR(p.created_at, 'DD/MM/YYYY HH24:MI') AS fecha_creada,
|
TO_CHAR(p.created_at, 'DD/MM/YYYY HH24:MI') AS fecha_creada,
|
||||||
TO_CHAR(p.updated_at, 'DD/MM/YYYY HH24:MI') AS fecha_updated,
|
TO_CHAR(p.updated_at, 'DD/MM/YYYY HH24:MI') AS fecha_updated,
|
||||||
array_length(regexp_split_to_array(TRIM(p.body_no_img), '\s+'), 1) / 375 as read_time_min,
|
GREATEST(1, CEIL(length(regexp_replace(p.body_no_img, '\s+', ' ', 'g')) / 5.0 / 375)) AS read_time_min,
|
||||||
p.title,
|
p.title,
|
||||||
p.body
|
p.body,
|
||||||
|
COUNT(pv.viewed) AS total_views
|
||||||
FROM
|
FROM
|
||||||
posts p
|
posts p
|
||||||
INNER JOIN
|
INNER JOIN
|
||||||
users u
|
users u ON p.id_usr = u.id
|
||||||
ON
|
LEFT JOIN
|
||||||
p.id_usr = u.id
|
posts_visited pv ON pv.id_post = p.id
|
||||||
WHERE
|
WHERE
|
||||||
p.id = %s;
|
p.id = %s
|
||||||
|
GROUP BY
|
||||||
|
u.nombre, u.apellido, p.created_at, p.updated_at, p.title, p.body, p.body_no_img;
|
||||||
"""
|
"""
|
||||||
t = (post_id,)
|
t = (post_id,)
|
||||||
data = dbUsers.get_data(q, t)
|
data = dbUsers.get_data(q, t)
|
||||||
@ -248,9 +213,6 @@ def blog_post(post_id):
|
|||||||
return render_template(v['blog']['post'], data=data)
|
return render_template(v['blog']['post'], data=data)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/contact", methods=['GET', 'POST'])
|
@app.route("/contact", methods=['GET', 'POST'])
|
||||||
def contact():
|
def contact():
|
||||||
form = ContactForm()
|
form = ContactForm()
|
||||||
@ -544,8 +506,6 @@ def save_post():
|
|||||||
body = data['body']
|
body = data['body']
|
||||||
time = cur_date()
|
time = cur_date()
|
||||||
|
|
||||||
print(time)
|
|
||||||
|
|
||||||
soup = BeautifulSoup(body, 'html.parser')
|
soup = BeautifulSoup(body, 'html.parser')
|
||||||
body_no_img = soup.get_text(separator=' ', strip=True)
|
body_no_img = soup.get_text(separator=' ', strip=True)
|
||||||
etiquetas_img = re.findall(r'<img[^>]+src=["\'](.*?)["\']', body)
|
etiquetas_img = re.findall(r'<img[^>]+src=["\'](.*?)["\']', body)
|
||||||
|
62
static/e_blog/b_share_btn.css
Normal file
62
static/e_blog/b_share_btn.css
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
.share {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #eee;
|
||||||
|
border-radius: 2rem;
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share:hover {
|
||||||
|
width: 18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share__wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share__toggle {
|
||||||
|
background: #549c67;
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
margin: 0.25rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share__button {
|
||||||
|
background: #555;
|
||||||
|
color: white;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fb { background: #1877f2; }
|
||||||
|
.tw { background: #000000; }
|
||||||
|
.in { background: #0077b5; }
|
||||||
|
.copy { background: #444; }
|
@ -1,9 +1,16 @@
|
|||||||
import { simpleNotification } from '../z_comps/notify.js';
|
import { simpleNotification } from '../z_comps/notify.js';
|
||||||
|
|
||||||
const url = window.location.href;
|
// Limpia el /src/... si ya viene en la URL
|
||||||
const url_encoded = encodeURIComponent(url);
|
let baseUrl = window.location.href.replace(/\/src\/\w+$/, '');
|
||||||
|
|
||||||
|
function encode_url(redoSocial = '') {
|
||||||
|
let finalUrl = baseUrl;
|
||||||
|
if (redoSocial !== '') {
|
||||||
|
finalUrl += `/src/${redoSocial}`;
|
||||||
|
}
|
||||||
|
return encodeURIComponent(finalUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// Función reutilizable para abrir ventanas de compartir
|
|
||||||
function openShareWindow(shareUrl) {
|
function openShareWindow(shareUrl) {
|
||||||
window.open(shareUrl, '_blank', 'width=600,height=400,noopener,noreferrer');
|
window.open(shareUrl, '_blank', 'width=600,height=400,noopener,noreferrer');
|
||||||
}
|
}
|
||||||
@ -13,7 +20,7 @@ const btn_copy = document.querySelector("button.copy");
|
|||||||
if (btn_copy) {
|
if (btn_copy) {
|
||||||
btn_copy.addEventListener("click", async () => {
|
btn_copy.addEventListener("click", async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(url);
|
await navigator.clipboard.writeText(baseUrl);
|
||||||
simpleNotification("URL Copiada", "URL copiada", "success");
|
simpleNotification("URL Copiada", "URL copiada", "success");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error al copiar: ', err);
|
console.error('Error al copiar: ', err);
|
||||||
@ -25,7 +32,7 @@ if (btn_copy) {
|
|||||||
const btn_in = document.querySelector("button.in");
|
const btn_in = document.querySelector("button.in");
|
||||||
if (btn_in) {
|
if (btn_in) {
|
||||||
btn_in.addEventListener("click", () => {
|
btn_in.addEventListener("click", () => {
|
||||||
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${url_encoded}`;
|
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encode_url('li')}`;
|
||||||
openShareWindow(linkedInUrl);
|
openShareWindow(linkedInUrl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -34,7 +41,7 @@ if (btn_in) {
|
|||||||
const btn_fb = document.querySelector("button.fb");
|
const btn_fb = document.querySelector("button.fb");
|
||||||
if (btn_fb) {
|
if (btn_fb) {
|
||||||
btn_fb.addEventListener("click", () => {
|
btn_fb.addEventListener("click", () => {
|
||||||
const fbShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${url_encoded}`;
|
const fbShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encode_url('fb')}`;
|
||||||
openShareWindow(fbShareUrl);
|
openShareWindow(fbShareUrl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -44,7 +51,7 @@ const btn_x = document.querySelector("button.tw");
|
|||||||
if (btn_x) {
|
if (btn_x) {
|
||||||
btn_x.addEventListener("click", () => {
|
btn_x.addEventListener("click", () => {
|
||||||
const tweetText = encodeURIComponent("Mira este post interesante:");
|
const tweetText = encodeURIComponent("Mira este post interesante:");
|
||||||
const xShareUrl = `https://twitter.com/intent/tweet?url=${url_encoded}&text=${tweetText}`;
|
const xShareUrl = `https://twitter.com/intent/tweet?url=${encode_url('x')}&text=${tweetText}`;
|
||||||
openShareWindow(xShareUrl);
|
openShareWindow(xShareUrl);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
0
static/h_tmp_user/d_read_post/iframe_fullscreen.js
Normal file
0
static/h_tmp_user/d_read_post/iframe_fullscreen.js
Normal file
@ -99,5 +99,26 @@
|
|||||||
clear: both;
|
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 */
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
text-align: justify; /* Justifica el texto */
|
||||||
|
margin-bottom: 0.5em; /* Espaciado entre elementos (opcional) */
|
||||||
|
}
|
||||||
|
|
||||||
|
li p, li span {
|
||||||
|
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 */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
132
templates/e_blog/a_all_posts copy.html
Normal file
132
templates/e_blog/a_all_posts copy.html
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
{% 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 %}
|
@ -12,120 +12,344 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<form method="get" class="mb-4">
|
<style>
|
||||||
<div class="input-group">
|
.card-img-top {
|
||||||
<input type="text" class="form-control" name="q" placeholder="Buscar título, autor o contenido..." value="{{ search }}">
|
max-height: 200px;
|
||||||
<button class="btn btn-outline-primary" type="submit">
|
object-fit: cover;
|
||||||
<i class="bi bi-search"></i> Buscar
|
transition: transform 0.3s ease;
|
||||||
</button>
|
}
|
||||||
|
|
||||||
|
.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" /> -->
|
||||||
|
|
||||||
|
<input id="search-input" type="text" class="form-control" placeholder="Buscar por autor, título o resumen..." autocomplete="off">
|
||||||
|
|
||||||
|
<!-- Botón para limpiar -->
|
||||||
|
<button id="clear-btn" class="btn btn-outline-secondary" type="button">×</button>
|
||||||
|
|
||||||
|
<span class="input-group-text">
|
||||||
|
<span id="result-count">0</span> resultado(s)
|
||||||
|
</span>
|
||||||
</div>
|
</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>
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 g-4" id="card-container"></div>
|
||||||
|
|
||||||
|
<nav aria-label="Page navigation" class="mt-4">
|
||||||
|
<ul class="pagination justify-content-center" id="pagination"></ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const DOM = {
|
||||||
|
container: document.getElementById("card-container"),
|
||||||
|
pagination: document.getElementById("pagination"),
|
||||||
|
searchInput: document.getElementById("search-input"),
|
||||||
|
loading: document.getElementById("loading-indicator"),
|
||||||
|
resultCount: document.getElementById("result-count")
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
url: window.location.href.replace(/\/$/, "") + "/api/posts",
|
||||||
|
postsPerPage: 12,
|
||||||
|
maxVisiblePages: 5,
|
||||||
|
searchDelay: 300
|
||||||
|
};
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
allPosts: [],
|
||||||
|
filteredPosts: [],
|
||||||
|
currentPage: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
searchTerm: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
const utils = {
|
||||||
|
debounce: (func, delay) => {
|
||||||
|
let timeout;
|
||||||
|
return (...args) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), delay);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
normalizePost: post => ({
|
||||||
|
...post,
|
||||||
|
_autor: post.autor.toLowerCase(),
|
||||||
|
_title: post.title.toLowerCase(),
|
||||||
|
_preview: post.preview.toLowerCase()
|
||||||
|
}),
|
||||||
|
|
||||||
|
saveState: () => {
|
||||||
|
localStorage.setItem("lastPage", state.currentPage);
|
||||||
|
localStorage.setItem("lastSearch", state.searchTerm);
|
||||||
|
},
|
||||||
|
|
||||||
|
animateCards: () => {
|
||||||
|
const cards = document.querySelectorAll('.card-wrapper');
|
||||||
|
cards.forEach((card, index) => {
|
||||||
|
setTimeout(() => card.classList.add('visible'), index * 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = {
|
||||||
|
showLoading: () => {
|
||||||
|
DOM.loading.style.display = 'block';
|
||||||
|
DOM.container.style.opacity = '0.5';
|
||||||
|
},
|
||||||
|
|
||||||
|
hideLoading: () => {
|
||||||
|
DOM.loading.style.display = 'none';
|
||||||
|
DOM.container.style.opacity = '1';
|
||||||
|
},
|
||||||
|
|
||||||
|
posts: () => {
|
||||||
|
DOM.container.innerHTML = "";
|
||||||
|
|
||||||
|
const start = (state.currentPage - 1) * CONFIG.postsPerPage;
|
||||||
|
const end = start + CONFIG.postsPerPage;
|
||||||
|
const postsToShow = state.filteredPosts.slice(start, end);
|
||||||
|
|
||||||
|
if (postsToShow.length === 0) {
|
||||||
|
DOM.container.innerHTML = `
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<h4>No se encontraron resultados</h4>
|
||||||
|
<p class="text-muted">Intenta con otros términos de búsqueda</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
postsToShow.forEach(post => {
|
||||||
|
const imgSrc = post.first_img || "{{ url_for('static', filename='y_img/other/no_img.png') }}";
|
||||||
|
const dateUpdate = post.author_updated ? `<i class="bi bi-arrow-repeat"></i> ${post.author_updated}<br>` : "";
|
||||||
|
|
||||||
|
const card = document.createElement("div");
|
||||||
|
card.className = "col card-wrapper";
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="card h-100">
|
||||||
|
<img src="${imgSrc}" class="card-img-top" alt="${post.title}" loading="lazy">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<div class="mb-3">
|
||||||
|
<a href="/blog/${post.id}" class="btn btn-info stretched-link" onclick="utils.saveState()">
|
||||||
|
<h5 class="card-title">${post.title}</h5>
|
||||||
|
</a>
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
<i class="bi bi-file-person-fill"></i> ${post.autor}<br>
|
||||||
|
<i class="bi bi-calendar-week"></i> ${post.author_creation}<br>
|
||||||
|
${dateUpdate}
|
||||||
|
<i class="bi bi-clock"></i> ${post.read_time_min} min.
|
||||||
|
</small>
|
||||||
|
<p class="card-text mt-2">${post.preview}...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
DOM.container.appendChild(card);
|
||||||
|
});
|
||||||
|
|
||||||
|
utils.animateCards();
|
||||||
|
},
|
||||||
|
|
||||||
|
pagination: () => {
|
||||||
|
DOM.pagination.innerHTML = "";
|
||||||
|
state.totalPages = Math.ceil(state.filteredPosts.length / CONFIG.postsPerPage);
|
||||||
|
DOM.resultCount.textContent = state.filteredPosts.length;
|
||||||
|
|
||||||
|
const pageItem = (page, label = null, disabled = false, active = false) => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = `page-item ${disabled ? 'disabled' : ''} ${active ? 'active' : ''}`;
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.className = "page-link";
|
||||||
|
a.href = "#";
|
||||||
|
a.textContent = label || page;
|
||||||
|
if (!disabled && !active) {
|
||||||
|
a.addEventListener("click", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
state.currentPage = page;
|
||||||
|
utils.saveState();
|
||||||
|
render.updateView();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
li.appendChild(a);
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageDots = () => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
li.className = "page-item disabled";
|
||||||
|
li.innerHTML = `<span class="page-link">...</span>`;
|
||||||
|
return li;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Botón anterior
|
||||||
|
DOM.pagination.appendChild(pageItem(state.currentPage - 1, "«", state.currentPage === 1));
|
||||||
|
|
||||||
|
const startPage = Math.max(1, state.currentPage - Math.floor(CONFIG.maxVisiblePages / 2));
|
||||||
|
const endPage = Math.min(state.totalPages, startPage + CONFIG.maxVisiblePages - 1);
|
||||||
|
|
||||||
|
if (startPage > 1) {
|
||||||
|
DOM.pagination.appendChild(pageItem(1));
|
||||||
|
if (startPage > 2) DOM.pagination.appendChild(pageDots());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
DOM.pagination.appendChild(pageItem(i, null, false, i === state.currentPage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endPage < state.totalPages) {
|
||||||
|
if (endPage < state.totalPages - 1) DOM.pagination.appendChild(pageDots());
|
||||||
|
DOM.pagination.appendChild(pageItem(state.totalPages));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Botón siguiente
|
||||||
|
DOM.pagination.appendChild(pageItem(state.currentPage + 1, "»", state.currentPage === state.totalPages));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateView: () => {
|
||||||
|
render.posts();
|
||||||
|
render.pagination();
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const search = {
|
||||||
|
filterPosts: term => {
|
||||||
|
const newSearchTerm = term.toLowerCase(); // convertir el término a minúsculas
|
||||||
|
const isSameSearch = newSearchTerm === state.searchTerm;
|
||||||
|
state.searchTerm = newSearchTerm;
|
||||||
|
|
||||||
|
if (!state.searchTerm) {
|
||||||
|
state.filteredPosts = [...state.allPosts];
|
||||||
|
} else {
|
||||||
|
state.filteredPosts = state.allPosts.filter(post =>
|
||||||
|
post._autor.toLowerCase().includes(state.searchTerm) ||
|
||||||
|
post._title.toLowerCase().includes(state.searchTerm) ||
|
||||||
|
post._preview.toLowerCase().includes(state.searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSameSearch) {
|
||||||
|
state.currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
render.updateView();
|
||||||
|
},
|
||||||
|
|
||||||
|
init: () => {
|
||||||
|
DOM.searchInput.addEventListener("input", utils.debounce(() => {
|
||||||
|
search.filterPosts(DOM.searchInput.value);
|
||||||
|
}, CONFIG.searchDelay));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const init = {
|
||||||
|
loadPosts: () => {
|
||||||
|
render.showLoading();
|
||||||
|
fetch(CONFIG.url)
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error('Error en la red');
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
state.allPosts = data.map(utils.normalizePost);
|
||||||
|
|
||||||
|
const lastSearch = localStorage.getItem("lastSearch") || "";
|
||||||
|
DOM.searchInput.value = lastSearch;
|
||||||
|
state.searchTerm = lastSearch.toLowerCase();
|
||||||
|
|
||||||
|
if (!state.searchTerm) {
|
||||||
|
state.filteredPosts = [...state.allPosts];
|
||||||
|
} else {
|
||||||
|
state.filteredPosts = state.allPosts.filter(post =>
|
||||||
|
post._autor.includes(state.searchTerm) ||
|
||||||
|
post._title.includes(state.searchTerm) ||
|
||||||
|
post._preview.includes(state.searchTerm)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastPage = parseInt(localStorage.getItem("lastPage")) || 1;
|
||||||
|
state.totalPages = Math.ceil(state.filteredPosts.length / CONFIG.postsPerPage);
|
||||||
|
state.currentPage = Math.min(lastPage, state.totalPages || 1);
|
||||||
|
|
||||||
|
render.updateView();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
DOM.container.innerHTML = `
|
||||||
|
<div class="col-12 text-center py-5">
|
||||||
|
<h4>Error al cargar los posts</h4>
|
||||||
|
<p class="text-muted">Por favor intenta recargar la página</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
render.hideLoading();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
init.loadPosts();
|
||||||
|
search.init();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- {# aos script #} -->
|
<!-- {# aos script #} -->
|
||||||
{% include 'z_comps/aos_script.html' %}
|
{% include 'z_comps/aos_script.html' %}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="{{ url_for( 'static', filename='h_tmp_user/d_read_post/read_post.css' ) }}">
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='h_tmp_user/d_read_post/read_post.css' ) }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for( 'static', filename='e_blog/b_share_btn.css' ) }}">
|
||||||
{% endblock css %}
|
{% endblock css %}
|
||||||
|
|
||||||
{% block navbar %}
|
{% block navbar %}
|
||||||
@ -10,87 +11,19 @@
|
|||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<style>
|
{{data}}
|
||||||
.share {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: #eee;
|
|
||||||
border-radius: 2rem;
|
|
||||||
width: 3rem;
|
|
||||||
height: 3rem;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share:hover {
|
|
||||||
width: 18rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share__wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share__toggle {
|
|
||||||
background: #549c67;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
margin: 0.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share__button {
|
|
||||||
background: #555;
|
|
||||||
color: white;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.fb { background: #1877f2; }
|
|
||||||
.tw { background: #000000; }
|
|
||||||
.in { background: #0077b5; }
|
|
||||||
.copy { background: #444; }
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="pst-cont">
|
<div class="pst-cont">
|
||||||
<h1>{{data[5]}}</h1>
|
<h1>{{data[5]}}</h1>
|
||||||
|
|
||||||
|
|
||||||
<spam>
|
<spam>
|
||||||
<a type="button" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Publicaciones</a> <br>
|
<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-person-circle"></i> {{data[0]}} {{data[1]}} |
|
||||||
<i class="bi bi-pencil-square"></i> {{data[2]}} |
|
<i class="bi bi-pencil-square"></i> {{data[2]}} |
|
||||||
{% if data[3] is not none %}
|
{% if data[3] is not none %}
|
||||||
<i class="bi bi-arrow-repeat"></i> {{data[3]}} |
|
<i class="bi bi-arrow-repeat"></i> {{data[3]}} |
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<i class="bi bi-clock-history"></i> {{ data[4] }} Minutos |
|
<i class="bi bi-clock-history"></i> {{ data[4] }} Minutos |
|
||||||
<i class="bi bi-eye"></i>
|
<i class="bi bi-eye"></i> {{ data[7] }}
|
||||||
</spam>
|
</spam>
|
||||||
|
|
||||||
<div class="share">
|
<div class="share">
|
||||||
@ -103,7 +36,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div >
|
<div class="bd_post">
|
||||||
|
|
||||||
{{data[6] | safe}}
|
{{data[6] | safe}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -119,8 +53,13 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
{% include "z_comps/read_progress.html" %}
|
||||||
|
|
||||||
<script type="module" src="{{ url_for('static', filename='e_blog/copy_url.js') }}"></script>
|
<script type="module" src="{{ url_for('static', filename='e_blog/copy_url.js') }}"></script>
|
||||||
|
|
||||||
|
@ -7,7 +7,26 @@
|
|||||||
{% block body %}
|
{% 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">
|
<div class="pst-cont">
|
||||||
<h1>{{data[2]}}</h1>
|
<h1>{{data[2]}}</h1>
|
||||||
@ -21,13 +40,30 @@
|
|||||||
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="bd-post">
|
||||||
{{data[3] | safe}}
|
{{data[3] | safe}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const iframes = document.querySelectorAll("iframe.note-video-clip");
|
||||||
|
iframes.forEach(iframe => {
|
||||||
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "video-responsive";
|
||||||
|
iframe.parentNode.insertBefore(wrapper, iframe);
|
||||||
|
wrapper.appendChild(iframe);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock body %}
|
{% endblock body %}
|
||||||
|
|
||||||
@ -36,4 +72,8 @@
|
|||||||
{% include 'z_comps/arrow_to_up.html' %}
|
{% include 'z_comps/arrow_to_up.html' %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% endblock js %}
|
{% endblock js %}
|
72
templates/z_comps/read_progress.html
Normal file
72
templates/z_comps/read_progress.html
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<!-- Bootstrap Progress Bar -->
|
||||||
|
<div class="progress reading-progress-bar" id="reading-progress">
|
||||||
|
<div class="progress-bar" role="progressbar"
|
||||||
|
style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
<span class="progress-label">0%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- CSS personalizado para que flote encima del navbar y tenga color amarillo -->
|
||||||
|
<style>
|
||||||
|
.reading-progress-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
/* fondo del contenedor */
|
||||||
|
}
|
||||||
|
|
||||||
|
.reading-progress-bar .progress-bar {
|
||||||
|
background-color: #ffc107;
|
||||||
|
height: 100%;
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: black;
|
||||||
|
/* Asegúrate que sea visible */
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- JavaScript para actualizar la barra -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
const progressBarContainer = document.getElementById("reading-progress");
|
||||||
|
const progressBar = progressBarContainer.querySelector(".progress-bar");
|
||||||
|
const postContainer = document.querySelector(".pst-cont");
|
||||||
|
|
||||||
|
function updateProgress() {
|
||||||
|
const scrollTop = window.scrollY;
|
||||||
|
const offsetTop = postContainer.offsetTop;
|
||||||
|
const postHeight = postContainer.scrollHeight;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
const totalScrollable = postHeight - offsetTop - windowHeight;
|
||||||
|
|
||||||
|
if (totalScrollable <= 0) {
|
||||||
|
progressBarContainer.style.display = "none";
|
||||||
|
} else {
|
||||||
|
const progress = Math.min((scrollTop - offsetTop) / totalScrollable, 1);
|
||||||
|
const progressPercent = progress >= 0 ? progress * 100 : 0;
|
||||||
|
progressBar.style.width = `${progressPercent}%`;
|
||||||
|
progressBar.setAttribute("aria-valuenow", progressPercent.toFixed(1));
|
||||||
|
progressBarContainer.style.display = "block";
|
||||||
|
progressBar.style.width = `${progressPercent}%`;
|
||||||
|
progressBar.setAttribute("aria-valuenow", progressPercent.toFixed(1));
|
||||||
|
progressBar.querySelector(".progress-label").textContent = `${Math.round(progressPercent)}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("scroll", updateProgress);
|
||||||
|
window.addEventListener("resize", updateProgress);
|
||||||
|
|
||||||
|
updateProgress();
|
||||||
|
});
|
||||||
|
</script>
|
Loading…
x
Reference in New Issue
Block a user