Skip to content
Merged
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,3 @@ Deployment is manual and triggered from GitHub Actions:
3. Click **Run workflow**

The site is deployed to GitHub Pages at `https://2026.es.pycon.org/`.

187 changes: 187 additions & 0 deletions src/components/JobsPage.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
---
import { jobsTexts } from '../i18n/jobs'
import { menuTexts } from '../i18n/menu'

interface Props {
lang: string
}

interface JobFrontmatter {
title: string
company: string
location: string
type: string
description: string
skills?: string[]
salary?: string
apply_url: string
tier?: string
draft?: boolean
}

const { lang } = Astro.props
const t = jobsTexts[(lang || 'es') as keyof typeof jobsTexts]
const menuT = menuTexts[(lang || 'es') as keyof typeof menuTexts]

const esJobs = Object.values(import.meta.glob('../data/jobs/es/*.md', { eager: true })) as {
frontmatter: JobFrontmatter
}[]
const enJobs = Object.values(import.meta.glob('../data/jobs/en/*.md', { eager: true })) as {
frontmatter: JobFrontmatter
}[]
const caJobs = Object.values(import.meta.glob('../data/jobs/ca/*.md', { eager: true })) as {
frontmatter: JobFrontmatter
}[]

const allJobsMap: Record<string, { frontmatter: JobFrontmatter }[]> = {
es: esJobs,
en: enJobs,
ca: caJobs,
}

const allJobs = allJobsMap[lang] || []

const jobs = allJobs
.filter((job) => job.frontmatter.draft !== true)
.sort((a, b) => {
const tierOrder = { platinum: 0, gold: 1, silver: 2, bronze: 3 }
const aTier = tierOrder[a.frontmatter.tier as keyof typeof tierOrder] ?? 4
const bTier = tierOrder[b.frontmatter.tier as keyof typeof tierOrder] ?? 4
if (aTier !== bTier) return aTier - bTier
return 0
})

const isFeatured = (tier?: string) => tier === 'gold' || tier === 'platinum'
---

<div class="jobs-container pb-20">
<section class="mb-12" aria-labelledby="jobs-heading">
<h1 id="jobs-heading" class="text-4xl md:text-6xl font-black text-white mb-6 uppercase tracking-tighter">
{t.hero}
</h1>
<p class="text-xl text-pycon-gray-25 max-w-2xl">{t.subtitle}</p>
</section>

{
jobs.length === 0 ? (
<p class="text-pycon-gray-25 text-lg">{t.no_jobs}</p>
) : (
<ul class="grid md:grid-cols-2 gap-8 list-none m-0 p-0">
{jobs.map(({ frontmatter: job }) => (
<li
class={`flex flex-col bg-pycon-black/40 p-6 rounded-2xl border transition-all motion-safe:hover:-translate-y-2 ${isFeatured(job.tier) ? 'border-pycon-orange/50 hover:border-pycon-orange' : 'border-white/5 hover:border-white/20'}`}
>
<div class="flex items-start justify-between mb-3">
<div>
<h2 class="text-xl font-bold text-white mb-1">{job.title}</h2>
<p class="text-pycon-orange text-lg font-medium">{job.company}</p>
</div>
{isFeatured(job.tier) && (
<span class="px-3 py-1 bg-pycon-orange/20 text-pycon-orange text-xs font-bold rounded-full uppercase">
{t.featured}
</span>
)}
</div>

<div class="h-20 mb-3">
<p class="text-pycon-gray-25 text-base leading-relaxed line-clamp-3">{job.description}</p>
</div>

{job.skills && job.skills.length > 0 && (
<div class="mb-3">
<p class="text-sm text-pycon-gray-50 mb-2 uppercase tracking-wide">{t.skills}</p>
<ul class="flex flex-wrap gap-2 list-none m-0 p-0" aria-label={t.skills}>
{job.skills.map((skill) => (
<li>
<span class="px-3 py-1.5 bg-gradient-to-r from-pycon-orange/30 to-pycon-yellow/20 text-white text-sm rounded-full border border-pycon-orange/30">
{skill}
</span>
</li>
))}
</ul>
</div>
)}

<div class="mt-auto">
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-base text-pycon-gray-50 mb-4">
<div class="flex items-center gap-2">
<svg
class="w-4 h-4 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<span>{job.location}</span>
</div>
<span class="px-2 py-0.5 bg-white/5 rounded text-xs">{job.type}</span>
{job.salary && <span class="text-pycon-yellow">{job.salary}</span>}
</div>

<a
href={job.apply_url}
target="_blank"
rel="noopener noreferrer"
aria-label={`${job.title} en ${job.company} ${t.apply} ${t.location}: ${job.location} ${menuT.new_tab}`}
class="inline-flex items-center gap-2 px-4 py-2 bg-pycon-orange text-white font-bold rounded-lg hover:bg-white hover:text-pycon-orange transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pycon-orange"
>
{t.apply}
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
</li>
))}
</ul>
)
}
</div>

<style>
.jobs-container {
animation: fadeIn 0.8s ease-out;
}

.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}

@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

@media (prefers-reduced-motion: reduce) {
.jobs-container {
animation: none;
}
}
</style>
12 changes: 12 additions & 0 deletions src/data/jobs/_plantilla-oferta.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: 'Nombre del puesto'
company: 'Nombre de la empresa'
location: 'Remoto / Madrid / Barcelona'
type: 'Full-time'
description: 'Descripción breve del puesto. Explica qué harás, el equipo, el proyecto, etc.'
skills: [Python, Django, PostgreSQL]
salary: '35k-50k'
apply_url: 'https://ejemplo.com/careers'
tier: 'gold'
draft: true
---
12 changes: 12 additions & 0 deletions src/data/jobs/ca/jetbrains-python-dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: 'Senior Python Developer'
company: 'JetBrains'
location: 'Remot'
type: 'Full-time'
description: "Uneix-te al nostre equip per treballar en eines de desenvolupament d'última generació. Formaràs part d'un equip que crea productes utilitzats per milions de desenvolupadors a tot el món."
skills: [Python, Django, PostgreSQL, AWS]
salary: '60k-80k'
apply_url: 'https://www.jetbrains.com/careers/'
tier: 'gold'
draft: true
---
12 changes: 12 additions & 0 deletions src/data/jobs/en/fever-senior-dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: 'Senior Backend Engineer'
company: 'Fever'
location: 'Madrid - Hybrid'
type: 'Full-time'
description: "We're looking for a Senior Backend Engineer to join our backend team, with outstanding software development talent."
skills: [Python, Django, PostgreSQL, Redis, AWS, Docker, Kubernetes]
salary: '50k-70k + 10% + stock options'
apply_url: 'https://careers.feverup.com/'
tier: 'gold'
draft: true
---
12 changes: 12 additions & 0 deletions src/data/jobs/en/jetbrains-python-dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: 'Senior Python Developer'
company: 'JetBrains'
location: 'Remote'
type: 'Full-time'
description: "Join our team to work on cutting-edge developer tools. You'll be part of a team that creates products used by millions of developers worldwide."
skills: [Python, Django, PostgreSQL, AWS]
salary: '60k-80k'
apply_url: 'https://www.jetbrains.com/careers/'
tier: 'gold'
draft: true
---
12 changes: 12 additions & 0 deletions src/data/jobs/es/fever-senior-dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: 'Senior Backend Engineer'
company: 'Fever'
location: 'Madrid - Hybrid'
type: 'Full-time'
description: "We're looking for a Senior Backend Engineer to join our backend team, with outstanding software development talent demonstrated by great work results and experience."
skills: [Python, Django, PostgreSQL, Redis, AWS, Docker, Kubernetes]
salary: '50k-70k + 10% + stock options'
apply_url: 'https://careers.feverup.com/'
tier: 'gold'
draft: true
---
12 changes: 12 additions & 0 deletions src/data/jobs/es/jetbrains-python-dev.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: 'Senior Python Developer'
company: 'JetBrains'
location: 'Remoto'
type: 'Full-time'
description: "Join our team to work on cutting-edge developer tools. You'll be part of a team that creates products used by millions of developers worldwide."
skills: [Python, Django, PostgreSQL, AWS]
salary: '60k-80k'
apply_url: 'https://www.jetbrains.com/careers/'
tier: 'gold'
draft: true
---
11 changes: 11 additions & 0 deletions src/i18n/jobs/ca.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const ca = {
title: 'Ofertes de treball | PyConES 2026',
hero: 'Ofertes de treball',
subtitle: 'Les següents ofertes han estat enviades pels patrocinadors de la conferència:',
apply: 'Veure detalls',
featured: 'Destacat',
salary: 'Sou',
location: 'Ubicació',
no_jobs: 'No hi ha ofertes de treball publicades en aquest idioma.',
skills: 'Tecnologies',
} as const
11 changes: 11 additions & 0 deletions src/i18n/jobs/en.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const en = {
title: 'Job offers | PyConES 2026',
hero: 'Job offers',
subtitle: 'The following offers have been submitted by conference sponsors:',
apply: 'View details',
featured: 'Featured',
salary: 'Salary',
location: 'Location',
no_jobs: 'No job offers published in this language.',
skills: 'Technologies',
} as const
11 changes: 11 additions & 0 deletions src/i18n/jobs/es.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const es = {
title: 'Ofertas de trabajo | PyConES 2026',
hero: 'Ofertas de trabajo',
subtitle: 'Las siguientes ofertas han sido enviadas por los patrocinadores de la conferencia:',
apply: 'Ver detalles',
featured: 'Destacado',
salary: 'Salario',
location: 'Ubicación',
no_jobs: 'No hay ofertas de trabajo publicadas en este idioma.',
skills: 'Tecnologías',
} as const
9 changes: 9 additions & 0 deletions src/i18n/jobs/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { es } from './es'
import { en } from './en'
import { ca } from './ca'

export const jobsTexts = {
es,
en,
ca,
} as const
4 changes: 4 additions & 0 deletions src/i18n/menu/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export const ca = {
},
],
},
{
label: 'Ofertes de treball',
href: '/jobs',
},
{
label: 'Edicions Anteriors',
children: [
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/menu/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export const en = {
},
],
},
{
label: 'Job offers',
href: '/jobs',
},
{
label: 'Past Editions',
children: [
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/menu/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export const es = {
},
],
},
{
label: 'Ofertas de trabajo',
href: '/jobs',
},
{
label: 'Ediciones Anteriores',
children: [
Expand Down
34 changes: 34 additions & 0 deletions src/pages/[lang]/jobs.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
import Layout from '../../layouts/Layout.astro'
import JobsPage from '../../components/JobsPage.astro'
import { jobsTexts } from '../../i18n/jobs'

export function getStaticPaths() {
return [{ params: { lang: 'es' } }, { params: { lang: 'en' } }, { params: { lang: 'ca' } }]
}

const { lang } = Astro.params

const titles = {
es: 'Ofertas de trabajo | PyConES 2026',
en: 'Job offers | PyConES 2026',
ca: 'Ofertes de treball | PyConES 2026',
}

const descriptions = {
es: 'Explora las ofertas de trabajo de los patrocinadores de PyConES 2026. Encuentra oportunidades como Python Developer, DevOps Engineer y más.',
en: 'Explore job offers from PyConES 2026 sponsors. Find opportunities like Python Developer, DevOps Engineer and more.',
ca: 'Explora les ofertes de treball dels patrocinadors de PyConES 2026. Troba oportunitats com Python Developer, DevOps Engineer i més.',
}

const title = titles[(lang || 'es') as keyof typeof titles]
const description = descriptions[(lang || 'es') as keyof typeof descriptions]
---

<Layout title={title} description={description}>
<div class="grow w-full pt-24">
<div class="container mx-auto px-4 md:px-8">
<JobsPage lang={lang || 'es'} />
</div>
</div>
</Layout>
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "astro/tsconfigs/base",
"include": ["src/**/*"],
"include": ["src/**/*"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
Expand Down
Loading