Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/`.

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

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 esJobs = Object.values(import.meta.glob('../data/jobs/es/*.md', { eager: true })) as {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creo que las ofertas van a estar en un solo idioma. Se podría confirmar en el discord de sponsors.

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>
) : (
<section aria-labelledby="jobs-list-heading" class="grid md:grid-cols-2 gap-8">
<h2 id="jobs-list-heading" class="sr-only">
{t.hero}
</h2>
{jobs.map(({ frontmatter: job }) => (
<article
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'}`}
>
<header 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>
)}
</header>

<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>
<div class="flex flex-wrap gap-2">
{job.skills.map((skill) => (
<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>
))}
</div>
</div>
)}

<footer 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}`}
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>
</footer>
</article>
))}
</section>
)
}
</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
27 changes: 27 additions & 0 deletions src/pages/[lang]/jobs.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
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 title = titles[(lang || 'es') as keyof typeof titles]
---

<Layout title={title}>
<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