mejoramiento de sección blog en vista pública

This commit is contained in:
David Itehua Xalamihua 2025-04-30 18:08:23 -06:00
parent b661a265ea
commit 8273b48b6b
10 changed files with 381 additions and 231 deletions

25
main.py
View File

@ -218,10 +218,11 @@ def blog():
@app.route('/blog/<int:post_id>')
# @cache.cached(timeout=43200)
def blog_post(post_id):
q_visited = "INSERT INTO posts_visited (id_post, viewed ) VALUES ( %s, %s);"
t_visited = (post_id, cur_date())
dbUsers.update_data(q_visited, t_visited)
# q_visited = "INSERT INTO posts_visited (id_post, viewed ) VALUES ( %s, %s);"
# t_visited = (post_id, cur_date())
# dbUsers.update_data(q_visited, t_visited)
# Obtener el post
q = fr"""
SELECT
@ -247,6 +248,9 @@ def blog_post(post_id):
return render_template(v['blog']['post'], data=data)
@app.route("/contact", methods=['GET', 'POST'])
def contact():
form = ContactForm()
@ -524,6 +528,7 @@ def download_db():
@app.route('/user/txt-editor')
@jwt_required()
@validate_user_exists
@cache.cached(timeout=43200)
def user_txteditor():
template_name = v['tmp_user'].get('txt_editor')
return render_template(template_name, active_page='user_txteditor')
@ -537,6 +542,9 @@ def save_post():
data = request.get_json()
title = data['title']
body = data['body']
time = cur_date()
print(time)
soup = BeautifulSoup(body, 'html.parser')
body_no_img = soup.get_text(separator=' ', strip=True)
@ -547,10 +555,10 @@ def save_post():
t = None
if "id" in data:
q = 'UPDATE posts SET updated_at = %s, title = %s, body = %s, body_no_img = %s, lista_imagenes = %s::jsonb WHERE id = %s AND id_usr = %s;'
t = (cur_date(), title, body, body_no_img, imagenes_json, data['id'], id_usr)
t = (time, title, body, body_no_img, imagenes_json, data['id'], id_usr)
else:
q = "INSERT INTO posts (id_usr, created_at, title, body, body_no_img, lista_imagenes) VALUES (%s, %s, %s, %s, %s, %s::jsonb);"
t = (id_usr, cur_date(), title, body, body_no_img, imagenes_json)
t = (id_usr, time, title, body, body_no_img, imagenes_json)
try:
dbUsers.update_data(q, t)
@ -583,7 +591,7 @@ def my_posts():
title,
LEFT(body_no_img, 180),
lista_imagenes->0,
array_length(regexp_split_to_array(TRIM(body_no_img), '\s+'), 1) / 375 as n_words
GREATEST(array_length(regexp_split_to_array(TRIM(body_no_img), '\s+'), 1) / 375, 1) as n_words
FROM
posts
WHERE
@ -689,6 +697,11 @@ def metrics():
@validate_user_exists
@admin_required
def data_metrics():
# SELECT pv.id_post, pv.viewed, u.id, 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')

50
static/e_blog/copy_url.js Normal file
View File

@ -0,0 +1,50 @@
import { simpleNotification } from '../z_comps/notify.js';
const url = window.location.href;
const url_encoded = encodeURIComponent(url);
// Función reutilizable para abrir ventanas de compartir
function openShareWindow(shareUrl) {
window.open(shareUrl, '_blank', 'width=600,height=400,noopener,noreferrer');
}
// Copiar al portapapeles
const btn_copy = document.querySelector("button.copy");
if (btn_copy) {
btn_copy.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(url);
simpleNotification("URL Copiada", "URL copiada", "success");
} catch (err) {
console.error('Error al copiar: ', err);
}
});
}
// LinkedIn
const btn_in = document.querySelector("button.in");
if (btn_in) {
btn_in.addEventListener("click", () => {
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${url_encoded}`;
openShareWindow(linkedInUrl);
});
}
// Facebook
const btn_fb = document.querySelector("button.fb");
if (btn_fb) {
btn_fb.addEventListener("click", () => {
const fbShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${url_encoded}`;
openShareWindow(fbShareUrl);
});
}
// X / Twitter
const btn_x = document.querySelector("button.tw");
if (btn_x) {
btn_x.addEventListener("click", () => {
const tweetText = encodeURIComponent("Mira este post interesante:");
const xShareUrl = `https://twitter.com/intent/tweet?url=${url_encoded}&text=${tweetText}`;
openShareWindow(xShareUrl);
});
}

View File

@ -0,0 +1,103 @@
.pst-cont{
width: 65%;
min-height: 80%;
margin: auto;
}
.pst-cont {
margin: 2rem auto;
padding: 2rem;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px 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);
}
}
.pst-cont > div:first-of-type {
margin-bottom: 2rem;
display: flex;
gap: 1rem;
}
/* Estilos para el contenido del post */
.pst-cont > div:last-of-type {
font-size: 1.1rem;
line-height: 1.8;
}
.pst-cont p {
text-align: justify;
text-justify: inter-word;
margin-bottom: 1.5rem;
}
.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 {
max-width: 100%;
margin-left: auto !important;
margin-right: auto !important;
border-radius: 4px;
margin: 1.5rem 0;
display: block;
}
.pst-cont::after {
content: "";
display: table;
clear: both;
}

View File

@ -72,37 +72,6 @@ document.querySelector('[name="title"]').value = title_post;
// ENVIAR LOS NUEVOS CAMBIOS A LA BASE DE DATOS
// async function a_sendpost(title, body, id) {
// try {
// let response = await fetch('/user/save-post', {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json'
// },
// body: JSON.stringify({ "id": id, "title": title, "body": body}),
// credentials: 'include'
// });
// if (!response.ok) {
// throw new Error('Error en la respuesta del servidor <-');
// }
// const result = await response.json();
// simpleNotification('Éxito', `Publicación actualizada: ${result.title_post}`, 'success');
// // Opcional: Limpiar el editor después de guardar
// $('#summernote').summernote('code', '');
// document.querySelector("input[name='title']").value = '';
// } catch (error) {
// console.error('Error al guardar:', error);
// simpleNotification('Error', 'No se pudo guardar la publicación', 'error');
// }
// }
async function a_sendpost(title, body, id) {
try {
let response = await fetch('/user/save-post', {

View File

@ -1,12 +1,12 @@
.form-control{
width: 100% !important;
width: 50% !important;
margin-bottom: 1em !important;
margin-top: 1em !important;
}
.note-editor {
width: 100% !important;
min-height: 65vh !important;
width: 50% !important;
min-height: 70vh !important;
}
.note-editable {
@ -25,6 +25,7 @@ div.note-toolbar{
div.note-editing-area {
background-color: white !important;
}
/* aplica en la sección de edición del post */
@ -39,4 +40,3 @@ div.note-modal-backdrop {
}

View File

@ -10,12 +10,12 @@
{% block body %}
<div class="container py-5">
<div class="container py-5" data-aos="fade-up" data-aos-delay="0" data-aos-duration="800">
<h2 class="text-center mb-4">Nuestra Metodología</h2>
<p class="text-center text-muted mb-5">Implementamos soluciones con enfoque humano, técnico y estratégico.</p>
<div class="row g-4">
<div class="col-md-6" data-aos="fade-up" data-aos-delay="0" data-aos-duration="800">
<div class="col-md-6">
<div class="card shadow-sm h-100" style="border: 2px solid#90b83b;">
<div class="card-body">
<h5 class="card-title" style="color: #90b83b;"><i class="bi bi-bar-chart-line me-2"></i> ANÁLISIS DE DATOS</h5>
@ -24,7 +24,7 @@
</div>
</div>
<div class="col-md-6" data-aos="fade-up" data-aos-delay="200" data-aos-duration="800">
<div class="col-md-6">
<div class="card shadow-sm h-100" style="border: 2px solid#8c1f5a;">
<div class="card-body">
<h5 class="card-title" style="color: #8c1f5a;"><i class="bi bi-sliders me-2"></i> PERSONALIZACIÓN</h5>
@ -33,7 +33,7 @@
</div>
</div>
<div class="col-md-6" data-aos="fade-up" data-aos-delay="400" data-aos-duration="800">
<div class="col-md-6">
<div class="card shadow-sm h-100" style="border: 2px solid#5d9dd1;">
<div class="card-body">
<h5 class="card-title" style="color: #5d9dd1;"><i class="bi bi-people me-2"></i> EXPERIENCIA DE SERVICIO</h5>
@ -42,7 +42,7 @@
</div>
</div>
<div class="col-md-6" data-aos="fade-up" data-aos-delay="600" data-aos-duration="800">
<div class="col-md-6">
<div class="card shadow-sm h-100" style="border: 2px solid#002e72;">
<div class="card-body">
<h5 class="card-title" style="color: #002e72;"><i class="bi bi-graph-up-arrow me-2"></i> IMPACTO</h5>
@ -51,7 +51,7 @@
</div>
</div>
<div class="col-md-12" data-aos="fade-up" data-aos-delay="800" data-aos-duration="800">
<div class="col-md-12">
<div class="card shadow-sm h-100" style="border: 2px solid#707272;">
<div class="card-body">
<h5 class="card-title" style="color: #707272;"><i class="bi bi-arrow-repeat me-2"></i> SOSTENIBILIDAD DEL CAMBIO</h5>

View File

@ -10,6 +10,8 @@
{% 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 }}">
@ -19,91 +21,17 @@
</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" data-aos="fade-up" data-aos-delay="0" data-aos-duration="800">
{% for post in data %}
<div class="col-12 col-sm-8 col-md-6 col-lg-4" >
<div class="card">
<img class="card-img" src="{{ post[7] if post[7] else url_for('static', filename='y_img/other/no_img.png') }}" alt="">
<!-- <div class="card-img-overlay"><a href="#" class="btn btn-light btn-sm">Cooking</a></div> -->
<div class="card-body">
<h4 class="card-title">
<a href="{{ url_for('blog_post', post_id = post[0] ) }}" class="btn btn-info">
{{ post[5] }} <!-- {# título #} -->
</a>
</h4>
<small class="text-muted cat">
<!-- {# autor #} -->
<i class="bi bi-person-circle"></i> {{ post[1] }} {{ post[2] }} <br>
<i class="bi bi-clock-history"></i> {{ post[8] }} Min.
<!-- <i class="far fa-clock text-info"></i> 30 minutes -->
<!-- <i class="fas fa-users text-info"></i> 4 portions -->
</small>
<!-- {# breve contenido #} -->
<p class="card-text">{{ post[6] }}...</p>
<!-- <a href="{{ post[0] }}" class="btn btn-info">
<i class="bi bi-eye-fill"></i>
</a> -->
</div>
<div class="card-footer text-muted d-flex justify-content-between bg-transparent border-top-0">
<div class="views">
<i class="bi bi-vector-pen"></i> {{ post[3] }}
{% if post[4] is not none %}
<i class="bi bi-arrow-repeat"></i> {{ post[4] }}
{% endif %}
</div>
<!-- {#
<div class="stats">
<i class="bi bi-eye-fill"></i> No. Vistas
<i class="far fa-comment"></i> 12
</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 %}">
@ -117,7 +45,75 @@
<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 #} -->

View File

@ -1,6 +1,7 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{ url_for( 'static', filename='h_tmp_user/d_read_post/read_post.css' ) }}">
{% endblock css %}
{% block navbar %}
@ -9,73 +10,120 @@
{% block body %}
<style>
.share {
position: relative;
display: flex;
align-items: center;
background: #eee;
border-radius: 2rem;
width: 3rem;
height: 3rem;
overflow: hidden;
transition: width 0.3s ease;
}
<h1>{{data[5]}}</h1>
<h5>
<i class="bi bi-person-circle"></i> {{data[0]}} {{data[1]}} |
<i class="bi bi-pencil-square"></i> {{data[2]}} |
{% if data[3] is not none %}
<i class="bi bi-arrow-repeat"></i> {{data[3]}} |
{% endif %}
<i class="bi bi-clock-history"></i> {{ data[4] }} Minutos
</h5>
.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">
<h1>{{data[5]}}</h1>
<spam>
<a type="button" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Publicaciones</a> <br>
<i class="bi bi-person-circle"></i> {{data[0]}} {{data[1]}} |
<i class="bi bi-pencil-square"></i> {{data[2]}} |
{% if data[3] is not none %}
<i class="bi bi-arrow-repeat"></i> {{data[3]}} |
{% endif %}
<i class="bi bi-clock-history"></i> {{ data[4] }} Minutos |
<i class="bi bi-eye"></i>
</spam>
<div class="share">
<div class="share__wrapper">
<div class="share__toggle"><i class="bi bi-share-fill"></i></div>
<button href="#" class="share__button fb"><i class="bi bi-facebook"></i></button>
<button href="#" class="share__button tw"><i class="bi bi-twitter-x"></i></button>
<button class="share__button in"><i class="bi bi-linkedin"></i></button>
<button class="share__button copy"><i class="bi bi-link-45deg"></i></button>
</div>
</div>
<div >
{{data[6] | safe}}
</div>
<div style="width: 60vw;">
{{data[6] | safe}}
</div>
<!-- inicio pruebas scroll to up -->
<style>
#btn-subir {
position: fixed;
bottom: 10%;
right: 5%;
width: 50px;
height: 50px;
border-radius: 50%;
background: #333;
color: white;
border: none;
cursor: pointer;
font-size: 18px;
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
transition: opacity 0.3s;
z-index: 1000;
}
#btn-subir:hover {
background: #555;
}
.oculto {
opacity: 0;
pointer-events: none;
}
</style>
<button id="btn-subir" class="oculto"></button>
<script>
const btnSubir = document.getElementById('btn-subir');
// Mostrar u ocultar el botón según el scroll
window.addEventListener('scroll', () => {
if (window.scrollY > 300) { // Mostrar después de 300px de scroll
btnSubir.classList.remove('oculto');
} else {
btnSubir.classList.add('oculto');
}
});
// Función para subir al inicio
btnSubir.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth' // Desplazamiento suave
});
document.querySelectorAll('.share__toggle').forEach(btn => {
btn.addEventListener('click', () => {
btn.closest('.share').classList.toggle('open');
});
});
</script>
<!-- fin pruebas -->
{% endblock body %}
{% block js %}
<script type="module" src="{{ url_for('static', filename='e_blog/copy_url.js') }}"></script>
<!-- {# flecha ir hasta arriba #} -->
{% include 'z_comps/arrow_to_up.html' %}
{% endblock js %}

View File

@ -1,48 +1,13 @@
{% extends 'h_tmp_usr/z_tmp.html' %}
{% block css %}
<link rel="stylesheet" href="{{ url_for( 'static', filename='h_tmp_user/d_read_post/read_post.css' ) }}">
{% endblock css %}
{% block body %}
<style>
.pst-cont{
width: 50% !important;
margin-left: auto;
margin-right: auto;
& img {
max-width: 100%; /* La imagen no superará el ancho del contenedor */
height: auto; /* Mantiene la proporción */
display: block; /* Elimina espacios no deseados debajo de la imagen */
}
& p {
text-align: justify !important;
text-justify: inter-word !important;
}
& .note-float-left, .note-float-right{
margin-top: 1em;
margin-bottom: 1em;
}
& .note-float-left {
margin-right: 1em;
}
& .note-float-right{
margin-left: 1em;
}
}
</style>
<div class="pst-cont">
<h1>{{data[2]}}</h1>
@ -56,10 +21,14 @@
<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>
{{data[3] | safe}}
<div>
{{data[3] | safe}}
</div>
</div>
{% endblock body %}
{% block js %}
@ -67,15 +36,4 @@
{% include 'z_comps/arrow_to_up.html' %}
<script>
let cont = document.querySelector('div.pst-cont');
// let lst_img = cont.querySelectorAll('p img[style="width: 2515px;]');
let lst_img = cont.querySelectorAll('p img[style*="width: 2515px;"]');
lst_img.forEach(ele => {
console.log(ele);
})
</script>
{% endblock js %}

View File

@ -5,6 +5,8 @@
<!-- {# estilos editor de texto #} -->
<link rel="stylesheet" href="{{ url_for('static', filename='h_tmp_user/text_editor.css' ) }}">
<link rel="stylesheet" href="{{ url_for( 'static', filename='h_tmp_user/d_read_post/read_post.css' ) }}">
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/summernote@0.9.0/dist/summernote-lite.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/summernote@0.9.0/dist/summernote-lite.min.js"></script>
@ -22,12 +24,23 @@
<div id="summernote"></div>
<button type="submit" class="btn btn-success" id="btn-submit"><i class="bi bi-file-earmark-richtext-fill"></i> Enviar Cambios</button>
<a class="btn btn-warning" href="{{ url_for('my_posts') }}" role="button" id="btn-cancel">Cancelar</a>
</form>
{% endblock body %}
{% block js %}
<!-- {# flecha ir hasta arriba #} -->
{% include 'z_comps/arrow_to_up.html' %}