preliminar beta

This commit is contained in:
David Itehua Xalamihua 2025-04-24 17:52:32 -06:00
parent 3bb9ac97d2
commit 485cc0684b
116 changed files with 18560 additions and 1337 deletions

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg2"
width="741.34265"
height="687.81335"
viewBox="0 0 741.34265 687.81335"
sodipodi:docname="logo_final.ai"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs6">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath16">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
id="path14" />
</clipPath>
</defs>
<sodipodi:namedview
id="namedview4"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1" />
<g
id="g8"
inkscape:groupmode="layer"
inkscape:label="logo_final"
transform="matrix(1.3333333,0,0,-1.3333333,0,687.81333)">
<g
id="g10">
<g
id="g12"
clip-path="url(#clipPath16)">
<g
id="g18"
transform="translate(278.0034,257.5454)">
<path
d="m 0,0 c -27.777,0 -50.295,22.518 -50.295,50.295 0,27.777 22.518,50.295 50.295,50.295 27.777,0 50.295,-22.518 50.295,-50.295 C 50.295,22.518 27.777,0 0,0 m 0,161.68 c -61.516,0 -111.385,-49.869 -111.385,-111.385 0,-61.516 49.869,-111.385 111.385,-111.385 61.517,0 111.385,49.869 111.385,111.385 C 111.385,111.811 61.517,161.68 0,161.68"
style="fill:#293172;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path20" />
</g>
<g
id="g22"
transform="translate(278.0034,257.5454)">
<path
d="m 0,0 c -27.777,0 -50.295,22.518 -50.295,50.295 0,27.777 22.518,50.295 50.295,50.295 27.777,0 50.295,-22.518 50.295,-50.295 C 50.295,22.518 27.777,0 0,0 m 0,153.912 c -57.226,0 -103.617,-46.391 -103.617,-103.617 0,-57.226 46.391,-103.617 103.617,-103.617 57.226,0 103.617,46.391 103.617,103.617 0,57.226 -46.391,103.617 -103.617,103.617"
style="fill:#189dd9;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path24" />
</g>
<g
id="g26"
transform="translate(232.5616,286.2715)">
<path
d="m 0,0 c -3.109,6.537 -4.853,13.849 -4.853,21.569 0,7.729 1.747,15.047 4.862,21.59 7.326,24.212 29.478,42.277 74.197,42.277 8.439,0 16.581,-1.262 24.252,-3.605 2.724,-0.831 4.536,2.798 2.221,4.456 -17.261,12.362 -38.405,19.644 -61.255,19.644 -54.683,0 -99.628,-41.658 -104.852,-94.968 5.343,-56.539 52.932,-100.779 110.87,-100.779 18.079,0 35.136,4.335 50.234,11.979 -6.849,-1.829 -14.044,-2.81 -21.47,-2.81 C 42.946,-80.647 9.739,-40.708 0,0"
style="fill:#a4195b;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path28" />
</g>
<g
id="g30"
transform="translate(381.6201,289.5588)">
<path
d="m 0,0 c 0,8.188 -1.537,16.013 -4.312,23.224 -0.146,0.381 -0.295,0.76 -0.448,1.137 -0.218,0.544 -0.452,1.079 -0.683,1.616 -0.724,1.677 -1.49,3.333 -2.344,4.936 -0.005,-0.029 -0.011,-0.058 -0.015,-0.087 -10.954,20.173 -32.324,33.869 -56.893,33.869 -5.355,0 -10.554,-0.663 -15.531,-1.89 16.618,-8.749 27.75,-26.517 26.854,-46.801 -1.143,-25.894 -22.283,-46.94 -48.182,-47.976 -20.408,-0.816 -38.239,10.541 -46.857,27.392 -9.913,13.159 -15.795,29.525 -15.795,47.27 0,5.351 0.54,10.575 1.562,15.624 0.532,2.628 -2.878,4.12 -4.503,1.988 -12.623,-16.564 -19.832,-37.474 -18.957,-60.104 1.889,-48.834 41.94,-88.287 90.797,-89.481 45.027,-1.102 83.018,29.749 92.971,71.479 0.327,1.369 0.614,2.754 0.878,4.146 0.693,3.22 1.148,6.527 1.342,9.905 0.011,0.141 0.029,0.28 0.04,0.421 C -0.032,-2.335 0,-1.335 0,-0.327 0,-0.278 -0.005,-0.23 -0.005,-0.181 -0.005,-0.12 0,-0.061 0,0"
style="fill:#7db928;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path32" />
</g>
<g
id="g34"
transform="translate(116.363,97.5947)">
<path
d="M 0,0 V 56.239 H 38.554 V 46.726 H 11.355 V 33.413 H 34.833 V 23.899 H 11.355 L 11.355,0 Z"
style="fill:#747474;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path36" />
</g>
<g
id="g38"
transform="translate(173.6766,125.7527)">
<path
d="m 0,0 c 0,-6.368 1.471,-11.196 4.412,-14.481 2.94,-3.288 6.675,-4.93 11.202,-4.93 4.526,0 8.241,1.63 11.143,4.89 2.903,3.262 4.355,8.153 4.355,14.674 0,6.445 -1.413,11.253 -4.239,14.425 -2.826,3.171 -6.579,4.757 -11.259,4.757 -4.681,0 -8.454,-1.606 -11.317,-4.814 C 1.432,11.31 0,6.47 0,0 m -11.701,-0.384 c 0,5.729 0.857,10.537 2.571,14.425 1.278,2.864 3.024,5.434 5.237,7.711 2.211,2.275 4.635,3.963 7.269,5.064 3.504,1.482 7.544,2.224 12.122,2.224 8.287,0 14.917,-2.57 19.892,-7.71 4.973,-5.141 7.461,-12.29 7.461,-21.446 0,-9.079 -2.469,-16.183 -7.404,-21.31 -4.936,-5.127 -11.534,-7.692 -19.795,-7.692 -8.363,0 -15.013,2.552 -19.949,7.655 -4.936,5.101 -7.404,12.128 -7.404,21.079"
style="fill:#747474;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path40" />
</g>
<g
id="g42"
transform="translate(236.7829,130.0493)">
<path
d="m 0,0 h 8.401 c 5.448,0 8.849,0.23 10.205,0.691 1.355,0.46 2.417,1.253 3.184,2.378 0.767,1.125 1.151,2.532 1.151,4.22 0,1.892 -0.506,3.419 -1.515,4.584 -1.011,1.164 -2.437,1.9 -4.278,2.207 -0.921,0.126 -3.683,0.191 -8.286,0.191 l -8.862,0 z m -11.355,-32.455 v 56.24 h 23.9 c 6.009,0 10.376,-0.506 13.101,-1.515 2.723,-1.011 4.903,-2.807 6.54,-5.39 1.636,-2.584 2.455,-5.538 2.455,-8.863 0,-4.218 -1.241,-7.704 -3.721,-10.453 -2.481,-2.75 -6.19,-4.483 -11.125,-5.199 2.455,-1.432 4.482,-3.004 6.081,-4.718 1.598,-1.713 3.753,-4.757 6.464,-9.13 l 6.866,-10.972 h -13.58 l -8.209,12.237 c -2.916,4.375 -4.911,7.13 -5.985,8.268 -1.074,1.138 -2.212,1.919 -3.414,2.34 -1.203,0.423 -3.108,0.633 -5.716,0.633 H 0 v -23.478 z"
style="fill:#747474;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path44" />
</g>
<g
id="g46"
transform="translate(281.9739,97.5947)">
<path
d="M 0,0 V 56.239 H 16.995 L 27.199,17.877 37.288,56.239 H 54.321 V 0 H 43.772 V 44.27 L 32.608,0 H 21.675 L 10.55,44.27 V 0 Z"
style="fill:#747474;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path48" />
</g>
<g
id="g50"
transform="translate(347.6121,97.5947)">
<path
d="M 0,0 V 56.239 H 11.355 V 34.105 h 22.25 V 56.239 H 44.96 V 0 H 33.605 V 24.591 H 11.355 L 11.355,0 Z"
style="fill:#7db928;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path52" />
</g>
<path
d="m 424.145,154.792 h 9.284 v -9.245 h -9.284 z m 2.571,-37.632 c -1.381,-0.461 -3.568,-1.011 -6.56,-1.651 -2.993,-0.639 -4.948,-1.265 -5.869,-1.879 -1.408,-0.997 -2.111,-2.264 -2.111,-3.798 0,-1.509 0.563,-2.814 1.688,-3.912 1.125,-1.101 2.558,-1.65 4.297,-1.65 1.943,0 3.798,0.639 5.562,1.918 1.305,0.972 2.161,2.16 2.571,3.568 0.281,0.921 0.422,2.672 0.422,5.255 z m -19.067,37.632 h 9.246 v -9.245 h -9.246 z m 4.643,-28.886 -9.783,1.765 c 1.1,3.938 2.992,6.854 5.677,8.747 2.686,1.892 6.676,2.838 11.97,2.838 4.808,0 8.388,-0.57 10.741,-1.707 2.353,-1.138 4.009,-2.583 4.968,-4.334 0.959,-1.753 1.439,-4.969 1.439,-9.648 l -0.115,-12.583 c 0,-3.582 0.173,-6.222 0.517,-7.922 0.346,-1.702 0.991,-3.524 1.938,-5.467 h -10.665 c -0.281,0.716 -0.627,1.776 -1.035,3.183 -0.181,0.639 -0.307,1.062 -0.385,1.267 -1.842,-1.791 -3.811,-3.134 -5.907,-4.029 -2.098,-0.894 -4.335,-1.342 -6.714,-1.342 -4.194,0 -7.499,1.137 -9.917,3.415 -2.416,2.275 -3.624,5.153 -3.624,8.631 0,2.302 0.549,4.354 1.649,6.157 1.1,1.803 2.64,3.184 4.623,4.144 1.981,0.958 4.84,1.796 8.574,2.512 5.038,0.946 8.528,1.828 10.473,2.648 v 1.074 c 0,2.07 -0.513,3.547 -1.534,4.43 -1.025,0.882 -2.955,1.323 -5.794,1.323 -1.918,0 -3.413,-0.378 -4.487,-1.132 -1.074,-0.753 -1.946,-2.077 -2.609,-3.97"
style="fill:#189dd9;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path54" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.2 KiB

10
Documents_ref/ayuda.txt Normal file
View File

@ -0,0 +1,10 @@
https://www.metricser.com/segmentacion-del-electorado/
https://htmlstream.com/preview/front-v3.2/documentation/aos.html
pg_dump -U postgres -d forma -F c -f ./forma.backup
pg_dump -U postgres -d forma -F p -f .forma.sql

View File

@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
{% block title %}{{ title | default('FORMHä') }}{% endblock %}
</title>
<!-- {# i bootstrap #} -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<!-- {# icons #}} -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<!-- {# f bootstrap #} -->
{% block css %}
{% endblock %}
<!-- {# i navbar css #} -->
<link rel="stylesheet" href="{{url_for('static', filename='template/tmp.css')}}">
<!-- {# navbar #} -->
<link rel="stylesheet" href="{{url_for('static', filename='template/navbar.css')}}">
<!-- {# f navbar css #} -->
<!-- {# i notify css #} -->
<!-- {# doc: https://github.com/simple-notify/simple-notify?tab=readme-ov-file #} -->
<!-- CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/simple-notify/dist/simple-notify.css" />
<!-- JS -->
<script src="https://cdn.jsdelivr.net/npm/simple-notify/dist/simple-notify.min.js"></script>
<!-- {# f notify js #} -->
</head>
<!-- <body> -->
<body>
{% block navbar %}
<!-- {# {% include 'comps/navbar_usr.html' %} #} -->
<nav class="navbar navbar-expand-lg bg-body-tertiary navbar-custom">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}">
{% include 'comps/formha.html' %}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 effect-3">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> {{nombre}}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#"><i class="bi bi-file-earmark-person-fill"></i> Usuarios</a></li>
<li><hr class="dropdown-divider"></li>
<!-- <li><a class="dropdown-item" href="#">Something else here</a></li> -->
<li>
<a class="dropdown-item" href="{{ url_for('logout') }}">
<i class="bi bi-door-open-fill"></i> Logout
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
{% endblock navbar %}
<!-- <main> -->
<main>
{% block body %}
{% endblock body %}
</main>
{% include 'comps/footer.html' %}
<!-- {# i bootstrap js #} -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
<!-- {# f bootstrap js #} -->
{% block js %}
{% include 'comps/notification.html' %}
{% if f_mnsj %}
<script>
simpleNotification("{{ f_mnsj.title }}", "{{ f_mnsj.body }}", "{{ f_mnsj.typeAlert }}");
</script>
{% endif %}
{% endblock js %}
</body>
</html>

View File

@ -14,29 +14,50 @@ CREATE DATABASE forma;
\c forma;
CREATE TABLE contact (
id SERIAL PRIMARY KEY,
fecha VARCHAR(10),
hora VARCHAR(10),
full_date_time TIMESTAMP WITH TIME ZONE,
nombre VARCHAR(50),
apellido VARCHAR(100),
email VARCHAR(150),
estado VARCHAR(30),
estado VARCHAR(50),
num_tel VARCHAR(20),
size_co VARCHAR(40),
rol_contacto VARCHAR(50),
industry_type VARCHAR(40),
tipo_req VARCHAR(255)
tipo_req VARCHAR(255),
status VARCHAR(50)
);
CREATE TABLE users(
id VARCHAR(25),
nombre VARCHAR(50),
apellido VARCHAR(100),
genero VARCHAR(2),
email VARCHAR(150),
pswd VARCHAR(100),
isAdmin boolean
lst_conn TIMESTAMP WITH TIME ZONE,
is_admin boolean
);
INSERT INTO users (id, nombre, apellido, email, pswd, isAdmin) VALUES
('4HlOjqJ6jLISxNQIbs2Hzz', 'David', 'Itehua Xalamihua', 'davidix1991@gmail.com', '$2b$12$dbJWK5mv89PszxPeXlql5Otd8vv7kz6M44JnKZcrwJdKoovayiqEm', true);
INSERT INTO users (id, nombre, apellido, genero, email, pswd, is_admin) VALUES
('4HlOjqJ6jLISxNQIbs2Hzz', 'David', 'Itehua Xalamihua', 'M', 'davidix1991@gmail.com', '$2b$12$dbJWK5mv89PszxPeXlql5Otd8vv7kz6M44JnKZcrwJdKoovayiqEm', false),
('4FyJhu54R6ARH2FlmroTxl', 'David', 'Itehua Xalamihua', 'M', 'davicho1991@live.com', '$2b$12$dbJWK5mv89PszxPeXlql5Otd8vv7kz6M44JnKZcrwJdKoovayiqEm', true );
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
id_usr VARCHAR(25),
created_at TIMESTAMP WITH TIME ZONE,
updated_at TIMESTAMP WITH TIME ZONE,
title VARCHAR(150) NOT NULL,
body TEXT NOT NULL,
body_no_img TEXT NOT NULL,
lista_imagenes JSONB
);
CREATE TABLE posts_visited(
id_post INT,
viewed TIMESTAMP WITH TIME ZONE
);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
# En producción debes descomentar estas

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,22 @@
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import InputRequired, Email, Length, DataRequired
class ChangePwsd(FlaskForm):
cur_pswd = PasswordField(
'Contrseña Actual',
validators=[
DataRequired(message="La contraseña es obligatoria")
],
description="contraseña actual",
render_kw={"placeholder": "Ingresa tu contraseña"}
)
new_pswd = PasswordField(
'Nueva Contraseña',
validators=[
DataRequired(message="La nueva contraseña es obligatoria")
],
description="Contraseña nueva",
render_kw={"placeholder": "Ingresa tu nueva contraseña"}
)

View File

@ -31,7 +31,7 @@ class DBContact:
:raises Exception: Si ocurre un error durante la ejecución de la consulta.
"""
query = sql.SQL("""
INSERT INTO contact (fecha, hora, nombre, apellido, email, num_tel, size_co, rol_contacto, industry_type, tipo_req)
INSERT INTO contact (full_date_time, nombre, apellido, email, estado, num_tel, size_co, rol_contacto, industry_type, tipo_req)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""")

View File

@ -65,6 +65,47 @@ class DBForma:
except Exception as e:
raise RuntimeError(f"Error al verificar credenciales: {e}")
def get_data(self, query: str, data_tuple: tuple):
try:
with self._get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(query, data_tuple)
result = cursor.fetchone()
return result
except psycopg2.DatabaseError as e:
conn.rollback() # Asegura que la conexión no quede en un estado erróneo
raise RuntimeError(f"Error al ejecutar la consulta: {e}")
except Exception as e:
raise RuntimeError(f"Error inesperado: {e}")
def get_all_data(self, query):
try:
with self._get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(query)
result = cursor.fetchall()
return result
except psycopg2.DatabaseError as e:
conn.rollback() # Asegura que la conexión no quede en un estado erróneo
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:
"""
"""
try:
with self._get_connection() as conn:
with conn.cursor() as cursor:
cursor.execute(query, data_tuple)
if cursor.rowcount == 0:
return False
conn.commit() # Confirmar explícitamente la transacción
return True
except Exception as e:
conn.rollback() # Revertir en caso de error
raise RuntimeError(f"Error al actualizar la contraseña: {e}")
def update_pswd(self, pswd: str, email: str) -> bool:
"""

View File

@ -0,0 +1,32 @@
from flask_wtf import FlaskForm
from wtforms import StringField, TextAreaField, SelectField
from wtforms.validators import InputRequired, Email, Length, Optional, DataRequired
from wtforms import HiddenField
class AddUser(FlaskForm):
id = HiddenField()
nombre = StringField(
"Nombre", validators=[InputRequired(message="Este campo es obligatorio"), Length(max=100, message="Máximo 100 caracteres")]
)
apellido = StringField(
"Apellido", validators=[InputRequired(message="Este campo es obligatorio"), Length(max=100, message="Máximo 100 caracteres")]
)
genero = SelectField("Género", choices=[
("", ""),
('M', 'Hombre'),
('F', 'Mujer')
], validators=[DataRequired(message="Debes seleccionar el género.")],)
email = StringField(
"Email", validators=[InputRequired(message="Este campo es obligatorio"), Email(message="Ingresa un email válido"), Length(max=120)]
)
isAdmin = SelectField( "Permisos de Administrador", choices=[
("", ""),
("true", ""),
("false", "No")
], validators=[DataRequired(message="Debes seleccionar el rol.")],)

View File

@ -16,39 +16,39 @@ class ContactForm(FlaskForm):
)
estado = SelectField("Estado", choices=[
("", ""),
(1, 'Aguascalientes'),
(2, 'Baja California'),
(3, 'Baja California Sur'),
(4, 'Campeche'),
(5, 'Chiapas'),
(6, 'Chihuahua'),
(7, 'Ciudad de México'),
(8, 'Coahuila'),
(9, 'Colima'),
(10, 'Durango'),
(11, 'Estado de México'),
(12, 'Guanajuato'),
(13, 'Guerrero'),
(14, 'Hidalgo'),
(15, 'Jalisco'),
(16, 'Michoacán'),
(17, 'Morelos'),
(18, 'Nayarit'),
(19, 'Nuevo León'),
(20, 'Oaxaca'),
(21, 'Puebla'),
(22, 'Querétaro'),
(23, 'Quintana Roo'),
(24, 'San Luis Potosí'),
(25, 'Sinaloa'),
(26, 'Sonora'),
(27, 'Tabasco'),
(28, 'Tamaulipas'),
(29, 'Tlaxcala'),
(30, 'Veracruz'),
(31, 'Yucatán'),
(32, 'Zacatecas')
("", "Selecciona tu Estado *"),
('Aguascalientes', 'Aguascalientes'),
('Baja California', 'Baja California'),
('Baja California Sur', 'Baja California Sur'),
('Campeche', 'Campeche'),
('Chiapas', 'Chiapas'),
('Chihuahua', 'Chihuahua'),
('Ciudad de México', 'Ciudad de México'),
('Coahuila', 'Coahuila'),
('Colima', 'Colima'),
('Durango', 'Durango'),
('Estado de México', 'Estado de México'),
('Guanajuato', 'Guanajuato'),
('Guerrero', 'Guerrero'),
('Hidalgo', 'Hidalgo'),
('Jalisco', 'Jalisco'),
('Michoacán', 'Michoacán'),
('Morelos', 'Morelos'),
('Nayarit', 'Nayarit'),
('Nuevo León', 'Nuevo León'),
('Oaxaca', 'Oaxaca'),
('Puebla', 'Puebla'),
('Querétaro', 'Querétaro'),
('Quintana Roo', 'Quintana Roo'),
('San Luis Potosí', 'San Luis Potosí'),
('Sinaloa', 'Sinaloa'),
('Sonora', 'Sonora'),
('Tabasco', 'Tabasco'),
('Tamaulipas', 'Tamaulipas'),
('Tlaxcala', 'Tlaxcala'),
('Veracruz', 'Veracruz'),
('Yucatán', 'Yucatán'),
('Zacatec', 'Zacatecas')
], validators=[DataRequired(message="Debes seleccionar un estado.")],)
@ -57,66 +57,66 @@ class ContactForm(FlaskForm):
)
size_co = SelectField( "Tamaño empresa", choices=[
("", ""),
("1-10", "1 - 20"),
("10-50", "21 - 50"),
("50-100", "51 - 100"),
("101-149", "101 - 149"),
("150-500", "150 - 500"),
("501-799", "501 - 799"),
("800-5,000", "800 - 5,000"),
("5,000-10,000", "5,001 - 10,000"),
("", "Tamaño de la Empresa *"),
("1 - 10", "1 - 20"),
("10 - 50", "21 - 50"),
("50 - 100", "51 - 100"),
("101 - 149", "101 - 149"),
("150 - 500", "150 - 500"),
("501 - 799", "501 - 799"),
("800 - 5,000", "800 - 5,000"),
("5,000 - 10,000", "5,001 - 10,000"),
("10,000+", "10,000+")
], validators=[DataRequired(message="Debes seleccionar el tamaño de la empresa.")],)
rol_contacto = SelectField( "Rol desempeñado", choices=[
("", ""),
(1, 'Capacitaciones'),
(2, 'Gerente de Operaciones'),
(3, 'Gerente de TI'),
(4, 'Nómina'),
(5, 'Desarrollo Organizacional'),
(6, 'Reclutamiento y selección'),
(7, 'Gerencia de Recursos Humanos'),
(8, 'Finanzas / Gerencia'),
(9, 'Dueño de mi propio negocio'),
(10, 'Estudiante'),
(11, 'Otro')
("", "Te desempeñas en * "),
('Capacitaciones', 'Capacitaciones'),
('Gerente de Operaciones', 'Gerente de Operaciones'),
('Gerente de TI', 'Gerente de TI'),
('Nómina', 'Nómina'),
('Desarrollo Organizacional', 'Desarrollo Organizacional'),
('Reclutamiento y selección', 'Reclutamiento y selección'),
('Gerencia de Recursos Humanos', 'Gerencia de Recursos Humanos'),
('Finanzas / Gerencia', 'Finanzas / Gerencia'),
('Dueño de mi propio negocio', 'Dueño de mi propio negocio'),
('Estudiante', 'Estudiante'),
('Otro', 'Otro')
], validators=[DataRequired(message="Debes selecionar tu rol.")],)
industry_type = SelectField( "Sector", choices=[
("", ""),
(1, 'Agricola'),
(2, 'Alimentos'),
(3, 'Automotriz'),
(4, 'Comercio'),
(5, 'Comunicaciones'),
(6, 'Construcción'),
(7, 'Consultora'),
(8, 'Educación'),
(9, 'Empresas B'),
(10, 'Energia'),
(11, 'Entretenimiento'),
(12, 'Financiera'),
(13, 'Fundación'),
(14, 'Holding'),
(15, 'Hoteleria'),
(16, 'Legal'),
(17, 'Logistica'),
(18, 'Manufacturera'),
(19, 'Marketing'),
(20, 'Minera'),
(21, 'Otra'),
(22, 'Sector Público'),
(23, 'Restoranes/Cafeteria'),
(24, 'RRHH'),
(25, 'Salud'),
(26, 'Seguridad'),
(27, 'Servicios'),
(28, 'Tecnología'),
(29, 'Transporte'),
(30, 'Utilities (gas, agua, electricidad)')
("", "Industria a la que pertenece la empresa *"),
('Agricola', 'Agricola'),
('Alimentos', 'Alimentos'),
('Automotriz', 'Automotriz'),
('Comercio', 'Comercio'),
('Comunicaciones', 'Comunicaciones'),
('Construcción', 'Construcción'),
('Consultora', 'Consultora'),
('Educación', 'Educación'),
('Empresas B', 'Empresas B'),
('Energia', 'Energia'),
('Entretenimiento', 'Entretenimiento'),
('Financiera', 'Financiera'),
('Fundación', 'Fundación'),
('Holding', 'Holding'),
('Hoteleria', 'Hoteleria'),
('Legal', 'Legal'),
('Logistica', 'Logistica'),
('Manufacturera', 'Manufacturera'),
('Marketing', 'Marketing'),
('Minera', 'Minera'),
('Otra', 'Otra'),
('Sector Público', 'Sector Público'),
('Restoranes/Cafeteria', 'Restoranes/Cafeteria'),
('RRHH', 'RRHH'),
('Salud', 'Salud'),
('Seguridad', 'Seguridad'),
('Servicios', 'Servicios'),
('Tecnología', 'Tecnología'),
('Transporte', 'Transporte'),
('Utilities (gas, agua, electricidad)', 'Utilities (gas, agua, electricidad)')
], validators=[DataRequired(message="Selecione una propiedad.")],)
tipo_req = TextAreaField(

View File

@ -1,33 +1,28 @@
from flask_wtf import FlaskForm
from wtforms import StringField, validators
from wtforms.validators import DataRequired, Email, Length
from wtforms import StringField, PasswordField
from wtforms.validators import InputRequired, Email, Length, DataRequired
class RecoverPswd(FlaskForm):
"""
Formulario para recuperación de contraseña.
Formulario de inicio de sesión que hereda de FlaskForm.
Campos:
email (StringField): Campo para el correo electrónico del usuario con validaciones.
email (StringField): Campo para el correo electrónico del usuario.
password (PasswordField): Campo para la contraseña del usuario.
Validaciones:
- Requerido (DataRequired)
- Formato válido de email (Email)
- Longitud máxima de 120 caracteres (Length)
- Sanitización automática (elimina espacios)
- Email: Requerido, formato válido y longitud máxima de 120 caracteres.
- Password: Requerido.
"""
email = StringField(
label="Correo Electrónico",
description="Ingresa el email asociado a tu cuenta",
filters=[lambda x: x.strip() if x else x], # Sanitización
"Email",
validators=[
DataRequired(message="El correo electrónico es requerido"),
Email(message="Por favor ingresa un email válido"),
InputRequired(message="Este campo es obligatorio"),
Email(message="Ingresa un email válido"),
Length(max=120, message="El email no puede exceder los 120 caracteres")
],
render_kw={
"placeholder": "ejemplo@correo.com",
"class": "form-control", # Para Bootstrap
"autocomplete": "email"
}
description="Correo electrónico registrado en la plataforma"
)

View File

@ -6,22 +6,45 @@ import string
import secrets
from flask_bcrypt import Bcrypt
import shortuuid
from datetime import datetime, timezone
from zoneinfo import ZoneInfo # Python 3.9+
bcrypt = Bcrypt()
v = {
'home': 'home/home.html',
'about-us': 'about-us/about-us.html',
'solutions': 'solutions/solutions.html',
'methodology': 'methodology/methodology.html',
'contact': 'contact/contact.html',
'login': 'login/login.html',
'usr_home': 'usr_home/usr_home.html',
'recover_pswd': 'login/recover_pswd.html'
'home': 'a_home/home.html',
'about-us': 'b_about-us/about-us.html',
'solutions': 'c_solutions/solutions.html',
'methodology': 'd_methodology/methodology.html',
"blog": {
"all_posts": "e_blog/a_all_posts.html",
"post": "e_blog/b_post.html",
},
'contact': 'f_contact/contact.html',
'login': 'g_login/login.html',
'recover_pswd': 'g_login/recover_pswd.html',
'tmp_user': {
'home': 'h_tmp_usr/a_home.html',
'txt_editor': 'h_tmp_usr/b_txt_editor.html',
'my_posts': 'h_tmp_usr/c_my_posts.html',
'read_post': 'h_tmp_usr/d_read_post.html',
'edit_post': 'h_tmp_usr/e_edit_post.html',
'change_pswd': 'h_tmp_usr/f_change_pswd.html',
'metrics': 'h_tmp_usr/g_metrics.html',
'manage_profiles': 'h_tmp_usr/h_manage_profiles.html'
}
}
l_flash_msj = lambda title, body, typeAlert: {'title': title, 'body': body, 'typeAlert': typeAlert}
def min_read_pst(n_words: int) -> str:
# https://uapas2.bunam.unam.mx/humanidades/velocidad_lectora/
# min 250 y más 500 = 750 / 2 = ~375
t = int(round(n_words / 375, 0))
resultado = f'{t} min' if t < 60 else f'{int(t/60)} hrs'
return resultado
def db_conf_obj(dbEnvVarName: str) -> object:
'''
@ -92,5 +115,25 @@ def getRandomId():
short_id_custom = shortuuid.uuid()
return short_id_custom
def saludo_hr():
hora = datetime.now().hour
if 5 <= hora < 12:
return "🌅 Buenos días"
elif 12 <= hora < 18:
return "🌞 Buenas tardes"
else:
return "🌙 Buenas noches"
def cur_date() -> datetime:
fecha_actual = datetime.now(ZoneInfo("America/Mexico_City"))
return fecha_actual
def get_date_n_time(timestamp: datetime) -> dict:
obj = {
"date": timestamp.strftime("%d/%m/%Y"),
"hour": timestamp.strftime("%H:%M:%S")
}
return obj

852
main.py
View File

@ -1,55 +1,120 @@
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity, get_jwt
from flask_jwt_extended import JWTManager, create_access_token, jwt_required, get_jwt_identity, get_jwt, decode_token, verify_jwt_in_request, set_access_cookies, get_csrf_token
from flask_mail import Message, Mail
from threading import Thread
from flask import Flask, render_template, redirect, url_for, flash, current_app # Añadí current_app
from flask import jsonify, make_response # Añade estas importaciones
from flask import Flask, render_template, redirect, url_for, flash, current_app, request, jsonify, make_response
from forms_py.cls_form_contact import ContactForm
from forms_py.cls_form_add_usr import AddUser
from forms_py.cls_form_login import LogIn
from forms_py.cls_db import DBContact
from forms_py.cls_db_usr import DBForma
from forms_py.functions import db_conf_obj, generar_contrasena, hash_password, v
from forms_py.cls_change_pswd import ChangePwsd
from forms_py.functions import db_conf_obj, generar_contrasena, hash_password, v, l_flash_msj, saludo_hr, cur_date, min_read_pst, get_date_n_time, getRandomId
from forms_py.cls_recover_pswd import RecoverPswd
import os
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from flask_bcrypt import Bcrypt
# from flask_wtf.csrf import CSRFProtect
from werkzeug.utils import secure_filename
import re
import json
from bs4 import BeautifulSoup
from functools import wraps
app = Flask(__name__)
bcrypt = Bcrypt(app)
# csrf = CSRFProtect(app)
email_sender = os.getenv("email_sender")
email_pswd = os.getenv("pswd_formha")
lst_email_to = ["davidix1991@gmail.com", "davicho1991@live.com"]
# Configuración de JWT (añade esto junto a tus otras configs)
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'super-secret-fallback-key') # Usa una clave segura en producción -> MOVER A VARIABLE DE ENTORNO
# app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1) # Token expira en 1 hora
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(minutes=30) # Token expira en 1 hora
app.config['JWT_COOKIE_SECURE'] = True # En producción debe ser True
app.config['JWT_COOKIE_CSRF_PROTECT'] = True # Recomendado para seguridad
app.config['JWT_TOKEN_LOCATION'] = ['cookies']
# FLASK EMAIL
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
# INICIO CONFIGURACIÓN JWT
app.config["JWT_SECRET_KEY"] = "k3y-$up3r-s3cret4" # Usa una clave segura en producción -> MOVER A VARIABLE DE ENTORNO
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=1.5) # timedelta(hours=1) Token expira en 1 hora | timedelta(minutes=1) ` timedelta(seconds=60)`
# EN LOCALHOST FALSE, EN PRODUCCIÓN TRUE
app.config['JWT_COOKIE_SECURE'] = False # True en producción con HTTPS
app.config['JWT_COOKIE_CSRF_PROTECT'] = False # True: Solo para producción, requiere HTTPS
app.config['JWT_TOKEN_LOCATION'] = ['cookies'] # Ubicación donde buscar el token
app.config['JWT_ACCESS_COOKIE_NAME'] = 'access_token_cookie' # Asegura que use el mismo nombre
# app.config['JWT_ACCESS_CSRF_COOKIE_NAME'] = 'csrf_access_token'
app.config['JWT_COOKIE_SAMESITE'] = 'Lax'
# app.config['SESSION_PERMANENT'] = False
# app.config['SESSION_TYPE'] = 'filesystem' # Asegura que la sesión no se guarde en cookies
jwt = JWTManager(app)
# FINAL CONFIGURACIÓN JWT
# /////////////////////////////////////////////////////////////////////////////////////////
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
# INICIO FLASK EMAIL
app.config['MAIL_SERVER'] = 'smtp.gmail.com'
app.config['MAIL_PORT'] = 465
app.config['MAIL_USE_SSL'] = True
app.config['MAIL_USERNAME'] = email_sender # email en variable de entorno
app.config['MAIL_PASSWORD'] = email_pswd # contraseña en variable de entorno
app.config['SECRET_KEY'] = 'FoRmHä$2025' # Necesario para CSRF y mensajes flash -> la debo colocar en variable de entono?
mail = Mail(app)
jwt = JWTManager(app)
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
# FINAL FLASK EMAIL
# /////////////////////////////////////////////////////////////////////////////////////////
# \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
# CONFIGURACIÓN PARA GUARDAR IMÁGENES EN EL BACKEND
# Configuración para guardar imágenes
# UPLOAD_FOLDER = 'uploads/' # Asegúrate de crear esta carpeta
# ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'webp'}
# app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
# app.add_url_rule('/uploads/<filename>', endpoint='uploaded_file', view_func=send_from_directory(UPLOAD_FOLDER))
# /////////////////////////////////////////////////////////////////////////////////////////
def cur_timestamp():
return int(datetime.now(timezone.utc).timestamp())
jsonDbContact = db_conf_obj("forma_db")
dbContact = DBContact(jsonDbContact)
dbUsers = DBForma(jsonDbContact)
# decorador para rutas protegidas en caso de que borres al vuelo a un usuario.
def validate_user_exists(f):
@wraps(f)
def decorated_function(*args, **kwargs):
user_id = get_jwt_identity()
q = "SELECT id FROM users WHERE id = %s;"
t = (user_id,)
exists = dbUsers.get_data(q, t) is not None
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
if not exists:
f_mnsj = l_flash_msj('Error', 'El usuario no existe en la base de datos.', 'error')
flash(f_mnsj)
return redirect(url_for('login'))
return f(*args, **kwargs)
return decorated_function
# ##################################################################
#
# /$$$$$$ /$$$$$$$$ /$$ /$$ /$$$$$$$$ /$$$$$$$ /$$$$$$ /$$
# /$$__ $$| $$_____/| $$$ | $$| $$_____/| $$__ $$ /$$__ $$| $$
# | $$ \__/| $$ | $$$$| $$| $$ | $$ \ $$| $$ \ $$| $$
# | $$ /$$$$| $$$$$ | $$ $$ $$| $$$$$ | $$$$$$$/| $$$$$$$$| $$
# | $$|_ $$| $$__/ | $$ $$$$| $$__/ | $$__ $$| $$__ $$| $$
# | $$ \ $$| $$ | $$\ $$$| $$ | $$ \ $$| $$ | $$| $$
# | $$$$$$/| $$$$$$$$| $$ \ $$| $$$$$$$$| $$ | $$| $$ | $$| $$$$$$$$
# \______/ |________/|__/ \__/|________/|__/ |__/|__/ |__/|________/
#
# Font Name: Big Money-ne | https://patorjk.com/software/taag/#p=testall&f=Graffiti&t=USER
# ##################################################################
@app.route('/')
def home():
@ -67,47 +132,147 @@ def solutions():
def methodology():
return render_template(v['methodology'], active_page='methodology')
@app.route('/blog')
def blog():
# if any(x in search for x in ["'", '"', " OR ", "--", ";", "1=1"]):
# app.logger.warning(f"Intento de SQL injection detectado: {search}")
# 🛑 IMPORTANTE: Este bloque acepta input del usuario y necesita sanitización adecuada.
# TODO: Reemplazar concatenación de strings por parámetros SQL usando psycopg2.sql.SQL y placeholders (%s)
# Ejemplo de ataque detectado: ' OR '1'='1
# Aunque no ejecuta el ataque, sí lanza error → posible vector de DoS
# Parámetros
page = request.args.get("page", 1, type=int)
search = request.args.get("q", "").strip()
per_page = 9
offset = (page - 1) * per_page
# Armado de condiciones SQL para búsqueda
search_filter = ""
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]
total_pages = (total_posts + per_page - 1) // per_page
# 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>')
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)
# Obtener el post
q = fr"""
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,
array_length(regexp_split_to_array(TRIM(p.body_no_img), '\s+'), 1) / 375 as read_time_min,
p.title,
p.body
FROM
posts p
INNER JOIN
users u
ON
p.id_usr = u.id
WHERE
p.id = %s;
"""
t = (post_id,)
data = dbUsers.get_data(q, t)
return render_template(v['blog']['post'], data=data)
@app.route("/contact", methods=['GET', 'POST'])
def contact():
form = ContactForm()
# print(cur_date())
# get_date_n_time
if form.validate_on_submit(): # Corregí "validate_on_sumbit" a "validate_on_submit"
cur_date = datetime.now().strftime("%d/%m/%Y")
cur_hour = datetime.now().strftime("%H:%M")
if form.validate_on_submit():
# Procesar datos del formulario
flash('¡Gracias por contactarnos! Te responderemos pronto.', 'success')
data = (cur_date, cur_hour, form.nombre.data, form.apellido.data,
form.email.data, form.num_tel.data, form.size_co.data,
form.rol_contacto.data, form.industry_type.data, form.tipo_req.data)
f_mnsj = l_flash_msj('Contacto', '¡Gracias por contactarnos! Te responderemos pronto. ☺️', 'success' )
flash(f_mnsj)
c_date = cur_date()
obj_datetime = get_date_n_time(c_date)
hora = obj_datetime['hour']
fecha = obj_datetime['date']
data = ( c_date, form.nombre.data, form.apellido.data, form.email.data, form.estado.data, form.num_tel.data, form.size_co.data, form.rol_contacto.data, form.industry_type.data, form.tipo_req.data )
# Guardar datos en la base de datos
dbContact.carga_contact(data)
# Configurar y enviar email asíncrono
msg = Message(
"Subject, una persona busca asesoria", sender=email_sender, recipients=lst_email_to
)
msg = Message( "Subject, una persona busca asesoria", sender=email_sender, recipients=lst_email_to )
msg.html = """
msg.html ="""
<h1>Nuevo contacto recibido</h1>
<p><strong>Nombre:</strong> {} {}</p>
<p><strong>Email:</strong> {}</p>
<p><strong>Teléfono:</strong> {}</p>
<p><strong>Mensaje:</strong> {}</p>
""".format(form.nombre.data, form.apellido.data, form.email.data,
form.num_tel.data, form.tipo_req.data)
<p><trong>Fecha: </strong> {}</p><p><strong>Hora:</strong> {}</p> <p><strong>Nombre:</strong> {} {}</p> <p><strong>Email:</strong> {}</p> <p><strong>Teléfono:</strong> {}</p> <p><strong>Mensaje:</strong> {}</p> <a href="http://127.0.0.1:8089/login" target='_blank'>Iniciar Sesión</a>
""".format(fecha, hora, form.nombre.data, form.apellido.data, form.email.data, form.num_tel.data, form.tipo_req.data)
# Enviar en segundo plano
thr = Thread(
target=send_async_email,
args=(current_app._get_current_object(), msg)
)
thr = Thread( target=send_async_email, args=(current_app._get_current_object(), msg) )
thr.start()
return redirect(url_for('contact'))
return render_template(v['contact'], form=form, active_page='contact')
@app.route("/login", methods=['GET', 'POST'])
def login():
form = LogIn()
@ -117,25 +282,30 @@ def login():
res_pswd_server = dbUsers.login((f_email))
if res_pswd_server is None:
flash('Usuario no registrado en la db', 'error')
f_mnsj = l_flash_msj('Información', 'No se cueta con información del usuario', 'warning')
flash(f_mnsj)
return redirect(url_for('login'))
res_pswd_server = res_pswd_server[0]
if bcrypt.check_password_hash(res_pswd_server, f_pswd):
id_user = dbUsers.get_id(f_email)[0]
is_admin = dbUsers.get_data('SELECT is_admin FROM users WHERE id = %s;', (id_user,))[0]
# Crear token JWT
access_token = create_access_token(identity=id_user)
# Crear token JWT: access_token = create_access_token(identity=id_user)
# access_token = create_access_token( identity=id_user )
access_token = create_access_token( identity=id_user, additional_claims={"is_admin": is_admin} )
# Redirigir a usr_home
response = make_response(redirect(url_for('usr_home')))
response.set_cookie('access_token_cookie', access_token, httponly=True, secure=True, samesite='Lax' )
flash('Inicio de sesión exitoso', 'success')
# Redirigir a usr_home o a Admin depende
# din_home = 'admin_home' if is_admin else 'user_home'
response = make_response(redirect(url_for("user_home")))
set_access_cookies(response, access_token)
return response
else:
flash('Credenciales incorrectas', 'error')
f_mnsj = l_flash_msj('Credenciales', 'Verificar usuario y/o contraseña.', 'error')
flash(f_mnsj)
return render_template(v['login'], form=form, active_page='login')
@ -147,7 +317,8 @@ def recover_pswd():
emailPswdReco = dbUsers.reset_pswd(f_email)
if emailPswdReco is None:
flash('Email no válido', 'error')
f_mnsj = l_flash_msj("Error", "Verificar el correo electrónico ingresado.", "error")
flash(f_mnsj)
return render_template(v['recover_pswd'], form=form, active_page='login')
emailPswdReco = emailPswdReco[0]
@ -177,54 +348,591 @@ def recover_pswd():
args=(current_app._get_current_object(), msg)
)
thr.start()
flash("Se ha enviado una contraseña temporal a tu correo electrónico", "success")
f_mnsj = l_flash_msj("Éxito", "Se ha enviado una contraseña temporal a tu correo electrónico.", "success")
flash(f_mnsj)
return redirect(url_for('login')) # Redirige en lugar de renderizar
return render_template(v['recover_pswd'], form=form, active_page='login')
# #################################################################################################################################
#
# $$\ $$\ $$$$$$\ $$$$$$$$\ $$$$$$$\ $$$$$$\ $$$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\
# $$ | $$ |$$ __$$\ $$ _____|$$ __$$\ $$ __$$\ $$ __$$\ $$$\ $$$ |\_$$ _|$$$\ $$ |
# $$ | $$ |$$ / \__|$$ | $$ | $$ | $$ / $$ |$$ | $$ |$$$$\ $$$$ | $$ | $$$$\ $$ |
# $$ | $$ |\$$$$$$\ $$$$$\ $$$$$$$ | $$$$$$\ $$$$$$$$ |$$ | $$ |$$\$$\$$ $$ | $$ | $$ $$\$$ |
# $$ | $$ | \____$$\ $$ __| $$ __$$< \______| $$ __$$ |$$ | $$ |$$ \$$$ $$ | $$ | $$ \$$$$ |
# $$ | $$ |$$\ $$ |$$ | $$ | $$ | $$ | $$ |$$ | $$ |$$ |\$ /$$ | $$ | $$ |\$$$ |
# \$$$$$$ |\$$$$$$ |$$$$$$$$\ $$ | $$ | $$ | $$ |$$$$$$$ |$$ | \_/ $$ |$$$$$$\ $$ | \$$ |
# \______/ \______/ \________|\__| \__| \__| \__|\_______/ \__| \__|\______|\__| \__|
#
# Font Name: Big Money-ne | https://patorjk.com/software/taag/#p=testall&f=Graffiti&t=USER
#
# #################################################################################################################################
@app.context_processor
@jwt_required(optional=True)
def inject_user_role():
try:
token_data = get_jwt()
return {'is_admin': token_data.get('is_admin', False)}
except Exception:
return {'is_admin': False}
def admin_required(view_func):
@wraps(view_func)
def wrapped_view(*args, **kwargs):
token_data = get_jwt()
is_admin = token_data.get('is_admin', False)
if not is_admin:
f_mnsj = l_flash_msj('Error', 'No tienes permisos para acceder a esta sección.', 'error')
flash(f_mnsj)
return redirect(url_for('user_home'))
return view_func(*args, **kwargs)
return wrapped_view
@app.route('/user/home')
@jwt_required() # Protege esta ruta
def usr_home():
current_user = get_jwt_identity() # Obtiene el identity (normalmente el email)
token_data = get_jwt() # Obtiene TODOS los datos del token decodificado
# print("Token completo:", token_data)
# print("Usuario:", current_user)
@jwt_required()
@validate_user_exists
def user_home():
# get_jwt_identity()
# todo el token
token_data = get_jwt()
is_admin = token_data.get('is_admin', False)
# timestamp expiración de la sesión
exp = token_data['exp']
return render_template(v['usr_home'], current_user=current_user, token_data=token_data)
# id del usuario activo:
usr_id = token_data['sub']
q = "SELECT nombre, genero, lst_conn FROM users WHERE id = %s;"
data_saludo = dbUsers.get_data(q, (usr_id,))
nombre, gender, lst_conn = data_saludo
lst_conn = '' if lst_conn is None else get_date_n_time(lst_conn)['date']
current_date = get_date_n_time(cur_date())['date']
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)
q = "UPDATE users SET lst_conn = %s WHERE id = %s;"
d = (cur_date(), usr_id)
dbUsers.update_data(q, d)
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
FROM
CONTACT
WHERE
STATUS <> 'Archivado'
OR STATUS IS NULL
ORDER BY
FULL_DATE_TIME DESC;
"""
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)
@app.route('/user/manage-record', methods=['POST'])
@jwt_required()
@validate_user_exists
def manage_record():
# print(request.get_json())
# print("Cookies recibida:", request.cookies)
# token = request.cookies.get('access_token_cookie')
# decoded_token = decode_token(token)
data = request.get_json()
id = data['id']
valor = data['value']
q = 'UPDATE contact SET status = %s WHERE id = %s;'
dbUsers.update_data(q, (valor, id))
return jsonify({"type": "success"})
@app.route('/user/get-contact-data', methods=['POST'])
@jwt_required()
@validate_user_exists
def get_contact_data():
data = request.get_json()
id = data['id']
q = "SELECT 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 WHERE id = %s;"
t = (id,)
dbData = dbUsers.get_data(q, t)
return jsonify({"data": dbData})
@app.route('/user/download-db', methods=['GET'])
@jwt_required()
@validate_user_exists
@admin_required
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
FROM
contact;
"""
dbData = dbUsers.get_all_data(q)
return jsonify({"data": dbData})
@app.route('/user/txt-editor')
@jwt_required()
@validate_user_exists
def user_txteditor():
template_name = v['tmp_user'].get('txt_editor')
return render_template(template_name)
@app.route('/user/save-post', methods=['POST'])
@jwt_required()
@validate_user_exists
def save_post():
id_usr = get_jwt()['sub']
data = request.get_json()
title = data['title']
body = data['body']
soup = BeautifulSoup(body, 'html.parser')
body_no_img = soup.get_text(separator=' ', strip=True)
etiquetas_img = re.findall(r'<img[^>]+src=["\'](.*?)["\']', body)
imagenes_json = json.dumps(etiquetas_img) if etiquetas_img else None
q = None
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)
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)
try:
dbUsers.update_data(q, t)
return jsonify({
"status": "success",
"title_post": title,
"redirect_url": url_for('my_posts') # o usa _external=True si se necesita URL completa
# url_for('my_posts', _external=True)
})
except Exception as e:
return jsonify({"status": "error", "message": str(e)}), 500
@app.route('/user/my-posts')
@jwt_required()
@validate_user_exists
def my_posts():
id_usr = get_jwt()['sub']
page = int(request.args.get("page", 1))
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"""
SELECT
id,
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,
LEFT(body_no_img, 180),
lista_imagenes->0,
array_length(regexp_split_to_array(TRIM(body_no_img), '\s+'), 1) / 375 as n_words
FROM
posts
WHERE
id_usr = '{id_usr}'
ORDER BY
COALESCE(updated_at, created_at) DESC,
created_at DESC;
"""
data_all = dbUsers.get_all_data(q_all)
total_posts = len(data_all)
total_pages = (total_posts + per_page - 1) // per_page
start = (page - 1) * per_page
end = start + per_page
data = data_all[start:end]
return render_template(
v['tmp_user']['my_posts'],
data=data,
current_page=page,
total_pages=total_pages
)
@app.route('/user/del-post', methods=['POST'])
@jwt_required()
@validate_user_exists
def del_post():
data = request.get_json()
t = (data['id'],)
q = 'DELETE FROM posts WHERE id = %s;'
dbUsers.update_data(q, t)
res = {'ok': True, 'message': f'El elemento se eliminó, pendiente manejar el error en el frontend', "id": t}
return jsonify(res), 200
@app.route('/user/<int:post_id>')
@jwt_required()
@validate_user_exists
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;"
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;"
n_words = dbUsers.get_data(q_nw, t)[0]
time_read = min_read_pst(n_words)
return render_template(v['tmp_user']['read_post'], data=data, time_read=time_read, post_id=post_id)
@app.route('/user/edit-post/<int:id_post>')
@jwt_required()
@validate_user_exists
def edit_post(id_post):
q = 'SELECT title, body FROM posts WHERE id = %s;'
t = (id_post,)
data = dbUsers.get_data(q, t)
return render_template(v['tmp_user']['edit_post'], data=data, id_post=id_post)
@app.route('/user/change-pswd', methods=['GET', 'POST'])
@jwt_required()
@validate_user_exists
def change_pswd():
form = ChangePwsd()
if form.validate_on_submit():
f_old_pswd = form.cur_pswd.data
f_new_pswd = form.new_pswd.data
usr_id = get_jwt()['sub']
q = "SELECT pswd FROM users WHERE id = %s;"
t = (usr_id,)
data = dbUsers.get_data(q, t)[0]
if bcrypt.check_password_hash(data, f_old_pswd):
new_hash_pswd = hash_password(f_new_pswd)
q = "UPDATE users SET pswd = %s WHERE id = %s;"
t = (new_hash_pswd, usr_id)
dbUsers.update_data(q, t)
f_mnsj = l_flash_msj('Éxito', 'La contraseña ha sido actualizada.', 'success')
flash(f_mnsj)
response = make_response(redirect(url_for('login')))
response.delete_cookie('access_token_cookie')
return response
else:
f_mnsj = l_flash_msj('Error', 'La contraseña a actualizar es incorrecta.', 'error')
flash(f_mnsj)
return render_template(v['tmp_user']['change_pswd'], active_page='change_pswd', form=form)
@app.route('/user/metrics')
@jwt_required()
@validate_user_exists
@admin_required
def metrics():
return render_template(v['tmp_user']['metrics'], active_page='metrics')
@app.route('/user/metrics/data')
@jwt_required()
@validate_user_exists
@admin_required
def data_metrics():
q_contact = r"""
SELECT
CASE EXTRACT(MONTH FROM full_date_time AT TIME ZONE 'America/Mexico_City')
WHEN 1 THEN 'Enero'
WHEN 2 THEN 'Febrero'
WHEN 3 THEN 'Marzo'
WHEN 4 THEN 'Abril'
WHEN 5 THEN 'Mayo'
WHEN 6 THEN 'Junio'
WHEN 7 THEN 'Julio'
WHEN 8 THEN 'Agosto'
WHEN 9 THEN 'Septiembre'
WHEN 10 THEN 'Octubre'
WHEN 11 THEN 'Noviembre'
WHEN 12 THEN 'Diciembre'
END AS mes,
COUNT(*) AS cantidad_registros,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS porcentaje
FROM
contact
GROUP BY
EXTRACT(MONTH FROM full_date_time AT TIME ZONE 'America/Mexico_City')
ORDER BY
EXTRACT(MONTH FROM full_date_time AT TIME ZONE 'America/Mexico_City');
"""
q_count_state = "SELECT estado, COUNT(estado) AS conteo_edo, ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS porcentaje FROM contact GROUP BY estado ORDER BY conteo_edo DESC;"
q_size_co = "SELECT size_co, COUNT(size_co) AS conteo_size FROM contact GROUP BY size_co ORDER BY conteo_size DESC;"
q_rol_contact = """
SELECT
rol_contacto,
COUNT(*) AS conteo,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS porcentaje
FROM contact
GROUP BY rol_contacto
ORDER BY conteo DESC;
"""
q_industry_type = """
SELECT
industry_type,
COUNT(industry_type) AS conteo,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS porcentaje
FROM
contact
GROUP BY
industry_type
ORDER BY
conteo DESC;
"""
q_group_status = """
SELECT
COALESCE(status, 'Sin Estatus') AS status,
COUNT(*) AS conteo,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 1) AS porcentaje
FROM
contact
GROUP BY
COALESCE(status, 'Sin Estatus')
ORDER BY
conteo;
"""
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)
}
return jsonify(data_contact)
@app.route('/user/manage-profiles', methods=['GET', 'POST'])
@jwt_required()
@validate_user_exists
@admin_required
def manage_profiles():
form = AddUser()
id_usr = get_jwt()['sub']
q_all_users = """
SELECT
u.id, u.nombre, u.apellido, u.email, COALESCE(TO_CHAR(u.lst_conn, 'DD/MM/YYYY HH24:MI'), 'Sin conexión') AS ultima_conexion,
CASE
WHEN u.is_admin = true THEN ''
WHEN u.is_admin = false THEN 'No'
END AS admin,
COALESCE(p.conteo, 0) AS conteo
FROM users u
LEFT JOIN (
SELECT id_usr, COUNT(*) AS conteo
FROM posts
GROUP BY id_usr
) p ON u.id = p.id_usr
ORDER BY conteo DESC;
"""
data_all_users = dbUsers.get_all_data(q_all_users)
if form.validate_on_submit():
f_nombre = f'{form.nombre.data}'.title().strip()
f_apellido = f'{form.apellido.data}'.title().strip()
f_genero = form.genero.data
f_email = f'{form.email.data}'.lower().strip()
f_isAdmin = form.isAdmin.data.strip().upper()
f_id = form.id.data.strip()
f_mnsj = None
q = None
t = None
subject = None
html_content = None
if f_id != '':
q = "UPDATE users SET nombre = %s, apellido = %s, genero = %s, email = %s, is_admin = %s WHERE id = %s;"
t = (f_nombre, f_apellido, f_genero, f_email, f_isAdmin, f_id)
f_mnsj = l_flash_msj('Éxito', f'Usuario actualizado: {f_nombre}', 'success')
subject = "Usuario actualizado"
html_content = """
<h1>¡Hola!</h1>
<p>Tu cuenta ha sido actualizada con éxito.</p>
<p><strong>Nombre:</strong> {}</p>
<p><strong>Apellido:</strong> {}</p>
<p><strong>Género:</strong> {}</p>
<p><strong>Email:</strong> {}</p>
""".format(f_nombre, f_apellido, f_genero, f_email)
else:
q_isDuplicated = "SELECT email FROM users WHERE email = %s;"
t_isDuplicated = (f_email,)
isDuplicated = dbUsers.get_data(q_isDuplicated, t_isDuplicated)
if isDuplicated is not None:
f_mnsj = l_flash_msj('Error', 'El correo electrónico ya existe.', 'error')
flash(f_mnsj)
return redirect(url_for('manage_profiles'))
random_id = getRandomId()
tmp_pswd = generar_contrasena()
hashed_pswd = hash_password(tmp_pswd)
q = "INSERT INTO users (id, nombre, apellido, genero, email, pswd, is_admin ) values (%s, %s, %s, %s, %s, %s, %s);"
t = (random_id, f_nombre, f_apellido, f_genero, f_email, hashed_pswd, f_isAdmin)
f_mnsj = l_flash_msj('Éxito', f'Usuario creado: {f_nombre}', 'success')
subject = "Nueva cuenta creada"
html_content = """
<h1>¡Bienvenido a Forma!</h1>
<p>Tu cuenta ha sido creada con éxito.</p>
<p><strong>Nombre:</strong> {}</p>
<p><strong>Apellido:</strong> {}</p>
<p><strong>Género:</strong> {}</p>
<p><strong>Email:</strong> {}</p>
<p><strong>Contraseña temporal:</strong> {}</p>
<p><strong>Una vez inicies sesión debes de cambiar la contraseña a una nueva que puedas recordar</strong></p>
""".format(f_nombre, f_apellido, f_genero, f_email, tmp_pswd)
# Crear el mensaje de correo con todo el contenido
msg = Message(
subject=subject,
sender=email_sender,
recipients=[f_email]
)
msg.html = html_content # Asignar el contenido HTML aquí
# Enviar en segundo plano
thr = Thread(target=send_async_email, args=(current_app._get_current_object(), msg))
thr.start()
dbUsers.update_data(q, t)
flash(f_mnsj)
return redirect(url_for('manage_profiles'))
return render_template(v['tmp_user']['manage_profiles'], form=form, data_all_users=data_all_users, active_page='manage_profiles', id_usr=id_usr)
@app.route('/user/manage-profiles/delete-usr', methods=['GET', 'POST'])
@jwt_required()
@validate_user_exists
@admin_required
def delete_user():
data = request.get_json()
id_usr = data['id']
q_del_usr = "DELETE FROM users WHERE id = %s;"
dbUsers.update_data(q_del_usr, (id_usr,))
q_del_posts = "DELETE FROM posts WHERE id_usr = %s;"
dbUsers.update_data(q_del_posts, (id_usr,))
res = {'ok': True, 'message': f'El elemento se eliminó, pendiente manejar el error en el frontend', "id": id_usr}
return jsonify(res), 200
@app.route('/user/manage-profiles/get-user', methods=['POST']) # Cambiado a POST
@jwt_required()
@validate_user_exists
@admin_required
def get_user():
data = request.get_json()
id_usr = data['id']
q = "SELECT id, nombre, apellido, genero, email, is_admin FROM users WHERE id = %s;"
t = (id_usr,)
dbData = dbUsers.get_data(q, t)
return jsonify({"data": dbData})
@app.route('/user/manage-profiles/update-user', methods=['POST'])
@jwt_required()
@validate_user_exists
@admin_required
def update_user():
data = request.get_json()
id_usr = data['id']
f_nombre = f'{data["nombre"]}'.title().strip()
f_apellido = f'{data["apellido"]}'.title().strip()
f_genero = data['genero']
f_email = f'{data["email"]}'.lower().strip()
f_isAdmin = data['isAdmin'].strip().lower()
q = "UPDATE users SET nombre = %s, apellido = %s, genero = %s, email = %s, is_admin = %s WHERE id = %s;"
t = (f_nombre, f_apellido, f_genero, f_email, f_isAdmin, id_usr)
dbUsers.update_data(q, t)
res = {'ok': True, 'message': 'El elemento se actualizó correctamente', "id": "id_usr"}
return jsonify(res), 200
# -------------------------------------------------------------
# MANEJO DE ERRORES DE JWT
@jwt.unauthorized_loader
def unauthorized_response(callback):
return jsonify({"error": "Token inválido o no proporcionado"}), 401
# Detecta si la petición viene del frontend (fetch)
def is_fetch_request():
return request.headers.get("X-Requested-With") == "XMLHttpRequest"
# Manejo de errores JWT
@jwt.expired_token_loader
def handle_expired_token(jwt_header, jwt_payload):
flash('Tu sesión ha expirado. Por favor inicia sesión nuevamente', 'warning')
# if is_fetch_request():
# return jsonify({"error": "El token ha expirado"}), 401
l_flash_msj('Sesión Expirada', 'Por favor inicia sesión nuevamente', 'warning')
# flash(l_flash_msj)
return redirect(url_for('login'))
@jwt.unauthorized_loader
def handle_unauthorized_error(reason):
if is_fetch_request():
return jsonify({"error": "Token inválido o no proporcionado"}), 401
flash(f'Debes iniciar sesión para acceder a esta página: {reason}', 'error')
return redirect(url_for('login'))
@jwt.invalid_token_loader
def handle_invalid_token_error(reason):
if is_fetch_request():
return jsonify({"error": "Sesión inválida"}), 401
flash(f'Sesión inválida: {reason}', 'error')
return redirect(url_for('login'))
@app.route("/logout")
def logout():
response = make_response(redirect(url_for('login')))
response.delete_cookie('access_token_cookie')
flash('Sesión cerrada correctamente', 'success')
return response
# -------------------------------------------------------------
@app.route("/logout")
def logout():
f_mnsj = l_flash_msj('👋🏼Logout', 'Se ha cerrado tu sesión.', 'success')
flash(f_mnsj)
response = make_response(redirect(url_for('login')))
response.delete_cookie('access_token_cookie')
return response
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=8089)

View File

@ -1,4 +1,5 @@
bcrypt==4.3.0
beautifulsoup4==4.13.3
blinker==1.9.0
click==8.1.8
dnspython==2.7.0
@ -16,5 +17,7 @@ psycopg2-binary==2.9.10
PyJWT==2.10.1
python-dotenv==1.1.0
shortuuid==1.0.13
soupsieve==2.6
typing_extensions==4.13.1
Werkzeug==3.1.3
WTForms==3.2.1

View File

@ -22,7 +22,7 @@
}
.r-i-ii {
background-image: url('/static/img/home/reunion.avif');
background-image: url('/static/y_img/home/reunion.avif');
background-size: 100%;
background-repeat: no-repeat;
background-position: center;

View File

@ -1,210 +0,0 @@
/**
* EXPANDING PANELS - Componente de paneles expandibles
*
* Estilos para un sistema de paneles con:
* - Efecto hover con animación
* - Texto expandible con scroll
* - Diseño responsive
*/
.expanding-panels {
display: grid;
gap: 1em;
/* ---------------------------- */
/* ESTILOS BASE DEL PANEL */
/* ---------------------------- */
.panel {
position: relative;
background-size: cover;
background-position: center;
border-radius: 10px;
overflow: hidden;
transition: all 0.5s ease;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
/* ---------------------------- */
/* CONTENIDO DEL PANEL */
/* ---------------------------- */
.panel-content {
text-align: center;
color: #000;
width: 90%;
}
/* Título del panel - siempre visible */
.panel-title {
background-color: rgba(255, 255, 255, 0.85);
padding: 0.5em 1em;
border-radius: 10px;
transition: all 0.5s ease;
h3 {
margin: 0;
}
}
/* Texto expandible - con scroll vertical cuando es necesario */
.panel-text {
max-height: 0;
opacity: 0;
overflow-y: hidden; /* Oculta scroll inicialmente */
transition: all 0.5s ease;
margin-top: 1em;
background-color: rgba(255, 255, 255, 0.85);
padding: 0 1em;
border-radius: 10px;
& p, ul {
text-align: justify;
text-justify: inter-word;
}
}
/* ---------------------------- */
/* EFECTOS HOVER */
/* ---------------------------- */
.panel:hover {
transform: scale(1.05);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
z-index: 2;
.panel-text {
max-height: 50vh; /* Altura máxima antes de mostrar scroll */
opacity: 1;
padding: 1em;
overflow-y: auto; /* Muestra scroll solo cuando es necesario */
/* Estilos personalizados para la barra de scroll */
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: rgba(0,0,0,0.2);
border-radius: 3px;
}
}
}
/* ---------------------------- */
/* IMÁGENES DE FONDO */
/* ---------------------------- */
.p1 { background-image: url('/static/img/about-us/team.avif'); }
.p2 { background-image: url('/static/img/about-us/mision.avif'); }
.p3 { background-image: url('/static/img/about-us/idea.avif'); }
.p4 { background-image: url('/static/img/about-us/mex.avif'); }
}
/* ============================================ */
/* MEDIA QUERIES - RESPONSIVE DESIGN */
/* ============================================ */
/* Smartphones (hasta 767px) */
@media (max-width: 767px) {
main{
width: 95vw;
margin: 1em auto;
min-height: 100vh;
margin-top: 1em;
margin-bottom: 1em;
}
.expanding-panels {
grid-template-rows: repeat(4, 1fr);
row-gap: 2em;
.panel {
height: 45vh;
}
}
}
/* Tablets (768px - 1023px) */
@media (min-width: 768px) and (max-width: 1023px) {
main{
width: 90vw;
margin: auto;
min-height: 85vh;
margin-top: 1em;
margin-bottom: 1em;
}
.expanding-panels {
grid-template-columns: repeat(2, 1fr);
.panel {
height: 45vh;
}
}
}
/* Laptops (1024px - 1439px) */
@media (min-width: 1024px) and (max-width: 1439px) {
main{
width: 90vw;
margin: auto;
min-height: 85vh;
margin-top: 1em;
margin-bottom: 1em;
}
.expanding-panels {
grid-template-columns: repeat(2, 1fr);
.panel {
height: 40vh;
}
}
}
/* PCs de escritorio (1440px - 1919px) */
@media (min-width: 1440px) and (max-width: 1919px) {
.expanding-panels {
grid-template-columns: repeat(4, 1fr);
.panel {
height: 75vh;
}
}
main{
width: 90vw;
margin: auto;
min-height: 85vh;
margin-top: 1em;
margin-bottom: 1em;
}
}
/* Pantallas Ultrawide (1920px en adelante) */
@media (min-width: 1920px) {
.expanding-panels {
grid-template-columns: repeat(4, 1fr);
.panel {
height: 75vh;
}
}
main{
width: 80vw;
margin: auto;
min-height: 85vh;
margin-top: 1em;
margin-bottom: 1em;
}
}

View File

@ -0,0 +1,197 @@
.expanding-panels {
display: grid;
gap: 1em;
}
/* ---------------------------- */
/* ESTILOS BASE DEL PANEL */
/* ---------------------------- */
.panel {
position: relative;
background-size: cover;
background-position: center;
border-radius: 10px;
overflow: hidden;
transition: all 0.5s ease;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
/* ---------------------------- */
/* CONTENIDO DEL PANEL */
/* ---------------------------- */
.panel-content {
text-align: center;
color: #000;
width: 90%;
}
.panel-title {
background-color: rgba(255, 255, 255, 0.85);
padding: 0.5em 1em;
border-radius: 10px;
transition: all 0.5s ease;
}
.panel-title h3 {
margin: 0;
}
.panel-text {
max-height: 0;
opacity: 0;
overflow-y: hidden;
transition: all 0.5s ease;
margin-top: 1em;
background-color: rgba(255, 255, 255, 0.85);
padding: 0 1em;
border-radius: 10px;
}
.panel-text p,
.panel-text ul {
text-align: justify;
text-justify: inter-word;
}
/* ---------------------------- */
/* EFECTOS HOVER */
/* ---------------------------- */
.panel:hover {
transform: scale(1.02);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
z-index: 2;
}
.panel:hover .panel-title {
background-color: rgba(255, 255, 255, 1);
}
.panel:hover .panel-text {
max-height: 50vh;
opacity: 1;
padding: 1em;
overflow-y: auto;
background-color: rgba(255, 255, 255, 1);
}
/* Barra de scroll personalizada */
.panel-text::-webkit-scrollbar {
width: 6px;
}
.panel-text::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
/* ---------------------------- */
/* IMÁGENES DE FONDO */
/* ---------------------------- */
.p1 { background-image: url('/static/y_img/about-us/team.avif'); }
.p2 { background-image: url('/static/y_img/about-us/mision.avif'); }
.p3 { background-image: url('/static/y_img/about-us/vision.avif'); }
.p4 { background-image: url('/static/y_img/about-us/valores.avif'); }
/* ============================================ */
/* MEDIA QUERIES - RESPONSIVE DESIGN */
/* ============================================ */
/* Smartphones (hasta 767px) */
@media (max-width: 767px) {
main {
width: 95vw;
margin: 1em auto;
min-height: 100vh;
margin-top: 1em;
margin-bottom: 1em;
}
.expanding-panels {
grid-template-rows: repeat(4, 1fr);
row-gap: 2em;
.panel {
height: 45vh;
}
}
}
/* Tablets (768px - 1023px) */
@media (min-width: 768px) and (max-width: 1023px) {
main {
width: 90vw;
margin: auto;
min-height: 85vh;
margin-top: 1em;
margin-bottom: 1em;
}
.expanding-panels {
grid-template-columns: repeat(2, 1fr);
.panel {
height: 45vh;
}
}
}
/* Laptops (1024px - 1439px) */
@media (min-width: 1024px) and (max-width: 1439px) {
main {
width: 90vw;
margin: auto;
min-height: 85vh;
margin-top: 1em;
margin-bottom: 1em;
}
.expanding-panels {
grid-template-columns: repeat(2, 1fr);
.panel {
height: 40vh;
}
}
}
/* PCs de escritorio (1440px - 1919px) */
@media (min-width: 1440px) and (max-width: 1919px) {
.expanding-panels {
grid-template-columns: repeat(4, 1fr);
.panel {
height: 75vh;
}
}
main {
width: 90vw;
margin: auto;
min-height: 85vh;
margin-top: 1em;
margin-bottom: 1em;
}
}
/* Pantallas Ultrawide (1920px en adelante) */
@media (min-width: 1920px) {
.expanding-panels {
grid-template-columns: repeat(4, 1fr);
.panel {
height: 80vh;
}
}
}

View File

@ -1,8 +1,7 @@
main {
background-image: url('/static/img/solutions/Imagen1.png');
background-image: url('/static/y_img/solutions/girl.png');
background-repeat: no-repeat;
background-position: center bottom;
background-color: #50164A;
}
@ -11,10 +10,8 @@ main {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
gap: 2em;
/* width: 75vw; */
/* Altura para que la grilla sea visible */
/* height: 80vh; */
/* gap: 2em; */
& div {
background-size: contain;
background-position: center;
@ -22,42 +19,43 @@ main {
}
}
.parent div {
transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
border: 2px solid transparent;
border-radius: 15px;
&:hover {
animation: vibrate 0.4s ease infinite;
transform: scale(1.03);
box-shadow: 0 5px 15px rgba(255, 255, 255, 0.4);
border-color: #00acc1;
filter: brightness(1.05);
z-index: 10;
border-radius: 15px;
animation: vibrate 0.4s ease infinite !important;
transform: scale(1.05) !important;
box-shadow: 0 5px 15px rgba(255, 255, 255, 0.5) !important;
border-color: #00acc1 !important;
filter: brightness(1.05) !important;
z-index: 10 !important;
border-radius: 15px !important;
}
}
/* Imágenes específicas */
.i-i {
background-image: url('/static/img/solutions/Imagen2.png');
background-image: url('/static/y_img/solutions/reclutamiento_seleccion.png');
}
.i-ii {
background-image: url('/static/img/solutions/Imagen3.png');
background-image: url('/static/y_img/solutions/liderazgo.png');
}
.i-iii {
background-image: url('/static/img/solutions/Imagen4.png');
background-image: url('/static/y_img/solutions/capacitacion.png');
}
.ii-i {
background-image: url('/static/img/solutions/Imagen5.png');
background-image: url('/static/y_img/solutions/cambio.png');
grid-row: 2;
}
.ii-ii {
background-image: url('/static/img/solutions/Imagen6.png');
background-image: url('/static/y_img/solutions/objetivos.png');
grid-column: 3;
grid-row: 2;
}
@ -148,13 +146,6 @@ main {
/* PCs de escritorio (1440px - 1919px) */
@media (min-width: 1440px) and (max-width: 1919px) {
main {
width: 80vw;
min-height: 85vh;
margin: 10px auto;
background-size: auto 50%;
border-radius: 12px;
}
.parent {
width: 70vw;
@ -162,24 +153,19 @@ main {
height: 70vh;
}
}
/* Pantallas Ultrawide (1920px en adelante) */
@media (min-width: 1920px) {
main {
width: 80vw;
min-height: 85vh;
margin: 10px auto;
background-size: auto 50%;
border-radius: 12px;
.parent {
width: 75vw;
height: 40vw;
gap: 2em;
}
.parent {
width: 70vw;
/* Altura para que la grilla sea visible */
height: 70vh;
main {
background-size: 50% auto; /* 30% del ancho del contenedor */
}
}

View File

@ -0,0 +1,5 @@
.container{
& p {
font-size: 1.5rem;
}
}

View File

@ -0,0 +1,25 @@
.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;
}
.cat {
display: inline-block;
margin-bottom: 1rem;
}
.fa-users {
margin-left: 1rem;
}
.card-footer {
font-size: 0.8rem;
}

View File

@ -0,0 +1,4 @@
import {simpleNotification} from '../z_comps/notify.js';
simpleNotification(data.title, data.body, data.typeAlert);

101
static/f_contact/form.css Normal file
View File

@ -0,0 +1,101 @@
.form-container {
/* max-width: 800px; */
min-width: 20vw;
margin: 2rem auto;
padding: 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
/* font-family: 'Segoe UI', Roboto, sans-serif; */
}
.form-header {
text-align: center;
margin-bottom: 1.5rem;
}
.form-row {
display: flex;
align-items: center;
margin-bottom: 1rem;
gap: 1rem;
& label {
font-size: 1.1em;
text-align: right;
}
}
.form-label {
flex: 0 0 180px;
font-weight: 500;
color: #2d3748;
}
.form-control,
.form-select {
flex: 1;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-control:focus,
.form-select:focus {
outline: none;
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
}
.btn-primary {
display: block;
width: 100%;
max-width: 200px;
margin: 1.5rem auto 0;
padding: 0.75rem;
background-color: #4299e1;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-primary:hover {
background-color: #3182ce;
}
/* -------------------------- */
.form-footer {
text-align: center;
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid #e2e8f0;
}
.forgot-password {
color: #4299e1;
text-decoration: none;
font-size: 0.9rem;
font-weight: 500;
transition: color 0.2s ease;
display: inline-block;
padding: 0.3rem 0.6rem;
border-radius: 4px;
}
.forgot-password:hover {
color: #3182ce;
text-decoration: underline;
background-color: rgba(66, 153, 225, 0.1);
}
/* Opcional: Alineación con el botón si quieres que quede centrado debajo */
.form-footer {
display: flex;
justify-content: center;
}

View File

@ -0,0 +1,18 @@
import {simpleNotification} from '../z_comps/notify.js';
// simpleNotification("test", "sample", "error");
const phoneInput = document.getElementById("num_tel");
phoneInput.addEventListener("change", () => {
let raw = phoneInput.value.replace(/\D/g, ''); // quitar todo lo que no sea número
if (raw.length === 10) {
// aplicar formato XX XXXX XXXX
const formatted = `${raw.slice(0, 2)} ${raw.slice(2, 6)} ${raw.slice(6, 10)}`;
phoneInput.value = formatted;
} else {
// no hace nada si no son 10 dígitos, pero podrías notificar si quieres
simpleNotification("Error", "El número de teléfono debe tener 10 dígitos.", "error");
}
});

View File

@ -0,0 +1,102 @@
import { simpleNotification } from '../z_comps/notify.js';
// https://summernote.org/getting-started/#without-bootstrap-lite
$('#summernote').summernote({
placeholder: 'Escribe aquí...',
tabsize: 2,
toolbar: [
['misc', ['undo', 'redo', 'clear']],
['heading', ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']], // tamaño de encabezado
['style', ['style','bold', 'italic', 'underline']],
['fontsize', ['fontsize']],
['color', ['color']],
['height', ['height']],
['font', ['strikethrough', 'superscript', 'subscript', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['object', ['link', 'table', 'picture', 'video', 'hr']],
['misc', [ 'help', 'codeview']], //'fullscreen'
['fontname', ['fontname']],
// ['alignment', ['justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull']],
// ['paragraph', ['indent', 'outdent']],
// ['cleaner', ['removeFormat']],
],
callbacks: {
onInit: function () {
const btn = document.getElementById('btn-submit');
const toolbar = document.querySelector('.note-toolbar');
const iTitle = document.querySelector('input[name="title"]');
if (btn && toolbar && iTitle) {
// Crear un contenedor para el título en una nueva línea
const titleWrapper = document.createElement('div');
titleWrapper.classList.add('note-title-wrapper', 'mb-2'); // margen inferior
// Clonar el input o moverlo (según tu preferencia)
titleWrapper.appendChild(iTitle);
// Insertar al inicio de la barra de herramientas
toolbar.prepend(titleWrapper);
// Crear un contenedor para el botón de enviar
const btnGroup = document.createElement('div');
btnGroup.classList.add('note-btn-group', 'ms-2'); // margen izquierdo
// agregar btn enviar cambios
btnGroup.appendChild(btn);
toolbar.appendChild(btnGroup);
}
}
}
});
async function a_sendpost(title, body) {
try {
let response = await fetch('/user/save-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({"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 guardada: ${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');
}
}
function send_post(e) {
e.preventDefault();
let title_post = document.querySelector("input[name='title']").value.trim();
let body_post = $('#summernote').summernote('code'); // Obtener el contenido del editor
let body_gt11 = body_post.length > 11 ? body_post : '';
if (!title_post || (!body_post || body_gt11 === '')) {
simpleNotification('Campos obligatorios', 'Todos los campos son obligatorios', 'warning');
} else {
a_sendpost(title_post, body_post);
}
}
document.getElementById("post-form").addEventListener("submit", send_post);

View File

@ -0,0 +1,30 @@
// import { simpleNotification } from '../comps/notify.js';
import { simpleNotification } from '../z_comps/notify.js';
document.addEventListener('DOMContentLoaded', function () {
const form = document.querySelector('form');
const newPswdInput = document.querySelector('#new_pswd');
const rules = {
lengt10: (pwd) => pwd.length >= 10,
atl1num: (pwd) => /\d/.test(pwd),
atl1mayusc: (pwd) => /[A-Z]/.test(pwd),
atl1chrspe: (pwd) => /[!"#$%&\/()=?¡]/.test(pwd)
};
newPswdInput.addEventListener('input', function () {
const pwd = this.value;
for (let id in rules) {
document.querySelector(`#${id}`).textContent = rules[id](pwd) ? '✅' : '❌';
}
});
form.addEventListener('submit', function (e) {
const pwd = newPswdInput.value;
const valid = Object.values(rules).every(fn => fn(pwd));
if (!valid) {
e.preventDefault();
simpleNotification("Error", "La contraseña no cumple con todos los requisitos indicados.", "error");
}
});
});

View File

@ -0,0 +1,164 @@
import { simpleNotification } from '../z_comps/notify.js';
// VACIAR LA INFORMACIÓN DE LA BASE DE DATOS EN LA UI
let title_post = data[0];
let body_post = data[1];
// https://summernote.org/getting-started/#without-bootstrap-lite
// configuración inicial de campo de editor de texto
// Inicializa Summernote con tus opciones
$('#summernote').summernote({
placeholder: 'Escribe aquí...',
tabsize: 2,
width: 1200,
height: 500,
toolbar: [
['misc', ['undo', 'redo', 'clear']],
['heading', ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']], // tamaño de encabezado
['style', ['style','bold', 'italic', 'underline']],
['fontsize', ['fontsize']],
['color', ['color']],
['height', ['height']],
['font', ['strikethrough', 'superscript', 'subscript', 'clear']],
['para', ['ul', 'ol', 'paragraph']],
['object', ['link', 'table', 'picture', 'video', 'hr']],
['misc', [ 'help', 'codeview']], //'fullscreen'
['fontname', ['fontname']],
],
callbacks: {
onInit: function() {
const btn_submit = document.getElementById('btn-submit');
const btn_cancel = document.getElementById('btn-cancel');
const toolbar = document.querySelector('.note-toolbar');
const iTitle = document.querySelector('input[name="title"]');
if (btn_submit && toolbar && iTitle) {
const titleWrapper = document.createElement('div');
titleWrapper.classList.add('note-title-wrapper', 'mb-2'); // margen inferior
// Clonar el input o moverlo (según tu preferencia)
titleWrapper.appendChild(iTitle);
// Insertar al inicio de la barra de herramientas
toolbar.prepend(titleWrapper);
// Crear un contenedor para el botón de enviar
const btnGroup = document.createElement('div');
btnGroup.classList.add('note-btn-group', 'ms-2');
// agregar btn enviar cambios
btnGroup.appendChild(btn_submit);
// agregar btn cancelar
btnGroup.appendChild(btn_cancel);
// agregar btnGroup al toolbar
toolbar.appendChild(btnGroup);
}
}
}
});
// Luego, carga el contenido en el editor
$('#summernote').summernote('code', body_post);
// También puedes llenar el campo de título si quieres:
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', {
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', `Actualizando: ${result.title_post}...`, 'success'); // Añade tiempo
// Redirigir después de 2 segundos (puedes ajustar)
setTimeout(() => {
window.location.href = result.redirect_url || '/user/my-posts';
}, 2000);
} catch (error) {
console.error('Error al guardar:', error);
simpleNotification('Error', 'No se pudo guardar la publicación', 'error');
}
}
document.getElementById('post-form').addEventListener('submit', function(e) {
e.preventDefault();
let new_title_post = document.querySelector("input[name='title']").value.trim();
let new_body_post = $('#summernote').summernote('code'); // Obtener el contenido del editor
let body_gt11 = new_body_post.length > 11 ? new_body_post : '';
// Validación (descomenta cuando necesites)
if (!new_title_post || (!new_body_post || body_gt11 === '')) {
simpleNotification('Error', 'El título y el contenido son obligatorios', 'error');
return;
}
// en caso que la información no cambie
if (new_title_post === title_post && new_body_post === body_post) {
simpleNotification('Sin Cambios', 'No se han realizado cambios en la publicación.', 'warning');
return;
}
// en caso de que si hayan cambios
else{
// console.log(id_post);
a_sendpost(new_title_post, new_body_post, id_post);
}
});

View File

@ -0,0 +1,42 @@
.form-control{
width: 100% !important;
margin-bottom: 1em !important;
margin-top: 1em !important;
}
.note-editor {
width: 100% !important;
min-height: 65vh !important;
}
.note-editable {
min-height: 65vh !important;
}
div.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;
}
div.note-editing-area {
background-color: white !important;
}
/* aplica en la sección de edición del post */
#btn-cancel{
margin-left: 1em !important;
}
/* IMPORTANTE, UNA ANIMACIÓN DE AOS INTEFIERE CON SUMMERNOTE */
div.note-modal-backdrop {
z-index: 1 !important;
display: none !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 773 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

View File

@ -1,7 +1,14 @@
:root {
--blue-formha: #5d9dd1;
--font-size-formha: 1.2rem;
}
.navbar-custom {
background-color: #50164A !important;
font-size: 20px;
background-color: var(--blue-formha) !important;
font-size: var(--font-size-formha);
padding-top: 15px;
padding-bottom: 15px;
/* min-height: 80px; */
& .navbar-brand, .nav-link {
@ -56,4 +63,59 @@
margin-left: 1em;
margin-right: 1em;
}
/* logo bordeado blanco */
& .logo{
background-color: white;
border-radius: 10px;
background-image: url('../y_img/logos/formha_blanco_vertical.png');
width: 150px;
height: 100px;
background-size: cover; /* o cover según lo que busques */
background-repeat: no-repeat;
background-position: center;
}
}
/* Estilos personalizados para el footer */
.footer {
background-color: var(--blue-formha);
font-size: var(--font-size-formha);
color: white;
padding: 3rem 0;
& .img-cont{
padding: 0.25em;
background-color: white;
border-radius: 10px;
}
}
.footer h5 {
color: #f8f9fa;
margin-bottom: 1.5rem;
font-weight: 600;
}
.footer a {
color: #bdc3c7;
text-decoration: none;
transition: color 0.3s;
}
.footer a:hover {
color: #ffffff;
}
.social-icons a {
display: inline-block;
margin-right: 15px;
font-size: 1.25rem;
}

View File

@ -1,13 +1,24 @@
main {
display: grid;
place-content: center;
body {
font-family: "Source Sans 3", sans-serif !important;
}
/* main {
width: 80%;
/* Pantallas Ultrawide (1920px en adelante) */
@media (min-width: 1920px) {
main {
/* border: 6px solid red; */
/* max-width: 80vw !important; */
width: 80vw !important;
min-height: 85vh !important;
margin: auto;
text-align: center;
margin-top: 5em;
margin-bottom: 5em;
min-height: 85vh;
height: 85vh;
} */
margin-top: 2em;
margin-bottom: 2em;
display: grid;
/* place-content: center; */
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

View File

Before

Width:  |  Height:  |  Size: 577 KiB

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 880 KiB

After

Width:  |  Height:  |  Size: 880 KiB

View File

Before

Width:  |  Height:  |  Size: 463 KiB

After

Width:  |  Height:  |  Size: 463 KiB

View File

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

View File

Before

Width:  |  Height:  |  Size: 446 KiB

After

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="info" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 515.86 515.86">
<!-- Generator: Adobe Illustrator 29.4.0, SVG Export Plug-In . SVG Version: 2.1.0 Build 152) -->
<defs>
<style>
.st0 {
fill: #699dd3;
}
.st1 {
fill: #003374;
}
.st2 {
fill: #861e5d;
}
.st3 {
fill: #93b635;
}
</style>
</defs>
<path class="st1" d="M257.93,0C115.48,0,0,115.48,0,257.93s115.48,257.93,257.93,257.93,257.93-115.48,257.93-257.93S400.38,0,257.93,0ZM257.93,374.4c-64.32,0-116.47-52.14-116.47-116.47s52.14-116.47,116.47-116.47,116.47,52.14,116.47,116.47-52.14,116.47-116.47,116.47Z"/>
<path class="st0" d="M257.93,17.99C125.41,17.99,17.99,125.41,17.99,257.93s107.43,239.94,239.94,239.94,239.94-107.43,239.94-239.94S390.45,17.99,257.93,17.99ZM257.93,374.4c-64.32,0-116.47-52.14-116.47-116.47s52.14-116.47,116.47-116.47,116.47,52.14,116.47,116.47-52.14,116.47-116.47,116.47Z"/>
<path class="st2" d="M152.7,307.88c-7.2-15.14-11.24-32.07-11.24-49.95s4.05-34.84,11.26-49.99c16.96-56.07,68.26-97.9,171.82-97.9,19.54,0,38.4,2.92,56.16,8.35,6.31,1.93,10.5-6.48,5.14-10.32-39.97-28.63-88.93-45.49-141.85-45.49C117.37,62.58,13.29,159.04,1.19,282.49c12.37,130.93,122.57,233.37,256.74,233.37,41.87,0,81.36-10.04,116.32-27.74-15.86,4.24-32.52,6.51-49.72,6.51-72.39,0-149.29-92.49-171.84-186.75Z"/>
<path class="st3" d="M497.87,300.26c0-18.96-3.56-37.08-9.99-53.78-.34-.88-.68-1.76-1.04-2.63-.5-1.26-1.05-2.5-1.58-3.74-1.68-3.88-3.45-7.72-5.43-11.43-.01.07-.02.13-.03.2-25.37-46.71-74.85-78.43-131.74-78.43-12.4,0-24.44,1.54-35.96,4.38,38.48,20.26,64.26,61.4,62.19,108.38-2.65,59.96-51.6,108.7-111.57,111.1-47.26,1.89-88.55-24.41-108.5-63.43-22.95-30.47-36.58-68.37-36.58-109.46,0-12.39,1.25-24.49,3.62-36.18,1.23-6.08-6.66-9.54-10.43-4.6-29.23,38.36-45.92,86.78-43.9,139.18,4.37,113.08,97.12,204.44,210.25,207.21,104.27,2.55,192.24-68.89,215.29-165.52.76-3.17,1.42-6.38,2.03-9.6,1.6-7.46,2.66-15.11,3.11-22.94.03-.33.07-.65.09-.98.1-2.31.18-4.62.18-6.96,0-.11-.01-.23-.01-.34,0-.14.01-.28.01-.42Z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -0,0 +1,188 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="1183.704"
height="460.58533"
viewBox="0 0 1183.704 460.58532"
sodipodi:docname="logo_final.ai"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath22">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-213.37938,-127.86631)"
id="path22" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath24">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-213.37938,-127.86631)"
id="path24" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath26">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-172.8545,-153.4841)"
id="path26" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath28">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-305.78463,-156.41581)"
id="path28" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath30">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-350.95893,-135.9466)"
id="path30" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath32">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-425.91109,-172.7705)"
id="path32" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath34">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-508.43884,-178.3894)"
id="path34" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath36">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-567.53771,-135.9466)"
id="path36" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath38">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-653.37649,-135.9466)"
id="path38" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath40">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-2.5000002e-6)"
id="path40" />
</clipPath>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:page
x="0"
y="0"
inkscape:label="2"
id="page20"
width="1183.704"
height="460.58533"
margin="97.848663 152.06134 97.849335"
bleed="0" />
</sodipodi:namedview>
<g
id="layer-MC0"
inkscape:groupmode="layer"
inkscape:label="fondo"
transform="translate(-761.34264)" />
<g
id="layer-MC1"
inkscape:groupmode="layer"
inkscape:label="info"
transform="translate(-761.34264)">
<path
id="path21"
d="m 0,0 c -24.771,0 -44.853,20.081 -44.853,44.853 0,24.772 20.082,44.853 44.853,44.853 24.772,0 44.853,-20.081 44.853,-44.853 C 44.853,20.081 24.772,0 0,0 m 0,144.186 c -54.86,0 -99.333,-44.473 -99.333,-99.333 0,-54.86 44.473,-99.333 99.333,-99.333 54.86,0 99.333,44.473 99.333,99.333 0,54.86 -44.473,99.333 -99.333,99.333"
style="fill:#29377c;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1045.8485,290.09693)"
clip-path="url(#clipPath22)" />
<path
id="path23"
d="m 0,0 c -24.771,0 -44.853,20.081 -44.853,44.853 0,24.772 20.082,44.853 44.853,44.853 24.772,0 44.853,-20.081 44.853,-44.853 C 44.853,20.081 24.772,0 0,0 m 0,137.258 c -51.034,0 -92.405,-41.371 -92.405,-92.405 0,-51.034 41.371,-92.405 92.405,-92.405 51.034,0 92.405,41.371 92.405,92.405 0,51.034 -41.371,92.405 -92.405,92.405"
style="fill:#46a0de;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1045.8485,290.09693)"
clip-path="url(#clipPath24)" />
<path
id="path25"
d="m 0,0 c -2.772,5.83 -4.328,12.35 -4.328,19.235 0,6.893 1.559,13.419 4.336,19.254 6.534,21.592 26.289,37.702 66.169,37.702 7.525,0 14.787,-1.125 21.628,-3.214 2.429,-0.742 4.045,2.495 1.98,3.974 -15.393,11.024 -34.249,17.518 -54.627,17.518 -48.766,0 -88.847,-37.15 -93.506,-84.692 4.765,-50.422 47.205,-89.875 98.873,-89.875 16.123,0 31.335,3.866 44.798,10.683 -6.107,-1.631 -12.524,-2.506 -19.146,-2.506 C 38.299,-71.921 8.685,-36.303 0,0"
style="fill:#9c135e;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,991.81533,255.93987)"
clip-path="url(#clipPath26)" />
<path
id="path27"
d="m 0,0 c 0,7.302 -1.37,14.281 -3.845,20.711 -0.131,0.34 -0.263,0.678 -0.4,1.015 -0.194,0.484 -0.403,0.962 -0.609,1.44 -0.646,1.496 -1.329,2.972 -2.091,4.402 -0.004,-0.026 -0.009,-0.051 -0.013,-0.077 -9.769,17.99 -28.826,30.204 -50.737,30.204 -4.776,0 -9.412,-0.591 -13.85,-1.685 14.82,-7.803 24.747,-23.648 23.948,-41.737 -1.019,-23.093 -19.872,-41.862 -42.969,-42.786 -18.2,-0.728 -34.101,9.401 -41.786,24.428 -8.841,11.736 -14.086,26.331 -14.086,42.156 0,4.772 0.481,9.431 1.392,13.933 0.475,2.344 -2.566,3.674 -4.015,1.773 -11.258,-14.772 -17.686,-33.419 -16.906,-53.6 1.684,-43.551 37.402,-78.734 80.972,-79.8 40.156,-0.982 74.036,26.531 82.912,63.745 0.292,1.222 0.547,2.456 0.783,3.698 0.617,2.871 1.024,5.82 1.196,8.833 0.01,0.126 0.027,0.25 0.036,0.376 0.04,0.889 0.068,1.78 0.068,2.679 0,0.044 -0.004,0.087 -0.004,0.131 C -0.004,-0.107 0,-0.054 0,0"
style="fill:#6bc62f;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1169.0555,252.03093)"
clip-path="url(#clipPath28)" />
<path
id="path29"
d="M 0,0 V 73.548 H 50.42 V 61.106 H 14.85 V 43.696 H 45.553 V 31.254 H 14.85 V 0 Z"
style="fill:#646769;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1229.2879,279.3232)"
clip-path="url(#clipPath30)" />
<path
id="path31"
d="m 0,0 c 0,-8.328 1.923,-14.641 5.769,-18.938 3.846,-4.299 8.73,-6.447 14.65,-6.447 5.92,0 10.777,2.132 14.573,6.396 3.796,4.265 5.695,10.661 5.695,19.19 0,8.428 -1.848,14.716 -5.543,18.863 -3.697,4.147 -8.605,6.221 -14.725,6.221 -6.121,0 -11.055,-2.099 -14.8,-6.296 C 1.873,14.791 0,8.462 0,0 m -15.301,-0.502 c 0,7.492 1.12,13.78 3.361,18.864 1.672,3.746 3.954,7.106 6.848,10.085 2.892,2.974 6.062,5.182 9.507,6.622 4.582,1.938 9.865,2.908 15.853,2.908 10.837,0 19.507,-3.361 26.013,-10.083 6.504,-6.723 9.758,-16.072 9.758,-28.045 0,-11.874 -3.229,-21.164 -9.683,-27.869 -6.455,-6.705 -15.084,-10.059 -25.887,-10.059 -10.937,0 -19.634,3.337 -26.088,10.01 -6.455,6.672 -9.682,15.861 -9.682,27.567"
style="fill:#646769;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1329.2241,230.22467)"
clip-path="url(#clipPath32)" />
<path
id="path33"
d="m 0,0 h 10.987 c 7.124,0 11.571,0.301 13.345,0.903 1.773,0.602 3.161,1.639 4.164,3.111 1.003,1.471 1.505,3.311 1.505,5.518 0,2.475 -0.661,4.472 -1.981,5.995 -1.322,1.523 -3.186,2.485 -5.595,2.886 -1.204,0.165 -4.816,0.25 -10.836,0.25 H 0 Z m -14.85,-42.443 v 73.548 h 31.255 c 7.859,0 13.57,-0.661 17.133,-1.982 3.561,-1.322 6.413,-3.671 8.554,-7.049 2.139,-3.378 3.21,-7.242 3.21,-11.59 0,-5.516 -1.623,-10.075 -4.866,-13.669 -3.244,-3.597 -8.095,-5.863 -14.549,-6.8 3.211,-1.873 5.861,-3.928 7.952,-6.169 2.09,-2.241 4.908,-6.221 8.453,-11.941 l 8.981,-14.348 H 33.513 L 22.777,-26.44 c -3.813,5.721 -6.422,9.324 -7.827,10.813 -1.404,1.487 -2.893,2.508 -4.465,3.059 -1.573,0.553 -4.063,0.828 -7.475,0.828 H 0 v -30.703 z"
style="fill:#646769;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1439.2611,222.7328)"
clip-path="url(#clipPath34)" />
<path
id="path35"
d="M 0,0 V 73.548 H 22.225 L 35.57,23.379 48.764,73.548 H 71.039 V 0 H 57.243 V 57.895 L 42.644,0 H 28.345 L 13.796,57.895 V 0 Z"
style="fill:#646769;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1518.0596,279.3232)"
clip-path="url(#clipPath36)" />
<path
id="path37"
d="M 0,0 V 73.548 H 14.85 V 44.601 h 29.097 v 28.947 h 14.85 V 0 H 43.947 V 32.159 H 14.85 V 0 Z"
style="fill:#6bc62f;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1632.5113,279.3232)"
clip-path="url(#clipPath38)" />
<path
id="path39"
d="m 753.463,210.747 h 12.141 v -12.09 h -12.141 z m 3.363,-49.214 c -1.806,-0.602 -4.667,-1.322 -8.58,-2.159 -3.913,-0.836 -6.471,-1.654 -7.675,-2.457 -1.842,-1.304 -2.76,-2.961 -2.76,-4.968 0,-1.973 0.735,-3.679 2.207,-5.115 1.472,-1.44 3.345,-2.158 5.619,-2.158 2.542,0 4.968,0.836 7.273,2.508 1.708,1.271 2.828,2.826 3.363,4.667 0.368,1.204 0.553,3.494 0.553,6.872 z m -24.935,49.214 h 12.092 v -12.09 h -12.092 z m 6.071,-37.776 -12.794,2.308 c 1.438,5.151 3.913,8.963 7.425,11.438 3.512,2.475 8.729,3.713 15.653,3.713 6.288,0 10.97,-0.746 14.047,-2.233 3.077,-1.487 5.243,-3.378 6.496,-5.668 1.256,-2.292 1.883,-6.498 1.883,-12.617 l -0.151,-16.455 c 0,-4.685 0.226,-8.138 0.677,-10.361 0.452,-2.225 1.296,-4.607 2.534,-7.149 h -13.947 c -0.368,0.936 -0.821,2.323 -1.354,4.163 -0.236,0.836 -0.401,1.389 -0.504,1.656 -2.408,-2.341 -4.983,-4.098 -7.724,-5.269 -2.744,-1.168 -5.67,-1.754 -8.78,-1.754 -5.485,0 -9.808,1.487 -12.969,4.466 -3.16,2.974 -4.74,6.738 -4.74,11.287 0,3.01 0.718,5.693 2.156,8.052 1.439,2.357 3.453,4.163 6.046,5.418 2.591,1.254 6.329,2.349 11.213,3.286 6.588,1.237 11.153,2.39 13.697,3.463 v 1.404 c 0,2.707 -0.671,4.639 -2.007,5.794 -1.34,1.153 -3.864,1.73 -7.577,1.73 -2.508,0 -4.464,-0.494 -5.868,-1.48 -1.405,-0.985 -2.544,-2.717 -3.412,-5.192"
style="fill:#46a0de;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,761.34267,460.58533)"
clip-path="url(#clipPath40)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="741.34265"
height="687.81335"
viewBox="0 0 741.34265 687.81335"
sodipodi:docname="logo_final.ai"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath2">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-278.00341,-257.5454)"
id="path2" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath4">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-278.00341,-257.5454)"
id="path4" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath6">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-232.5616,-286.27151)"
id="path6" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath8">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-381.62011,-289.55881)"
id="path8" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath10">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-116.36301,-97.5947)"
id="path10" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath12">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-173.6766,-125.7527)"
id="path12" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath14">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-236.7829,-130.0493)"
id="path14" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath16">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-281.97391,-97.5947)"
id="path16" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath18">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-347.61211,-97.5947)"
id="path18" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath20">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
id="path20" />
</clipPath>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:page
x="0"
y="0"
inkscape:label="1"
id="page1"
width="741.34265"
height="687.81335"
margin="128.84653 155.15067 128.84534"
bleed="0" />
</sodipodi:namedview>
<g
id="layer-MC0"
inkscape:groupmode="layer"
inkscape:label="fondo" />
<g
id="layer-MC1"
inkscape:groupmode="layer"
inkscape:label="info">
<path
id="path1"
d="m 0,0 c -27.777,0 -50.295,22.518 -50.295,50.295 0,27.777 22.518,50.295 50.295,50.295 27.777,0 50.295,-22.518 50.295,-50.295 C 50.295,22.518 27.777,0 0,0 m 0,161.68 c -61.516,0 -111.385,-49.869 -111.385,-111.385 0,-61.516 49.869,-111.385 111.385,-111.385 61.517,0 111.385,49.869 111.385,111.385 C 111.385,111.811 61.517,161.68 0,161.68"
style="fill:#29377c;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,370.6712,344.41947)"
clip-path="url(#clipPath2)" />
<path
id="path3"
d="m 0,0 c -27.777,0 -50.295,22.518 -50.295,50.295 0,27.777 22.518,50.295 50.295,50.295 27.777,0 50.295,-22.518 50.295,-50.295 C 50.295,22.518 27.777,0 0,0 m 0,153.912 c -57.226,0 -103.617,-46.391 -103.617,-103.617 0,-57.226 46.391,-103.617 103.617,-103.617 57.226,0 103.617,46.391 103.617,103.617 0,57.226 -46.391,103.617 -103.617,103.617"
style="fill:#46a0de;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,370.6712,344.41947)"
clip-path="url(#clipPath4)" />
<path
id="path5"
d="m 0,0 c -3.109,6.537 -4.853,13.849 -4.853,21.569 0,7.729 1.747,15.047 4.862,21.59 7.326,24.212 29.478,42.277 74.197,42.277 8.439,0 16.581,-1.262 24.252,-3.605 2.724,-0.831 4.536,2.798 2.221,4.456 -17.261,12.362 -38.405,19.644 -61.255,19.644 -54.683,0 -99.628,-41.658 -104.852,-94.968 5.343,-56.539 52.932,-100.779 110.87,-100.779 18.079,0 35.136,4.335 50.234,11.979 -6.849,-1.829 -14.044,-2.81 -21.47,-2.81 C 42.946,-80.647 9.739,-40.708 0,0"
style="fill:#9c135e;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,310.08213,306.118)"
clip-path="url(#clipPath6)" />
<path
id="path7"
d="m 0,0 c 0,8.188 -1.537,16.013 -4.312,23.224 -0.146,0.381 -0.295,0.76 -0.448,1.137 -0.218,0.544 -0.452,1.079 -0.683,1.616 -0.724,1.677 -1.49,3.333 -2.344,4.936 -0.005,-0.029 -0.011,-0.058 -0.015,-0.087 -10.954,20.173 -32.324,33.869 -56.893,33.869 -5.355,0 -10.554,-0.663 -15.531,-1.89 16.618,-8.749 27.75,-26.517 26.854,-46.801 -1.143,-25.894 -22.283,-46.94 -48.182,-47.976 -20.408,-0.816 -38.239,10.541 -46.857,27.392 -9.913,13.159 -15.795,29.525 -15.795,47.27 0,5.351 0.54,10.575 1.562,15.624 0.532,2.628 -2.878,4.12 -4.503,1.988 -12.623,-16.564 -19.832,-37.474 -18.957,-60.104 1.889,-48.834 41.94,-88.287 90.797,-89.481 45.027,-1.102 83.018,29.749 92.971,71.479 0.327,1.369 0.614,2.754 0.878,4.146 0.693,3.22 1.148,6.527 1.342,9.905 0.011,0.141 0.029,0.28 0.04,0.421 C -0.032,-2.335 0,-1.335 0,-0.327 0,-0.278 -0.005,-0.23 -0.005,-0.181 -0.005,-0.12 0,-0.061 0,0"
style="fill:#6bc62f;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,508.8268,301.73493)"
clip-path="url(#clipPath8)" />
<path
id="path9"
d="M 0,0 V 56.239 H 38.554 V 46.726 H 11.355 V 33.413 H 34.833 V 23.899 H 11.355 V 0 Z"
style="fill:#646769;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,155.15067,557.68707)"
clip-path="url(#clipPath10)" />
<path
id="path11"
d="m 0,0 c 0,-6.368 1.471,-11.196 4.412,-14.481 2.94,-3.288 6.675,-4.93 11.202,-4.93 4.526,0 8.241,1.63 11.143,4.89 2.903,3.262 4.355,8.153 4.355,14.674 0,6.445 -1.413,11.253 -4.239,14.425 -2.826,3.171 -6.579,4.757 -11.259,4.757 -4.681,0 -8.454,-1.606 -11.317,-4.814 C 1.432,11.31 0,6.47 0,0 m -11.701,-0.384 c 0,5.729 0.857,10.537 2.571,14.425 1.278,2.864 3.024,5.434 5.237,7.711 2.211,2.275 4.635,3.963 7.269,5.064 3.504,1.482 7.544,2.224 12.122,2.224 8.287,0 14.917,-2.57 19.892,-7.71 4.973,-5.141 7.461,-12.29 7.461,-21.446 0,-9.079 -2.469,-16.183 -7.404,-21.31 -4.936,-5.127 -11.534,-7.692 -19.795,-7.692 -8.363,0 -15.013,2.552 -19.949,7.655 -4.936,5.101 -7.404,12.128 -7.404,21.079"
style="fill:#646769;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,231.5688,520.14307)"
clip-path="url(#clipPath12)" />
<path
id="path13"
d="m 0,0 h 8.401 c 5.448,0 8.849,0.23 10.205,0.691 1.355,0.46 2.417,1.253 3.184,2.378 0.767,1.125 1.151,2.532 1.151,4.22 0,1.892 -0.506,3.419 -1.515,4.584 -1.011,1.164 -2.437,1.9 -4.278,2.207 -0.921,0.126 -3.683,0.191 -8.286,0.191 H 0 Z m -11.355,-32.455 v 56.24 h 23.9 c 6.009,0 10.376,-0.506 13.101,-1.515 2.723,-1.011 4.903,-2.807 6.54,-5.39 1.636,-2.584 2.455,-5.538 2.455,-8.863 0,-4.218 -1.241,-7.704 -3.721,-10.453 -2.481,-2.75 -6.19,-4.483 -11.125,-5.199 2.455,-1.432 4.482,-3.004 6.081,-4.718 1.598,-1.713 3.753,-4.757 6.464,-9.13 l 6.866,-10.972 h -13.58 l -8.209,12.237 c -2.916,4.375 -4.911,7.13 -5.985,8.268 -1.074,1.138 -2.212,1.919 -3.414,2.34 -1.203,0.423 -3.108,0.633 -5.716,0.633 H 0 v -23.478 z"
style="fill:#646769;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,315.71053,514.41427)"
clip-path="url(#clipPath14)" />
<path
id="path15"
d="M 0,0 V 56.239 H 16.995 L 27.199,17.877 37.288,56.239 H 54.321 V 0 H 43.772 V 44.27 L 32.608,0 H 21.675 L 10.55,44.27 V 0 Z"
style="fill:#646769;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,375.9652,557.68707)"
clip-path="url(#clipPath16)" />
<path
id="path17"
d="M 0,0 V 56.239 H 11.355 V 34.105 h 22.25 V 56.239 H 44.96 V 0 H 33.605 V 24.591 H 11.355 V 0 Z"
style="fill:#6bc62f;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,463.4828,557.68707)"
clip-path="url(#clipPath18)" />
<path
id="path19"
d="m 424.145,154.792 h 9.284 v -9.245 h -9.284 z m 2.571,-37.632 c -1.381,-0.461 -3.568,-1.011 -6.56,-1.651 -2.993,-0.639 -4.948,-1.265 -5.869,-1.879 -1.408,-0.997 -2.111,-2.264 -2.111,-3.798 0,-1.509 0.563,-2.814 1.688,-3.912 1.125,-1.101 2.558,-1.65 4.297,-1.65 1.943,0 3.798,0.639 5.562,1.918 1.305,0.972 2.161,2.16 2.571,3.568 0.281,0.921 0.422,2.672 0.422,5.255 z m -19.067,37.632 h 9.246 v -9.245 h -9.246 z m 4.643,-28.886 -9.783,1.765 c 1.1,3.938 2.992,6.854 5.677,8.747 2.686,1.892 6.676,2.838 11.97,2.838 4.808,0 8.388,-0.57 10.741,-1.707 2.353,-1.138 4.009,-2.583 4.968,-4.334 0.959,-1.753 1.439,-4.969 1.439,-9.648 l -0.115,-12.583 c 0,-3.582 0.173,-6.222 0.517,-7.922 0.346,-1.702 0.991,-3.524 1.938,-5.467 h -10.665 c -0.281,0.716 -0.627,1.776 -1.035,3.183 -0.181,0.639 -0.307,1.062 -0.385,1.267 -1.842,-1.791 -3.811,-3.134 -5.907,-4.029 -2.098,-0.894 -4.335,-1.342 -6.714,-1.342 -4.194,0 -7.499,1.137 -9.917,3.415 -2.416,2.275 -3.624,5.153 -3.624,8.631 0,2.302 0.549,4.354 1.649,6.157 1.1,1.803 2.64,3.184 4.623,4.144 1.981,0.958 4.84,1.796 8.574,2.512 5.038,0.946 8.528,1.828 10.473,2.648 v 1.074 c 0,2.07 -0.513,3.547 -1.534,4.43 -1.025,0.882 -2.955,1.323 -5.794,1.323 -1.918,0 -3.413,-0.378 -4.487,-1.132 -1.074,-0.753 -1.946,-2.077 -2.609,-3.97"
style="fill:#46a0de;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,0,687.81333)"
clip-path="url(#clipPath20)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="1183.704"
height="460.58533"
viewBox="0 0 1183.704 460.58532"
sodipodi:docname="logo_final.ai"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath64">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-213.37941,-127.86631)"
id="path64" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath66">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-213.37941,-127.86631)"
id="path66" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath68">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-172.8545,-153.4841)"
id="path68" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath70">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-305.78458,-156.41581)"
id="path70" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath72">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-350.95888,-135.9466)"
id="path72" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath74">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-425.91111,-172.7705)"
id="path74" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath76">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-508.43879,-178.3894)"
id="path76" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath78">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-567.53774,-135.9466)"
id="path78" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath80">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(-653.37652,-135.9466)"
id="path80" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath82">
<path
d="M 0,345.439 H 887.778 V 0 H 0 Z"
transform="translate(2.5000001e-5)"
id="path82" />
</clipPath>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:page
x="0"
y="0"
inkscape:label="4"
id="page61"
width="1183.704"
height="460.58533"
margin="0"
bleed="0" />
</sodipodi:namedview>
<g
id="layer-MC0"
inkscape:groupmode="layer"
inkscape:label="fondo"
transform="translate(-2726.3893)">
<path
id="path62"
d="M 0,0 H 887.778 V 345.439 H 0 Z"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2726.3893,460.58533)" />
</g>
<g
id="layer-MC1"
inkscape:groupmode="layer"
inkscape:label="info"
transform="translate(-2726.3893)">
<path
id="path63"
d="m 0,0 c -24.771,0 -44.853,20.081 -44.853,44.853 0,24.772 20.082,44.853 44.853,44.853 24.772,0 44.853,-20.081 44.853,-44.853 C 44.853,20.081 24.772,0 0,0 m 0,144.186 c -54.86,0 -99.333,-44.473 -99.333,-99.333 0,-54.86 44.473,-99.333 99.333,-99.333 54.86,0 99.333,44.473 99.333,99.333 0,54.86 -44.473,99.333 -99.333,99.333"
style="fill:#29377c;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,3010.8952,290.09693)"
clip-path="url(#clipPath64)" />
<path
id="path65"
d="m 0,0 c -24.771,0 -44.853,20.081 -44.853,44.853 0,24.772 20.082,44.853 44.853,44.853 24.772,0 44.853,-20.081 44.853,-44.853 C 44.853,20.081 24.772,0 0,0 m 0,137.258 c -51.034,0 -92.405,-41.371 -92.405,-92.405 0,-51.034 41.371,-92.405 92.405,-92.405 51.034,0 92.405,41.371 92.405,92.405 0,51.034 -41.371,92.405 -92.405,92.405"
style="fill:#46a0de;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,3010.8952,290.09693)"
clip-path="url(#clipPath66)" />
<path
id="path67"
d="m 0,0 c -2.772,5.83 -4.328,12.35 -4.328,19.235 0,6.893 1.559,13.419 4.336,19.254 6.534,21.592 26.289,37.702 66.169,37.702 7.525,0 14.787,-1.125 21.628,-3.214 2.429,-0.742 4.045,2.495 1.98,3.974 -15.393,11.024 -34.249,17.518 -54.627,17.518 -48.766,0 -88.847,-37.15 -93.506,-84.692 4.765,-50.422 47.205,-89.875 98.873,-89.875 16.123,0 31.335,3.866 44.798,10.683 -6.107,-1.631 -12.524,-2.506 -19.146,-2.506 C 38.299,-71.921 8.685,-36.303 0,0"
style="fill:#9c135e;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2956.862,255.93987)"
clip-path="url(#clipPath68)" />
<path
id="path69"
d="m 0,0 c 0,7.302 -1.37,14.281 -3.845,20.711 -0.131,0.34 -0.263,0.678 -0.4,1.015 -0.194,0.484 -0.403,0.962 -0.609,1.44 -0.646,1.496 -1.329,2.972 -2.091,4.402 -0.004,-0.026 -0.009,-0.051 -0.013,-0.077 -9.769,17.99 -28.826,30.204 -50.737,30.204 -4.776,0 -9.412,-0.591 -13.85,-1.685 14.82,-7.803 24.747,-23.648 23.948,-41.737 -1.019,-23.093 -19.872,-41.862 -42.969,-42.786 -18.2,-0.728 -34.101,9.401 -41.786,24.428 -8.841,11.736 -14.086,26.331 -14.086,42.156 0,4.772 0.481,9.431 1.392,13.933 0.475,2.344 -2.566,3.674 -4.015,1.773 -11.258,-14.772 -17.686,-33.419 -16.906,-53.6 1.684,-43.551 37.402,-78.734 80.972,-79.8 40.156,-0.982 74.036,26.531 82.912,63.745 0.292,1.222 0.547,2.456 0.783,3.698 0.617,2.871 1.024,5.82 1.196,8.833 0.01,0.126 0.027,0.25 0.036,0.376 0.04,0.889 0.068,1.78 0.068,2.679 0,0.044 -0.004,0.087 -0.004,0.131 C -0.004,-0.107 0,-0.054 0,0"
style="fill:#6bc62f;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,3134.1021,252.03093)"
clip-path="url(#clipPath70)" />
<path
id="path71"
d="M 0,0 V 73.548 H 50.42 V 61.106 H 14.85 V 43.696 H 45.553 V 31.254 H 14.85 V 0 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,3194.3345,279.3232)"
clip-path="url(#clipPath72)" />
<path
id="path73"
d="m 0,0 c 0,-8.328 1.923,-14.641 5.769,-18.938 3.846,-4.299 8.73,-6.447 14.65,-6.447 5.92,0 10.777,2.132 14.573,6.396 3.796,4.265 5.695,10.661 5.695,19.19 0,8.428 -1.848,14.716 -5.543,18.863 -3.697,4.147 -8.605,6.221 -14.725,6.221 -6.121,0 -11.055,-2.099 -14.8,-6.296 C 1.873,14.791 0,8.462 0,0 m -15.301,-0.502 c 0,7.492 1.12,13.78 3.361,18.864 1.672,3.746 3.954,7.106 6.848,10.085 2.892,2.974 6.062,5.182 9.507,6.622 4.582,1.938 9.865,2.908 15.853,2.908 10.837,0 19.507,-3.361 26.013,-10.083 6.504,-6.723 9.758,-16.072 9.758,-28.045 0,-11.874 -3.229,-21.164 -9.683,-27.869 -6.455,-6.705 -15.084,-10.059 -25.887,-10.059 -10.937,0 -19.634,3.337 -26.088,10.01 -6.455,6.672 -9.682,15.861 -9.682,27.567"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,3294.2708,230.22467)"
clip-path="url(#clipPath74)" />
<path
id="path75"
d="m 0,0 h 10.987 c 7.124,0 11.571,0.301 13.345,0.903 1.773,0.602 3.161,1.639 4.164,3.111 1.003,1.471 1.505,3.311 1.505,5.518 0,2.475 -0.661,4.472 -1.981,5.995 -1.322,1.523 -3.186,2.485 -5.595,2.886 -1.204,0.165 -4.816,0.25 -10.836,0.25 H 0 Z m -14.85,-42.443 v 73.548 h 31.255 c 7.859,0 13.57,-0.661 17.133,-1.982 3.561,-1.322 6.413,-3.671 8.554,-7.049 2.139,-3.378 3.21,-7.242 3.21,-11.59 0,-5.516 -1.623,-10.075 -4.866,-13.669 -3.244,-3.597 -8.095,-5.863 -14.549,-6.8 3.211,-1.873 5.861,-3.928 7.952,-6.169 2.09,-2.241 4.908,-6.221 8.453,-11.941 l 8.981,-14.348 H 33.513 L 22.777,-26.44 c -3.813,5.721 -6.422,9.324 -7.827,10.813 -1.404,1.487 -2.893,2.508 -4.465,3.059 -1.573,0.553 -4.063,0.828 -7.475,0.828 H 0 v -30.703 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,3404.3077,222.7328)"
clip-path="url(#clipPath76)" />
<path
id="path77"
d="M 0,0 V 73.548 H 22.225 L 35.57,23.379 48.764,73.548 H 71.039 V 0 H 57.243 V 57.895 L 42.644,0 H 28.345 L 13.796,57.895 V 0 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,3483.1063,279.3232)"
clip-path="url(#clipPath78)" />
<path
id="path79"
d="M 0,0 V 73.548 H 14.85 V 44.601 h 29.097 v 28.947 h 14.85 V 0 H 43.947 V 32.159 H 14.85 V 0 Z"
style="fill:#6bc62f;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,3597.558,279.3232)"
clip-path="url(#clipPath80)" />
<path
id="path81"
d="m 753.463,210.747 h 12.141 v -12.09 h -12.141 z m 3.363,-49.214 c -1.806,-0.602 -4.667,-1.322 -8.58,-2.159 -3.913,-0.836 -6.471,-1.654 -7.675,-2.457 -1.842,-1.304 -2.76,-2.961 -2.76,-4.968 0,-1.973 0.735,-3.679 2.207,-5.115 1.472,-1.44 3.345,-2.158 5.619,-2.158 2.542,0 4.968,0.836 7.273,2.508 1.708,1.271 2.828,2.826 3.363,4.667 0.368,1.204 0.553,3.494 0.553,6.872 z m -24.935,49.214 h 12.092 v -12.09 h -12.092 z m 6.071,-37.776 -12.794,2.308 c 1.438,5.151 3.913,8.963 7.425,11.438 3.512,2.475 8.729,3.713 15.653,3.713 6.288,0 10.97,-0.746 14.047,-2.233 3.077,-1.487 5.243,-3.378 6.496,-5.668 1.256,-2.292 1.883,-6.498 1.883,-12.617 l -0.151,-16.455 c 0,-4.685 0.226,-8.138 0.677,-10.361 0.452,-2.225 1.296,-4.607 2.534,-7.149 h -13.947 c -0.368,0.936 -0.821,2.323 -1.354,4.163 -0.236,0.836 -0.401,1.389 -0.504,1.656 -2.408,-2.341 -4.983,-4.098 -7.724,-5.269 -2.744,-1.168 -5.67,-1.754 -8.78,-1.754 -5.485,0 -9.808,1.487 -12.969,4.466 -3.16,2.974 -4.74,6.738 -4.74,11.287 0,3.01 0.718,5.693 2.156,8.052 1.439,2.357 3.453,4.163 6.046,5.418 2.591,1.254 6.329,2.349 11.213,3.286 6.588,1.237 11.153,2.39 13.697,3.463 v 1.404 c 0,2.707 -0.671,4.639 -2.007,5.794 -1.34,1.153 -3.864,1.73 -7.577,1.73 -2.508,0 -4.464,-0.494 -5.868,-1.48 -1.405,-0.985 -2.544,-2.717 -3.412,-5.192"
style="fill:#46a0de;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2726.3893,460.58533)"
clip-path="url(#clipPath82)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="741.34265"
height="687.81335"
viewBox="0 0 741.34266 687.81334"
sodipodi:docname="logo_final.ai"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1">
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath43">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-278.00343,-257.5454)"
id="path43" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath45">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-278.00343,-257.5454)"
id="path45" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath47">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-232.56161,-286.27151)"
id="path47" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath49">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-381.62013,-289.55881)"
id="path49" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath51">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-116.36298,-97.5947)"
id="path51" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath53">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-173.67663,-125.7527)"
id="path53" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath55">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-236.78291,-130.0493)"
id="path55" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath57">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-281.97393,-97.5947)"
id="path57" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath59">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-347.61213,-97.5947)"
id="path59" />
</clipPath>
<clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath61">
<path
d="M 0,515.86 H 556.007 V 0 H 0 Z"
transform="translate(-2.5000001e-5)"
id="path61" />
</clipPath>
</defs>
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:page
x="0"
y="0"
inkscape:label="3"
id="page40"
width="741.34265"
height="687.81335"
margin="0"
bleed="0" />
</sodipodi:namedview>
<g
id="layer-MC0"
inkscape:groupmode="layer"
inkscape:label="fondo"
transform="translate(-1965.0467)">
<path
id="path41"
d="M 0,0 H 556.007 V 515.86 H 0 Z"
style="fill:#231f20;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1965.0467,687.81333)" />
</g>
<g
id="layer-MC1"
inkscape:groupmode="layer"
inkscape:label="info"
transform="translate(-1965.0467)">
<path
id="path42"
d="m 0,0 c -27.777,0 -50.295,22.518 -50.295,50.295 0,27.777 22.518,50.295 50.295,50.295 27.777,0 50.295,-22.518 50.295,-50.295 C 50.295,22.518 27.777,0 0,0 m 0,161.68 c -61.516,0 -111.385,-49.869 -111.385,-111.385 0,-61.516 49.869,-111.385 111.385,-111.385 61.517,0 111.385,49.869 111.385,111.385 C 111.385,111.811 61.517,161.68 0,161.68"
style="fill:#29377c;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2335.7179,344.41947)"
clip-path="url(#clipPath43)" />
<path
id="path44"
d="m 0,0 c -27.777,0 -50.295,22.518 -50.295,50.295 0,27.777 22.518,50.295 50.295,50.295 27.777,0 50.295,-22.518 50.295,-50.295 C 50.295,22.518 27.777,0 0,0 m 0,153.912 c -57.226,0 -103.617,-46.391 -103.617,-103.617 0,-57.226 46.391,-103.617 103.617,-103.617 57.226,0 103.617,46.391 103.617,103.617 0,57.226 -46.391,103.617 -103.617,103.617"
style="fill:#46a0de;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2335.7179,344.41947)"
clip-path="url(#clipPath45)" />
<path
id="path46"
d="m 0,0 c -3.109,6.537 -4.853,13.849 -4.853,21.569 0,7.729 1.747,15.047 4.862,21.59 7.326,24.212 29.478,42.277 74.197,42.277 8.439,0 16.581,-1.262 24.252,-3.605 2.724,-0.831 4.536,2.798 2.221,4.456 -17.261,12.362 -38.405,19.644 -61.255,19.644 -54.683,0 -99.628,-41.658 -104.852,-94.968 5.343,-56.539 52.932,-100.779 110.87,-100.779 18.079,0 35.136,4.335 50.234,11.979 -6.849,-1.829 -14.044,-2.81 -21.47,-2.81 C 42.946,-80.647 9.739,-40.708 0,0"
style="fill:#9c135e;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2275.1288,306.118)"
clip-path="url(#clipPath47)" />
<path
id="path48"
d="m 0,0 c 0,8.188 -1.537,16.013 -4.312,23.224 -0.146,0.381 -0.295,0.76 -0.448,1.137 -0.218,0.544 -0.452,1.079 -0.683,1.616 -0.724,1.677 -1.49,3.333 -2.344,4.936 -0.005,-0.029 -0.011,-0.058 -0.015,-0.087 -10.954,20.173 -32.324,33.869 -56.893,33.869 -5.355,0 -10.554,-0.663 -15.531,-1.89 16.618,-8.749 27.75,-26.517 26.854,-46.801 -1.143,-25.894 -22.283,-46.94 -48.182,-47.976 -20.408,-0.816 -38.239,10.541 -46.857,27.392 -9.913,13.159 -15.795,29.525 -15.795,47.27 0,5.351 0.54,10.575 1.562,15.624 0.532,2.628 -2.878,4.12 -4.503,1.988 -12.623,-16.564 -19.832,-37.474 -18.957,-60.104 1.889,-48.834 41.94,-88.287 90.797,-89.481 45.027,-1.102 83.018,29.749 92.971,71.479 0.327,1.369 0.614,2.754 0.878,4.146 0.693,3.22 1.148,6.527 1.342,9.905 0.011,0.141 0.029,0.28 0.04,0.421 C -0.032,-2.335 0,-1.335 0,-0.327 0,-0.278 -0.005,-0.23 -0.005,-0.181 -0.005,-0.12 0,-0.061 0,0"
style="fill:#6bc62f;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2473.8735,301.73493)"
clip-path="url(#clipPath49)" />
<path
id="path50"
d="M 0,0 V 56.239 H 38.554 V 46.726 H 11.355 V 33.413 H 34.833 V 23.899 H 11.355 V 0 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2120.1973,557.68707)"
clip-path="url(#clipPath51)" />
<path
id="path52"
d="m 0,0 c 0,-6.368 1.471,-11.196 4.412,-14.481 2.94,-3.288 6.675,-4.93 11.202,-4.93 4.526,0 8.241,1.63 11.143,4.89 2.903,3.262 4.355,8.153 4.355,14.674 0,6.445 -1.413,11.253 -4.239,14.425 -2.826,3.171 -6.579,4.757 -11.259,4.757 -4.681,0 -8.454,-1.606 -11.317,-4.814 C 1.432,11.31 0,6.47 0,0 m -11.701,-0.384 c 0,5.729 0.857,10.537 2.571,14.425 1.278,2.864 3.024,5.434 5.237,7.711 2.211,2.275 4.635,3.963 7.269,5.064 3.504,1.482 7.544,2.224 12.122,2.224 8.287,0 14.917,-2.57 19.892,-7.71 4.973,-5.141 7.461,-12.29 7.461,-21.446 0,-9.079 -2.469,-16.183 -7.404,-21.31 -4.936,-5.127 -11.534,-7.692 -19.795,-7.692 -8.363,0 -15.013,2.552 -19.949,7.655 -4.936,5.101 -7.404,12.128 -7.404,21.079"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2196.6155,520.14307)"
clip-path="url(#clipPath53)" />
<path
id="path54"
d="m 0,0 h 8.401 c 5.448,0 8.849,0.23 10.205,0.691 1.355,0.46 2.417,1.253 3.184,2.378 0.767,1.125 1.151,2.532 1.151,4.22 0,1.892 -0.506,3.419 -1.515,4.584 -1.011,1.164 -2.437,1.9 -4.278,2.207 -0.921,0.126 -3.683,0.191 -8.286,0.191 H 0 Z m -11.355,-32.455 v 56.24 h 23.9 c 6.009,0 10.376,-0.506 13.101,-1.515 2.723,-1.011 4.903,-2.807 6.54,-5.39 1.636,-2.584 2.455,-5.538 2.455,-8.863 0,-4.218 -1.241,-7.704 -3.721,-10.453 -2.481,-2.75 -6.19,-4.483 -11.125,-5.199 2.455,-1.432 4.482,-3.004 6.081,-4.718 1.598,-1.713 3.753,-4.757 6.464,-9.13 l 6.866,-10.972 h -13.58 l -8.209,12.237 c -2.916,4.375 -4.911,7.13 -5.985,8.268 -1.074,1.138 -2.212,1.919 -3.414,2.34 -1.203,0.423 -3.108,0.633 -5.716,0.633 H 0 v -23.478 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2280.7572,514.41427)"
clip-path="url(#clipPath55)" />
<path
id="path56"
d="M 0,0 V 56.239 H 16.995 L 27.199,17.877 37.288,56.239 H 54.321 V 0 H 43.772 V 44.27 L 32.608,0 H 21.675 L 10.55,44.27 V 0 Z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2341.0119,557.68707)"
clip-path="url(#clipPath57)" />
<path
id="path58"
d="M 0,0 V 56.239 H 11.355 V 34.105 h 22.25 V 56.239 H 44.96 V 0 H 33.605 V 24.591 H 11.355 V 0 Z"
style="fill:#6bc62f;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,2428.5295,557.68707)"
clip-path="url(#clipPath59)" />
<path
id="path60"
d="m 424.145,154.792 h 9.284 v -9.245 h -9.284 z m 2.571,-37.632 c -1.381,-0.461 -3.568,-1.011 -6.56,-1.651 -2.993,-0.639 -4.948,-1.265 -5.869,-1.879 -1.408,-0.997 -2.111,-2.264 -2.111,-3.798 0,-1.509 0.563,-2.814 1.688,-3.912 1.125,-1.101 2.558,-1.65 4.297,-1.65 1.943,0 3.798,0.639 5.562,1.918 1.305,0.972 2.161,2.16 2.571,3.568 0.281,0.921 0.422,2.672 0.422,5.255 z m -19.067,37.632 h 9.246 v -9.245 h -9.246 z m 4.643,-28.886 -9.783,1.765 c 1.1,3.938 2.992,6.854 5.677,8.747 2.686,1.892 6.676,2.838 11.97,2.838 4.808,0 8.388,-0.57 10.741,-1.707 2.353,-1.138 4.009,-2.583 4.968,-4.334 0.959,-1.753 1.439,-4.969 1.439,-9.648 l -0.115,-12.583 c 0,-3.582 0.173,-6.222 0.517,-7.922 0.346,-1.702 0.991,-3.524 1.938,-5.467 h -10.665 c -0.281,0.716 -0.627,1.776 -1.035,3.183 -0.181,0.639 -0.307,1.062 -0.385,1.267 -1.842,-1.791 -3.811,-3.134 -5.907,-4.029 -2.098,-0.894 -4.335,-1.342 -6.714,-1.342 -4.194,0 -7.499,1.137 -9.917,3.415 -2.416,2.275 -3.624,5.153 -3.624,8.631 0,2.302 0.549,4.354 1.649,6.157 1.1,1.803 2.64,3.184 4.623,4.144 1.981,0.958 4.84,1.796 8.574,2.512 5.038,0.946 8.528,1.828 10.473,2.648 v 1.074 c 0,2.07 -0.513,3.547 -1.534,4.43 -1.025,0.882 -2.955,1.323 -5.794,1.323 -1.918,0 -3.413,-0.378 -4.487,-1.132 -1.074,-0.753 -1.946,-2.077 -2.609,-3.97"
style="fill:#46a0de;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,1965.0467,687.81333)"
clip-path="url(#clipPath61)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

34
static/z_comps/notify.js Normal file
View File

@ -0,0 +1,34 @@
// https://simple-notify.github.io/simple-notify/
// https://github.com/simple-notify/simple-notify/tree/master
// const btn = document.querySelector('#btn')
/**
* Muestra una notificación personalizada
* @param {string} title - Título de la notificación
* @param {string} body - Cuerpo/Contenido de la notificación
* @param {'error'|'warning'|'success'|'info'} [typeAlert='info'] - Tipo de notificación
*/
function simpleNotification(title, body, typeAlert, autotimeout=8000){
new Notify ({
// 'error', 'warning', 'success', 'info'
status: typeAlert,
title: title,
text: body,
effect: 'fade',
speed: 500,
customClass: '',
customIcon: '',
showIcon: true,
showCloseButton: true,
autoclose: true,
autotimeout: autotimeout,
notificationsGap: null,
notificationsPadding: null,
type: 'outline',
position: 'x-center top',
customWrapper: '',
})
}
export { simpleNotification };

View File

@ -0,0 +1,16 @@
// validar que exista el item exp
if (!localStorage.getItem("exp")) {
localStorage.setItem('exp', JSON.stringify({ exp: exp, min15: true, min10: true, min2: true }));
// en caso de exista validar que el elemento guardado el timestamp guardado en el front = timestamp enviado por el backend
} else {
let tmstmp = parseInt(JSON.parse(localStorage.getItem("exp"))['exp'])
if (tmstmp != exp) {
localStorage.setItem('exp', JSON.stringify({ exp: exp, min15: true, min10: true, min2: true }));
// console.log("el elemento se actualizo")
}
// else {
// // console.log("tl timestamp no ha sido cambiado")
// }
}

View File

@ -0,0 +1,45 @@
import { simpleNotification } from './notify.js'
function validarExpiracion() {
let timestampActual = Math.floor(Date.now() / 1000); // Timestamp en segundos
let jsonData = JSON.parse(localStorage.getItem('exp'));
let expiration = parseInt(jsonData?.exp); // Usamos encadenamiento opcional
if (isNaN(expiration)) {
console.warn("No se encontró un valor válido para 'exp' en localStorage.");
return;
}
let tiempoRestante = expiration - timestampActual;
let minutosRestantes = (tiempoRestante / 60).toFixed(0); // 1 decimal
let actualizado = false;
let txt_min = '';
if (tiempoRestante < 1020 && tiempoRestante > 780 && jsonData.min15) {
txt_min = minutosRestantes;
jsonData.min15 = false;
actualizado = true;
} else if (tiempoRestante < 660 && tiempoRestante > 420 && jsonData.min10) {
txt_min = minutosRestantes;
jsonData.min10 = false;
actualizado = true;
} else if (tiempoRestante < 300 && tiempoRestante > 60 && jsonData.min2) {
txt_min = minutosRestantes;
jsonData.min2 = false;
actualizado = true;
} else if (tiempoRestante <= 0) {
window.location.href = "/logout";
return; // Importante para que no se ejecute el setItem innecesariamente
}
if (actualizado) {
localStorage.setItem('exp', JSON.stringify(jsonData));
simpleNotification('⚠️ !Atención¡ ⚠️', `La sesión expirará en ~${txt_min} minutos`, 'warning', 30000)
}
}
// Ejecutar la validación cada 5 segundos | 1 seg = 1000 milisegundos | 1 min = 60,000 milisegundos
setInterval(validarExpiracion, 5000); //180,000 = 3 minutos

116
templates/a_home/home.html Normal file
View File

@ -0,0 +1,116 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{url_for('static', filename='a_home/home.css')}}">
{% endblock css %}
{% block navbar %}
{% include 'z_comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<div class="r-i">
<div class="r-i-ii">
<spam>
Potenciamos el talento con soluciones personalizadas que alinean personas, información y propósito. Diseñamos
estrategias basadas en información, ordenada, confiable, disponible y segura, dando FORMHä a tus decisiones.
</spam>
</div>
</div>
<div class="r-ii">
<!-- Tarjeta 1 -->
<div class="card-flyer">
<div class="image-box">
<img src="{{url_for('static', filename='y_img/home/software.avif')}}" alt="Software">
</div>
<div class="text-container">
<h6>
Actualización del proceso de reclutamiento y selección.
</h6>
<p>
Actualmente desde la AI hasta las técnicas de entrevista, han abierto un nuevo horizonte en el marco del
reclutamiento y selección de personal…
</p>
</div>
</div>
<!-- Tarjeta 2 -->
<div class="card-flyer">
<div class="image-box">
<img src="{{url_for('static', filename='y_img/home/work.avif')}}" alt="Productividad">
</div>
<div class="text-container">
<h6>
Beneficios e impacto en la productividad.
</h6>
<p>
En la medida que adoptemos la NOM 035, factores de riesgos psicosocial en el trabajo, identificación y
prevención; como una guía para optimizar el clima y…
</p>
</div>
</div>
<!-- Tarjeta 3 -->
<div class="card-flyer">
<div class="image-box">
<img src="{{url_for('static', filename='y_img/home/chair.avif')}}" alt="Ley Silla">
</div>
<div class="text-container">
<h6>
Alcances y objetivos de la Ley Silla.
</h6>
<p>
El pasado 19 de diciembre de 2024, se publico en el Diario Oficial de la Federación el decreto por el cual
se reforman y adicionan diversas…
</p>
</div>
</div>
</div>
<div id="carouselExampleCaptions" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-indicators">
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="0" class="active" aria-current="true" aria-label="Slide 1"></button>
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="1" aria-label="Slide 2"></button>
<button type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide-to="2" aria-label="Slide 3"></button>
</div>
<div class="carousel-inner">
<div class="carousel-item active">
<img src="{{url_for('static', filename='y_img/home/software.avif')}}" class="d-block w-100" alt="...">
<div class="carousel-caption d-none d-md-block">
<h5>First slide label</h5>
<p>Some representative placeholder content for the first slide.</p>
</div>
</div>
<div class="carousel-item">
<img src="{{url_for('static', filename='y_img/home/work.avif')}}" class="d-block w-100" alt="...">
<div class="carousel-caption d-none d-md-block">
<h5>Second slide label</h5>
<p>Some representative placeholder content for the second slide.</p>
</div>
</div>
<div class="carousel-item">
<img src="{{url_for('static', filename='y_img/home/chair.avif')}}" class="d-block w-100" alt="...">
<div class="carousel-caption d-none d-md-block">
<h5>Third slide label</h5>
<p>Some representative placeholder content for the third slide.</p>
</div>
</div>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#carouselExampleCaptions" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
{% endblock body %}

View File

@ -1,11 +1,11 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{url_for('static', filename='about-us/about-us.css')}}">
<link rel="stylesheet" href="{{url_for('static', filename='b_about-us/about-us.css')}}">
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% include 'z_comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
@ -13,21 +13,21 @@
<div class="expanding-panels">
<!-- Quiénes Somos -->
<div class="panel p1">
<div class="panel p1" data-aos="zoom-in" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
<div class="panel-content">
<div class="panel-title">
<h3>¿Quiénes Somos?</h3>
</div>
<div class="panel-text">
<p>Somos Cecilia y Enrique, profesionistas en Capital Humano y Desarrollo Organizacional, con más de 20 años de experiencia impulsando el crecimiento de empresas a través de estrategias de talento 360º.</p>
<p>Fundamos {% include 'comps/forma.html' %} para transformar la gestión del talento con soluciones personalizadas, basadas en datos confiables y estrategias innovadoras. A través de alianzas estratégicas y nuestras propias iniciativas, ayudamos a las empresas a tomar decisiones informadas, optimizando su capital humano con herramientas efectivas y de alto impacto.</p>
<p>Con {% include 'comps/forma.html' %}, tu equipo no solo crece, sino que evoluciona.</p>
<p>Fundamos {% include 'z_comps/formha.html' %} para transformar la gestión del talento con soluciones personalizadas, basadas en datos confiables y estrategias innovadoras. A través de alianzas estratégicas y nuestras propias iniciativas, ayudamos a las empresas a tomar decisiones informadas, optimizando su capital humano con herramientas efectivas y de alto impacto.</p>
<p>Con {% include 'z_comps/formha.html' %}, tu equipo no solo crece, sino que evoluciona.</p>
</div>
</div>
</div>
<!-- Misión -->
<div class="panel p2">
<div class="panel p2" data-aos="zoom-in" data-aos-delay="250" data-aos-duration="800" data-aos-easing="ease-in-out">
<div class="panel-content">
<div class="panel-title">
<h3>Misión</h3>
@ -40,7 +40,7 @@
</div>
<!-- Visión -->
<div class="panel p3">
<div class="panel p3" data-aos="zoom-in" data-aos-delay="450" data-aos-duration="800" data-aos-easing="ease-in-out">
<div class="panel-content">
<div class="panel-title">
<h3>Visión</h3>
@ -53,7 +53,7 @@
</div>
<!-- Valores -->
<div class="panel p4">
<div class="panel p4" data-aos="zoom-in" data-aos-delay="650" data-aos-duration="800" data-aos-easing="ease-in-out">
<div class="panel-content">
<div class="panel-title">
<h3>Valores</h3>
@ -73,6 +73,12 @@
</div>
</div>
{% endblock body %}
{% block js %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
{% endblock js %}

View File

@ -2,37 +2,53 @@
{% block css %}
<!-- {# modal bootstrap #} -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="{{url_for('static', filename='solutions/solutions.css')}}">
<link rel="stylesheet" href="{{url_for('static', filename='c_solutions/solutions.css')}}">
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% include 'z_comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<div class="parent">
<!-- <div class="parent">
<div class="i-i" data-toggle="modal" data-target="#forma-reclutamiento"></div>
<div class="i-ii" data-toggle="modal" data-target="#forma-liderazgo"></div>
<div class="i-iii" data-toggle="modal" data-target="#forma-capacitacion"></div>
<div class="ii-i" data-toggle="modal" data-target="#forma-cambio"></div>
<div class="ii-ii" data-toggle="modal" data-target="#forma-objetivos"></div>
</div> -->
<div class="parent">
<div class="i-i" data-toggle="modal" data-target="#forma-reclutamiento"
data-aos="zoom-in" data-aos-delay="0" data-aos-duration="800"></div>
<div class="i-ii" data-toggle="modal" data-target="#forma-liderazgo"
data-aos="zoom-in" data-aos-delay="200" data-aos-duration="800"></div>
<div class="i-iii" data-toggle="modal" data-target="#forma-capacitacion"
data-aos="zoom-in" data-aos-delay="300" data-aos-duration="800"></div>
<div class="ii-i" data-toggle="modal" data-target="#forma-cambio"
data-aos="zoom-in" data-aos-delay="400" data-aos-duration="800"></div>
<div class="ii-ii" data-toggle="modal" data-target="#forma-objetivos"
data-aos="zoom-in" data-aos-delay="500" data-aos-duration="800"></div>
</div>
<!-- da forma reclutamiento -->
<div class="modal fade" id="forma-reclutamiento" tabindex="-1" role="dialog" aria-labelledby="forma-reclutamientoTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-content" style="border: 6px solid #90b83b;">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Impulsa tu equipo con un reclutamiento más preciso, ágil y estratégico.</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<!-- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> -->
</div>
<div class="modal-body">
<p>
En {% include 'comps/forma.html' %}, combinamos tecnología y metodologías avanzadas para atraer, identificar y seleccionar talento de alto
En {% include 'z_comps/formha.html' %}, combinamos tecnología y metodologías avanzadas para atraer, identificar y seleccionar talento de alto
potencial.
<ul>
<li><b>Plataformas digitales e inteligencia artificial:</b> Utilizamos herramientas especializadas, para optimizar
@ -50,9 +66,9 @@
</ul>
</p>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
@ -60,17 +76,15 @@
<!-- da forma a tu liderazgo-->
<div class="modal fade" id="forma-liderazgo" tabindex="-1" role="dialog" aria-labelledby="forma-liderazgoTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-content" style="border: 6px solid #8c1f5a;">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Da {% include 'comps/forma.html' %} a tu liderazgo.</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title" id="exampleModalLongTitle">Da {% include 'z_comps/formha.html' %} a tu liderazgo.</h5>
<!-- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> -->
</div>
<div class="modal-body">
<p>
Prepara a tus líderes para afrontar los desafíos del entorno empresarial actual con estrategias que potencian su
crecimiento y efectividad. En {% include 'comps/forma.html' %}, diseñamos soluciones para identificar, desarrollar y fortalecer el liderazgo
crecimiento y efectividad. En {% include 'z_comps/formha.html' %}, diseñamos soluciones para identificar, desarrollar y fortalecer el liderazgo
en todos los niveles de la organización. <br>
Nos especializamos en:
<ul>
@ -82,9 +96,9 @@
</ul>
</p>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
@ -92,12 +106,10 @@
<!-- da forma a tu capacitación-->
<div class="modal fade" id="forma-capacitacion" tabindex="-1" role="dialog" aria-labelledby="forma-capacitacionTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-content" style="border: 6px solid #5d9dd1;">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Da {% include 'comps/forma.html' %} a tu capacitación. </h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h5 class="modal-title" id="exampleModalLongTitle">Da {% include 'z_comps/formha.html' %} a tu capacitación. </h5>
<!-- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> -->
</div>
<div class="modal-body">
<div>
@ -133,9 +145,9 @@
</p>
</div>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
@ -143,12 +155,10 @@
<!-- da forma al cambio-->
<div class="modal fade" id="forma-cambio" tabindex="-1" role="dialog" aria-labelledby="forma-cambioTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-content" style="border: 6px solid #002e72;">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Simplifica, optimiza y fortalece tu capital humano con asesoría especializada.</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<!-- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> -->
</div>
<div class="modal-body">
<p>
@ -164,9 +174,9 @@
</ul>
</p>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
@ -174,12 +184,10 @@
<!-- da forma a tus objetivos-->
<div class="modal fade" id="forma-objetivos" tabindex="-1" role="dialog" aria-labelledby="forma-objetivosTitle" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" role="document">
<div class="modal-content">
<div class="modal-content" style="border: 6px solid #707272;">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">Convierte datos en decisiones estratégicas para impulsar el crecimiento de tu empresa</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<!-- <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> -->
</div>
<div class="modal-body">
<p>
@ -193,9 +201,9 @@
</ul>
</p>
</div>
<!-- <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div> -->
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
@ -203,8 +211,13 @@
{% endblock body %}
{% block js %}
<!-- 2. Al final del BODY (en este orden) -->
<!-- {# modal bootstrap #} -->
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/js/bootstrap.min.js"></script>
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
{% endblock js %}

View File

@ -1,73 +0,0 @@
<style>
/* Estilos personalizados para el footer */
.footer {
background-color: #50164A ; /* Color de fondo oscuro */
color: #ffffff; /* Color del texto */
padding: 20px 0;
font-size: 20px;
}
</style>
<!-- Footer -->
<footer class="footer">
<div class="container">
<div class="row">
<!-- Columna 1: Información de contacto -->
<div class="col-md-6 text-center">
<h5>Contacto</h5>
<button type="button" class="btn btn-light">
<i class="bi bi-telephone-fill"></i> +52 5480-xxxx
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #000000;" width="16" height="16" fill="currentColor" class="bi bi-envelope-fill" viewBox="0 0 16 16">
<path d="M.05 3.555A2 2 0 0 1 2 2h12a2 2 0 0 1 1.95 1.555L8 8.414zM0 4.697v7.104l5.803-3.558zM6.761 8.83l-6.57 4.027A2 2 0 0 0 2 14h12a2 2 0 0 0 1.808-1.144l-6.57-4.027L8 9.586zm3.436-.586L16 11.801V4.697z"/>
</svg>
</button>
</div>
<!-- Columna 2: Enlaces útiles -->
<div class="col-md-6 text-center">
<h5>Redes Sociales</h5>
<div class="r-i-i">
<!-- <button onclick="window.location.href='{{ url_for('home') }}'"> </button> -->
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #3b5998;" width="16" height="16" fill="currentColor" class="bi bi-facebook" viewBox="0 0 16 16">
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"/>
</svg>
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #000000;" width="16" height="16" fill="currentColor" class="bi bi-twitter-x" viewBox="0 0 16 16">
<path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865z"/>
</svg>
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #F56040;" width="16" height="16" fill="currentColor" class="bi bi-instagram" viewBox="0 0 16 16">
<path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.9 3.9 0 0 0-1.417.923A3.9 3.9 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.9 3.9 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.9 3.9 0 0 0-.923-1.417A3.9 3.9 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599s.453.546.598.92c.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.5 2.5 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.5 2.5 0 0 1-.92-.598 2.5 2.5 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233s.008-2.388.046-3.231c.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92s.546-.453.92-.598c.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92m-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217m0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334"/>
</svg>
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #FF0000;" width="16" height="16" fill="currentColor" class="bi bi-youtube" viewBox="0 0 16 16">
<path d="M8.051 1.999h.089c.822.003 4.987.033 6.11.335a2.01 2.01 0 0 1 1.415 1.42c.101.38.172.883.22 1.402l.01.104.022.26.008.104c.065.914.073 1.77.074 1.957v.075c-.001.194-.01 1.108-.082 2.06l-.008.105-.009.104c-.05.572-.124 1.14-.235 1.558a2.01 2.01 0 0 1-1.415 1.42c-1.16.312-5.569.334-6.18.335h-.142c-.309 0-1.587-.006-2.927-.052l-.17-.006-.087-.004-.171-.007-.171-.007c-1.11-.049-2.167-.128-2.654-.26a2.01 2.01 0 0 1-1.415-1.419c-.111-.417-.185-.986-.235-1.558L.09 9.82l-.008-.104A31 31 0 0 1 0 7.68v-.123c.002-.215.01-.958.064-1.778l.007-.103.003-.052.008-.104.022-.26.01-.104c.048-.519.119-1.023.22-1.402a2.01 2.01 0 0 1 1.415-1.42c.487-.13 1.544-.21 2.654-.26l.17-.007.172-.006.086-.003.171-.007A100 100 0 0 1 7.858 2zM6.4 5.209v4.818l4.157-2.408z"/>
</svg>
</button>
<button type="button" class="btn btn-light">
<svg xmlns="http://www.w3.org/2000/svg" style="color: #0e76a8;" width="16" height="16" fill="currentColor" class="bi bi-linkedin" viewBox="0 0 16 16">
<path d="M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"/>
</svg>
</button>
</div>
</div>
</div>
<!-- Derechos de autor -->
<div class="row mt-4">
<div class="col-12 text-center">
<p class="mb-0">&copy; 2025{% include 'comps/forma.html' %}. Todos los derechos reservados.</p>
</div>
</div>
</div>
</footer>

View File

@ -1,34 +0,0 @@
<nav class="navbar navbar-expand-lg bg-body-tertiary navbar-custom">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('home') }}">
{% include 'comps/forma.html' %}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0 effect-3">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-person-circle"></i> Usuario
</a>
<ul class="dropdown-menu">
<!-- <li><a class="dropdown-item" href="#">Another action</a></li> -->
<li><hr class="dropdown-divider"></li>
<!-- <li><a class="dropdown-item" href="#">Something else here</a></li> -->
<li>
<a class="dropdown-item" href="{{ url_for('logout') }}">
<i class="bi bi-door-open-fill"></i> Logout
</a>
</li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- Asegúrate de tener esto al final del body -->
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> -->

View File

@ -1,172 +0,0 @@
{% extends 'template.html' %}
{% block css %}
{% endblock css %}
{% block navbar %}
{% include 'comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<style>
.form-container {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background: white;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
font-family: 'Segoe UI', Roboto, sans-serif;
animation: fadeIn 0.5s ease-in-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.form-header {
text-align: center;
margin-bottom: 1.5rem;
}
.form-row {
display: flex;
align-items: center;
margin-bottom: 1.2rem;
}
.form-label {
flex: 0 0 180px;
text-align: left;
font-weight: 500;
color: #2d3748;
}
.form-control {
flex: 1;
padding: 0.75rem;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 1rem;
transition: all 0.2s ease;
}
.form-control:focus {
outline: none;
border-color: #4299e1;
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.2);
}
.btn-primary {
display: block;
width: 100%;
max-width: 200px;
margin: 1.5rem auto 0;
padding: 0.75rem;
background-color: #4299e1;
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.btn-primary:hover {
background-color: #3182ce;
}
</style>
<div class="form-container">
<div class="form-header">
<h2>Contáctanos</h2>
<p>Déjanos tus datos y nos pondremos en contacto contigo</p>
</div>
<!-- Mensajes Flash -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }}">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST" action="{{ url_for('contact') }}">
{{ form.hidden_tag() }}
{{ form.csrf_token }}
<!-- {# Nombre #} -->
<div class="form-row">
{{ form.nombre.label(class_="form-label") }}
{{ form.nombre(placeholder="Tu nombre", class_="form-control", maxlength="30") }}
</div>
<!-- {# Apellido #} -->
<div class="form-row">
{{ form.apellido.label(class_="form-label") }}
{{ form.apellido(placeholder="Tu apellido", class_="form-control", maxlength="50") }}
</div>
<!-- {# Email #} -->
<div class="form-row">
{{ form.email.label(class_="form-label") }}
{{ form.email(placeholder="ejemplo@email.com", class_="form-control", maxlength="35") }}
</div>
<!-- {# Estado #} -->
<div class="form-row">
{{ form.estado.label(class_="form-label") }}
{{ form.estado(class_="form-select", maxlength="35") }}
</div>
<!-- {# Número telefónico #} -->
<div class="form-row">
{{ form.num_tel.label(class_="form-label") }}
{{ form.num_tel(placeholder="+52 55 1234 5678", class_="form-control", maxlength="16") }}
</div>
<!-- {# Tamaño de la compañia #} -->
<div class="form-row">
{{ form.size_co.label(class_="form-label") }}
{{ form.size_co(class_="form-select", maxlength="40") }}
</div>
<!-- {# rol del contacto dentro de la empresa #} -->
<div class="form-row">
{{ form.rol_contacto.label(class_="form-label") }}
{{ form.rol_contacto(placeholder="Tu puesto o rol", class_="form-select", maxlength="50") }}
</div>
<!-- {# Sector de la empresa #} -->
<div class="form-row">
{{ form.industry_type.label(class_="form-label") }}
{{ form.industry_type(placeholder="Sector de tu empresa", class_="form-select", maxlength="40") }}
</div>
<div class="form-row">
{{ form.tipo_req.label(class_="form-label") }}
{{ form.tipo_req(placeholder="Describe cómo podemos ayudarte...", class_="form-control", maxlength="250") }}
</div>
<button type="submit" class="btn btn-primary" >
Enviar Mensaje
<i class="bi bi-send-check-fill"></i>
</button>
</form>
</div>
{% endblock body %}

View File

@ -0,0 +1,74 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{url_for('static', filename='d_methodology/methodology.css')}}">
{% endblock css %}
{% block navbar %}
{% include 'z_comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<div class="container py-5">
<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="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>
<p class="card-text">Identificamos información clave para entender las metas, retos y oportunidades estratégicas de tu empresa.</p>
</div>
</div>
</div>
<div class="col-md-6" data-aos="fade-up" data-aos-delay="200" data-aos-duration="800">
<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>
<p class="card-text">Diseñamos estrategias a la medida, utilizando metodologías especializadas y herramientas tecnológicas.</p>
</div>
</div>
</div>
<div class="col-md-6" data-aos="fade-up" data-aos-delay="400" data-aos-duration="800">
<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>
<p class="card-text">Acompañamos cada paso del proyecto, asegurando una experiencia cercana, ágil y efectiva.</p>
</div>
</div>
</div>
<div class="col-md-6" data-aos="fade-up" data-aos-delay="600" data-aos-duration="800">
<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>
<p class="card-text">Medimos resultados con métricas claras para asegurar que cada intervención tenga efectos tangibles.</p>
</div>
</div>
</div>
<div class="col-md-12" data-aos="fade-up" data-aos-delay="800" data-aos-duration="800">
<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>
<p class="card-text">Reforzamos capacidades internas para que el valor se mantenga a largo plazo, más allá del proyecto.</p>
</div>
</div>
</div>
</div>
</div>
{% endblock body %}
{% block js %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
{% endblock js %}

View File

@ -0,0 +1,135 @@
{% 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">
{% for post in data %}
<div class="col-12 col-sm-8 col-md-6 col-lg-4">
<div class="card" data-aos="fade-up" data-aos-delay="0" data-aos-duration="800">
<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 %}">
<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 %}

View File

@ -0,0 +1,81 @@
{% extends 'template.html' %}
{% block css %}
{% endblock css %}
{% block navbar %}
{% include 'z_comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<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>
<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
});
});
</script>
<!-- fin pruebas -->
{% endblock body %}
{% block js %}
{% endblock js %}

View File

@ -0,0 +1,122 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{ url_for('static', filename='f_contact/form.css') }}">
{% endblock css %}
{% block navbar %}
{% include 'z_comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<style>
.form-header{
width: 100% !important;
& img {
width: 100px;
height: auto !important;
animation: rotacion 10s linear infinite;
transform-origin: center center; /* Asegura que gire desde el centro */
display: inline-block; /* Para mejor comportamiento en algunos navegadores */
}
}
@keyframes rotacion {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<div class="form-container" data-aos="zoom-out-down" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
<div class="form-header">
<h2>Contáctanos</h2>
<p>Déjanos tus datos y nos pondremos en contacto contigo.</p>
<img src="{{ url_for( 'static', filename='y_img/logos/circulo_logo.png' ) }}" alt="">
</div>
<form method="POST" action="{{ url_for('contact') }}">
{{ form.hidden_tag() }}
<!-- {# {{ form.csrf_token }} #} -->
<!-- {# Nombre #} -->
<div class="form-row">
<!-- {{ form.nombre.label(class_="form-label") }} -->
{{ form.nombre(placeholder="Tu nombre", class_="form-control", maxlength="30") }}
</div>
<!-- {# Apellido #} -->
<div class="form-row">
<!-- {{ form.apellido.label(class_="form-label") }} -->
{{ form.apellido(placeholder="Tu apellido", class_="form-control", maxlength="50") }}
</div>
<!-- {# Email #} -->
<div class="form-row">
<!-- {{ form.email.label(class_="form-label") }} -->
{{ form.email(placeholder="ejemplo@email.com", class_="form-control", maxlength="35", type="email") }}
</div>
<!-- {# Estado #} -->
<div class="form-row">
<!-- {{ form.estado.label(class_="form-label") }} -->
{{ form.estado(class_="form-select", maxlength="35") }}
</div>
<!-- {# Número telefónico #} -->
<div class="form-row">
{{ form.num_tel(placeholder="55 1234 5678", class_="form-control", maxlength="16", pattern="^\d{2}\s\d{4}\s\d{4}$", title="Formato ejemplo: 55 1234 5678") }}
</div>
<!-- {# Tamaño de la compañia #} -->
<div class="form-row">
{{ form.size_co(class_="form-select", maxlength="40") }}
</div>
<!-- {# rol del contacto dentro de la empresa #} -->
<div class="form-row">
{{ form.rol_contacto(placeholder="Tu puesto o rol", class_="form-select", maxlength="50") }}
</div>
<!-- {# Sector de la empresa #} -->
<div class="form-row">
{{ form.industry_type(placeholder="Sector de tu empresa", class_="form-select", maxlength="40") }}
</div>
<div class="form-row">
{{ form.tipo_req(placeholder="Describe cómo podemos ayudarte...", class_="form-control", maxlength="250", rows="6") }}
</div>
<button type="submit" class="btn btn-primary">
Enviar Mensaje
<i class="bi bi-send-check-fill"></i>
</button>
</form>
</div>
{% endblock body %}
{% block js %}
<script type="module" src="{{ url_for('static', filename='f_contact/re_phone.js') }}"></script>
<!-- {# if flash #} -->
{% include 'z_comps/if_flash.html' %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
{% endblock js %}

View File

@ -0,0 +1,64 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{ url_for('static', filename='f_contact/form.css') }}">
{% endblock css %}
{% block navbar %}
{% include 'z_comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<style>
main{
place-items: center;
}
</style>
<div class="form-container" data-aos="fade-down" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
<div class="form-header">
<h2 class="form-title">Bienvenido de vuelta</h2>
<p class="form-subtitle">Ingresa tus credenciales para continuar</p>
</div>
<form method="POST" action="{{ url_for('login') }}" class="login-form">
{{ form.hidden_tag() }}
<!-- {# email usuario #} -->
<div class="form-row">
{{ form.email(class_="form-control", placeholder="usuario@email.com", type="email") }}
</div>
<!-- {# password usuario #} -->
<div class="form-row">
{{ form.password(class_="form-control", placeholder="L4_c0ntr4z3ñ4", autocomplete="on") }}
</div>
<button type="submit" class="btn btn-primary">
Iniciar Sesión
</button>
<div class="form-footer">
<a href="{{ url_for('recover_pswd') }}" class="forgot-password">¿Olvidaste tu contraseña?</a>
</div>
</form>
</div>
{% endblock body %}
{% block js %}
<!-- {# if flash #} -->
{% include 'z_comps/if_flash.html' %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
{% endblock js %}

View File

@ -0,0 +1,64 @@
{% extends 'template.html' %}
{% block css %}
<link rel="stylesheet" href="{{ url_for('static', filename='f_contact/form.css') }}">
{% endblock css %}
{% block navbar %}
{% include 'z_comps/navbar.html' %}
{% endblock navbar %}
{% block body %}
<style>
main{
place-items: center;
}
</style>
<div class="form-container" data-aos="fade-up" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
<div class="form-header">
<h2 class="form-title">Recuperar Contraseña</h2>
<p class="form-subtitle">Ingresa tu email de usuario para recibir una contraseña temporal.</p>
</div>
<form method="POST" class="login-form" action="{{ url_for('recover_pswd') }}" >
{{ form.hidden_tag() }}
<!-- {# {{ form.csrf_token }} #} -->
<div class="form-row">
<!-- {{ form.email.label(class="form-label") }} -->
{{ form.email(class="form-control", placeholder="usuario@email.com", type="email") }}
</div>
<button type="submit" class="btn btn-primary w-100 py-2 mb-3">
Recuperar Contraseña
</button>
<div class="form-footer">
<a href="{{ url_for('login') }}" class="forgot-password">
<i class="bi bi-arrow-left me-1"></i> Regresar al Log In
</a>
</div>
</form>
</div>
{% endblock body %}
{% block js %}
<!-- {# if flash #} -->
{% include 'z_comps/if_flash.html' %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
{% endblock js %}

View File

@ -0,0 +1,285 @@
{% extends 'h_tmp_usr/z_tmp.html' %}
{% block css %}
<!-- {# Librería de aos.js [animaciones] #} -->
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<!-- {# bootstrap datatables #} -->
<link rel="stylesheet" href="https://cdn.datatables.net/2.2.2/css/dataTables.bootstrap5.css">
<!-- {# cdn download xlsx #} -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
{% endblock css %}
{% block body %}
<h1>Solicitudes de Contacto</h1>
{% if is_admin %}
{% include 'z_comps/download_xlsx.html' %}
{% endif %}
<table id="dbContact" class="table table-striped" style="width:100%" data-aos="fade-right" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
<thead>
<tr>
<th>Fecha</th>
<th>Nombre Completo</th>
<th>Estado</th>
<th>Tamaño Empresa</th>
<th>Rol Contacto</th>
<th>Tipo Industria</th>
<th>Estatus</th>
</tr>
</thead>
<tbody>
{% for item in data_contact %}
<tr data-id="{{item[0]}}">
<td>
<button data-id="{{item[0]}}" type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modalContactData">
{{item[1]}}
</button>
</td>
<td>
{{item[2]}} {{item[3]}}
</td>
<td>{{item[4]}}</td>
<td>{{item[5]}}</td>
<td>{{item[6]}}</td>
<td>{{item[7]}}</td>
<td>
<select class="form-select" data-id="{{ item[0] }}">
<option value="Null" {% if item[8] is none %}selected{% endif %}></option>
<option value="Opción 1" {% if item[8] == 'Opción 1' %}selected{% endif %}>Opción 1</option>
<option value="Opción 2" {% if item[8] == 'Opción 2' %}selected{% endif %}>Opción 2</option>
<option value="Opción 3" {% if item[8] == 'Opción 3' %}selected{% endif %}>Opción 3</option>
<option value="Opción 4" {% if item[8] == 'Opción 4' %}selected{% endif %}>Opción 4</option>
<option value="Opción 5" {% if item[8] == 'Opción 5' %}selected{% endif %}>Opción 5</option>
<option value="Opción 6" {% if item[8] == 'Opción 6' %}selected{% endif %}>Opción 6</option>
<option value="Opción 7" {% if item[8] == 'Opción 7' %}selected{% endif %}>Opción 7</option>
<option value="Archivado" >Archivar</option>
</select>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Modal -->
<div class="modal fade" id="modalContactData" tabindex="-1" aria-labelledby="modalContactDataLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalContactDataLabel"><i class="bi bi-database-fill-check"></i> Información del Registro:</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="modalBody">
...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cerrar</button>
</div>
</div>
</div>
</div>
{% endblock body %}
{% block js %}
<!-- {# if flash #} -->
{% include 'z_comps/if_flash.html' %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
<script>
function getFechaHoraFormato() {
const now = new Date();
const pad = (n) => n.toString().padStart(2, '0');
const dia = pad(now.getDate());
const mes = pad(now.getMonth() + 1); // Enero es 0
const anio = now.getFullYear();
const hora = pad(now.getHours());
const minuto = pad(now.getMinutes());
return `${dia}.${mes}.${anio}_${hora}_${minuto}`;
}
</script>
<script>
async function get_contact_data(btn) {
try {
let id = btn.dataset.id;
let modalBody = document.querySelector("#modalBody");
let response = await fetch('/user/get-contact-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
credentials: 'include'
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
let { data } = await response.json();
let contact = {
fecha: data[0],
fecha_hora: data[1],
nombre: data[2],
apellido: data[3],
correo: data[4],
estado: data[5],
telefono: data[6],
tamano_empresa: data[7],
rol_contacto: data[8],
tipo_industria: data[9],
comentarios: data[10],
estatus: data[11] ?? "Sin Estatus"
};
modalBody.innerHTML = `
<p><b><i class="bi bi-calendar4-week"></i></b> ${contact.fecha} - ${contact.fecha_hora}</p>
<p><b><i class="bi bi-person-fill"></i></b> ${contact.nombre} ${contact.apellido}</p>
<p><b><i class="bi bi-envelope-at-fill"></i></b> ${contact.correo}</p>
<p><b><i class="bi bi-pin-map-fill"></i></b> ${contact.estado}</p>
<p><b><i class="bi bi-telephone-fill"></i></b> ${contact.telefono}</p>
<p><b><i class="bi bi-people-fill"></i></b> ${contact.tamano_empresa}</p>
<p><b><i class="bi bi-person-vcard"></i></b> ${contact.rol_contacto}</p>
<p><b><i class="bi bi-shop"></i></b> ${contact.tipo_industria}</p>
<p><b><i class="bi bi-wrench-adjustable"></i></b> ${contact.estatus}</p>
<p><b><i class="bi bi-file-earmark-font-fill"></i></b> ${contact.comentarios}</p>
`;
} catch (error) {
console.error('Error al obtener datos:', error);
}
}
document.getElementById('dbContact').addEventListener('click', (event) => {
const button = event.target.closest('button.btn-primary[data-id]');
if (button) {
get_contact_data(button);
}
});
</script>
<script>
function send_data(target) {
let id = target.dataset.id;
let valor = target.value != "Null" ? target.value : null;
let data = { id, value: valor };
if (valor === 'Archivado') {
let table = $('#dbContact').DataTable();
let row = table.row(`tr[data-id="${id}"]`);
row.remove().draw();
}
fetch('/user/manage-record', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
credentials: 'include'
});
}
document.getElementById('dbContact').addEventListener('change', (event) => {
if (event.target.matches('select.form-select[data-id]')) {
send_data(event.target);
}
});
</script>
<script>
</script>
<!-- js datatables -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.3.0/js/bootstrap.bundle.min.js"></script>
<!-- jQuery (necesario para DataTables) -->
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
<!-- Popper.js (necesario para los dropdowns de Bootstrap) -->
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<!-- Bootstrap JS Bundle con Popper -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- DataTables JS -->
<script src="https://cdn.datatables.net/2.2.2/js/dataTables.js"></script>
<!-- DataTables Bootstrap 5 JS -->
<script src="https://cdn.datatables.net/2.2.2/js/dataTables.bootstrap5.js"></script>
<script>
new DataTable('#dbContact', {
initComplete: function() {
// Agrega campos de filtro para cada columna
this.api().columns().every(function() {
let column = this;
let header = $(column.header());
let title = header.text().trim();
// Excluir la columna "Estatus" del filtro
if (title !== 'Estatus') {
// Crea input de filtro
header.append('<div class="filter"><input type="text" class="form-control" placeholder="'+title+'" /></div>');
// Aplica el filtro al escribir
$('input', header)
.on('keyup change', function() {
if (column.search() !== this.value) {
column.search(this.value).draw();
}
});
}
});
}
});
</script>
<!-- <script>
new DataTable('#dbContact', {
initComplete: function() {
// Agrega campos de filtro para cada columna
this.api().columns().every(function() {
let column = this;
let header = $(column.header());
let title = header.text().trim();
// Crea input de filtro
// <i class="bi bi-filter"></i>
header.append('<div class="filter"><input type="text" class="form-control" placeholder="'+title+'" /></div>');
// Aplica el filtro al escribir
$('input', header)
.on('keyup change', function() {
if (column.search() !== this.value) {
column.search(this.value).draw();
}
});
});
}
});
</script> -->
<script>
let exp = "{{exp}}";
</script>
<script src="{{ url_for('static', filename='z_comps/save_exp_timestamp.js') }}"></script>
{% endblock js %}

View File

@ -0,0 +1,35 @@
{% extends 'h_tmp_usr/z_tmp.html' %}
{% block css %}
<!-- {# estilos editor de texto #} -->
<link rel="stylesheet" href="{{ url_for('static', filename='h_tmp_user/text_editor.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>
{% endblock css %}
{% block body %}
<h1>Crear una publicación</h1>
<form id="post-form" data-aos="fade-right" data-aos-delay="0" data-aos-duration="800">
<input type="text" name="title" class="form-control" placeholder="Título de la publicación" maxlength="100">
<div id="summernote"></div>
<button id="btn-submit" type="submit" class="btn btn-success"><i class="bi bi-file-earmark-richtext-fill"></i> Publicar Documento</button>
</form>
{% endblock body %}
{% block js %}
<script type="module" src="{{ url_for('static', filename='h_tmp_user/add_pst.js' ) }}"></script>
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
<!-- {# flecha ir hasta arriba #} -->
{% include 'z_comps/arrow_to_up.html' %}
{% endblock js %}

View File

@ -0,0 +1,232 @@
{% extends 'h_tmp_usr/z_tmp.html' %}
{% block css %}
<!-- {# i jconfirm #} -->
<link rel="stylesheet" href="https://htmlguyllc.github.io/jConfirm/jConfirm.min.css">
<!-- {# f jconfirm #} -->
{% endblock css %}
{% block body %}
<h2 class="display-5 mb-4">Mis Publicaciones</h2>
<!-- https://codepen.io/componentity/embed/RwajNdW?height=500&theme-id=dark&default-tab=result&user=componentity&slug-hash=RwajNdW&pen-title=Blog%20Card%20Section%20Bootstrap%20-%202nd&name=cp_embed_1 -->
<!-- https://codepen.io/anon/embed/dgmjKK?height=602&theme-id=dark&slug-hash=dgmjKK&default-tab=result&animations=run&editable=&embed-version=2&user=anon&name=cp_embed_23 -->
<!-- https://bootstrapbrain.com/component/bootstrap-5-blog-card/#code -->
<!-- <div class="col-md-12"> -->
<style>
.fade-out {
opacity: 0;
transition: opacity 0.3s ease;
}
</style>
<div class="container">
<!-- {# i pagination #} -->
{% if total_pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mt-4">
<li class="page-item {% if current_page == 1 %}disabled{% endif %}">
<a class="page-link" href="?page={{ current_page - 1 }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == current_page %}active{% endif %}">
<a class="page-link" href="?page={{ p }}">{{ p }}</a>
</li>
{% endfor %}
<li class="page-item {% if current_page == total_pages %}disabled{% endif %}">
<a class="page-link" href="?page={{ current_page + 1 }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
{% endif %}
<!-- {# 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">
{% for ele in data %}
<div class="col card-wrapper" data-id="{{ele[0]}}" data-aos="zoom-out-left" data-aos-delay="{{loop.index * 100 }}" data-aos-duration="800" data-aos-easing="ease-in-out">
<div class="card h-100">
<img src="{{ (ele[5] if ele[5] is not none else url_for('static', filename='y_img/other/no_img.png'))|safe }}"
class="card-img-top" alt="card image">
<div class="card-body d-flex flex-column">
<div class="mb-3">
<h5 class="card-title">{{ ele[3] }}</h5>
<p class="card-text">{{ ele[4] }}...</p>
<small class="text-muted">
<i class="bi bi-calendar-week"></i> {{ ele[1] }}<br>
{% if ele[2] is not none %}
<i class="bi bi-arrow-repeat"></i> {{ ele[2] }}<br>
{% endif %}
<i class="bi bi-clock"></i> {{ ele[6] }} min.
</small>
</div>
<div class="mt-auto">
<a class="btn btn-primary btn-sm" href="{{ url_for('post', post_id=ele[0]) }}"><i
class="bi bi-eye-fill"></i></a>
<a class="btn btn-secondary btn-sm" href="{{ url_for('edit_post', id_post=ele[0]) }}"><i
class="bi bi-vector-pen"></i></a>
<button type="button" class="btn btn-danger btn-sm delete-btn" data-id="{{ele[0]}}">
<i class="bi bi-trash3"></i>
</button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- {# i pagination #} -->
{% if total_pages > 1 %}
<nav aria-label="Page navigation">
<ul class="pagination justify-content-center mt-4">
<li class="page-item {% if current_page == 1 %}disabled{% endif %}">
<a class="page-link" href="?page={{ current_page - 1 }}" aria-label="Previous">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
{% for p in range(1, total_pages + 1) %}
<li class="page-item {% if p == current_page %}active{% endif %}">
<a class="page-link" href="?page={{ p }}">{{ p }}</a>
</li>
{% endfor %}
<li class="page-item {% if current_page == total_pages %}disabled{% endif %}">
<a class="page-link" href="?page={{ current_page + 1 }}" aria-label="Next">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
{% endif %}
<!-- {# f pagination #} -->
</div>
<!-- <script>
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.addEventListener('click', () => {
const wrapper = btn.closest('.card-wrapper');
wrapper.classList.add('fade-out');
setTimeout(() => {
wrapper.remove();
}, 300);
});
});
</script> -->
{% endblock body %}
{% block js %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
<!-- jQuery y jConfirm -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://htmlguyllc.github.io/jConfirm/jConfirm.min.js"></script>
<script>
function delete_post(e) {
let btn = $(this); // `this` es el botón que lanzó el jConfirm
let postId = btn.data('id'); // lee el data-id
let data = { 'id': postId };
fetch('/user/del-post', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data),
credentials: 'include'
})
.then(response => {
if (!response.ok) {
throw new Error('Error en la respuesta del servidor');
}
return response.json();
})
.then(data => {
if (data.ok) {
// Aquí puedes agregar código para eliminar el post del DOM
console.log(data.message);
document.querySelector(`div [data-id='${data.id}']`).remove();
// Por ejemplo, si el post está en un elemento con clase 'post':
btn.closest('.post').remove();
} else {
console.error('Error:', data.message);
}
})
.catch(error => {
console.error('Error:', error);
});
}
// https://htmlguyllc.github.io/jConfirm/
$(function(){
$('.delete-btn').jConfirm({
//false|array: if provided, this will override the default confirm/deny buttons (see below for an example)
btns: false,
//string: question displayed to the user
question: '¿Deseas continuar con la eliminación?',
//string: confirm button text
confirm_text: 'Sí',
//string: deny button text
deny_text: 'No',
//boolean: if true, when the confirm button is clicked the user will be redirected to the button's href location
// follow_href: true,
//boolean: if true and follow_href is true, the href will be opened in a new window
open_new_tab: false,
//boolean: if true, the tooltip will be hidden if you click outside of it
hide_on_click: true,
//string ('auto','top','bottom','left','right'): preferred location of the tooltip (defaults to auto if no space)
position: 'auto',
//boolean: if true, the deny button will be shown
show_deny_btn: true,
//string ('black', 'white', 'bootstrap-4', 'bootstrap-4-white')
theme: 'bootstrap-4',
//string ('tiny', 'small', 'medium', 'large')
size: 'medium',
//boolean: show the tooltip immediately on instantiation
show_now: false,
//string: class(es) to add to the tooltip
'class': ''
}).on('confirm', delete_post)
.on('deny', function(e){
var btn = $(this);
//do something on deny
}).on('jc-show', function(e, tooltip){
// console.log("el tooltip es visible");
//do something when tooltip is shown
//tooltip dom element is passed as the second parameter
}).on('jc-hide', function(e){
//do something when tooltip is hidden
});
});
</script>
{% endblock js %}

View File

@ -0,0 +1,81 @@
{% extends 'h_tmp_usr/z_tmp.html' %}
{% block 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>
<span>
<i class="bi bi-calendar-week"></i> {{data[0]}}
{% if data[1] is not none %} | <i class="bi bi-arrow-repeat"></i> {{data[1]}}{% endif %}
| <i class="bi bi-clock-fill"></i> {{time_read}}
</span>
<div>
<a type="button" class="btn btn-info" href="{{ url_for('my_posts') }}"><i class="bi bi-arrow-left"></i> Mis Publicaciones.</a>
<a type="button" class="btn btn-secondary" href="{{ url_for('edit_post', id_post= post_id ) }}"><i class="bi bi-vector-pen"></i> Editar.</a>
</div>
{{data[3] | safe}}
</div>
{% endblock body %}
{% block js %}
<!-- {# flecha ir hasta arriba #} -->
{% 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

@ -0,0 +1,44 @@
{% extends 'h_tmp_usr/z_tmp.html' %}
{% block css %}
<!-- {# estilos editor de texto #} -->
<link rel="stylesheet" href="{{ url_for('static', filename='h_tmp_user/text_editor.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>
{% endblock css %}
{% block body %}
<h2 class="display-5 mb-4">Editar Publicación</h2>
<form id="post-form" data-aos="fade-right" data-aos-delay="0" data-aos-duration="800">
<input type="text" name="title" class="form-control" placeholder="Título de la publicación">
<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' %}
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
<script type="module" src="{{ url_for( 'static', filename='h_tmp_user/edit-post.js' ) }}"></script>
<script>
let data = {{ data | tojson | safe }};
let id_post = {{ id_post }};
</script>
{% endblock js %}

View File

@ -0,0 +1,62 @@
{% extends 'h_tmp_usr/z_tmp.html' %}
{% block css %}
<link rel="stylesheet" href="{{ url_for('static', filename='f_contact/form.css') }}">
{% endblock css %}
{% block body %}
<style>
main{
place-items: center;
}
</style>
<div class="form-container" data-aos="fade-down" data-aos-delay="0" data-aos-duration="800" data-aos-easing="ease-in-out">
<div class="form-header">
<h2>Cambio de Contraseña</h2>
</div>
<form method="POST" action="{{ url_for('change_pswd') }}" class="login-form">
{{ form.hidden_tag() }}
<div class="form-row">
{{ form.cur_pswd(class_="form-control", placeholder="Contraseña actual", id="cur_pswd", maxlength="60") }}
</div>
<div class="form-row">
{{ form.new_pswd(class_="form-control", placeholder="Nueva Contraseña", id="new_pswd", maxlength="60") }}
</div>
<ul style="list-style-type:none;">
<li><span id="lengt10"></span> Al menos 10 caracteres.</li>
<li><span id="atl1num"></span> Al menos un número.</li>
<li><span id="atl1mayusc"></span> Al menos una mayúscula.</li>
<li><span id="atl1chrspe"></span> Al menos un caracter especial: !"#$%&/()=?¡".</li>
</ul>
<button type="submit" class="btn btn-primary">
<i class="bi bi-lock-fill"></i> Cambiar de Contraseña
</button>
</form>
</div>
{% endblock body %}
{% block js %}
<script type="module" src="{{ url_for( 'static', filename='h_tmp_user/change_pswd.js' ) }}"></script>
<!-- {# aos script #} -->
{% include 'z_comps/aos_script.html' %}
<!-- {# if flash #} -->
{% include 'z_comps/if_flash.html' %}
{% endblock js %}

Some files were not shown because too many files have changed in this diff Show More