diff --git a/content/images/2023-07-hackaton-ey.webp b/content/images/2023-07-hackaton-ey.webp new file mode 100644 index 0000000..f4cc7c5 Binary files /dev/null and b/content/images/2023-07-hackaton-ey.webp differ diff --git a/content/images/meetup/2023-11-meetup.webp b/content/images/meetup/2023-11-meetup.webp new file mode 100644 index 0000000..b261d86 Binary files /dev/null and b/content/images/meetup/2023-11-meetup.webp differ diff --git a/content/images/meetup/2025-11-meetup-checkr.webp b/content/images/meetup/2025-11-meetup-checkr.webp new file mode 100644 index 0000000..686e679 Binary files /dev/null and b/content/images/meetup/2025-11-meetup-checkr.webp differ diff --git a/content/images/pycon2026/anuncio-pycon.webp b/content/images/pycon2026/anuncio-pycon.webp new file mode 100644 index 0000000..ca67235 Binary files /dev/null and b/content/images/pycon2026/anuncio-pycon.webp differ diff --git a/objects/events/_2023.py b/objects/events/_2023.py index 9a26ac2..93c8319 100644 --- a/objects/events/_2023.py +++ b/objects/events/_2023.py @@ -96,7 +96,8 @@ 'track': 'EY', 'date': date(2023, 7, 1), 'city': 'Santiago', - 'challenges': 1 + 'challenges': 1, + 'image': 'images/2023-07-hackaton-ey.webp' }, { 'type': 'Meetup', @@ -122,7 +123,9 @@ 'track': 'Noviembre 2023', 'date': date(2023, 11, 9), 'talks': 1, - 'meetup': 297124086 + 'meetup': 297124086, + 'image': 'images/meetup/2023-11-meetup.webp' + }, { 'type': 'PyCon Chile', diff --git a/objects/events/_2025.py b/objects/events/_2025.py index 069b700..9ee7c47 100644 --- a/objects/events/_2025.py +++ b/objects/events/_2025.py @@ -36,6 +36,7 @@ 'city': 'Copiapó', 'viewers': 505, 'talks': 23, + 'attendees': 90, }, { 'type': 'Meetup', @@ -79,7 +80,8 @@ 'track': 'Noviembre 2025', 'date': date(2025, 11, 11), 'talks': 2, - 'meetup': 311855939 + 'meetup': 311855939, + 'image': 'images/meetup/2025-11-meetup-checkr.webp' }, { 'type': 'Meetup', diff --git a/objects/events/_2026.py b/objects/events/_2026.py index 30bf00f..f4de7bd 100644 --- a/objects/events/_2026.py +++ b/objects/events/_2026.py @@ -37,18 +37,25 @@ 'track': 'Rancagua 2026', 'city': 'Rancagua', 'date': date(2026, 8, 19), - 'talks': 4 + 'youtube': 'up60em_ycSQ', + 'viewers': 52, + 'talks': 4, + 'attendees': 121 }, { 'type': 'Meetup', 'track': 'Agosto 2026', 'date': date(2026, 8, 26), - 'talks': 1 + 'youtube': 'Bf2-ZQQiSF0', + 'viewers': 280, + 'talks': 1, + 'meetup': 316161132 }, { 'type': 'PyCon Chile', 'track': '2026 Santiago', 'city': 'Santiago', - 'date': date(2026, 11, 7) + 'date': date(2026, 11, 7), + 'image': 'images/pycon2026/anuncio-pycon.webp' }, ] diff --git a/objects/events/__init__.py b/objects/events/__init__.py index 0dde03c..5efcc7e 100644 --- a/objects/events/__init__.py +++ b/objects/events/__init__.py @@ -16,15 +16,46 @@ *EVENTS_2025, *EVENTS_2026 ] +MONTHS = [ + '', 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', + 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre' +] + +TYPE_SLUGS = { + 'Meetup': 'meetup', + 'PyCon Chile': 'pycon', + 'PyDay': 'pyday', + 'Hackaton': 'hackaton', +} + for event in EVENTS: if 'city' not in event: event['city'] = 'Online' + event_date = event['date'] + event['date_display'] = '{} de {} de {}'.format( + event_date.day, MONTHS[event_date.month], event_date.year + ) + event['type_slug'] = TYPE_SLUGS.get(event['type'], 'otro') today = date.today() -EVENTS_TYPES = list({event.get('type') for event in EVENTS}) -CITIES = list({event.get('city') for event in EVENTS}) -YEARS = list({event.get('date').year for event in EVENTS}) -UPCOMING_EVENTS = [event for event in EVENTS if event['date'] >= today] + +# Orden preferido para los tipos de evento (de mayor a menor escala). +# Los tipos no listados aqui se agregan al final en orden de aparicion. +TYPE_ORDER = ['PyCon Chile', 'PyDay', 'Meetup', 'Hackaton'] + +EVENTS_TYPES = TYPE_ORDER + list({ + event.get('type') for event in EVENTS + if event.get('type') and event.get('type') not in TYPE_ORDER +}) + +CITIES = sorted(list({event.get('city') for event in EVENTS if event.get('city')})) + +YEARS = sorted({event.get('date').year for event in EVENTS}) + +UPCOMING_EVENTS = sorted( + [event for event in EVENTS if event['date'] >= today], + key=lambda e: e['date'] +) PAST_EVENTS = {} for event in reversed(EVENTS): if event['date'] >= today: @@ -60,3 +91,12 @@ SESSIONS_COUNTS.append({'year': year, 'label': label, 'count': count}) ATTENDEES_COUNTS = [{'year': year, 'label': 'asistentes', 'count': count} for year, count in attendees_counts.items()] CURRENT_YEAR = date.today().year + +# Estadisticas de impacto (se calculan solas a partir de los eventos). +STATS = { + 'eventos': len(EVENTS), + 'charlas': sum(e.get('talks', 0) for e in EVENTS), + 'talleres': sum(e.get('workshops', 0) for e in EVENTS), + 'viewers': sum(e.get('viewers', 0) for e in EVENTS), + 'ciudades': len({e.get('city') for e in EVENTS if e.get('city') and e['city'] != 'Online'}), +} diff --git a/pelicanconf.py b/pelicanconf.py index 7a1be41..d998c9d 100644 --- a/pelicanconf.py +++ b/pelicanconf.py @@ -13,7 +13,8 @@ EVENTS_TYPES, EVENTS_COUNTS, SESSIONS_COUNTS, - ATTENDEES_COUNTS + ATTENDEES_COUNTS, + STATS ) AUTHOR = "Python Chile" diff --git a/pycltheme/static/css/cl.css b/pycltheme/static/css/cl.css index 2e9ab8c..65bcb99 100644 --- a/pycltheme/static/css/cl.css +++ b/pycltheme/static/css/cl.css @@ -575,27 +575,493 @@ ul.navbar-nav { #eventos { text-align: center; } + +/* Card destacado de próximos eventos */ +.lista-proximos { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1.5rem; + margin-bottom: 3rem; +} +.evento-proximo { + display: flex; + flex-direction: column; + width: 100%; + max-width: 560px; + text-align: left; + border-radius: 1em; + overflow: hidden; + background: var(--color-background); + border: 1px solid var(--color-border); + box-shadow: rgba(14, 63, 126, 0.08) 0px 10px 30px -12px; + transition: box-shadow .2s, transform .2s; +} +.evento-proximo:hover { + transform: translateY(-3px); + box-shadow: rgba(14, 63, 126, 0.15) 0px 16px 36px -14px; +} +@media (min-width: 576px) { + .evento-proximo { flex-direction: row; } + .evento-proximo-media { width: 40%; flex-shrink: 0; } +} +.evento-proximo-media { + position: relative; + min-height: 160px; + background: linear-gradient(135deg, var(--cl-blue) 0%, var(--color-primary) 100%); +} +.evento-proximo-media img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.evento-proximo-media-icono { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + min-height: 160px; + font-size: 3.5rem; + color: rgba(255, 255, 255, 0.9); +} +.evento-proximo-body { + flex: 1; + padding: 1.5rem 1.75rem; +} +.evento-proximo-badge { + display: inline-block; + font-family: 'Montserrat', sans-serif; + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.05em; + text-transform: uppercase; + color: white; + background: var(--color-text-secondary); + padding: 0.25em 0.8em; + border-radius: 1em; + margin-bottom: 0.7rem; +} +.evento-proximo-body h4 { + color: var(--color-text); + font-size: 1.3rem; + margin-bottom: 0.75rem; +} +.evento-proximo-meta { + display: flex; + flex-wrap: wrap; + gap: 0.35rem 1.25rem; + color: var(--color-text-secondary); + font-size: 0.9rem; + margin-bottom: 1.25rem; +} +.evento-proximo-meta i { color: var(--cl-blue); margin-right: 0.35em; } +.btn-proximo { + display: inline-flex; + align-items: center; + gap: 0.4em; + font-weight: 600; + text-decoration: none; + padding: 0.55em 1.3em; + border-radius: 2em; + transition: transform .15s, box-shadow .15s; +} +.btn-proximo-primary { + background: var(--cl-red); + color: white; +} +.btn-proximo-primary:hover { + color: white; + transform: translateY(-2px); + box-shadow: rgba(226,41,20,0.3) 0px 6px 16px -4px; +} +.btn-proximo-ghost { + background: transparent; + color: var(--cl-blue); + border: 1px solid var(--color-border); +} +.btn-proximo-ghost:hover { + border-color: var(--cl-blue); + transform: translateY(-2px); +} + +/* Cards compactos (próximos eventos secundarios) */ +.lista-proximos-compactos { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1rem; + margin-bottom: 3rem; +} +.evento-compacto { + display: flex; + align-items: center; + gap: 1rem; + flex: 1 1 280px; + max-width: 340px; + text-align: left; + padding: 1rem 1.25rem; + border-radius: 0.7em; + background: var(--color-background); + border: 1px solid var(--color-border); + border-left: 4px solid var(--cl-blue); + transition: box-shadow .2s, transform .2s; +} +.evento-compacto:hover { + transform: translateY(-2px); + box-shadow: rgba(14, 63, 126, 0.1) 0px 8px 20px -8px; +} +.evento-compacto-icono { + flex-shrink: 0; + width: 44px; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 1.1rem; + color: white; + background: linear-gradient(135deg, var(--cl-blue), var(--color-primary)); +} +.evento-compacto-body { min-width: 0; } +.evento-compacto-badge { + display: inline-block; + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: white; + background: var(--color-text-secondary); + padding: 0.15em 0.6em; + border-radius: 1em; + margin-bottom: 0.3rem; +} +.evento-compacto-body h5 { + font-size: 0.95rem; + color: var(--color-text); + margin-bottom: 0.2rem; +} +.evento-compacto-meta { + font-size: 0.8rem; + color: var(--color-text-secondary); + margin: 0 0 0.4rem; +} +.evento-compacto-meta i { color: var(--cl-blue); margin-right: 0.3em; } +.evento-compacto-link { + font-size: 0.82rem; + font-weight: 600; + color: var(--cl-red); +} +.evento-compacto-link i { font-size: 0.72rem; margin-left: 0.2em; } + +/* Color del icono/badge compacto por tipo */ +.evento-compacto.tipo-pycon { border-left-color: var(--cl-red); } +.evento-compacto.tipo-pycon .evento-compacto-icono { background: linear-gradient(135deg, #b91f0f, var(--cl-red)); } +.evento-compacto.tipo-pycon .evento-compacto-badge { background: var(--cl-red); } +.evento-compacto.tipo-pyday .evento-compacto-badge { background: var(--cl-blue); } +.evento-compacto.tipo-meetup .evento-compacto-icono { background: linear-gradient(135deg, var(--color-primary), #4c9aff); } +.evento-compacto.tipo-meetup .evento-compacto-badge { background: #4c9aff; } +.evento-compacto.tipo-hackaton .evento-compacto-icono { background: linear-gradient(135deg, #3a9410, #52c41a); } +.evento-compacto.tipo-hackaton .evento-compacto-badge { background: #52c41a; } + +/* Color del media/badge por tipo */ +.evento-proximo.tipo-pycon .evento-proximo-media { background: linear-gradient(135deg, #b91f0f 0%, var(--cl-red) 100%); } +.evento-proximo.tipo-pycon .evento-proximo-badge { background: var(--cl-red); } +.evento-proximo.tipo-pyday .evento-proximo-badge { background: var(--cl-blue); } +.evento-proximo.tipo-meetup .evento-proximo-media { background: linear-gradient(135deg, var(--color-primary) 0%, #4c9aff 100%); } +.evento-proximo.tipo-meetup .evento-proximo-badge { background: #4c9aff; } +.evento-proximo.tipo-hackaton .evento-proximo-media { background: linear-gradient(135deg, #3a9410 0%, #52c41a 100%); } +.evento-proximo.tipo-hackaton .evento-proximo-badge { background: #52c41a; } + +/* Introducción de la página de eventos */ +.eventos-intro { + max-width: 820px; + margin: 0 auto 2.5rem; +} +.eventos-intro > p { + font-size: 1.05rem; + line-height: 1.7; + color: var(--color-text-secondary); + margin-bottom: 1.5rem; +} +.eventos-stats { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1.5rem 2.5rem; + padding: 1.5rem 1rem; + margin-bottom: 2rem; + border-top: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); +} +.ev-stat { + text-align: center; +} +.ev-stat-num { + display: block; + font-family: 'Montserrat', sans-serif; + font-weight: 700; + font-size: 2.1rem; + line-height: 1.1; + color: var(--cl-blue); +} +.ev-stat-label { + display: block; + margin-top: 0.2rem; + font-size: 0.8rem; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--color-text-secondary); +} + +.tipos-evento { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1rem; + margin-bottom: 1.5rem; +} +.tipo-evento-item { + flex: 1 1 180px; + max-width: 240px; + text-align: left; + padding: 1rem 1.1rem; + border-radius: 0.6em; + background: var(--color-background-alt); + border-left: 3px solid var(--color-primary); +} +.tipo-evento-item h5 { + color: var(--color-text); + font-size: 1rem; + margin-bottom: 0.35rem; +} +.tipo-evento-item p { + font-size: 0.85rem; + line-height: 1.5; + color: var(--color-text-secondary); + margin: 0; +} +.eventos-cta { + font-size: 0.95rem; + color: var(--color-text-secondary); + background: var(--color-background-alt); + border-radius: 0.6em; + padding: 1rem 1.25rem; +} #eventos ul { text-align: left; + padding-left: 1.2em; + margin-bottom: 0; } .lista-eventos { display: flex; flex-wrap: wrap; justify-content: center; + gap: 12px; + margin-bottom: 1.5rem; } .evento { - border: solid 1px black; - margin: 4px; + display: flex; + flex-direction: column; + border: 1px solid var(--color-border); + border-radius: 0.6em; width: 256px; + text-align: left; + background-color: var(--color-background); + overflow: hidden; + box-shadow: rgba(42, 51, 69, 0.04) 0px 1px 1px -0.5px, + rgba(42, 51, 70, 0.04) 0px 3px 3px -1.5px, + rgba(14, 63, 126, 0.04) 0px 6px 6px -3px; + transition: box-shadow .2s, transform .2s; +} +.evento:hover { + box-shadow: rgba(14, 63, 126, 0.08) 0px 4px 12px -2px; + transform: translateY(-2px); +} + +/* Thumbnail */ +.evento-thumb { + position: relative; + aspect-ratio: 16 / 9; + background: var(--color-background-alt); +} +.evento-thumb a { + display: block; + width: 100%; + height: 100%; +} +.evento-thumb img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} +.evento-thumb-play { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 2.5rem; + text-shadow: 0 2px 8px rgba(0,0,0,0.4); + opacity: 0.9; + transition: opacity .2s, transform .2s; +} +.evento-thumb a:hover .evento-thumb-play { + opacity: 1; + transform: scale(1.1); +} +.evento-thumb-placeholder { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + font-size: 3rem; + color: var(--color-primary-light); + background: linear-gradient(135deg, #eef4fb 0%, #f7fafd 100%); +} + +/* Body */ +.evento-body { + flex: 1 1 auto; + padding: 12px 16px 14px; + border-top: 3px solid var(--color-border); +} +.evento-badge { + display: inline-block; + font-family: 'Montserrat', sans-serif; + font-size: 0.65rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: white; + background: var(--color-text-secondary); + padding: 0.2em 0.7em; + border-radius: 1em; + margin-bottom: 0.5rem; +} +.evento h6 { + color: var(--color-text); + margin-bottom: 0.4rem; + font-size: 0.95rem; } .evento p { margin: 0px; + font-size: 0.8rem; + color: var(--color-text-secondary); } +.evento p i { + width: 1.1em; + opacity: 0.7; +} +.evento ul { + margin-top: 0.6rem; + margin-bottom: 0; + padding-left: 1.1em; + font-size: 0.8rem; +} +.evento ul a { + text-decoration: none; +} + +/* Colores por tipo de evento */ +.tipo-meetup .evento-body { border-top-color: #4c9aff; } +.tipo-meetup .evento-badge { background: #4c9aff; } +.tipo-pycon .evento-body { border-top-color: var(--cl-red); } +.tipo-pycon .evento-badge { background: var(--cl-red); } +.tipo-pyday .evento-body { border-top-color: var(--cl-blue); } +.tipo-pyday .evento-badge { background: var(--cl-blue); } +.tipo-hackaton .evento-body { border-top-color: #52c41a; } +.tipo-hackaton .evento-badge { background: #52c41a; } .etiqueta-eventos { cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + width: 1.5em; + height: 1.5em; + border-radius: 50%; + background: var(--color-background-alt); + border: 1px solid var(--color-border); + font-size: 0.8em; + user-select: none; } + +/* Gráficos */ #events-charts { display: flex; - margin: 0 8px; - flex-wrap: balance; + flex-wrap: wrap; + justify-content: center; + gap: 1.5rem; + margin: 0 8px 1.5rem; +} +.chart-box { + flex: 1 1 300px; + max-width: 340px; + min-width: 0; +} +.chart-box svg { + max-width: 100%; + height: auto; +} +.chart-legend { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.4rem 1rem; + margin-top: 0.5rem; + font-size: 0.8rem; +} +.chart-legend-item { + display: inline-flex; + align-items: center; + gap: 0.35em; +} +.chart-legend-swatch { + display: inline-block; + width: 0.9em; + height: 0.9em; + border-radius: 0.2em; + flex-shrink: 0; +} + +/* Filtros */ +#filter-events { + margin-bottom: 1.5rem; +} +#filter-events p { + margin-bottom: 0.4rem; + font-weight: 600; +} +#filter-events button { + border: 1px solid var(--color-border); + border-radius: 1em; + background: white; + padding: 0.25em 0.9em; + margin: 0 0.25em 0.5em 0; + font-size: 0.85rem; + cursor: pointer; + transition: background .15s, border-color .15s; +} +#filter-events button:hover { + border-color: var(--color-primary); +} + +@media screen and (max-width: 768px) { + #events-charts { + flex-direction: column; + align-items: center; + gap: 2rem; + } + .chart-box { + width: 100%; + max-width: 360px; + } + .evento { + width: 100%; + max-width: 320px; + } } diff --git a/pycltheme/static/js/eventos-charts.js b/pycltheme/static/js/eventos-charts.js index 56fc533..64c7cb6 100644 --- a/pycltheme/static/js/eventos-charts.js +++ b/pycltheme/static/js/eventos-charts.js @@ -1,157 +1,30 @@ -function legend(color, { - title, - tickSize = 6, - width = 300, - height = 44 + tickSize, - marginTop = 18, - marginRight = 16, - marginBottom = 16 + tickSize, - marginLeft = 16, - ticks = width / 64, - tickFormat, - tickValues -} = {}) { - - function ramp(color, n = 256) { - const canvas = document.createElement("canvas"); - canvas.width = n; - canvas.height = 1; - const context = canvas.getContext("2d"); - for (let i = 0; i < n; ++i) { - context.fillStyle = color(i / (n - 1)); - context.fillRect(i, 0, 1, 1); - } - return canvas; - } - - const svg = d3.create("svg") - .attr("width", width) - .attr("height", height) - .attr("viewBox", [0, 0, width, height]) - .style("overflow", "visible") - .style("display", "block"); - - let tickAdjust = g => g.selectAll(".tick line").attr("y1", marginTop + marginBottom - height); - let x; - - // Continuous - if (color.interpolate) { - const n = Math.min(color.domain().length, color.range().length); - - x = color.copy().rangeRound(d3.quantize(d3.interpolate(marginLeft, width - marginRight), n)); - - svg.append("image") - .attr("x", marginLeft) - .attr("y", marginTop) - .attr("width", width - marginLeft - marginRight) - .attr("height", height - marginTop - marginBottom) - .attr("preserveAspectRatio", "none") - .attr("xlink:href", ramp(color.copy().domain(d3.quantize(d3.interpolate(0, 1), n))).toDataURL()); - } - - // Sequential - else if (color.interpolator) { - x = Object.assign(color.copy() - .interpolator(d3.interpolateRound(marginLeft, width - marginRight)), - {range() { return [marginLeft, width - marginRight]; }}); - - svg.append("image") - .attr("x", marginLeft) - .attr("y", marginTop) - .attr("width", width - marginLeft - marginRight) - .attr("height", height - marginTop - marginBottom) - .attr("preserveAspectRatio", "none") - .attr("xlink:href", ramp(color.interpolator()).toDataURL()); - - // scaleSequentialQuantile doesn’t implement ticks or tickFormat. - if (!x.ticks) { - if (tickValues === undefined) { - const n = Math.round(ticks + 1); - tickValues = d3.range(n).map(i => d3.quantile(color.domain(), i / (n - 1))); - } - if (typeof tickFormat !== "function") { - tickFormat = d3.format(tickFormat === undefined ? ",f" : tickFormat); - } - } - } - - // Threshold - else if (color.invertExtent) { - const thresholds - = color.thresholds ? color.thresholds() // scaleQuantize - : color.quantiles ? color.quantiles() // scaleQuantile - : color.domain(); // scaleThreshold - - const thresholdFormat - = tickFormat === undefined ? d => d - : typeof tickFormat === "string" ? d3.format(tickFormat) - : tickFormat; - - x = d3.scaleLinear() - .domain([-1, color.range().length - 1]) - .rangeRound([marginLeft, width - marginRight]); - - svg.append("g") - .selectAll("rect") - .data(color.range()) - .join("rect") - .attr("x", (d, i) => x(i - 1)) - .attr("y", marginTop) - .attr("width", (d, i) => x(i) - x(i - 1)) - .attr("height", height - marginTop - marginBottom) - .attr("fill", d => d); - - tickValues = d3.range(thresholds.length); - tickFormat = i => thresholdFormat(thresholds[i], i); - } - - // Ordinal - else { - x = d3.scaleBand() - .domain(color.domain()) - .rangeRound([marginLeft, width - marginRight]); - - svg.append("g") - .selectAll("rect") - .data(color.domain()) - .join("rect") - .attr("x", x) - .attr("y", marginTop) - .attr("width", Math.max(0, x.bandwidth() - 1)) - .attr("height", height - marginTop - marginBottom) - .attr("fill", color); - - tickAdjust = () => {}; - } - - svg.append("g") - .attr("transform", `translate(0,${height - marginBottom})`) - .call(d3.axisBottom(x) - .ticks(ticks, typeof tickFormat === "string" ? tickFormat : undefined) - .tickFormat(typeof tickFormat === "function" ? tickFormat : undefined) - .tickSize(tickSize) - .tickValues(tickValues)) - .call(tickAdjust) - .call(g => g.select(".domain").remove()) - .call(g => g.append("text") - .attr("x", marginLeft) - .attr("y", marginTop + marginBottom - height - 6) - .attr("fill", "currentColor") - .attr("text-anchor", "start") - .attr("font-weight", "bold") - .attr("class", "title") - .text(title)); - - return svg.node(); +// Paleta compartida entre gráficos, leyendas y cards de eventos. +// Los colores por tipo de evento coinciden con los definidos en cl.css. +const LABEL_COLORS = { + // Tipos de evento + 'PyCon Chile': '#e22914', + 'PyDay': '#0057a8', + 'Meetup': '#4c9aff', + 'Hackaton': '#52c41a', + // Sesiones + 'Charlas': '#0057a8', + 'Talleres': '#4c9aff', + 'Desafíos': '#52c41a', + // Asistentes + 'asistentes': '#0057a8', +}; + +function colorForLabel(label) { + return LABEL_COLORS[label] || '#ccc'; } function plot(containerTag, data){ const width = 300; const height = 200; - const marginTop = 0; + const marginTop = 10; const marginRight = 16; - const marginBottom = 16; - const marginLeft = 16; + const marginBottom = 20; + const marginLeft = 32; const series = d3.stack() .keys(d3.union(data.map(d => d.label))) @@ -167,10 +40,10 @@ function plot(containerTag, data){ .domain([0, d3.max(series, d => d3.max(d, d => d[1]))]) .rangeRound([height - marginBottom, marginTop]); - const colors = series.length > 2 ? d3.schemeSpectral[series.length] : d3.schemeCategory10 + const keys = series.map(d => d.key); const color = d3.scaleOrdinal() - .domain(series.map(d => d.key)) - .range(colors) + .domain(keys) + .range(keys.map(colorForLabel)) .unknown("#ccc"); const svg = d3.create("svg") @@ -203,10 +76,25 @@ function plot(containerTag, data){ .attr("transform", `translate(${marginLeft},0)`) .call(d3.axisLeft(y).ticks(null, "s")) .call(g => g.selectAll(".domain").remove()); - const obj = Object.assign(svg.node(), {scales: {color}}); - document.getElementById(`${containerTag}-chart`).append(obj) - const legendObj = legend(color, colors) - document.getElementById(`${containerTag}-legend`)?.append(legendObj) + + document.getElementById(`${containerTag}-chart`).append(svg.node()); + + const legendContainer = document.getElementById(`${containerTag}-legend`); + if (legendContainer) { + legendContainer.innerHTML = ''; + legendContainer.classList.add('chart-legend'); + keys.forEach(key => { + const item = document.createElement('span'); + item.className = 'chart-legend-item'; + const swatch = document.createElement('span'); + swatch.className = 'chart-legend-swatch'; + swatch.style.background = colorForLabel(key); + const label = document.createElement('span'); + label.textContent = key; + item.append(swatch, label); + legendContainer.append(item); + }); + } } plot('events-count', eventsCounts); plot('sessions-count', sessionsCounts); diff --git a/pycltheme/static/js/eventos.js b/pycltheme/static/js/eventos.js index bdcc777..1c0846c 100644 --- a/pycltheme/static/js/eventos.js +++ b/pycltheme/static/js/eventos.js @@ -1,26 +1,70 @@ let cityFilter = 'all'; let typeFilter = 'all'; +function setYearOpen(toggle, open) { + const eventsContainer = document.getElementById(toggle.id.slice(9)); + if (!eventsContainer) return; + toggle.innerHTML = open ? '-' : '+'; + eventsContainer.style.display = open ? 'flex' : 'none'; +} + function toggleYearEvents(target) { - const eventsContainer = document.getElementById(target.id.slice(9)); - if(target.innerHTML == '-') { - target.innerHTML = '+'; - eventsContainer.style.display = 'none'; - } else { - target.innerHTML = '-'; - eventsContainer.style.display = 'flex'; + setYearOpen(target, target.innerHTML == '+'); +} + +// Abre todos los años cuando hay un filtro activo; si no, deja solo el año +// actual abierto (el que arranca con "-" en el template). +function syncYearVisibility() { + const filtering = cityFilter !== 'all' || typeFilter !== 'all'; + const toggles = document.getElementsByClassName('etiqueta-eventos'); + for (const toggle of toggles) { + if (filtering) { + setYearOpen(toggle, true); + } else { + // año actual = el que tiene mayor valor numérico en su id + setYearOpen(toggle, toggle.id === currentYearToggleId); + } } } + +// Detecta el año más reciente para saber cuál dejar abierto al limpiar filtros. +const currentYearToggleId = (function () { + let maxYear = -Infinity; + let id = null; + for (const toggle of document.getElementsByClassName('etiqueta-eventos')) { + const year = parseInt(toggle.id.replace('etiqueta-eventos-', ''), 10); + if (!isNaN(year) && year > maxYear) { + maxYear = year; + id = toggle.id; + } + } + return id; +})(); + function filter(){ for (event of document.getElementById('past-events').getElementsByClassName('evento')){ let showByCity = cityFilter == 'all' || event.dataset.city == cityFilter; let showByType = typeFilter == 'all' || event.dataset.type == typeFilter; event.style.display = showByCity && showByType ? 'block' : 'none'; } + syncYearVisibility(); + hideEmptyYears(); +} + +// Oculta el bloque de un año completo si no le quedan eventos visibles +// tras aplicar los filtros. +function hideEmptyYears() { + for (const block of document.getElementsByClassName('year-block')) { + const eventos = block.getElementsByClassName('evento'); + let visibles = 0; + for (const ev of eventos) { + if (ev.style.display !== 'none') visibles++; + } + block.style.display = visibles > 0 ? '' : 'none'; + } } function filterByCity(city){ - console.log(`etiqueta-city-${cityFilter}`) document.getElementById(`etiqueta-ciudad-${cityFilter}`).style.background = 'white'; document.getElementById(`etiqueta-ciudad-${city}`).style.background = 'lightblue'; cityFilter = city; diff --git a/pycltheme/templates/evento-proximo-compacto.html b/pycltheme/templates/evento-proximo-compacto.html new file mode 100644 index 0000000..2dd11a8 --- /dev/null +++ b/pycltheme/templates/evento-proximo-compacto.html @@ -0,0 +1,19 @@ +{% set iconos = {'meetup': 'fa-users', 'pycon': 'fa-python', 'pyday': 'fa-calendar-day', 'hackaton': 'fa-code', 'otro': 'fa-calendar-alt'} %} +
+
+ +
+
+ {{ event.type }} +
{{ event.type }} {{ event.track }}
+

+ {{ event.date_display }} + {% if event.city %} · {{ event.city }}{% endif %} +

+ {% if event.registro %} + Inscríbete + {% elif event.meetup %} + Inscríbete + {% endif %} +
+
diff --git a/pycltheme/templates/evento-proximo.html b/pycltheme/templates/evento-proximo.html new file mode 100644 index 0000000..82599f1 --- /dev/null +++ b/pycltheme/templates/evento-proximo.html @@ -0,0 +1,35 @@ +{% set iconos = {'meetup': 'fa-users', 'pycon': 'fa-python', 'pyday': 'fa-calendar-day', 'hackaton': 'fa-code', 'otro': 'fa-calendar-alt'} %} +
+
+ {% if event.image %} + {{ event.type }} {{ event.track }} + {% else %} +
+ +
+ {% endif %} +
+
+ {{ event.type }} +

{{ event.type }} {{ event.track }}

+

+ {{ event.date_display }} + {% if event.city %} {{ event.city }}{% endif %} +

+
+ {% if event.registro %} + + Inscríbete + + {% elif event.meetup %} + + Inscríbete + + {% else %} + + Más info en nuestras redes + + {% endif %} +
+
+
diff --git a/pycltheme/templates/evento.html b/pycltheme/templates/evento.html index a2ff581..f70277f 100644 --- a/pycltheme/templates/evento.html +++ b/pycltheme/templates/evento.html @@ -1,37 +1,53 @@ -
-
{{ event.type }} {{ event.track }}
-

{{ event.date }}

-

{{ event.city }}

- - -
\ No newline at end of file + + + diff --git a/pycltheme/templates/eventos.html b/pycltheme/templates/eventos.html index 0a15590..cd910f2 100644 --- a/pycltheme/templates/eventos.html +++ b/pycltheme/templates/eventos.html @@ -2,31 +2,114 @@ {% block content %}
- {%if 1%} + + +
+

Eventos

+

+ A lo largo del país organizamos distintas actividades para aprender y + compartir en torno a Python. + Conoce nuestros formatos, revisa nuestros próximos eventos y explora nuestro historial por ciudad y tipo de evento. +

+ +
+
+ +{{ STATS.eventos }} + Eventos +
+
+ +{{ STATS.charlas }} + Charlas +
+
+ +{{ STATS.talleres }} + Talleres +
+
+ +{{ (STATS.viewers | round(-3) // 1000) | int }}K + Visualizaciones +
+
+ + + + +
+

Nuestros formatos

+
+
Meetups
+

Encuentros mensuales, presenciales y online, con charlas y talleres para todos los niveles.

+
+
+
PyDay
+

Jornadas regionales de un día para acercar Python a nuevas ciudades y personas.

+
+
+
PyCon Chile
+

La conferencia nacional de Python: días de charlas, talleres y comunidad.

+
+
+
PySchool
+

Iniciativa educativa que acerca Python a estudiantes, con material abierto en pyschool.cl.

+
+
+ +

+ Siempre estamos buscando nuevos temas de charlas o talleres. ¿Te gustaría + dar una charla o facilitar un espacio para un meetup? Escríbenos a + meetup@pythonchile.cl o únete a + nuestro grupo de Meetup. +

+

+ ¿Quieres apoyar a la comunidad Python en Chile? Si tu empresa u organización + está interesada en patrocinar o colaborar con nuestras iniciativas, + escríbenos a financiamiento@pythonchile.cl. +

+
+ + {% if UPCOMING_EVENTS %}

Próximos eventos

-
- {% for event in UPCOMING_EVENTS %} - {% include 'evento.html' %} + {# El primer evento (más cercano) se muestra destacado #} +
+ {% with event = UPCOMING_EVENTS[0] %} + {% include 'evento-proximo.html' %} + {% endwith %} +
+ {# El resto de próximos eventos, en cards compactos #} + {% if UPCOMING_EVENTS|length > 1 %} +
+ {% for event in UPCOMING_EVENTS[1:] %} + {% include 'evento-proximo-compacto.html' %} {% endfor %}
+ {% endif %} {% endif %} -

Eventos pasados

+

Estadísticas históricas

-
+
Eventos
-
-
+
+
-
+
Sesiones
-
-
+
+
-
+
Asistentes
-
+
+

Eventos pasados

Ciudades:

{%for city in CITIES%} @@ -49,10 +132,10 @@
Asistentes
{% for year, events in PAST_EVENTS.items() %} -
+
{{ year }} - - + {% if year == CURRENT_YEAR %}-{% else %}+{% endif %}
- - {% endblock content %} diff --git a/pycltheme/templates/index.html b/pycltheme/templates/index.html index 4bec05a..f660c8a 100644 --- a/pycltheme/templates/index.html +++ b/pycltheme/templates/index.html @@ -8,14 +8,15 @@
{% block content_title %} {% endblock %} + {% for event in UPCOMING_EVENTS[:1] %} -
-
-

PRÓXIMO EVENTO

-
-
- {% include 'evento.html' %} -
+
+
+
+

+ Próximo evento +

+ {% include 'evento-proximo.html' %}