|
| 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> |
0 commit comments