Skip to content

Commit 9250680

Browse files
itziarZGfrancescarpiclaudeitziarAPSL
authored
Feat: add jobs page (#177)
## Summary - New job offers page at `/es/jobs/`, `/en/jobs/`, `/ca/jobs/` - Jobs managed via markdown files in `src/data/jobs/{lang}/` - Cards with description (fixed height with ellipsis), skills (colored gradient tags), location, salary, and apply button - Featured jobs (gold/platinum tier) highlighted with orange border - Draft support for sponsors to create unpublished offers (draft: true/false) - Accessibility: semantic HTML (section/article/header/footer), aria-labels on external links, reduced motion support - Menu updated with new "Ofertas de trabajo" / "Job offers" / "Ofertes de treball" link <img width="425" height="1033" alt="Screenshot from 2026-05-10 19-08-28" src="https://github.com/user-attachments/assets/e0964dde-06cf-4ee4-910d-9c4f39b6f50e" /> <img width="1301" height="697" alt="Screenshot from 2026-05-10 19-08-05" src="https://github.com/user-attachments/assets/aa8bfbc1-d2e6-4330-9dbb-5b7c5fb2743f" /> ## Files - `src/components/JobsPage.astro` - Job cards component - `src/pages/[lang]/jobs.astro` - Page route - `src/i18n/jobs/es.ts`, `en.ts`, `ca.ts`, `index.ts` - Translations - `src/data/jobs/{es,en,ca}/*.md` - Example job offers + template - `src/i18n/menu/es.ts`, `en.ts`, `ca.ts` - Updated menu --------- Co-authored-by: Francesc Arpi Roca <francesc.arpi@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Itziar Zameza García <izameza@apsl.net>
1 parent 3154527 commit 9250680

17 files changed

Lines changed: 348 additions & 2 deletions

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,3 @@ Deployment is manual and triggered from GitHub Actions:
9696
3. Click **Run workflow**
9797

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

src/components/JobsPage.astro

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
---
2+
import { jobsTexts } from '../i18n/jobs'
3+
import { menuTexts } from '../i18n/menu'
4+
5+
interface Props {
6+
lang: string
7+
}
8+
9+
interface JobFrontmatter {
10+
title: string
11+
company: string
12+
location: string
13+
type: string
14+
description: string
15+
skills?: string[]
16+
salary?: string
17+
apply_url: string
18+
tier?: string
19+
draft?: boolean
20+
}
21+
22+
const { lang } = Astro.props
23+
const t = jobsTexts[(lang || 'es') as keyof typeof jobsTexts]
24+
const menuT = menuTexts[(lang || 'es') as keyof typeof menuTexts]
25+
26+
const esJobs = Object.values(import.meta.glob('../data/jobs/es/*.md', { eager: true })) as {
27+
frontmatter: JobFrontmatter
28+
}[]
29+
const enJobs = Object.values(import.meta.glob('../data/jobs/en/*.md', { eager: true })) as {
30+
frontmatter: JobFrontmatter
31+
}[]
32+
const caJobs = Object.values(import.meta.glob('../data/jobs/ca/*.md', { eager: true })) as {
33+
frontmatter: JobFrontmatter
34+
}[]
35+
36+
const allJobsMap: Record<string, { frontmatter: JobFrontmatter }[]> = {
37+
es: esJobs,
38+
en: enJobs,
39+
ca: caJobs,
40+
}
41+
42+
const allJobs = allJobsMap[lang] || []
43+
44+
const jobs = allJobs
45+
.filter((job) => job.frontmatter.draft !== true)
46+
.sort((a, b) => {
47+
const tierOrder = { platinum: 0, gold: 1, silver: 2, bronze: 3 }
48+
const aTier = tierOrder[a.frontmatter.tier as keyof typeof tierOrder] ?? 4
49+
const bTier = tierOrder[b.frontmatter.tier as keyof typeof tierOrder] ?? 4
50+
if (aTier !== bTier) return aTier - bTier
51+
return 0
52+
})
53+
54+
const isFeatured = (tier?: string) => tier === 'gold' || tier === 'platinum'
55+
---
56+
57+
<div class="jobs-container pb-20">
58+
<section class="mb-12" aria-labelledby="jobs-heading">
59+
<h1 id="jobs-heading" class="text-4xl md:text-6xl font-black text-white mb-6 uppercase tracking-tighter">
60+
{t.hero}
61+
</h1>
62+
<p class="text-xl text-pycon-gray-25 max-w-2xl">{t.subtitle}</p>
63+
</section>
64+
65+
{
66+
jobs.length === 0 ? (
67+
<p class="text-pycon-gray-25 text-lg">{t.no_jobs}</p>
68+
) : (
69+
<ul class="grid md:grid-cols-2 gap-8 list-none m-0 p-0">
70+
{jobs.map(({ frontmatter: job }) => (
71+
<li
72+
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'}`}
73+
>
74+
<div class="flex items-start justify-between mb-3">
75+
<div>
76+
<h2 class="text-xl font-bold text-white mb-1">{job.title}</h2>
77+
<p class="text-pycon-orange text-lg font-medium">{job.company}</p>
78+
</div>
79+
{isFeatured(job.tier) && (
80+
<span class="px-3 py-1 bg-pycon-orange/20 text-pycon-orange text-xs font-bold rounded-full uppercase">
81+
{t.featured}
82+
</span>
83+
)}
84+
</div>
85+
86+
<div class="h-20 mb-3">
87+
<p class="text-pycon-gray-25 text-base leading-relaxed line-clamp-3">{job.description}</p>
88+
</div>
89+
90+
{job.skills && job.skills.length > 0 && (
91+
<div class="mb-3">
92+
<p class="text-sm text-pycon-gray-50 mb-2 uppercase tracking-wide">{t.skills}</p>
93+
<ul class="flex flex-wrap gap-2 list-none m-0 p-0" aria-label={t.skills}>
94+
{job.skills.map((skill) => (
95+
<li>
96+
<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">
97+
{skill}
98+
</span>
99+
</li>
100+
))}
101+
</ul>
102+
</div>
103+
)}
104+
105+
<div class="mt-auto">
106+
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 text-base text-pycon-gray-50 mb-4">
107+
<div class="flex items-center gap-2">
108+
<svg
109+
class="w-4 h-4 shrink-0"
110+
fill="none"
111+
stroke="currentColor"
112+
viewBox="0 0 24 24"
113+
aria-hidden="true"
114+
>
115+
<path
116+
stroke-linecap="round"
117+
stroke-linejoin="round"
118+
stroke-width="2"
119+
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"
120+
/>
121+
<path
122+
stroke-linecap="round"
123+
stroke-linejoin="round"
124+
stroke-width="2"
125+
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
126+
/>
127+
</svg>
128+
<span>{job.location}</span>
129+
</div>
130+
<span class="px-2 py-0.5 bg-white/5 rounded text-xs">{job.type}</span>
131+
{job.salary && <span class="text-pycon-yellow">{job.salary}</span>}
132+
</div>
133+
134+
<a
135+
href={job.apply_url}
136+
target="_blank"
137+
rel="noopener noreferrer"
138+
aria-label={`${job.title} en ${job.company} ${t.apply} ${t.location}: ${job.location} ${menuT.new_tab}`}
139+
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"
140+
>
141+
{t.apply}
142+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
143+
<path
144+
stroke-linecap="round"
145+
stroke-linejoin="round"
146+
stroke-width="2"
147+
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
148+
/>
149+
</svg>
150+
</a>
151+
</div>
152+
</li>
153+
))}
154+
</ul>
155+
)
156+
}
157+
</div>
158+
159+
<style>
160+
.jobs-container {
161+
animation: fadeIn 0.8s ease-out;
162+
}
163+
164+
.line-clamp-3 {
165+
display: -webkit-box;
166+
-webkit-line-clamp: 3;
167+
-webkit-box-orient: vertical;
168+
overflow: hidden;
169+
}
170+
171+
@keyframes fadeIn {
172+
from {
173+
opacity: 0;
174+
transform: translateY(20px);
175+
}
176+
to {
177+
opacity: 1;
178+
transform: translateY(0);
179+
}
180+
}
181+
182+
@media (prefers-reduced-motion: reduce) {
183+
.jobs-container {
184+
animation: none;
185+
}
186+
}
187+
</style>

src/data/jobs/_plantilla-oferta.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: 'Nombre del puesto'
3+
company: 'Nombre de la empresa'
4+
location: 'Remoto / Madrid / Barcelona'
5+
type: 'Full-time'
6+
description: 'Descripción breve del puesto. Explica qué harás, el equipo, el proyecto, etc.'
7+
skills: [Python, Django, PostgreSQL]
8+
salary: '35k-50k'
9+
apply_url: 'https://ejemplo.com/careers'
10+
tier: 'gold'
11+
draft: true
12+
---
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: 'Senior Python Developer'
3+
company: 'JetBrains'
4+
location: 'Remot'
5+
type: 'Full-time'
6+
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."
7+
skills: [Python, Django, PostgreSQL, AWS]
8+
salary: '60k-80k'
9+
apply_url: 'https://www.jetbrains.com/careers/'
10+
tier: 'gold'
11+
draft: true
12+
---
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: 'Senior Backend Engineer'
3+
company: 'Fever'
4+
location: 'Madrid - Hybrid'
5+
type: 'Full-time'
6+
description: "We're looking for a Senior Backend Engineer to join our backend team, with outstanding software development talent."
7+
skills: [Python, Django, PostgreSQL, Redis, AWS, Docker, Kubernetes]
8+
salary: '50k-70k + 10% + stock options'
9+
apply_url: 'https://careers.feverup.com/'
10+
tier: 'gold'
11+
draft: true
12+
---
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: 'Senior Python Developer'
3+
company: 'JetBrains'
4+
location: 'Remote'
5+
type: 'Full-time'
6+
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."
7+
skills: [Python, Django, PostgreSQL, AWS]
8+
salary: '60k-80k'
9+
apply_url: 'https://www.jetbrains.com/careers/'
10+
tier: 'gold'
11+
draft: true
12+
---
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: 'Senior Backend Engineer'
3+
company: 'Fever'
4+
location: 'Madrid - Hybrid'
5+
type: 'Full-time'
6+
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."
7+
skills: [Python, Django, PostgreSQL, Redis, AWS, Docker, Kubernetes]
8+
salary: '50k-70k + 10% + stock options'
9+
apply_url: 'https://careers.feverup.com/'
10+
tier: 'gold'
11+
draft: true
12+
---
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
title: 'Senior Python Developer'
3+
company: 'JetBrains'
4+
location: 'Remoto'
5+
type: 'Full-time'
6+
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."
7+
skills: [Python, Django, PostgreSQL, AWS]
8+
salary: '60k-80k'
9+
apply_url: 'https://www.jetbrains.com/careers/'
10+
tier: 'gold'
11+
draft: true
12+
---

src/i18n/jobs/ca.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const ca = {
2+
title: 'Ofertes de treball | PyConES 2026',
3+
hero: 'Ofertes de treball',
4+
subtitle: 'Les següents ofertes han estat enviades pels patrocinadors de la conferència:',
5+
apply: 'Veure detalls',
6+
featured: 'Destacat',
7+
salary: 'Sou',
8+
location: 'Ubicació',
9+
no_jobs: 'No hi ha ofertes de treball publicades en aquest idioma.',
10+
skills: 'Tecnologies',
11+
} as const

src/i18n/jobs/en.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const en = {
2+
title: 'Job offers | PyConES 2026',
3+
hero: 'Job offers',
4+
subtitle: 'The following offers have been submitted by conference sponsors:',
5+
apply: 'View details',
6+
featured: 'Featured',
7+
salary: 'Salary',
8+
location: 'Location',
9+
no_jobs: 'No job offers published in this language.',
10+
skills: 'Technologies',
11+
} as const

0 commit comments

Comments
 (0)