Repository: midudev/landing-infojobs
Branch: main
Commit: 0d817eaf1bf2
Files: 67
Total size: 136.6 KB
Directory structure:
gitextract_csw6vtjg/
├── .github/
│ └── workflows/
│ └── contributors.yaml
├── .gitignore
├── .vercel/
│ └── project.json
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── README.md
├── astro.config.mjs
├── package.json
├── src/
│ ├── components/
│ │ ├── AdevintaInfo.astro
│ │ ├── BentoInfo.astro
│ │ ├── CoolJobBackground.astro
│ │ ├── CoolJobBrand.astro
│ │ ├── CoolJobCard.astro
│ │ ├── CoolJobs.astro
│ │ ├── EmergentPositions.astro
│ │ ├── FollowUs.astro
│ │ ├── FollowUsLink.astro
│ │ ├── Footer.astro
│ │ ├── Header.astro
│ │ ├── HeroSearch.astro
│ │ ├── InfographicBody.astro
│ │ ├── InfographicHeader.astro
│ │ ├── InfographicModal.astro
│ │ ├── InfographicTips.astro
│ │ ├── KingsLeagueInfo.astro
│ │ ├── LinksColumns.astro
│ │ ├── LiteYoutube.astro
│ │ ├── OportunidadesTikTok.astro
│ │ ├── PreFooter.astro
│ │ ├── SectionContainer.astro
│ │ ├── SocialBest.astro
│ │ ├── StandsInterviews.astro
│ │ ├── Subtitle.astro
│ │ ├── TiktokVideo.astro
│ │ ├── YourNextJob.astro
│ │ └── ui/
│ │ ├── Button.astro
│ │ └── Icon.astro
│ ├── const.ts
│ ├── env.d.ts
│ ├── icons/
│ │ ├── AppStore.astro
│ │ ├── ChevronDown.astro
│ │ ├── CloseIcon.astro
│ │ ├── Facebook.astro
│ │ ├── GooglePlay.astro
│ │ ├── LeftArrow.astro
│ │ ├── Play.astro
│ │ ├── RightArrow.astro
│ │ ├── Search.astro
│ │ ├── Spinner.astro
│ │ ├── TikTok.astro
│ │ ├── Twitter.astro
│ │ └── YouTube.astro
│ ├── layouts/
│ │ └── Layout.astro
│ ├── lib/
│ │ ├── generate-infojobs-keywords-url.ts
│ │ ├── generate-infojobs-url.ts
│ │ ├── get-ij-studies.ts
│ │ ├── list-provinces-ids.ts
│ │ ├── mocks.ts
│ │ ├── query.ts
│ │ └── queryKeywords.ts
│ ├── pages/
│ │ └── index.astro
│ └── types.ts
├── tailwind.config.mjs
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/contributors.yaml
================================================
name: Add contributors
on:
schedule:
- cron: '20 20 * * *'
jobs:
add-contributors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: BobAnkh/add-contributors@master
with:
CONTRIBUTOR: '## Contribuidores'
COLUMN_PER_ROW: '6'
ACCESS_TOKEN: ${{secrets.GITHUB_TOKEN}}
IMG_WIDTH: '100'
FONT_SIZE: '14'
PATH: '/README.md'
COMMIT_MESSAGE: 'docs(README): update contributors'
AVATAR_SHAPE: 'round'
================================================
FILE: .gitignore
================================================
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
#locks
bun.lockb
pnpm-lock.yaml
package-lock.json
# environment variables
.env
.env.production
.env.local
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
================================================
FILE: .vercel/project.json
================================================
{"projectId":"prj_SsanVSwAoWqNeWqovW1OdwWn8CrZ","orgId":"team_pzEis5cENnPrEa0YhyEfJ7Ek"}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}
================================================
FILE: .vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"cSpell.words": ["Coméntalo", "infojobs", "jovenes", "Twitea"]
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Código de Conducta para Contribuyentes
## Nuestro compromiso
Nosotros, como miembros, contribuyentes y administradores nos comprometemos a hacer de la participación en nuestra comunidad una experiencia libre de acoso para todos, independientemente de la edad, dimensión corporal, discapacidad visible o invisible, etnicidad, características sexuales, identidad y expresión de género, nivel de experiencia, educación, nivel socio-económico, nacionalidad, apariencia personal, raza, religión, o identidad u orientación sexual.
Nos comprometemos a actuar e interactuar de maneras que contribuyan a una comunidad abierta, acogedora, diversa, inclusiva y sana.
## Nuestros estándares
Ejemplos de comportamiento que contribuyen a crear un ambiente positivo:
* Demostrar empatía y amabilidad ante otras personas
* Respeto a diferentes opiniones, puntos de vista y experiencias
* Dar y aceptar adecuadamente retroalimentación constructiva
* Aceptar la responsabilidad y disculparse ante quienes se vean afectados por nuestros errores, aprendiendo de la experiencia
* Centrarse en lo que sea mejor no sólo para nosotros como individuos, sino para la comunidad en general
Ejemplos de comportamiento inaceptable:
* El uso de lenguaje o imágenes sexualizadas, y aproximaciones o atenciones sexuales de cualquier tipo
* Comentarios despectivos (_trolling_), insultantes o derogatorios, y ataques personales o políticos
* El acoso en público o privado
* Publicar información privada de otras personas, tales como direcciones físicas o de correo electrónico, sin su permiso explícito
* Otras conductas que puedan ser razonablemente consideradas como inapropiadas en un entorno profesional
## Aplicación de las responsabilidades
Los administradores de la comunidad son responsables de aclarar y hacer cumplir nuestros estándares de comportamiento aceptable y tomarán acciones apropiadas y correctivas de forma justa en respuesta a cualquier comportamiento que consideren inapropiado, amenazante, ofensivo o dañino.
Los administradores de la comunidad tendrán el derecho y la responsabilidad de eliminar, editar o rechazar comentarios, _commits_, código, ediciones de páginas de wiki, _issues_ y otras contribuciones que no se alineen con este Código de Conducta, y comunicarán las razones para sus decisiones de moderación cuando sea apropiado.
## Alcance
Este código de conducta aplica tanto a espacios del proyecto como a espacios públicos donde un individuo esté en representación del proyecto o comunidad. Ejemplos de esto incluyen el uso de la cuenta oficial de correo electrónico, publicaciones a través de las redes sociales oficiales, o presentaciones con personas designadas en eventos en línea o no.
## Aplicación
Instancias de comportamiento abusivo, acosador o inaceptable de otro modo podrán ser reportadas a los administradores de la comunidad responsables del cumplimiento a través de <miduga@gmail.com>. Todas las quejas serán evaluadas e investigadas de una manera puntual y justa.
Todos los administradores de la comunidad están obligados a respetar la privacidad y la seguridad de quienes reporten incidentes.
## Guías de Aplicación
Los administradores de la comunidad seguirán estas Guías de Impacto en la Comunidad para determinar las consecuencias de cualquier acción que juzguen como un incumplimiento de este Código de Conducta:
### 1. Corrección
**Impacto en la Comunidad**: El uso de lenguaje inapropiado u otro comportamiento considerado no profesional o no acogedor en la comunidad.
**Consecuencia**: Un aviso escrito y privado de los administradores de la comunidad, proporcionando claridad alrededor de la naturaleza de este incumplimiento y una explicación de por qué el comportamiento es inaceptable. Una disculpa pública podría ser solicitada.
### 2. Aviso
**Impacto en la Comunidad**: Un incumplimiento causado por un único incidente o por una cadena de acciones.
**Consecuencia**: Un aviso con consecuencias por comportamiento prolongado. No se interactúa con las personas involucradas, incluyendo interacción no solicitada con quienes se encuentran aplicando el Código de Conducta, por un período específico de tiempo. Esto incluye evitar las interacciones en espacios de la comunidad, así como a través de canales externos como las redes sociales. Incumplir estos términos puede conducir a una expulsión temporal o permanente.
### 3. Expulsión temporal
**Impacto en la Comunidad**: Una serie de incumplimientos de los estándares de la comunidad, incluyendo comportamiento inapropiado continuo.
**Consecuencia**: Una expulsión temporal de cualquier forma de interacción o comunicación pública con la comunidad durante un intervalo de tiempo especificado. No se permite interactuar de manera pública o privada con las personas involucradas, incluyendo interacciones no solicitadas con quienes se encuentran aplicando el Código de Conducta, durante este período. Incumplir estos términos puede conducir a una expulsión permanente.
### 4. Expulsión permanente
**Impacto en la Comunidad**: Demostrar un patrón sistemático de incumplimientos de los estándares de la comunidad, incluyendo conductas inapropiadas prolongadas en el tiempo, acoso de individuos, o agresiones o menosprecio a grupos de individuos.
**Consecuencia**: Una expulsión permanente de cualquier tipo de interacción pública con la comunidad del proyecto.
## Atribución
Este Código de Conducta es una adaptación del [Contributor Covenant][homepage], versión 2.0,
disponible en https://www.contributor-covenant.org/es/version/2/0/code_of_conduct.html
Las Guías de Impacto en la Comunidad están inspiradas en la [escalera de aplicación del código de conducta de Mozilla](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
Para respuestas a las preguntas frecuentes de este código de conducta, consulta las FAQ en
https://www.contributor-covenant.org/faq. Hay traducciones disponibles en
https://www.contributor-covenant.org/translations.
================================================
FILE: CONTRIBUTING.md
================================================
# Contribuir a Landing de InfoJobs
Primero que nada, ¡gracias por tomarte el tiempo para contribuir! ❤️
Se anima y valora todo tipo de contribuciones. Consulta la [Tabla de Contenidos](#tabla-de-contenidos) para conocer las diferentes maneras de ayudar y detalles sobre cómo maneja este proyecto las contribuciones. Asegúrate de leer la sección relevante antes de hacer tu contribución, lo que facilitará mucho el trabajo a los mantenedores y mejorará la experiencia para todos los involucrados. La comunidad espera tus contribuciones con entusiasmo. 🎉
> Y si te gusta el proyecto pero no tienes tiempo para contribuir, no te preocupes. Hay otras formas sencillas de apoyar el proyecto y mostrar tu aprecio, las cuales también agradeceríamos mucho:
> - Dale una estrella al proyecto
> - Twitea sobre él
> - Menciona este proyecto en el README de tu proyecto
> - Coméntalo en reuniones locales y cuéntaselo a tus amigos/colegas
<!-- omit in toc -->
## Tabla de Contenidos
- [Código de Conducta](#código-de-conducta)
- [Tengo una Pregunta](#tengo-una-pregunta)
- [Quiero Contribuir](#quiero-contribuir)
- [Reportar Errores](#reportar-errores)
- [Sugerir Mejoras](#sugerir-mejoras)
- [Tu Primera Contribución de Código](#tu-primera-contribución-de-código)
- [Mensajes de Commit](#mensajes-de-commit)
## Código de Conducta
Este proyecto y todos los que participan en él están regidos por el [Código de Conducta de Landing de InfoJobs](https://github.com/midudev/landing-infojobs/blob/main/CODE_OF_CONDUCT.md). Al participar, se espera que cumplas con este código. Por favor, reporta cualquier comportamiento inaceptable a <miduga@gmail.com>.
## Tengo una Pregunta
> Si quieres hacer una pregunta, asumimos que has leído la [Documentación](https://github.com/midudev/landing-infojobs) disponible.
Antes de hacer una pregunta, lo mejor es buscar [Issues](https://github.com/midudev/landing-infojobs/issues) existentes que puedan ayudarte. En caso de encontrar un issue adecuado y aún necesitar aclaraciones, puedes escribir tu pregunta en ese issue. También es aconsejable buscar respuestas en internet primero.
Si después de esto todavía sientes la necesidad de hacer una pregunta y necesitas aclaración, te recomendamos lo siguiente:
- Abre un [Issue](https://github.com/midudev/landing-infojobs/issues/new).
- Proporciona el mayor contexto posible sobre el problema que estás enfrentando.
- Proporciona versiones del proyecto y la plataforma (Node.js, npm, etc.), dependiendo de lo que parezca relevante.
Nos encargaremos del issue lo antes posible.
## Quiero Contribuir
> ### Aviso Legal <!-- omit in toc -->
> Al contribuir a este proyecto, debes aceptar que eres el autor del 100% del contenido, que tienes los derechos necesarios para el contenido, y que el contenido que contribuyas puede ser proporcionado bajo la licencia del proyecto.
### Reportar Errores
#### Antes de Enviar un Informe de Error
Un buen informe de error no debería dejar a otros necesitando más información. Por lo tanto, te pedimos que investigues cuidadosamente, recopiles información y describas el problema en detalle en tu informe. Completa los siguientes pasos para ayudarnos a solucionar cualquier error potencial lo más rápido posible.
- Asegúrate de estar utilizando la última versión.
- Verifica si el problema realmente es un error y no un problema de configuración (Asegúrate de haber leído la [documentación](https://github.com/midudev/landing-infojobs). Si buscas soporte, podrías consultar [esta sección](#tengo-una-pregunta)).
- Para ver si otros usuarios han experimentado (y posiblemente resuelto) el mismo problema, verifica si ya existe un informe para ese error en el [rastreador de errores](https://github.com/midudev/landing-infojobs/issues?q=label%3Abug).
- También asegúrate de buscar en internet (incluyendo Stack Overflow) para ver si usuarios fuera de la comunidad de GitHub han discutido el problema.
- Recopila información sobre el error:
- Traza de pila (Stack trace)
- Sistema operativo, plataforma y versión (Windows, Linux, macOS, x86, ARM)
- Versión del intérprete, compilador, SDK, entorno de ejecución, gestor de paquetes, dependiendo de lo que parezca relevante.
- Posiblemente, tu entrada y salida
- ¿Puedes reproducir el problema de manera confiable? ¿Y también con versiones anteriores?
#### ¿Cómo Enviar un Buen Informe de Error?
> Nunca debes informar problemas relacionados con la seguridad, vulnerabilidades o errores que incluyan información sensible en el rastreador de issues o en otro lugar público. En su lugar, los errores sensibles deben ser enviados por correo electrónico a <miduga@gmail.com>.
Utilizamos issues de GitHub para rastrear errores. Si encuentras un problema con el proyecto:
- Abre un [Issue](https://github.com/midudev/landing-infojobs/issues/new). (Dado que en este punto no estamos seguros si es un error, te pedimos que no lo llames "bug" aún ni lo etiquetes).
- Explica el comportamiento esperado y el comportamiento real.
- Proporciona tanto contexto como sea posible y describe los *pasos para reproducir* que alguien más puede seguir para recrear el problema. Esto usualmente incluye tu código. Para buenos informes de error, deberías aislar el problema y crear un caso de prueba reducido.
- Proporciona la información que recopilaste en la sección anterior.
Una vez enviado:
- El equipo del proyecto etiquetará el issue según corresponda.
- Un miembro del equipo intentará reproducir el problema con los pasos proporcionados. Si no hay pasos para reproducir o no es obvio cómo hacerlo, el equipo te pedirá esos pasos y marcará el issue como `needs-repro`. Los errores con la etiqueta `needs-repro` no serán atendidos hasta que se puedan reproducir.
- Si el equipo puede reproducir el problema, será etiquetado como `needs-fix`, además de posiblemente otras etiquetas (como `critical`), y el issue quedará disponible para ser [implementado por alguien](#tu-primera-contribución-de-código).
### Sugerir Mejoras
Esta sección te guiará en la presentación de una sugerencia de mejora para Landing de InfoJobs, **incluyendo nuevas características y mejoras menores de la funcionalidad existente**. Seguir estas pautas ayudará a los mantenedores y la comunidad a entender tu sugerencia y encontrar sugerencias relacionadas.
#### Antes de Enviar una Mejora
- Asegúrate de estar utilizando la última versión.
- Lee la [documentación](https://github.com/midudev/landing-infojobs) detenidamente y verifica si la funcionalidad ya está cubierta, tal vez por una configuración individual.
- Realiza una [búsqueda](https://github.com/midudev/landing-infojobs/issues) para ver si la mejora ya ha sido sugerida. Si es así, añade un comentario al issue existente en lugar de abrir uno nuevo.
- Asegúrate de que tu idea encaje con el alcance y los objetivos del proyecto. Es tu responsabilidad hacer una fuerte argumentación para convencer a los desarrolladores del proyecto de los méritos de esta característica. Ten en cuenta que buscamos características que sean útiles para la mayoría de nuestros usuarios y no solo para un pequeño grupo. Si solo estás apuntando a una minoría de usuarios, considera escribir una biblioteca de complementos.
#### ¿Cómo Enviar una Buena Sugerencia de Mejora?
Las sugerencias de mejora se rastrean como [issues de GitHub](https://github.com/midudev/landing-infojobs/issues).
- Usa un **título claro y descriptivo** para el issue que identifique la sugerencia.
- Proporciona una **descripción paso a paso de la mejora sugerida** con tantos detalles como sea posible.
- **Describe el comportamiento actual** y **explica qué comportamiento esperabas ver en su lugar** y por qué. En este punto, también puedes indicar qué alternativas no funcionan para ti.
- Puedes **incluir capturas de pantalla y GIFs animados** que te ayuden a demostrar los pasos o señalar la parte relacionada con la sugerencia. Puedes usar [esta herramienta](https://www.cockos.com/licecap/) para grabar GIFs en macOS y Windows, y [esta herramienta](https://github.com/colinkeenan/silentcast) o [esta herramienta](https://github.com/GNOME/byzanz) en Linux.
- **Explica por qué esta mejora sería útil** para la mayoría de los usuarios de Landing de InfoJobs. También puedes mencionar otros proyectos que hayan resuelto mejor el problema y que podrían servir de inspiración.
### Tu Primera Contribución de Código
¿Listo para contribuir con código? ¡Genial! A continuación, te mostramos cómo empezar:
1. **Bifurca (Fork) el repositorio** en tu cuenta de GitHub.
2. **Clona el repositorio bifurcado** en tu máquina local:
```bash
git clone https://github.com/tu-usuario/landing-infojobs.git
```
3. **Crea una rama nueva** para tu contribución. Utiliza un nombre de rama descriptivo:
```bash
git checkout -b nombre-de-tu-rama
```
4. **Haz tus cambios**. Asegúrate de seguir las pautas de codificación del proyecto y de que tus contribuciones sean claras y concisas. Si estás agregando una nueva característica, asegúrate de que tenga cobertura de pruebas.
5. **Prueba tus cambios** para verificar que todo funcione correctamente:
```bash
npm run preview
```
6. **Haz commit de tus cambios** siguiendo las convenciones de mensajes de commit (ver la sección [Mensajes de Commit](#mensajes-de-commit)).
7. **Sube tu rama** a GitHub:
```bash
git push origin nombre-de-tu-rama
```
8. **Abre un Pull Request (PR)** desde tu rama y describe claramente los cambios que has realizado. Si tu PR resuelve un issue existente, asegúrate de mencionarlo en la descripción.
9. **Espera la revisión**. Un mantenedor revisará tu PR, te proporcionará comentarios y lo aprobará si está listo para ser fusionado.
### Mensajes de Commit
Para mantener un historial de cambios consistente y claro, utilizamos una convención para los mensajes de commit. El formato es el siguiente:
```scss
tipo(área): descripción corta [opcional]
```
- **tipo**: indica la naturaleza del cambio. Algunos ejemplos son:
- `feat`: una nueva característica para el proyecto.
- `fix`: corrección de errores.
- `docs`: cambios en la documentación.
- `style`: cambios que no afectan la lógica del código (formato, espacios, etc.).
- `refactor`: cambios en el código que no corrigen errores ni agregan funciones.
- `test`: agregar o corregir pruebas.
- `chore`: cambios en el proceso de construcción o herramientas auxiliares.
- **área**: especifica la parte del proyecto que se ve afectada (por ejemplo, un archivo o módulo).
- **descripción breve**: explica de manera concisa qué se ha hecho. Usa el modo imperativo (por ejemplo, "agrega", "corrige").
- **descripción detallada opcional**: puede incluir más detalles sobre el commit.
<br/>
**Ejemplo de mensaje de commit:**
```scss
docs(readme.md): add local install instructions
```
Este mensaje indica que se han añadido instrucciones de instalación local en el archivo `readme.md`.
<br/>
¡Gracias por contribuir! Tu ayuda hace una gran diferencia para el proyecto.
<br/>
## Atribución
Esta guía está basada en **contributing-gen**. [¡Haz la tuya!](https://github.com/bttger/contributing-gen)
================================================
FILE: README.md
================================================
# Landing de InfoJobs
Este proyecto es una landing page para InfoJobs, diseñada para proporcionar una presentación moderna y atractiva de la plataforma, destacando sus características y beneficios. La landing page busca captar la atención de los usuarios y motivarlos a explorar los servicios de InfoJobs, promoviendo una mejor experiencia de búsqueda de empleo.
## Tech Stack



## Contribuidores
<table>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/midudev>
<img src=https://avatars.githubusercontent.com/u/1561955?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Miguel Ángel Durán/>
<br />
<sub style="font-size:14px"><b>Miguel Ángel Durán</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/luchofseven>
<img src=https://avatars.githubusercontent.com/u/82046975?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Luciano Fernández/>
<br />
<sub style="font-size:14px"><b>Luciano Fernández</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/andreasop01>
<img src=https://avatars.githubusercontent.com/u/124921019?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Andrea Llovera De Sousa/>
<br />
<sub style="font-size:14px"><b>Andrea Llovera De Sousa</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/diego-dev018>
<img src=https://avatars.githubusercontent.com/u/175571311?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Diego Ronaldo Sanchez/>
<br />
<sub style="font-size:14px"><b>Diego Ronaldo Sanchez</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Melissa1221>
<img src=https://avatars.githubusercontent.com/u/121834468?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Melissa Iman Noriega/>
<br />
<sub style="font-size:14px"><b>Melissa Iman Noriega</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/jordigd20>
<img src=https://avatars.githubusercontent.com/u/60585963?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Jordi Gómez Devesa/>
<br />
<sub style="font-size:14px"><b>Jordi Gómez Devesa</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/EduWTR>
<img src=https://avatars.githubusercontent.com/u/139919492?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Edu/>
<br />
<sub style="font-size:14px"><b>Edu</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/OctaEDLP00>
<img src=https://avatars.githubusercontent.com/u/42822581?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=DevMetal00/>
<br />
<sub style="font-size:14px"><b>DevMetal00</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/alesdevux>
<img src=https://avatars.githubusercontent.com/u/76450853?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=alesdevux/>
<br />
<sub style="font-size:14px"><b>alesdevux</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/DiegoT4l>
<img src=https://avatars.githubusercontent.com/u/128425675?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=DiegoT4l/>
<br />
<sub style="font-size:14px"><b>DiegoT4l</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/gusCreator>
<img src=https://avatars.githubusercontent.com/u/147654561?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Luis Gustavo Sequeiros Condori/>
<br />
<sub style="font-size:14px"><b>Luis Gustavo Sequeiros Condori</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/javimata>
<img src=https://avatars.githubusercontent.com/u/2237207?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Javi Mata/>
<br />
<sub style="font-size:14px"><b>Javi Mata</b></sub>
</a>
</td>
</tr>
<tr>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/IzanMen>
<img src=https://avatars.githubusercontent.com/u/175528066?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Izan Sánchez Ginés/>
<br />
<sub style="font-size:14px"><b>Izan Sánchez Ginés</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/juanjk24>
<img src=https://avatars.githubusercontent.com/u/147955917?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Juan Cuellar/>
<br />
<sub style="font-size:14px"><b>Juan Cuellar</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/Anthonazo>
<img src=https://avatars.githubusercontent.com/u/118082256?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Anthony Moya Ochoa/>
<br />
<sub style="font-size:14px"><b>Anthony Moya Ochoa</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/MiguelVallina2002>
<img src=https://avatars.githubusercontent.com/u/93439131?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Miguel Vallina Samaniego/>
<br />
<sub style="font-size:14px"><b>Miguel Vallina Samaniego</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/KoenigDev>
<img src=https://avatars.githubusercontent.com/u/160176319?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Nunu/>
<br />
<sub style="font-size:14px"><b>Nunu</b></sub>
</a>
</td>
<td align="center" style="word-wrap: break-word; width: 150.0; height: 150.0">
<a href=https://github.com/astrobot-houston>
<img src=https://avatars.githubusercontent.com/u/108291165?v=4 width="100;" style="border-radius:50%;align-items:center;justify-content:center;overflow:hidden;padding-top:10px" alt=Houston (Bot)/>
<br />
<sub style="font-size:14px"><b>Houston (Bot)</b></sub>
</a>
</td>
</tr>
</table>
## Correr Localmente
Clona el proyecto
```bash
git clone https://github.com/midudev/landing-infojobs.git
```
Dirigete al directorio del proyecto
```bash
cd landing-infojobs
```
Instala las dependencias
```bash
npm install
```
Inicia el servidor
```bash
npm run start
```
## Variables de entorno
Para correr este proyecto, necesitarás agregar las siguientes variable de entorno `API_INFOJOBS_TOKEN` en tu archivo .env
> [!NOTE]
> Puedes conseguir el token en esta url [Developer Site InfoJobs](https://developer.infojobs.net/).
> Debes loguearte con tu cuenta infojobs, y al crear una app, te darán tus credenciales.
> [!WARNING]
> Deshabilitado en este momento.
## Contribuciones
Las contribuciones son siempre bienvenidas.
Consulta `contributing.md` para saber cómo empezar.
Por favor, respete el `código de conducta` de este proyecto.
================================================
FILE: astro.config.mjs
================================================
// @ts-check
import { defineConfig } from 'astro/config';
import tailwind from '@astrojs/tailwind';
// https://astro.build/config
export default defineConfig({
build: {
inlineStylesheets: 'always'
},
integrations: [tailwind()]
});
================================================
FILE: package.json
================================================
{
"name": "landing-jovenes-infojobs",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "0.9.4",
"@astrojs/tailwind": "5.1.2",
"astro": "4.16.13",
"tailwindcss": "3.4.15",
"typescript": "5.6.3"
}
}
================================================
FILE: src/components/AdevintaInfo.astro
================================================
---
const LINKS = [
{
label: "JobisJob",
href: "https://www.jobisjob.es/",
},
{
label: "Fotocasa",
href: "https://www.fotocasa.es/es",
},
{
label: "habitaclia",
href: "https://www.habitaclia.com/",
},
{
label: "Coches.net",
href: "https://www.coches.net/",
},
{
label: "Motos.net",
href: "https://motos.coches.net/",
},
{
label: "Milanuncios",
href: "https://www.milanuncios.com/",
},
]
---
<div class="flex flex-col items-center px-10">
<ul
class="hidden md:inline-flex gap-x-4 gap-y-4 flex-wrap justify-center text-sm w-full pb-10 underline-offset-[3px]"
>
<li>
<span>InfoJobs es partner de </span>
</li>
{
LINKS.map(({ label, href }, index: number) => (
<li
class={`${index !== LINKS.length - 1 && "after:content-['|'] after:ml-4"} text-center`}
>
<a
class="hover:underline"
href={href}
target="_blank"
aria-label={`Visita ${label}`}
>
{label}
</a>
</li>
))
}
<li class="text-center">
<span>© 2024 Adevinta Jobs S.L.U.</span>
</li>
</ul>
</div>
================================================
FILE: src/components/BentoInfo.astro
================================================
---
import SectionContainer from "@components/SectionContainer.astro"
import Subtitle from "@components/Subtitle.astro"
const ARTICLES = [
{
title: "Qué esperar de un proceso de selección",
contentId: "selection-process",
class:
"bg-gradient-to-r from-[#fef6db] to-[#fce197] text-ij-yellow md:aspect-[16/8] md:col-span-2 md:row-span-1 h-full",
image: "/bento-info/selection-process.webp",
imageClass:
"w-full h-auto object-contain object-bottom md:h-auto md:w-auto md:object-right md:max-h-none",
action: "modal",
track: "blog_selection_process",
},
{
title: "Los puestos emergentes que van a petarlo",
contentId: "emergent-positions",
class:
"bg-gradient-to-r from-[#ffd2d2] to-[#f9b395] text-ij-red md:col-start-3 md:row-span-2 md:h-full",
image: "/bento-info/emergent-positions.webp",
imageClass:
"w-full h-full object-cover object-[center_30%] md:object-center",
action: "modal",
track: "blog_emerging_jobs",
},
{
title: "Las opinión de las empresas",
contentId: "company-opinion",
class: "bg-sky-800 text-ij-blue md:h-row-span-1",
image: "/bento-info/company-opinion.webp",
imageClass: "w-full h-full object-cover",
action: "link",
link: "https://www.tiktok.com/@infojobs/video/7291016517217520928?_r=1&%3B_t=8oUBsz4let7",
track: "blog_companies_opinions",
},
{
title: "¿Sin experiencia? Destaca en las entrevistas",
contentId: "no-experience",
class:
"bg-gradient-to-r from-[#e6f6ee] to-[#8bcfad] text-ij-green md:h-row-span-1 md:h-full",
image: "/bento-info/no-experience.webp",
imageClass: "w-3/4 h-auto object-contain mb-4",
action: "modal",
track: "blog_interview_tips",
},
]
---
<SectionContainer>
<Subtitle>Prepárate para ganar</Subtitle>
<div class="grid grid-cols-1 md:grid-cols-3 md:grid-rows-2 gap-4 md:p-0">
{
ARTICLES.map(
({
class: className,
title,
contentId,
image,
imageClass,
action,
link,
track,
}) =>
action === "link" ? (
<a
href={link}
target="_blank"
class:list={[
"aspect-square w-full h-full rounded-2xl overflow-hidden relative group",
className,
]}
data-track={track}
>
<div
class:list={[
"absolute inset-0 flex overflow-hidden p-0",
"items-end justify-end",
]}
>
<img
src={image}
alt=""
class:list={[
"transition-transform duration-300 ease-in-out group-hover:scale-110",
imageClass,
]}
/>
</div>
<div class="absolute inset-0 p-4 flex flex-col justify-start items-start z-10">
<span class="font-medium text-3xl text-left text-pretty tracking-separated pr-3 md:max-w-64">
{title}
</span>
</div>
</a>
) : (
<button
class:list={[
"aspect-square w-full h-full rounded-2xl overflow-hidden relative group",
className,
]}
data-content-id={contentId}
data-track={track}
>
<div
class:list={[
"absolute inset-0 flex overflow-hidden p-0",
contentId === "selection-process"
? "items-end justify-center md:items-stretch md:justify-end"
: "",
contentId === "emergent-positions"
? "items-start md:items-center"
: "",
contentId === "no-experience"
? "items-end justify-center"
: "items-end justify-end",
]}
>
<img
src={image}
alt=""
class:list={[
"transition-transform duration-300 ease-in-out group-hover:scale-110",
imageClass,
contentId === "selection-process"
? "max-h-[70%] md:max-h-none md:h-full md:w-auto md:pr-0"
: "",
]}
/>
</div>
<div class="absolute inset-0 p-4 flex flex-col justify-start items-start z-10">
<span class="font-medium text-3xl text-left text-pretty tracking-separated pr-3 md:max-w-64">
{title}
</span>
</div>
</button>
)
)
}
</div>
</SectionContainer>
<script>
document.addEventListener("DOMContentLoaded", () => {
// modal
const $modal = document.getElementById("infographic-modal")
const $modalBody = document.getElementById("infographic-modal-body")
const $btnModal = document.getElementById("btn-modal")
const $body = document.body
// buttons bento
const $buttons = document.querySelectorAll("button[data-content-id]")
const openModal = (id: string) => {
const contentId = `infographic-${id}`
const content = document.getElementById(contentId)
if (content && $modalBody) {
$modal?.classList.add("modal-active")
$modalBody.replaceChildren(content.cloneNode(true))
$modalBody.firstElementChild?.classList.replace("hidden", "flex")
$body.style.overflow = "hidden"
}
}
const closeModal = () => {
if ($modal?.classList.contains("modal-active")) {
$modal?.classList.remove("modal-active")
}
const currentContent = $modalBody?.firstChild
if (currentContent) {
$modalBody?.removeChild(currentContent)
}
$body.style.overflow = ""
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
closeModal()
}
}
$buttons.forEach((button) => {
button.addEventListener("click", () => {
const id = button.getAttribute("data-content-id")
if (id) openModal(id)
})
})
$btnModal?.addEventListener("click", () => closeModal())
document.addEventListener("keydown", handleKeyDown)
})
</script>
================================================
FILE: src/components/CoolJobBackground.astro
================================================
<img
src="/img/cool-jobs/pattern.webp"
alt="Cool Jobs Background Pattern"
class="hidden lg:block lg:absolute lg:top-16 lg:right-2"
/>
================================================
FILE: src/components/CoolJobBrand.astro
================================================
<a
href="https://cooljobs.infojobs.net/"
target="_blank"
rel="noopener noreferrer"
data-track="cool_jobs"
class:list={[
"group transition hover:bg-[#fe5230] bg-[#fe52308f] h-24 max-w-[312px] rounded-2xl gap-4 lg:flex-col lg:items-start lg:justify-center m-auto",
Astro.props.class,
]}
>
<div class="flex flex-col justify-center items-center p-6 gap-1">
<img
src="/img/cool-jobs/cool-jobs.webp"
alt="Logo Cool Jobs"
class="px-0 group-hover:scale-105 transition-transform"
/>
<p
class="text-white text-sm p-0 text-center md:pl-1 md:text-start md:w-full"
>
{"Ver todos >"}
</p>
</div>
</a>
================================================
FILE: src/components/CoolJobCard.astro
================================================
---
const { image, brand, title, id } = Astro.props
---
<a
href=`https://cooljobs.infojobs.net/#${id}`
target="_blank"
data-track="cool_jobs"
>
<article
class="flex items-center p-4 group min-w-[274px] md:p-6 md:max-w-[312px] h-24 rounded-2xl border border-[#FE523052] overflow-hidden bg-white snap-center"
>
<div
class="flex items-center justify-center gap-4 transition-transform group-hover:scale-105"
>
<div class="flex items-center h-10">
<img src={image} alt={brand} class="w-20 h-auto" loading="lazy" />
</div>
<p class="font-medium md:font-semibold">
{title}
</p>
</div>
</article>
</a>
================================================
FILE: src/components/CoolJobs.astro
================================================
---
import CoolJobCard from "@components/CoolJobCard.astro"
import type { CoolJobs } from "@types"
import CoolJobBackground from "@components/CoolJobBackground.astro"
import CoolJobBrand from "@components/CoolJobBrand.astro"
const coolJobs: CoolJobs[] = [
{
image: "/img/cool-jobs/port-aventura.webp",
title: "Probador de montañas rusas",
brand: "Port Aventura World",
section_id: "lp-pom-box-499",
},
{
image: "/img/cool-jobs/maxibon.webp",
title: "Catadora de helados",
brand: "Maxibon",
section_id: "lp-pom-box-515",
},
{
image: "/img/cool-jobs/wizink-center.webp",
title: "Probadora de conciertos",
brand: "WiZink Center",
section_id: "lp-pom-box-528",
},
{
image: "/img/cool-jobs/dominos.webp",
title: "Catadora de pizzas",
brand: "Domino's",
section_id: "lp-pom-box-534",
},
{
image: "/img/cool-jobs/btravel.webp",
title: "Probadora de viajes",
brand: "B travel",
section_id: "lp-pom-box-584",
},
{
image: "/img/cool-jobs/grefusa.webp",
title: "Probadora de snacks",
brand: "Grefusa",
section_id: "lp-pom-box-522",
},
{
image: "/img/cool-jobs/visa.webp",
title: "Probador de pagos con el móvil",
brand: "Visa",
section_id: "lp-pom-box-598",
},
]
---
<section class="w-full mx-auto lg:max-w-7xl">
<article
class="p-4 pb-6 flex flex-col gap-6 bg-gradient-to-r from-[#FFDEDA] to-[#FCEDEB] md:py-[70px] md:px-10 md:items-center md:relative md:rounded-2xl md:mx-4"
>
<CoolJobBackground />
<h2
class="text-3xl font-semibold text-center text-ij-red tracking-separated lg:text-4xl lg:z-10 lg:mb-4"
>
Los trabajos más cool
</h2>
<div
class="flex items-center px-16 pb-6 -mx-4 overflow-x-auto no-scrollbar gap-7 snap-x snap-mandatory max-w-[1536px] md:overflow-visible md:grid md:grid-cols-2 lg:grid-cols-3 lg:p-0 lg:m-0 lg:z-10 md:justify-items-center xl:grid-cols-4"
>
{
coolJobs.map(({ image, title, brand, section_id }) => (
<CoolJobCard
image={image}
title={title}
brand={brand}
id={section_id}
/>
))
}
<CoolJobBrand class="hidden md:flex" />
</div>
<CoolJobBrand class="md:hidden min-w-[328px]" />
</article>
</section>
================================================
FILE: src/components/EmergentPositions.astro
================================================
---
import InfographicBody from "./InfographicBody.astro";
import InfographicHeader from "./InfographicHeader.astro";
const standsInterviewsBig = [
{
title: "Inteligencia Artificial",
description: "Ingeniero/a de Machine Learning",
requirements: [
{
label: "📖 Estudios",
tag: ["Ciencia Computacional", "Matemáticas", "Estadística"],
},
{
label: "📝 Condiciones",
tag: ["Contrato indefinido", "Salario de 35k-58k €/año"],
},
{
label: "🦾 Hard Skills",
tag: ["Python", "R", "TensorFlow", "PyTorch"],
},
{
label: "👋 Soft Skills",
tag: ["Pensamiento critico", "Trabajo en equipo", "Analisis"],
},
],
location: [
{
label: "📍 Dónde",
tag1: "Madrid",
tag2: "Barcelona",
},
],
image1:
"bg-[url(/img/emergent-positions/madrid.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center ",
image2:
"bg-[url(/img/emergent-positions/barcelona.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
backgroundColor:
"bg-[url(/img/emergent-positions/bg2Img1.webp)] text-[#3C49F0] md:bg-cover md:bg-[url(/img/emergent-positions/bgImg1.webp)] ",
backgroundColor2: "bg-[#CCCCFA]",
backgroundColor3: "bg-[#9B9DF6]",
},
{
title: "Data/Analytics",
description: "Data Engineer",
requirements: [
{
label: "📖 Estudios",
tag: ["Ingeniería informática", "Telecomunicaciones"],
},
{
label: "📝 Condiciones",
tag: ["Contrato indefinido", "Salario de 33k-60k €/año"],
},
{
label: "🦾 Hard Skills",
tag: ["SQL", "Python", "Big Data", "Spark"],
},
{
label: "👋 Soft Skills",
tag: ["Resolución de problemas", "Adaptación al cambio"],
},
],
location: [
{
label: "📍 Dónde",
tag1: "Barcelona",
tag2: "Valencia",
},
],
image1:
"bg-[url(/img/emergent-positions/barcelona.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
image2:
"bg-[url(/img/emergent-positions/valencia.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
backgroundColor:
" text-[#008949] bg-[url(/img/emergent-positions/bg2Img2.webp)] md:bg-cover md:bg-[url(/img/emergent-positions/bgImg2.webp)] ",
backgroundColor2: "bg-[#B4E5CD]",
backgroundColor3: "bg-[#3EB87E]",
},
{
title: " Building Information Modeling (BIM)",
description: "Arquitecto/a BIM",
requirements: [
{
label: "📖 Estudios",
tag: ["Arquitectura", "Ingeneiría de Edificación"],
},
{
label: "📝 Condiciones",
tag: ["Contrato indefinido", "Salario de 30k-50k €/año"],
},
{
label: "🦾 Hard Skills",
tag: ["Revit", "AutoCAD", "Gestión de proyectos"],
},
{
label: "👋 Soft Skills",
tag: ["Comunicación", "Liderazgo", "Autogestión"],
},
],
location: [
{
label: "📍 Dónde",
tag1: "Barcelona",
tag2: "Sevilla",
},
],
image1:
"bg-[url(/img/emergent-positions/barcelona.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
image2:
"bg-[url(/img/emergent-positions/sevilla.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
backgroundColor:
" text-[#D1A22E] bg-[url(/img/emergent-positions/bg2Img3.webp)] md:bg-cover md:bg-[url(/img/emergent-positions/bgImg3.webp)]",
backgroundColor2: "bg-[#FDECBE]",
backgroundColor3: "bg-[#F7CE5B] text-xs md:text-[0.7rem]",
},
{
title: " Ciberseguridad / Cloud",
description: "Cloud Security Engineer",
requirements: [
{
label: "📖 Estudios",
tag: ["Informática", "Ingeneiría en telecomunicaciones"],
},
{
label: "📝 Condiciones",
tag: ["Contrato indefinido", "Salario de 40k-60k €/año"],
},
{
label: "🦾 Hard Skills",
tag: ["AWS", "Azure", "Seguridad en la nube"],
},
{
label: "👋 Soft Skills",
tag: ["Atención al detalle", "Pensamiento analítico"],
},
],
location: [
{
label: "📍 Dónde",
tag1: "Madrid",
tag2: "Málaga",
},
],
image1:
"bg-[url(/img/emergent-positions/madrid.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
image2:
"bg-[url(/img/emergent-positions/malaga.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
backgroundColor:
"bg-[url(/img/emergent-positions/bg2Img4.webp)] md:bg-cover md:bg-[url(/img/emergent-positions/bgImg4.webp)] text-[#006895]",
backgroundColor2: "bg-[#C4DFEB]",
backgroundColor3: "bg-[#7BB6D3] text-xs",
},
{
title: "Quality Assurance (QA)",
description: "QA Automation Engineer",
requirements: [
{
label: "📖 Estudios",
tag: ["Ingeneiría Técnica o Superior "],
},
{
label: "📝 Condiciones",
tag: ["Contrato indefinido", "Salario de 30k-50k €/año"],
},
{
label: "🦾 Hard Skills",
tag: ["Selenium", "Jenkins", "Python"],
},
{
label: "👋 Soft Skills",
tag: ["Proactividad", "Resolución de problemas"],
},
],
location: [
{
label: "📍 Dónde",
tag1: "Madrid",
tag2: "Bilbao",
},
],
image1:
"bg-[url(/img/emergent-positions/madrid.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
image2:
"bg-[url(/img/emergent-positions/bilbao.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
backgroundColor:
"bg-[url(/img/emergent-positions/bg2Img5.webp)] md:bg-cover md:bg-[url(/img/emergent-positions/bgImg5.webp)] text-[#DF4D3A]",
backgroundColor2: "bg-[#FAC7C8]",
backgroundColor3: "bg-[#F1696E] text-xs",
},
{
title: "Eficiencia Energética",
description: "Ingeniero/a de Eficiencia Energética",
requirements: [
{
label: "📖 Estudios",
tag: ["Ingeniero/a de Eficiencia Energética"],
},
{
label: "📝 Condiciones",
tag: ["Contrato indefinido", "Salario de 30k-50k €/año"],
},
{
label: "🦾 Hard Skills",
tag: ["Presto", "DIALux", "Análisis de costes"],
},
{
label: "👋 Soft Skills",
tag: ["Organización", "Planificación"],
},
],
location: [
{
label: "📍 Dónde",
tag1: "Valencia",
tag2: "Zaragoza",
},
],
image1:
"bg-[url(/img/emergent-positions/valencia.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
image2:
"bg-[url(/img/emergent-positions/zaragoza.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
backgroundColor:
"bg-[url(/img/emergent-positions/bg2Img6.webp)] md:bg-cover md:bg-[url(/img/emergent-positions/bgImg6.webp)] text-[#009B4E] ",
backgroundColor2: "bg-[#B4E5CD]",
backgroundColor3: "bg-[#008949] md:text-[0.6rem]",
},
{
title: "Vocación social",
description: "Psicólogo/a",
requirements: [
{
label: "📖 Estudios",
tag: ["Psicología", "Trabajo Social"],
},
{
label: "📝 Condiciones",
tag: ["Contrato indefinido", "Salario de 12k-24k €/año"],
},
{
label: "🦾 Hard Skills",
tag: ["Intervención psicológica", "Programas sociales"],
},
{
label: "👋 Soft Skills",
tag: ["Empatía", "Paciencia", "Comunicación asertiva"],
},
],
location: [
{
label: "📍 Dónde",
tag1: "Madrid",
tag2: "Valencia",
},
],
image1:
"bg-[url(/img/emergent-positions/madrid.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
image2:
"bg-[url(/img/emergent-positions/valencia.webp)] w-[31.25rem] md:h-[11rem] h-[7rem] bg-cover bg-center",
backgroundColor:
"bg-[url(/img/emergent-positions/bg2Img7.webp)] md:bg-cover md:bg-[url(/img/emergent-positions/bgImg7.webp)] text-[#006895]",
backgroundColor2: "bg-[#C4DFEB]",
backgroundColor3: "bg-[#7BB6D3] md:text-[0.6rem]",
},
// Agrega más objetos según sea necesario
];
const { infographicId } = Astro.props;
---
<section
id={`infographic-${infographicId}`}
class="mx-auto px-4 max-w-7xl w-full flex-col md:gap-[50px] py-[72px] hidden"
>
<InfographicHeader
badgeText="Puestos Emergentes"
title="¿Quieres saber qué estudiar para lograr los trabajos del futuro?"
subtitle="¡Te lo explicamos!"
/>
<InfographicBody bentoInfoModalCardsBig={standsInterviewsBig} />
</section>
================================================
FILE: src/components/FollowUs.astro
================================================
---
import FollowUsLink from "@components/FollowUsLink.astro";
import Icon from "./ui/Icon.astro";
const hoverSocialNetworks = "md:transition-transform md:hover:scale-110";
const hoverStoreApps = "md:transition-opacity md:duration-150 md:ease-in-out md:hover:opacity-90";
---
<section class="flex flex-col items-center justify-between md:flex-row md:p-10">
<article
class="w-full flex items-center justify-center gap-2 py-4 flex-wrap md:justify-start"
>
<p class="text-xl tracking-[0.38px] leading-6 font-semibold">
¡Síguenos!
</p>
<span class="flex items-center gap-2">
<FollowUsLink
href="https://www.tiktok.com/@infojobs"
class={hoverSocialNetworks}
socialPlatform="Visítanos en TikTok."
>
<Icon name="tiktok" />
</FollowUsLink>
<FollowUsLink
href="https://www.youtube.com/InfoJobs"
class={hoverSocialNetworks}
socialPlatform="Visítanos en YouTube."
>
<Icon name="youtube" />
</FollowUsLink>
<FollowUsLink
href="https://x.com/InfoJobs"
class={hoverSocialNetworks}
socialPlatform="Visítanos en X."
>
<Icon name="twitter" />
</FollowUsLink>
<FollowUsLink
href="https://www.facebook.com/InfoJobs"
class={hoverSocialNetworks}
socialPlatform="Visítanos en Facebook."
>
<Icon name="facebook" />
</FollowUsLink>
</span>
</article>
<article
class="w-full flex items-center justify-center gap-4 py-4 flex-wrap md:flex-nowrap md:p-0 md:gap-2 md:justify-end"
>
<FollowUsLink
href="https://infojobs.go.link/?adj_t=1b8zxouv_1bnixj2z"
class={hoverStoreApps}
socialPlatform="Visítanos en Google."
>
<Icon name="googleplay" />
</FollowUsLink>
<FollowUsLink
href="https://infojobs.onelink.me/2882600328/rjh2wn2f"
class={hoverStoreApps}
socialPlatform="Visítanos en la AppStore."
>
<Icon name="appstore" />
</FollowUsLink>
</article>
</section>
================================================
FILE: src/components/FollowUsLink.astro
================================================
---
const { href, socialPlatform } = Astro.props;
---
<a
href={href}
target="_blank"
rel="noopener noreferrer"
class={Astro.props.class}
aria-label={socialPlatform}
>
<slot />
</a>
================================================
FILE: src/components/Footer.astro
================================================
---
import LinksColumns from "@components/LinksColumns.astro"
import FollowUs from "@components/FollowUs.astro"
import AdevintaInfo from "@components/AdevintaInfo.astro"
---
<footer class="bg-[#F5F5F5] py-6 lg:py-10 mt-20">
<section class="mx-auto max-w-7xl px-4 w-full">
<LinksColumns />
<FollowUs />
<AdevintaInfo />
</section>
</footer>
================================================
FILE: src/components/Header.astro
================================================
---
import Button from "./ui/Button.astro"
---
<header
id="header"
class="py-4 px-8 flex justify-between items-center md:max-w-[1400px] md:mx-auto sticky top-0 right-0 left-0 bg-white/70 z-50 mb-4 backdrop-blur-xl"
>
<a href="#" aria-label="Regresar a la página de inicio"
><img src="/logo.svg" alt="Logo InfoJobs" class="w-auto h-[30px]" /></a
>
<Button
variant="solid"
color="primary"
size="lg"
radius="full"
as="link"
href="https://www.infojobs.net/candidate/candidate-login/candidate-login.xhtml"
>
Crea tu cuenta
</Button>
</header>
================================================
FILE: src/components/HeroSearch.astro
================================================
---
import SearchIcon from "@icons/Search.astro"
import { getStudies } from "@lib/get-ij-studies"
import SectionContainer from "@components/SectionContainer.astro"
import Button from "@ui/Button.astro"
import ChevronDown from "@icons/ChevronDown.astro"
const SHORTCUTS = [
{
label: "🌱 Sin experiencia",
href: "https://www.infojobs.net/jobsearch/search-results/list.xhtml?keyword=&segmentId=&page=1&sortBy=PUBLICATION_DATE&onlyForeignCountry=false&countryIds=17&sinceDate=ANY&experienceMin=_0_YEARS&experienceMax=_0_YEARS",
track: "explore_no_experience",
},
{
label: "🗓️ Findes",
href: "https://www.infojobs.net/ofertas-trabajo/fines-de-semana",
track: "explore_weekend_jobs",
},
{
label: "🕒 Media jornada",
href: "https://www.infojobs.net/ofertas-trabajo/media-jornada",
track: "explore_part_time",
},
{
label: "💼 Prácticas",
href: "https://www.infojobs.net/ofertas-trabajo/practicas",
track: "explore_internships",
},
] as const
const studies = await getStudies()
const sortedStudies = studies.sort((a, b) => a.order - b.order)
---
<SectionContainer>
<section
class="bg-hero-pattern rounded-3xl px-4 py-6 md:w-full md:max-w-7xl md:mx-auto"
>
<h1
class="text-black/80 font-semibold tracking-[0.35px] text-2xl text-center pb-4 sm:text-4xl lg:hidden"
>
Tu carrera empieza aquí
</h1>
<h1
class="hidden text-black/80 font-semibold tracking-[0.35px] text-2xl text-center pb-4 sm:text-4xl lg:pb-8 lg:block"
>
Conecta tus estudios con el empleo ideal
</h1>
<search class="bg-white rounded-3xl p-4 lg:mx-10">
<form
id="hero-form"
class="mx-5 lg:flex lg:justify-between lg:items-center lg:gap-2"
>
<div
id="study-level-container"
class="flex items-center border-b border-gray-300 lg:border-none relative select-none outline-none lg:focus-visible:outline lg:focus-visible:outline-[#167DB7] lg:focus-visible:outline-1 lg:focus-visible:outline-offset-0 lg:focus-visible:rounded-lg rounded-md lg:w-full lg:px-4"
tabindex="0"
aria-haspopup="listbox"
aria-expanded="false"
aria-labelledby="study-level-container"
>
<div
class="w-full py-4 text-gray-600 bg-transparent cursor-pointer flex justify-between items-center"
aria-label="Selecciona tu nivel de estudios"
>
<span id="study-level-text"> Nivel de estudios </span>
<ChevronDown id="study-level-chevron" width="24" height="24" />
</div>
<ul
id="study-level-options"
role="listbox"
class="absolute top-full mt-2 space-y-1 rounded-lg left-0 p-1 max-h-80 w-full border border-gray-300 bg-white shadow-md hidden overflow-auto no-scrollbar z-20"
data-track="study_level"
>
{
sortedStudies.map(({ id, value }) => {
const isDefault = id === 0
const literal = isDefault ? "Nivel de estudios" : value
return (
<li
id="study-level-option"
role="option"
class={`p-3 transition-colors duration-200 ease-in-out rounded-md outline-none cursor-pointer hover:bg-[#F3F9FB] focus-visible:bg-[#F3F9FB] ${isDefault ? "text-gray-400 bg-none pointer-events-none" : "text-gray-600"}`}
data-value={id}
aria-disabled={isDefault}
aria-selected={isDefault}
tabindex={isDefault ? -1 : 0}
>
{literal}
</li>
)
})
}
</ul>
</div>
<div class="lg:w-full relative">
<input
autocomplete="off"
id="hero-keywords"
name="keywords"
placeholder="Estudios, puesto, empresa..."
class="w-full py-3 lg:px-3 border-b border-gray-300 lg:border-none text-gray-600 focus:outline-none lg:focus-visible:bg-[#E8F2F8] lg:focus-visible:outline lg:focus-visible:outline-[#167DB7] lg:focus-visible:outline-1 lg:focus-visible:outline-offset-0 lg:focus-visible:rounded-lg"
role="combobox"
aria-expanded="false"
aria-controls="hero-keywords-list"
aria-autocomplete="list"
/>
<ul
id="hero-keywords-list"
class="bg-white w-full mx-auto absolute inset-0 top-14 min-h-[224px] h-full max-h-[224px] z-[999] flex flex-col gap-1 p-1 rounded-lg border border-gray-300 shadow-md no-scrollbar [&>li]:rounded-md [&>li]:transition-colors [&>li]:duration-300 [&>li]:text-ij-black [&>li:hover]:bg-[#F3F9FB] [&>li]:p-3 [&>li]:cursor-pointer [&>li:focus]:bg-[#F3F9FB] [&>li:focus]:outline-none"
role="listbox"
aria-label="Lista de sugerencias"
tabindex="0"
>
</ul>
</div>
<div class="mb-8 lg:mb-0 lg:w-full">
<input
name="geolocation"
placeholder="Provincia o población"
class="w-full py-3 lg:px-3 border-b border-gray-300 lg:border-none text-gray-600 focus:outline-none lg:focus-visible:bg-[#E8F2F8] lg:focus-visible:outline lg:focus-visible:outline-[#167DB7] lg:focus-visible:outline-1 lg:focus-visible:outline-offset-0 lg:focus-visible:rounded-lg"
/>
</div>
<Button
type="submit"
color="secondary"
variant="solid"
size="md"
radius="full"
class="shadow-md rounded-full text-lg font-medium bg-accent hover:saturate-150 transition text-white px-4 py-2 inline-flex gap-x-2 justify-center items-center w-full lg:size-12 lg:mt-1 lg:hover:scale-105"
ariaLabel="Buscar ofertas"
track="job_search"
>
<SearchIcon />
<p class="lg:hidden">Buscar ofertas</p>
</Button>
</form>
</search>
<footer>
<p class="text-center pt-8 pb-4">Explora trabajos que se adaptan a ti</p>
<div
class="max-xs:grid-cols-1 max-sm:grid max-sm:grid-cols-2 flex flex-wrap gap-4 justify-center"
>
{
SHORTCUTS.map(({ label, href, track }) => (
<Button
as="link"
href={href}
color="default"
variant="solid"
radius="full"
size="lg"
class="bg-white whitespace-nowrap text-center rounded-full p-4 lg:px-6 shadow-sm text-primary transition hover:scale-105 hover:text-white hover:bg-primary !font-light"
disableSaturateHover={true}
track={track}
>
{label}
</Button>
))
}
</div>
</footer>
</section>
</SectionContainer>
<script>
const select = document.querySelector("select")
function updateColors() {
if (select?.value !== "indicar-nivel") {
select?.classList.add("text-gray-600")
}
}
document.addEventListener("DOMContentLoaded", updateColors)
select?.addEventListener("change", updateColors)
</script>
<script>
import { generateInfoJobsURL } from "@lib/generate-infojobs-url"
const form = document.querySelector("#hero-form") as HTMLFormElement
form?.addEventListener("submit", (event) => {
event.preventDefault()
const formData = new FormData(form)
const level = formData.get("study-level")?.toString() ?? ""
const keyword = formData.get("keywords")?.toString() ?? ""
const provinceIds =
form.querySelector('input[name="geolocation"]')?.getAttribute("value") ??
""
const searchURL = generateInfoJobsURL({
keyword,
level,
provinceIds,
})
window.location.href = searchURL
})
</script>
<script>
import { queryKeywords } from "@lib/queryKeywords"
const inputKeyword = document.querySelector(
"#hero-keywords"
) as HTMLInputElement
const keywordsContainer = document.querySelector(
"#hero-keywords-list"
) as HTMLUListElement
const renderedKeywords = new Set()
// debounce para retrasar el fetch hacia la API que devuelve las keywords.
const debounce = (func: Function, delay: number) => {
let timeoutId: ReturnType<typeof setTimeout>
return (...args: any[]) => {
clearTimeout(timeoutId)
timeoutId = setTimeout(() => func(...args), delay)
}
}
const debouncedFetchKeywords = debounce((value: string) => {
fetchKeywords(value)
}, 300)
inputKeyword?.addEventListener("input", (event) => {
const value = (event.target as HTMLInputElement).value
if (value.length > 1) {
debouncedFetchKeywords(value)
openListElement()
} else {
closeListElement()
}
})
const fetchKeywords = async (value: string) => {
const keywordsList: [] = await queryKeywords(value)
keywordsContainer.innerHTML = ""
renderedKeywords.clear()
if (keywordsList.length > 0) {
keywordsList.forEach(({ value: queryValue }) => {
if (!renderedKeywords.has(queryValue)) {
renderedKeywords.add(queryValue)
const li = document.createElement("li")
li.setAttribute("role", "option")
li.tabIndex = 0
li.id = `keyword-${queryValue}`
li.innerHTML = getHighlightedKeyword(value, queryValue)
li.addEventListener("click", () => {
inputKeyword.value = li.textContent!
closeListElement()
})
keywordsContainer.appendChild(li)
}
})
openListElement()
} else {
closeListElement()
}
}
const getHighlightedKeyword = (
inputValue: string,
queryValue: string
): string => {
const regex = new RegExp(inputValue, "gi")
return queryValue.replace(
regex,
(match) => `<mark class="font-bold bg-transparent">${match}</mark>`
)
}
const handleClickOutsideList = (event: any) => {
if (!event.target.matches("#hero-keywords-list *")) {
closeListElement()
} else {
document.removeEventListener("click", handleClickOutsideList)
}
}
const openListElement = () => {
keywordsContainer.classList.add("list-active")
inputKeyword.ariaExpanded = "true"
document.addEventListener("keydown", handleKeyPress)
}
const closeListElement = () => {
keywordsContainer.classList.remove("list-active")
keywordsContainer.ariaExpanded = "false"
keywordsContainer.innerHTML = ""
renderedKeywords.clear()
document.removeEventListener("keydown", handleKeyPress)
}
const handleKeyPress = (event: KeyboardEvent) => {
if (event.code === "ArrowDown" || event.code === "ArrowUp") {
event.preventDefault()
const activeElement = document.activeElement
if (activeElement?.tagName !== "LI") {
const firstElement =
keywordsContainer.firstElementChild as HTMLLIElement
firstElement.tabIndex = 0
firstElement.focus()
return
}
const nextElement =
event.code === "ArrowDown"
? (activeElement.nextElementSibling as HTMLLIElement)
: (activeElement.previousElementSibling as HTMLLIElement)
if (nextElement) {
activeElement.setAttribute("tabindex", "-1")
nextElement.setAttribute("tabindex", "0")
nextElement.focus()
}
}
if (event.code === "Enter" || event.code === "Space") {
event.preventDefault()
const activeElement = document.activeElement
if (activeElement?.tagName === "LI") {
inputKeyword.value = activeElement.textContent!
closeListElement()
}
}
if (event.code === "Escape") {
event.preventDefault()
closeListElement()
}
}
document.addEventListener("click", handleClickOutsideList)
</script>
<style>
#hero-keywords-list {
visibility: hidden;
opacity: 0;
overflow-y: scroll;
scrollbar-width: none;
}
#hero-keywords-list.list-active {
visibility: visible;
opacity: 1;
}
</style>
<script>
import { getProvinceId } from "@lib/list-provinces-ids"
const geolocationInput = document.querySelector(
'input[name="geolocation"]'
) as HTMLInputElement | null
geolocationInput?.addEventListener("input", function () {
getProvinceId(this)
})
</script>
<script>
const $ = document.querySelector.bind(document)
const studyLevelContainer = $("#study-level-container")
const studyLevelOptions = $("#study-level-options") as HTMLUListElement
const studyLevelOption = $("#study-level-option")
const studyLevelText = $("#study-level-text")
const studyLevelChevron = $("#study-level-chevron")
const hiddenSelect = document.createElement("input")
hiddenSelect.type = "hidden"
hiddenSelect.name = "study-level"
studyLevelContainer?.appendChild(hiddenSelect)
studyLevelContainer?.addEventListener("click", () => {
const isExpanded =
studyLevelContainer?.getAttribute("aria-expanded") === "true"
studyLevelContainer?.setAttribute("aria-expanded", `${!isExpanded}`)
studyLevelChevron?.classList.toggle("rotate-180")
studyLevelOptions?.classList.toggle("hidden")
})
document.addEventListener("click", (event) => {
if (!studyLevelContainer?.contains(event.target as Node)) {
studyLevelContainer?.setAttribute("aria-expanded", "false")
studyLevelChevron?.classList.remove("rotate-180")
studyLevelOptions?.classList.add("hidden")
}
})
studyLevelContainer?.addEventListener("keydown", (evt) => {
const event = evt as KeyboardEvent
const isExpanded =
studyLevelContainer.getAttribute("aria-expanded") === "true"
if (event.key === "Enter" || event.key === " ") {
if (!isExpanded) {
studyLevelContainer.setAttribute("aria-expanded", "true")
studyLevelChevron?.classList.toggle("rotate-180")
studyLevelOptions?.classList.toggle("hidden")
event.preventDefault()
} else {
const activeOption = document.activeElement as Element | null
const selectedOption = activeOption?.closest("[data-value]")
if (
selectedOption &&
selectedOption.getAttribute("data-value") !== "0"
) {
const value = selectedOption.getAttribute("data-value")
const text = selectedOption.textContent?.trim()
if (studyLevelText && hiddenSelect) {
studyLevelText.textContent = text as string
hiddenSelect.value = value as string
}
studyLevelOptions
?.querySelectorAll("[data-value]")
.forEach((option) => {
option.classList.remove("bg-[#F3F9FB]")
})
selectedOption.classList.add("bg-[#F3F9FB]")
studyLevelContainer?.setAttribute("aria-expanded", "false")
studyLevelChevron?.classList.remove("rotate-180")
studyLevelOptions?.classList.add("hidden")
event.preventDefault()
}
}
}
if (isExpanded && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
const options = studyLevelOptions?.querySelectorAll(
"[data-value]"
) as NodeListOf<HTMLElement>
let index = Array.from(options).findIndex(
(option) => option === document.activeElement
)
if (event.key === "ArrowDown") index++
if (event.key === "ArrowUp") index--
if (index >= 0 && index < options.length) options[index]?.focus()
event.preventDefault()
}
if (isExpanded && event.key === "Escape") {
studyLevelContainer?.setAttribute("aria-expanded", "false")
studyLevelChevron?.classList.remove("rotate-180")
studyLevelOptions?.classList.add("hidden")
}
})
studyLevelOptions?.addEventListener("click", (event) => {
const target = event.target as Element | null
const selectedOption = target?.closest("[data-value]")
if (selectedOption && selectedOption.getAttribute("data-value") !== "0") {
const value = selectedOption.getAttribute("data-value")
const text = selectedOption.textContent?.trim()
if (studyLevelText && hiddenSelect) {
studyLevelText.textContent = text as string
hiddenSelect.value = value as string
}
studyLevelOptions.querySelectorAll("[data-value]").forEach((option) => {
option.classList.remove("bg-[#F3F9FB]")
})
selectedOption.classList.add("bg-[#F3F9FB]")
}
})
studyLevelOption?.addEventListener("click", () => {
studyLevelContainer?.setAttribute("aria-expanded", "false")
studyLevelChevron?.classList.remove("rotate-180")
studyLevelOptions?.classList.add("hidden")
})
studyLevelOptions?.addEventListener("wheel", (evt) => {
const event = evt as WheelEvent
const { deltaY } = event
const { scrollTop, scrollHeight, offsetHeight } = studyLevelOptions
if (
(deltaY < 0 && scrollTop === 0) ||
(deltaY > 0 && scrollTop + offsetHeight >= scrollHeight)
) {
event.preventDefault()
}
})
</script>
================================================
FILE: src/components/InfographicBody.astro
================================================
---
type Props = {
bentoInfoModalCards?: readonly BentoInfoModalCard[]; // Ahora es opcional
bentoInfoModalCardsBig?: BentoInfoModalCardBig[];
};
type BentoInfoModalCard = {
image: string;
title: string;
description: string;
firstBgColor: string;
secondBgColor: string;
textColor: string;
};
type BentoInfoModalCardBig = {
title: string;
description: string;
requirements: { label: string; tag: string[] }[];
location: { label: string; tag1: string; tag2: string }[];
image1: string;
image2: string;
backgroundColor: string;
backgroundColor2: string;
backgroundColor3: string;
};
const { bentoInfoModalCards = [], bentoInfoModalCardsBig = [] } =
Astro.props as Props;
---
<section
class='grid gap-[48px] md:justify-between grid-cols-1 lg:grid-cols-2 overflow-hidden'
>
{
bentoInfoModalCards.map(
({
image,
title,
description,
firstBgColor,
secondBgColor,
textColor,
}) => (
<article
class='file-card w-auto h-full mx-auto max-h-[456px] max-w-[330px] md:max-w-[600px] md:max-h-[630px] lg:min-h-[700px]'
style={{
background: `linear-gradient(to bottom right, ${firstBgColor}, ${secondBgColor})`,
}}
>
<div class='w-full h-full p-2 my-7'>
<img
src={image}
alt={`${title} Card`}
class='min-h-[115px] max-h-[115px] lg:min-h-[210px] lg:max-h-[210px] w-auto mx-auto my-7'
/>
<div class='py-10 px-15 flex flex-col gap-6 items-center'>
<h4
class='text-[26px] leading-[26px] md:text-5xl md:leading-[48px] text-center'
style={{ color: textColor }}
>
{title}
</h4>
<p
class='text-[15px] leading-[15px] md:text-[28px] md:leading-7 text-center'
style={{ color: textColor }}
>
{description}
</p>
</div>
</div>
</article>
)
)
}
</section>
<section class='w-auto h-auto'>
{
bentoInfoModalCardsBig.map((card) => (
<article
class={`${card.backgroundColor} p-5 mb-20 rounded-3xl `}
style='background-size: 100% 100%;'
>
<h5
class={` ${card.backgroundColor3} uppercase py-2 px-4 w-fit md:w-fit rounded-lg text-white text-sm lg:text-xl md:text-xs leading-5 text-center tracking-[2%] `}
>
{card.title}
</h5>
<p class=' lg:text-[3.5rem] lg:my-9 text-2xl my-3'>
{card.description}
</p>
<div class='grid grid-cols-1 md:grid-cols-2 sm:grid-cols-2 gap-6 mb-6'>
{card.requirements.map((requirement) => (
<div
class={`flex flex-col gap-3 ${card.backgroundColor2} rounded-[1.25rem] px-5 py-5 `}
>
<h6 class='rounded-xl text-base md:text-2xl bg-white text-black w-fit py-2 px-3'>
{requirement.label}
</h6>
<div class=' flex flex-wrap gap-2 md:gap-5 mt-2'>
{requirement.tag.map((tag) => (
<span class='text-black sm:text-base md:text-2xl lg:text-3xl px-5 py-1 rounded-[2rem] border-[1px] border-[#5B666C80] w-auto'>
{tag}
</span>
))}
</div>
</div>
))}
</div>
<div>
{card.location.map((location) => (
<div
class={`flex flex-col gap-2 justify-between ${card.backgroundColor2} rounded-[20px] px-5 py-6 `}
>
<h6 class='rounded-xl text-base md:text-2xl bg-white text-black w-fit py-2 px-3'>
{location.label}
</h6>
<div class='flex flex-col md:flex-row justify-around gap-2 mt-2'>
<div
class={`rounded-xl ${card.image1} pl-4 pt-2 w-auto md:w-[48%]`}
>
<span class='text-black text-base md:text-xl w-full uppercase'>
{location.tag1}
</span>
</div>
<div
class={`rounded-xl ${card.image2} pl-4 p-2 w-auto md:w-[48%] `}
>
<span class='text-black text-base md:text-xl w-full uppercase'>
{location.tag2}
</span>
</div>
</div>
</div>
))}
</div>
</article>
))
}
</section>
<style>
.file-card {
border-radius: 2rem;
clip-path: polygon(
0% 0%,
calc(100% - 116.6px) 0.3px,
calc(100% - 112px) 1.2px,
calc(100% - 107.5px) 2.7px,
calc(100% - 103.4px) 4.8px,
calc(100% - 99.6px) 7.6px,
calc(100% - 7.2px) 102.1px,
calc(100% - 4.4px) 105.9px,
calc(100% - 2.3px) 110.1px,
calc(100% - 0.8px) 114.5px,
100% 121.5px,
100% 100%,
0% 100%
);
}
</style>
================================================
FILE: src/components/InfographicHeader.astro
================================================
---
const { badgeText, title, subtitle } = Astro.props
---
<div class="flex flex-col gap-[10px] md:gap-[35px]">
<span
class="uppercase bg-[#499AC3] py-2 px-4 w-full md:w-fit rounded-lg text-white text-xl leading-5 text-center tracking-[2%]"
>
{badgeText}
</span>
<div class="py-10 flex flex-col gap-6">
<h2
class="text-[32px] leading-[35.2px] md:text-[64px] md:leading-[70px] text-[#0073AA] max-w-5xl"
>
{title}
</h2>
<h4 class="text-3xl leading-[30px] text-[#499AC3]">{subtitle}</h4>
</div>
</div>
================================================
FILE: src/components/InfographicModal.astro
================================================
---
import CloseIcon from "@icons/CloseIcon.astro";
---
<section
id="infographic-modal"
class="transition-all fixed inset-0 bg-white py-2 z-[999] opacity-0 invisible"
>
<header class="w-full h-[40px] flex items-center justify-end px-4 lg:px-8">
<button
id="btn-modal"
class="p-2 rounded-full transition-colors md:hover:bg-gray-200"
>
<CloseIcon />
</button>
</header>
<div
id="infographic-modal-body"
class="h-full w-full rounded-lg overflow-y-auto"
>
</div>
</section>
<style>
.modal-active {
opacity: 1;
visibility: visible;
}
</style>
================================================
FILE: src/components/InfographicTips.astro
================================================
---
const TIPS = [
{
title: "Favoritos",
description: "Marca como favoritas las empresas que te molen",
image: "/img/bento-info-modal-cards/heart-on-fire.webp",
firstBgColor: "#FFB3B6",
secondBgColor: "#FCD3D3",
titleColor: "#EA2E39",
descriptionColor: "#F1696E",
},
{
title: "Notificaciones",
description: "¡Recibirás un aviso cuando publiquen una oferta!",
image: "/img/bento-info-modal-cards/bell.webp",
firstBgColor: "#FFB3B6",
secondBgColor: "#FCD3D3",
titleColor: "#EA2E39",
descriptionColor: "#F1696E",
},
{
title: "Revisa tu CV",
description:
"Y tómate tu tiempo para contestar bien a las preguntas que hacen ",
image: "/img/bento-info-modal-cards/eyes.webp",
firstBgColor: "#FFB3B6",
secondBgColor: "#FCD3D3",
titleColor: "#EA2E39",
descriptionColor: "#F1696E",
},
] as const;
---
<section class="relative mt-8 md:mt-0">
<img
class="absolute top-0 left-1/2 -translate-x-1/2 h-auto w-8 xs:w-10 sm:w-14 md:w-16 lg:w-20 xl:w-24 z-10"
src="/img/bento-info-modal-cards/fire.webp"
alt="Icono de fuego"
/>
<div class="folder-shape rounded-lg md:rounded-2xl lg:rounded-3xl w-full h-full bg-[#FDE8E8]">
<h3
class="pt-8 xs:pt-10 sm:pt-10 md:pt-14 lg:pt-20 xl:pt-24 text-center text-[#EA2E39] text-xl xs:text-2xl sm:text-3xl md:text-4xl lg:text-5xl xl:text-6xl"
>
Trucos para petarlo
</h3>
<div
class="grid grid-cols-3 gap-2 xs:gap-4 md:gap-6 xl:gap-8 p-2 xs:p-4 md:p-6 xl:p-8"
>
{
TIPS.map(
({
image,
title,
description,
firstBgColor,
secondBgColor,
titleColor,
descriptionColor,
}) => (
<article
class="file-card rounded-lg md:rounded-2xl w-full h-full p-2 md:p-4 lg:p-8 bg-[#FFB3B6] flex flex-col items-center justify-center"
style={{
background: `linear-gradient(to bottom right, ${firstBgColor} 0%, ${secondBgColor} 100%)`,
}}
>
<img
src={image}
alt={`${title} Card`}
class="pb-1 md:pb-4 h-auto w-8 xs:w-10 sm:w-14 md:w-16 lg:w-20 xl:w-24"
/>
<h4
class="text-xs xs:text-base sm:text-lg md:text-xl lg:text-2xl xl:text-3xl"
style={{ color: titleColor }}
>
{title}
</h4>
<p
class="text-xs xs:text-sm sm:text-base md:text-lg lg:text-xl xl:text-2xl text-center py-2"
style={{ color: descriptionColor }}
>
{description}
</p>
</article>
),
)
}
</div>
</div>
</section>
<style>
/* prettier-ignore */
.folder-shape {
clip-path: polygon(0% 0%,26.37% 0%,26.55% 0.01%,27.07% 0.22%,27.55% 0.65%,27.98% 1.3%,30.54% 6.21%,30.96% 6.86%,31.28% 7.17%,31.79% 7.46%,32.14% 7.51%,97.64% 7.51%,98.38% 7.75%,98.72% 8.04%,99.03% 8.43%,99.53% 9.47%,99.87% 10.78%,99.99% 12.29%,100% 100%,0% 100%);
}
/* prettier-ignore */
.file-card {
clip-path: polygon(0% 0%,81.66% 0.03%,83.02% 0.19%,84.29% 0.66%,85.76% 1.72%,97.84% 13.61%,98.71% 14.70%,99.57% 17.31%,100% 100%,0% 100%);
}
</style>
================================================
FILE: src/components/KingsLeagueInfo.astro
================================================
<div
class="w-full aspect-[9/16] sm:aspect-[16/9] sm:col-span-2 rounded-3xl overflow-hidden"
>
<div class="w-full h-full relative">
<a
href="https://kingsleague.ofertas-trabajo.infojobs.net/"
target="_blank"
class="absolute inset-0 group"
data-track="kings_league"
>
<img
src="/img/kings-league/background.webp"
alt="Kings League InfoJobs Background"
class="w-full h-full object-cover object-[center_top]"
/>
<img
src="/img/kings-league/players.webp"
alt="Kings League InfoJobs Players"
class="absolute bottom-0 h-full w-full md:w-fit md:h-fit object-cover object-[center_top] transition-transform duration-700 ease-in-out group-hover:scale-110 md:scale-100"
/>
<div
class="absolute inset-0 p-4 sm:p-6 flex flex-col justify-start items-start z-10"
>
<h3 class="text-2xl sm:text-2xl text-white/80">Patrocinador oficial</h3>
<h2 class="text-5xl sm:text-5xl font-bold text-white">
Kings League InfoJobs
</h2>
</div>
</a>
</div>
</div>
================================================
FILE: src/components/LinksColumns.astro
================================================
---
const linksInfo = [
{
title: "Nosotros",
links: [
{
label: "Ayuda",
href: "https://ayuda.infojobs.net/hc/es",
showOnMobile: true,
},
{
label: "Seguridad",
href: "https://nosotros.infojobs.net/infojobs-seguridad",
},
{
label: "Condiciones legales",
href: "https://www.infojobs.net/lex.xhtml",
showOnMobile: true,
},
{
label: "Política de privacidad",
href: "https://www.infojobs.net/privacy-policy/extended.xhtml",
},
{
label: "Uso del servicio",
href: "https://www.infojobs.net/rules-and-services.xhtml",
},
{
label: "Política de cookies",
href: "https://www.infojobs.net/lex.xhtml#cookies-policy",
},
{
label: "Gestión de cookies",
href: "https://www.figma.com/exit?url=javascript%3Avoid(null)%3B",
},
],
},
{
title: "Sobre InfoJobs",
links: [
{
label: "InfoJobs hoy",
href: "https://nosotros.infojobs.net/",
},
{
label: "Trabaja con nosotros",
href: "https://www.infojobs.net/candidate/candidate-login/candidate-login.xhtml?error=caducado&dgv=8615867714346349222",
},
{
label: "Ofertas de empleo",
href: "https://www.infojobs.net/ofertas-empleo",
},
],
},
{
title: "+ InfoJobs",
links: [
{
label: "InfoJobs Awards",
href: "https://awards.infojobs.net/",
},
{
label: "InfoJobs Academy",
href: "https://infojobsacademy.com/",
},
{
label: "Orientación laboral",
href: "https://orientacion-laboral.infojobs.net/",
},
{
label: "InfoJobs Formación",
href: "https://formacion.infojobs.net/",
},
{
label: "Blog empresas",
href: "https://recursos-humanos.infojobs.net/",
},
{
label: "Offerte di lavoro in Italia",
href: "https://www.infojobs.it/",
},
],
},
{
title: "Prensa",
links: [
{
label: "Indicadores de InfoJobs",
href: "https://nosotros.infojobs.net/indicadores-infojobs",
},
{
label: "Notas de prensa",
href: "https://nosotros.infojobs.net/prensa/notas-prensa",
},
{
label: "Contacto de prensa",
href: "https://nosotros.infojobs.net/contacto-prensa/contacto-de-prensa",
},
],
},
]
---
<div
class="px-[40px] items-center md:justify-between md:items-start flex flex-col md:flex-row"
>
{
linksInfo.map(({ title, links }, index) => {
return (
<div class={`${index === 0 ? "" : "hidden sm:block"}`}>
<h2 class="text-black font-semibold text-[18px] hidden md:block mb-2">
{title}
</h2>
<ul class="flex flex-row md:flex-col underline md:no-underline gap-4 md:gap-0">
{links.map(({ label, href, showOnMobile }) => {
return (
<li
class:list={[
`mb-2 hover:underline hover:underline-offset-[3px]`,
showOnMobile ? "" : "hidden md:block",
]}
>
<a href={href} class="text-[14px] leading-6" target="_blank" rel="noopener noreferrer" aria-label={label}>
{label}
</a>
</li>
)
})}
</ul>
</div>
)
})
}
</div>
================================================
FILE: src/components/LiteYoutube.astro
================================================
---
import Icon from "./ui/Icon.astro"
interface Props {
videoId: string
title?: string
}
const { videoId, title } = Astro.props
---
<lite-youtube
class="rounded-2xl bg-cover h-[570px] w-80 snap-center"
videoid={videoId}
tabindex="0"
aria-label="button"
>
<a
href={`https://www.youtube.com/shorts/${videoId}`}
class="lty-playbtn p-4 pl-[18px] bg-white rounded-full absolute bottom-6 right-6 hover:scale-110 transition-transform"
title={title}
role="button"
tabindex="0"
>
<span class="lyt-visually-hidden">{title}</span>
<Icon name="play" class="text-primary" />
</a>
</lite-youtube>
<script>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
/*
Component extracted from: https://github.com/paulirish/lite-youtube-embed/tree/master
*/
/**
* A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint.
*
* Thx to these as the inspiration
* https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
* https://autoplay-youtube-player.glitch.me/
*
* Once built it, I also found these:
* https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍)
* https://github.com/Daugilas/lazyYT
* https://github.com/vb/lazyframe
*/
class LiteYTEmbed extends HTMLElement {
connectedCallback() {
const video = this.getAttribute("videoid").split("?")
this.videoId = video[0]
this.short = video[1]
let playBtnEl = this.querySelector(".lty-playbtn")
// A label for the button takes priority over a [playlabel] attribute on the custom-element
this.playLabel =
(playBtnEl && playBtnEl.textContent.trim()) ||
this.getAttribute("playlabel") ||
"Reproducir presentación de La Velada del Año"
this.dataset.title = this.getAttribute("title") || ""
/**
* Lo, the youtube poster image! (aka the thumbnail, image placeholder, etc)
*
* See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md
*/
if (!this.style.backgroundImage) {
this.style.backgroundImage = `url("https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg")`
this.upgradePosterImage()
}
// Set up play button, and its visually hidden label
if (!playBtnEl) {
playBtnEl = document.createElement("button")
playBtnEl.type = "button"
playBtnEl.classList.add("lty-playbtn")
this.append(playBtnEl)
}
if (!playBtnEl.textContent) {
const playBtnLabelEl = document.createElement("span")
playBtnLabelEl.className = "lyt-visually-hidden"
playBtnLabelEl.textContent = this.playLabel
playBtnEl.append(playBtnLabelEl)
}
this.addNoscriptIframe()
// On hover (or tap), warm up the TCP connections we're (likely) about to use.
this.addEventListener("pointerover", LiteYTEmbed.warmConnections, { once: true })
// Once the user clicks, add the real iframe and drop our play button
// TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time
// We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003
this.addEventListener("click", this.activate)
// For accessibility, handle keypresses as well
this.addEventListener("keydown", this.handleKeyPress)
// Chrome & Edge desktop have no problem with the basic YouTube Embed with ?autoplay=1
// However Safari desktop and most/all mobile browsers do not successfully track the user gesture of clicking through the creation/loading of the iframe,
// so they don't autoplay automatically. Instead we must load an additional 2 sequential JS files (1KB + 165KB) (un-br) for the YT Player API
// TODO: Try loading the the YT API in parallel with our iframe and then attaching/playing it. #82
this.needsYTApi =
this.hasAttribute("js-api") ||
navigator.vendor.includes("Apple") ||
navigator.userAgent.includes("Mobi")
}
/**
* Add a <link rel={preload | preconnect} ...> to the head
*/
static addPrefetch(kind, url, as) {
const linkEl = document.createElement("link")
linkEl.rel = kind
linkEl.href = url
if (as) {
linkEl.as = as
}
document.head.append(linkEl)
}
/**
* Begin pre-connecting to warm up the iframe load
* Since the embed's network requests load within its iframe,
* preload/prefetch'ing them outside the iframe will only cause double-downloads.
* So, the best we can do is warm up a few connections to origins that are in the critical path.
*
* Maybe `<link rel=preload as=document>` would work, but it's unsupported: http://crbug.com/593267
* But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity.
*/
static warmConnections() {
if (LiteYTEmbed.preconnected) return
// The iframe document and most of its subresources come right off youtube.com
LiteYTEmbed.addPrefetch("preconnect", "https://www.youtube-nocookie.com")
// The botguard script is fetched off from google.com
LiteYTEmbed.addPrefetch("preconnect", "https://www.google.com")
// Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling.
LiteYTEmbed.addPrefetch("preconnect", "https://googleads.g.doubleclick.net")
LiteYTEmbed.addPrefetch("preconnect", "https://static.doubleclick.net")
LiteYTEmbed.preconnected = true
}
fetchYTPlayerApi() {
if (window.YT || (window.YT && window.YT.Player)) return
/* global YT */
this.ytApiPromise = new Promise((resolve, reject) => {
const el = document.createElement("script")
el.src = "https://www.youtube.com/iframe_api"
el.async = true
el.onload = (_) => {
YT.ready(resolve)
}
el.onerror = reject
this.append(el)
})
}
/** Return the YT Player API instance. (Public L-YT-E API) */
async getYTPlayer() {
if (!this.playerPromise) {
await this.activate()
}
return this.playerPromise
}
async addYTPlayerIframe() {
this.fetchYTPlayerApi()
await this.ytApiPromise
const videoPlaceholderEl = document.createElement("div")
this.append(videoPlaceholderEl)
const paramsObj = Object.fromEntries(this.getParams().entries())
this.playerPromise = new Promise((resolve) => {
const player = new YT.Player(videoPlaceholderEl, {
width: "100%",
videoId: this.videoId,
playerVars: paramsObj,
events: {
onReady: (event) => {
event.target.playVideo()
resolve(player)
},
},
})
})
}
// Add the iframe within <noscript> for indexability discoverability. See https://github.com/paulirish/lite-youtube-embed/issues/105
addNoscriptIframe() {
const iframeEl = this.createBasicIframe()
const noscriptEl = document.createElement("noscript")
// Appending into noscript isn't equivalant for mysterious reasons: https://html.spec.whatwg.org/multipage/scripting.html#the-noscript-element
noscriptEl.innerHTML = iframeEl.outerHTML
this.append(noscriptEl)
}
getParams() {
const params = new URLSearchParams(this.getAttribute("params") || [])
params.append("autoplay", "1")
params.append("playsinline", "1")
params.append("color", "white")
return params
}
async activate() {
if (this.classList.contains("lyt-activated")) return
const playBtnEl = this.querySelector(".lty-playbtn")
if (playBtnEl && playBtnEl.hasAttribute("href")) {
playBtnEl.removeAttribute("href")
}
this.classList.add("lyt-activated")
if (this.style.backgroundImage !== "unset") {
this.style.backgroundImage = "unset"
}
if (this.needsYTApi) {
return this.addYTPlayerIframe(this.getParams())
}
const iframeEl = this.createBasicIframe()
this.append(iframeEl)
// Set focus for a11y
iframeEl.focus()
}
handleKeyPress(event) {
if (event.code === "Enter" || event.code === "Space") {
this.activate()
}
}
createBasicIframe() {
const iframeEl = document.createElement("iframe")
iframeEl.width = 560
iframeEl.height = 315
// No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include
iframeEl.title = this.playLabel
iframeEl.allow = "accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
iframeEl.allowFullscreen = true
// AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL
// https://stackoverflow.com/q/64959723/89484
iframeEl.src = !this.short
? `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${this.getParams().toString()}`
: `https://www.youtube.com/embed/${encodeURIComponent(this.videoId)}?${this.getParams().toString()}&${this.short}`
return iframeEl
}
/**
* In the spirit of the `lowsrc` attribute and progressive JPEGs, we'll upgrade the reliable
* poster image to a higher resolution one, if it's available.
* Interestingly this sddefault webp is often smaller in filesize, but we will still attempt it second
* because getting _an_ image in front of the user if our first priority.
*
* See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md for more details
*/
upgradePosterImage() {
// Defer to reduce network contention.
setTimeout(() => {
const webpUrl = `https://i.ytimg.com/vi_webp/${this.videoId}/sddefault.webp`
const img = new Image()
img.fetchPriority = "low" // low priority to reduce network contention
img.referrerpolicy = "origin" // Not 100% sure it's needed, but https://github.com/ampproject/amphtml/pull/3940
img.src = webpUrl
img.onload = (e) => {
// A pretty ugly hack since onerror won't fire on YouTube image 404. This is (probably) due to
// Youtube's style of returning data even with a 404 status. That data is a 120x90 placeholder image.
// … per "annoying yt 404 behavior" in the .md
const noAvailablePoster = e.target.naturalHeight === 90 && e.target.naturalWidth === 120
if (noAvailablePoster) return
this.style.backgroundImage = `url("${webpUrl}")`
}
}, 100)
}
}
// Register custom element
customElements.define("lite-youtube", LiteYTEmbed)
</script>
<style is:global>
lite-youtube {
background-color: #000;
position: relative;
display: block;
contain: content;
background-position: center center;
background-size: cover;
cursor: pointer;
aspect-ratio: 16/9;
width: 100%;
height: auto;
border: 2px solid var(--color-accent);
transition: all 0.3s ease;
box-shadow: 0px 0px 15px rgb(212, 255, 0, 0.1);
}
/* gradient */
lite-youtube::before {
content: attr(data-title);
display: block;
position: absolute;
top: 0;
/* Pixel-perfect port of YT's gradient PNG, using https://github.com/bluesmoon/pngtocss plus optimizations */
background-image: linear-gradient(
180deg,
rgb(0 0 0 / 67%) 0%,
rgb(0 0 0 / 54%) 14%,
rgb(0 0 0 / 15%) 54%,
rgb(0 0 0 / 5%) 72%,
rgb(0 0 0 / 0%) 94%
);
height: 99px;
width: 100%;
font-family: "YouTube Noto", Roboto, Arial, Helvetica, sans-serif;
color: hsl(0deg 0% 93.33%);
text-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
font-size: 18px;
padding: 25px 20px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
box-sizing: border-box;
}
lite-youtube:hover::before {
color: white;
}
/* responsive iframe with a 16:9 aspect ratio
thanks https://css-tricks.com/responsive-iframes/
*/
lite-youtube::after {
content: "";
display: block;
padding-bottom: calc(100% / (16 / 9));
}
lite-youtube > iframe {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border: 0;
}
/* Post-click styles */
lite-youtube.lyt-activated {
cursor: unset;
}
lite-youtube.lyt-activated::before,
lite-youtube.lyt-activated > .lty-playbtn {
opacity: 0;
pointer-events: none;
}
.lyt-visually-hidden {
clip: rect(0 0 0 0);
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
</style>
================================================
FILE: src/components/OportunidadesTikTok.astro
================================================
---
const cardsOportunidades = [
{
title1: "Oferta de empleo",
title2: "Miles de oportunidades",
img: "./img/pre-footer/oferta-empleo.webp",
gradient: "bg-gradient-to-t from-[#8BCFAD] to-[#E6F6EE]",
colorTitle: "text-[#0E8247]",
imageClass: "p-4 sm:p-8 md:p-4 lg:p-8 !pb-0",
href: "https://www.infojobs.net/",
track: "job_offers",
},
{
title1: "InfoJobs en TikTok",
title2: "Consejos para encontrar trabajo",
img: "./img/pre-footer/info-tiktok.webp",
gradient: "bg-gradient-to-t from-[#FFE4A2] to-[#FEF6DB]",
colorTitle: "text-[#EFA500]",
imageClass: "",
href: "https://www.tiktok.com/@infojobs/playlist/Encontrar%20trabajo-7401023016819378977 ",
track: "tik_tok",
},
] as const
---
<div class="grid grid-cols-1 sm:grid-cols-2 w-auto h-auto gap-6 sm:gap-[40px]">
{
cardsOportunidades.map((card) => (
<a
class={`group flex flex-col rounded-[16px] pt-4 px-4 md:px-12 lg:px-15 lg:pt-6 xl:pt-10 aspect-[328/436] md:aspect-square w-full overflow-hidden ${card.gradient}`}
href={card.href}
target="_blank"
data-track={card.track}
>
<p class="mt-4 text-2xl font-semibold text-gray-900">{card.title1}</p>
<p class={`mt-2 text-3xl lg:text-5xl font-semibold ${card.colorTitle}`}>
{card.title2}
</p>
<div class="flex justify-center mt-auto w-full h-auto md:mb-0">
<img
class="w-full h-full scale-100 transition group-hover:scale-105 group-hover:z-10 object-contain object-bottom"
class:list={card.imageClass}
src={card.img}
alt="InfoJobs"
/>
</div>
</a>
))
}
</div>
================================================
FILE: src/components/PreFooter.astro
================================================
---
import KingsLeagueInfo from "@components/KingsLeagueInfo.astro"
import SectionContainer from "@components/SectionContainer.astro"
import OportunidadesTikTok from "@components/OportunidadesTikTok.astro"
import Subtitle from "@components/Subtitle.astro"
---
<SectionContainer>
<Subtitle>InfoJobs, ¿la de trabajar te la sabes?</Subtitle>
<div class="flex flex-col gap-10">
<KingsLeagueInfo />
<OportunidadesTikTok />
</div>
</SectionContainer>
================================================
FILE: src/components/SectionContainer.astro
================================================
<section class="mx-auto max-w-7xl w-full">
<div class:list={["mx-auto w-full px-4", Astro.props.class]}>
<slot />
</div>
</section>
================================================
FILE: src/components/SocialBest.astro
================================================
---
import SectionContainer from "./SectionContainer.astro"
import TiktokVideo from "./TiktokVideo.astro"
import Icon from "./ui/Icon.astro"
import Subtitle from "./Subtitle.astro"
const VIDEOS = [
{
videoId: "7222709244125007109",
thumbnailUrl: "/img/thumbnails/ibai.webp",
title: "La relación de Ibai con Marcos",
},
{
videoId: "7258016065563807003",
thumbnailUrl: "/img/thumbnails/jijantes.webp",
title: "Fichaje del nuevo reportero de Jijantes",
},
{
videoId: "7343332399662533920",
thumbnailUrl: "/img/thumbnails/midudev.webp",
title: "Fichajes de la web de La Velada junto a Midudev",
},
{
videoId: "7265231719068880160",
thumbnailUrl: "/img/thumbnails/cristinini.webp",
title: "Ganadora de la Beca Infojobs Cristinini",
},
{
videoId: "7285448407756393761",
thumbnailUrl: "/img/thumbnails/marcos.webp",
title: "Entrevista a Marcos de Olañeta",
},
] as const
---
<SectionContainer class="px-0">
<div class="flex items-center justify-between pb-6 md:pb-12">
<Subtitle class="!pb-0">Trabaja con los mejores</Subtitle>
<div class="hidden md:flex items-center gap-x-4">
<button
class="bg-[#E2E2E5] text-[#636365] opacity-45 p-1 rounded-full enabled:hover:scale-110 transition-all enabled:active:scale-100"
id="left-button"
disabled="true"
aria-label="Ir al elemento anterior"
>
<Icon name="leftarrow" />
</button>
<button
class="bg-[#E2E2E5] text-[#636365] p-1 rounded-full enabled:hover:scale-110 transition-all enabled:active:scale-100"
id="right-button"
aria-label="Ir al siguiente elemento"
>
<Icon name="rightarrow" />
</button>
</div>
</div>
<div
class="relative carousel-container -mr-4 pr-4 md:mr-0 md:pr-0"
id="carousel-container"
style="--left-opacity: 0; --right-opacity: 1;"
>
<div
class="flex overflow-scroll custom-scrollbar gap-x-4 md:gap-x-8 snap-mandatory snap-x -mr-4 pr-4 md:mr-0 md:pr-0"
id="carousel"
>
{
VIDEOS.map(({ videoId, thumbnailUrl, title }) => (
<TiktokVideo
videoId={videoId}
thumbnailUrl={thumbnailUrl}
title={title}
/>
))
}
</div>
</div>
</SectionContainer>
<style>
.custom-scrollbar::-webkit-scrollbar {
display: none;
}
.custom-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.carousel-container:before {
background-image: linear-gradient(to left, transparent, #fff);
content: "";
height: 570px;
left: 0;
opacity: var(--left-opacity);
pointer-events: none;
position: absolute;
top: 0;
transition: opacity cubic-bezier(0.4, 0, 0.2, 1) 150ms;
width: 64px;
z-index: 10;
}
.carousel-container:after {
background-image: linear-gradient(to right, transparent, #fff);
content: "";
height: 570px;
opacity: var(--right-opacity);
pointer-events: none;
position: absolute;
right: 0;
top: 0;
transition: opacity cubic-bezier(0.4, 0, 0.2, 1) 150ms;
width: 64px;
z-index: 10;
}
@media (max-width: 768px) {
.carousel-container:before {
opacity: 0 !important;
}
.carousel-container:after {
opacity: 0 !important;
}
}
</style>
<script>
const rightButton = document.querySelector(
"#right-button"
) as HTMLButtonElement
const leftButton = document.querySelector("#left-button") as HTMLButtonElement
const carousel = document.querySelector("#carousel") as HTMLDivElement
const carouselContainer = document.querySelector(
"#carousel-container"
) as HTMLDivElement
leftButton.addEventListener("click", () => {
carousel.scrollTo({
left: carousel.scrollLeft - 320,
behavior: "smooth",
})
})
rightButton.addEventListener("click", () => {
carousel.scrollTo({
left: carousel.scrollLeft + 320,
behavior: "smooth",
})
})
carousel.addEventListener("scroll", () => {
const tolerance = 2 // 0 + 2px to account for floating point errors
const updateButtonState = (button: HTMLButtonElement, disable: boolean) => {
button.classList.toggle("opacity-45", disable)
button.disabled = disable
}
const updateOpacity = (property: string, value: string) => {
carouselContainer.style.setProperty(property, value)
}
if (carousel.scrollLeft <= tolerance) {
updateButtonState(leftButton, true)
} else {
updateButtonState(leftButton, false)
}
if (
carousel.scrollLeft + carousel.clientWidth >=
carousel.scrollWidth - tolerance
) {
updateButtonState(rightButton, true)
updateOpacity("--right-opacity", "0")
} else {
updateButtonState(rightButton, false)
updateOpacity("--right-opacity", "1")
}
if (carousel.scrollLeft > tolerance) {
updateOpacity("--left-opacity", "1")
} else {
updateOpacity("--left-opacity", "0")
}
})
</script>
================================================
FILE: src/components/StandsInterviews.astro
================================================
---
import InfographicBody from "./InfographicBody.astro"
import InfographicHeader from "./InfographicHeader.astro"
const standsInterviews = [
{
image: "/img/bento-info-modal-cards/sincerity-and-trust.webp",
title: "Sinceridad y confianza",
description:
"¡Si te han llamado será por algo! Demuestra que lo que te falta de experiencia, te sobra de aptitud.",
firstBgColor: "#B4E5CD",
secondBgColor: "#E1F4EB",
textColor: "#008949",
},
{
image: "/img/bento-info-modal-cards/show-enthusiasm.webp",
title: "Demuestra entusiasmo",
description:
"Infórmate sobre la empresa y el puesto para explicar al entrevistador/a tu interés.",
firstBgColor: "#FDECBE",
secondBgColor: "#FFF7E4",
textColor: "#B08B2B",
},
{
image: "/img/bento-info-modal-cards/focus-on-your-skills.webp",
title: "Céntrate en tus habilidades",
description:
"Muestra iniciativa y remarca tus conocimientos: tecnologías, herramientas digitales, idiomas o deportes",
firstBgColor: "#FFBFB5",
secondBgColor: "#FBE9E7",
textColor: "#FF523C",
},
{
image: "/img/bento-info-modal-cards/learning-ability.webp",
title: "Capacidad de aprendizaje",
description:
"Si no tienes experiencia laboral, muestra tus ganas por seguir formándote, dentro y fuera de la compañía.",
firstBgColor: "#64C6F6",
secondBgColor: "#D2EEFA",
textColor: "#085C81",
},
{
image: "/img/bento-info-modal-cards/questions-and-answers.webp",
title: "Practica preguntas y respuestas",
description:
"Prepárate posibles respuestas y preguntas sobre la compañía y tu rol, ¡así verán que vas a por todas!",
firstBgColor: "#88DBFF",
secondBgColor: "#E1F8FF",
textColor: "#0073AA",
},
{
image: "/img/bento-info-modal-cards/double-authenticity.webp",
title: "La autenticidad puntúa doble",
description:
"Mira a los ojos, exprésate con las manos, tú tienes el control de la situación. ¡Y sonríe de forma natural!",
firstBgColor: "#FFBEBF",
secondBgColor: "#FBE9E7",
textColor: "#DF4D3A",
},
] as const
const { infographicId } = Astro.props
---
<section
id={`infographic-${infographicId}`}
class="mx-auto px-4 max-w-7xl w-full flex-col md:gap-[50px] py-[72px] hidden"
>
<InfographicHeader
badgeText="Destaca en las entrevistas"
title="¿Quieres triunfar en una entrevista aunque no tengas experiencia para ese trabajo?"
subtitle="¡Sin problema!"
/>
<InfographicBody bentoInfoModalCards={standsInterviews} />
</section>
================================================
FILE: src/components/Subtitle.astro
================================================
<h2 class:list={["font-semibold text-xl pb-4 sm:pb-6 sm:text-2xl md:text-4xl md:pb-12", Astro.props.class]}>
<slot />
</h2>
================================================
FILE: src/components/TiktokVideo.astro
================================================
---
import Icon from "./ui/Icon.astro"
interface Props {
videoId: string
thumbnailUrl: string
title: string
}
const { videoId, thumbnailUrl, title } = Astro.props
---
<tiktok-video
class="rounded-[16px] h-[570px] w-80 snap-center bg-black relative shrink-0 cursor-pointer bg-center bg-[length:100%] hover:bg-[length:105%] transition-[background-size]"
videoid={videoId}
thumbnailurl={thumbnailUrl}
data-title={title}
data-track="play_video"
aria-label="Reproducir video"
role="button"
tabindex="0"
>
<div
class="p-4 pl-[18px] bg-white rounded-full absolute bottom-6 right-6 hover:scale-110 transition-transform duration-300"
id=`Id${videoId}`
title={title}
>
<Icon name="play" class="text-primary" />
</div>
</tiktok-video>
<script>
class TiktokVideo extends HTMLElement {
videoId?: string
connectedCallback() {
const thumbnailUrl = this.getAttribute("thumbnailurl")
this.videoId = this.getAttribute("videoid")!
this.style.backgroundImage = `url(${thumbnailUrl})`
this.addEventListener("click", this.activateVideo)
this.addEventListener("keydown", this.handleKeyPress)
}
activateVideo() {
this.style.backgroundImage = "unset"
this.querySelector(`#Id${this.videoId}`)?.remove()
const iframeEl = this.createIframe()
this.append(iframeEl)
iframeEl.focus()
this.unMutePlayerByDefault()
}
handleKeyPress(event: KeyboardEvent) {
if (event.code === "Enter" || event.code === "Space") {
this.activateVideo()
}
}
createIframe() {
const iframeEl = document.createElement("iframe")
iframeEl.width = "320"
iframeEl.height = "570"
iframeEl.classList.add("rounded-2xl", "w-full", "h-full", "snap-center")
iframeEl.title = this.getAttribute("data-title")!
iframeEl.allow =
"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; transparency"
iframeEl.allowFullscreen = true
iframeEl.src = `https://www.tiktok.com/player/v1/${this.videoId}?autoplay=1`
return iframeEl
}
unMutePlayerByDefault() {
const messageHandler = (event: MessageEvent) => {
if (
event.origin === "https://www.tiktok.com" &&
event.data.type === "onPlayerReady"
) {
this.querySelector("iframe")!.contentWindow!.postMessage(
{ type: "unMute", "x-tiktok-player": true },
"*"
)
window.removeEventListener("message", messageHandler)
}
}
window.addEventListener("message", messageHandler)
}
}
customElements.define("tiktok-video", TiktokVideo)
</script>
================================================
FILE: src/components/YourNextJob.astro
================================================
---
import InfographicBody from "./InfographicBody.astro"
import InfographicHeader from "./InfographicHeader.astro"
import InfographicTips from "./InfographicTips.astro"
const yourNextJobContent = [
{
image: "/img/bento-info-modal-cards/style-your-cv.webp",
title: "Pon guapo tu CV",
description: "Actualízalo, sube una foto profesional ¡y rellena tus habilidades/skills! (sobre todo si te falta experiencia).",
firstBgColor: "#FDECBE",
secondBgColor: "#FFF7E4",
textColor: "#B08B2B",
},
{
image: "/img/bento-info-modal-cards/search-ideal-offer.webp",
title: "Busca tu oferta ideal",
description: "Utiliza las recomendaciones que te ofrece InfoJobs o busca a tu medida según el sector que más te interesa.",
firstBgColor: "#B4E5CD",
secondBgColor: "#E1F4EB",
textColor: "#008949",
},
{
image: "/img/bento-info-modal-cards/register.webp",
title: "¡Inscríbete!",
description: "Lee bien la oferta, ajusta tu CV y adjúntalo. Y no olvides añadir una carta de presentación personalizada a la oferta. ¡Todo suma!",
firstBgColor: "#BADAE8",
secondBgColor: "#E3EFF5",
textColor: "#006895",
},
{
image: "/img/bento-info-modal-cards/you-are-their-candidate.webp",
title: "Ya eres su candidato/a",
description: "Toca esperar y cruzar los dedos, pero valora inscribirte en otras ofertas que también te interesen",
firstBgColor: "#FFD1C8",
secondBgColor: "#FBE9E7",
textColor: "#FF523C",
}
] as const
const { infographicId } = Astro.props
---
<section
id={`infographic-${infographicId}`}
class="mx-auto px-4 max-w-7xl w-full flex-col md:gap-[50px] py-[72px] hidden"
>
<InfographicHeader
badgeText="Sácale partido a InfoJobs"
title="¿Quieres conseguir tu próximo empleo con InfoJobs?"
subtitle="¡Vamos a ello!"
/>
<InfographicBody bentoInfoModalCards={yourNextJobContent} />
<InfographicTips />
</section>
================================================
FILE: src/components/ui/Button.astro
================================================
---
import Icon from "@ui/Icon.astro"
interface ButtonProps {
variant?: "solid" | "bordered" | "light" | "ghost" | "flat"
color?: "default" | "primary" | "secondary" | "success" | "warning" | "danger"
size?: "default" | "sm" | "lg" | "full" | "icon"
radius?: "none" | "lg" | "xl" | "xxl" | "full" | string
class?: string
type?: "button" | "submit" | "reset"
href?: string
as?: "button" | "link"
role?: "button" | "link"
disabled?: boolean
icon?: string
iconPosition?: "left" | "right"
block?: boolean
iconOnly?: boolean
loading?: boolean
ariaLabel?: string
track?: string
disableSaturateHover?: boolean
}
// Estilos por variante
const solid = {
default: "bg-neutral-300 text-neutral-900",
primary: "bg-primary text-white",
secondary: "bg-accent text-white",
success: "bg-ij-green text-white",
warning: "bg-ij-yellow text-white",
danger: "bg-red-500 text-white",
foreground: "bg-black text-white",
}
const bordered = {
default: "border-2 border-neutral-300 text-neutral-900",
primary: "border-2 border-primary text-primary",
secondary: "border-2 border-gray-500 text-gray-500",
success: "border-2 border-ij-green text-ij-green",
warning: "border-2 border-ij-yellow text-yellow-500",
danger: "border-2 border-red-500 text-red-500",
foreground: "border-2 border-black text-black",
}
const light = {
default: "border-transparent text-black hover:bg-neutral-300",
primary: "border-transparent text-primary hover:bg-primary/20",
secondary: "border-transparent text-accent hover:bg-accent/20",
success: "border-transparent text-ij-green hover:bg-ij-green/20",
warning: "border-transparent text-ij-yellow hover:bg-ij-yellow/20",
danger: "border-transparent text-red-500 hover:bg-ij-red/20",
foreground: "border-transparent text-black hover:bg-black/20",
}
const ghost = {
default: "border-2 border-neutral-300 text-neutral-900 hover:bg-neutral-300",
primary:
"border-2 border-primary text-primary hover:bg-primary hover:text-white",
secondary: "border-2 border-gray-500 text-gray-500 hover:bg-accent",
success:
"border-2 border-ij-green text-ij-green hover:bg-ij-green hover:text-white",
warning:
"border-2 border-ij-yellow text-ij-yellow hover:bg-ij-yellow hover:text-white",
danger: "border-2 border-ij-red text-ij-red hover:bg-ij-red hover:text-white",
foreground:
"border-2 border-black text-black hover:bg-black hover:text-white",
}
const flat = {
default: "bg-neutral-300/40 text-neutral-900",
primary: "bg-primary/20 text-primary",
secondary: "bg-accent/20 text-accent",
success: "bg-ij-green/20 text-ij-green",
warning: "bg-ij-yellow/20 text-ij-yellow",
danger: "bg-ij-red/20 text-ij-red",
foreground: "bg-black/20 text-black",
}
const variants = {
solid,
bordered,
light,
ghost,
flat,
}
// Función para obtener clases por variante y color
function getVariantClasses(
variant: keyof typeof variants,
color: keyof typeof solid
) {
const variantGroup = variants[variant] || variants.solid // fallback a 'solid'
return variantGroup[color] ?? variantGroup.default
}
// Clases por tamaño
const sizeClasses = {
default: "px-4 py-2 text-lg gap-x-2 font-medium",
sm: "px-3 min-w-16 h-8 text-sm gap-x-2",
md: "px-4 min-w-20 h-10 text-base gap-x-2",
lg: "px-6 min-w-24 h-12 text-lg gap-3 font-medium",
full: "px-6 w-full py-2.5 text-lg gap-x-3 font-medium",
icon: "h-10 w-10",
}
// Clases por radio
const radiusClasses = {
none: "rounded-none",
lg: "rounded-xl",
xl: "rounded-2xl",
xxl: "rounded-4xl",
full: "rounded-full",
}
// Desestructuración de props
const {
variant = "solid",
color = "primary",
size = "default",
radius = "xl",
class: extraClasses = "",
type = "button",
href,
as = "button",
role = "button",
disabled = false,
icon,
iconPosition = "left",
iconOnly = false,
loading = false,
ariaLabel,
disableSaturateHover = false,
track,
} = Astro.props as ButtonProps
// Definición de clases
const customRadiusClass =
typeof radius === "string" && radius.startsWith("rounded-")
? radius
: radiusClasses[radius as keyof typeof radiusClasses] || ""
const variantClasses = getVariantClasses(variant, color)
const baseClasses = [
"z-0 group relative inline-flex items-center justify-center",
"box-border appearance-none select-none whitespace-nowrap min-w-max",
"subpixel-antialiased overflow-hidden tap-highlight-transparent",
"focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-double",
"focus-visible:outline-primary focus-visible:outline-offset-2",
"disabled:opacity-75 disabled:select-none disabled:cursor-not-allowed",
"disabled:active:scale-100",
"active:scale-[0.99]",
disableSaturateHover ? "" : "hover:saturate-150",
"transition-transform-colors-opacity motion-reduce:transition-none",
]
.filter(Boolean)
.join(" ")
const buttonClasses = [
baseClasses,
variantClasses,
sizeClasses[size],
customRadiusClass,
iconOnly ? "p-2" : "",
extraClasses,
loading ? "loading" : "",
]
.filter(Boolean)
.join(" ")
---
{
as === "button" ? (
<button
class={buttonClasses}
type={type}
disabled={disabled || loading}
role={role}
aria-label={ariaLabel || (iconOnly ? `${icon} button` : undefined)}
aria-busy={loading}
aria-hidden={iconOnly ? "true" : undefined}
data-track={track}
>
{loading ? (
<>
<Icon name="Spinner" class="animate-spin w-5 h-5" />
<span>Processing...</span>
</>
) : (
<>
{icon && iconPosition === "left" && <Icon name={icon} />}
<slot />
{icon && iconPosition === "right" && <Icon name={icon} />}
</>
)}
</button>
) : as === "link" && href ? (
<a
class={buttonClasses}
href={href}
role={role || "link"}
aria-label={ariaLabel || (iconOnly ? `${icon} button` : undefined)}
aria-hidden={iconOnly ? "true" : undefined}
data-track={track}
>
{icon && iconPosition === "left" && <Icon name={icon} />}
<slot />
{icon && iconPosition === "right" && <Icon name={icon} />}
</a>
) : null
}
<style>
.tap-highlight-transparent {
-webkit-tap-highlight-color: transparent;
}
</style>
================================================
FILE: src/components/ui/Icon.astro
================================================
---
const icons = import.meta.glob("@icons/*.astro", { eager: true })
interface IconMap {
[key: string]: (_props: Record<string, any>) => any
}
const iconMap: IconMap = Object.fromEntries(
Object.entries(icons).map(([path, module]) => [
path.split("/").pop()?.replace(".astro", "").toLowerCase() || "",
(module as any).default,
])
)
const iconName = Astro.props.name?.toLowerCase()
const IconComponent = iconMap[iconName]
---
{
IconComponent ? (
<IconComponent {...Astro.props} class={Astro.props.class} />
) : (
<div class="text-sm">
Icon not found:{" "}
<span class="font-bold text-base">{Astro.props.name}</span>
</div>
)
}
================================================
FILE: src/const.ts
================================================
export const DICTIONARY_TYPE = {
AVAILABILITY: 'availability',
CANDIDATE_EXPERIENCE: 'candidate-experience',
CANDIDATE_SUBSEGMENT: 'candidate-subsegment',
CATEGORY: 'category',
CHANNEL: 'channel',
CITY: 'city',
CONTRACT_TYPE: 'contract-type',
COUNTRY: 'country',
DRIVER_LICENSE: 'driver-license',
EMPLOYMENT_STATUS: 'employment-status',
EMPLOYER_TYPE: 'employer-type',
EXPERIENCE_MIN: 'experience-min',
GENDER: 'gender',
GRADE: 'grade',
ID_TYPE: 'id-type',
INDUSTRY: 'industry',
LANGUAGE: 'language',
LAST_JOB_SEARCH: 'last-job-search',
LEGAL_FORM: 'legal-form',
MANAGER_PROFESSIONAL_LEVEL: 'manager-professional-level',
MANDATORY_STEPS: 'mandatory-steps',
OFFER_RESIDENCE: 'offer-residence',
OFFER_STATE: 'offer-state',
PERIODOS_INTERVALOS: 'periodos-intervalos',
PROFESSIONAL_LEVEL: 'professional-level',
PROVINCE: 'province',
READING_LEVEL: 'reading-level',
REGION: 'region',
REPORT_REASONS: 'report-reason',
REPORTING_TO: 'reporting-to',
SALARY_BENEFITS: 'salary-benefits',
SALARY_PERIOD: 'salary-period',
SALARY_RANGE: 'salary-range',
STUDY: 'study',
SKILL_LEVEL: 'skill-level',
SPEAKING_LEVEL: 'speaking-level',
STAFF: 'staff',
STUDY_DETAIL: 'study-detail',
SUBCATEGORY: 'subcategory',
TELEWORKING: 'teleworking',
TIMELINE_EVENT: 'timeline-event',
URL_TYPE: 'url-type',
WORK_PERMIT: 'work-permit',
WORKDAY: 'workday',
WRITING_LEVEL: 'writing-level'
} as const
================================================
FILE: src/env.d.ts
================================================
/// <reference path="../.astro/types.d.ts" />
================================================
FILE: src/icons/AppStore.astro
================================================
<img
src="/footer-app-store.webp"
alt="Logo App Store"
class="h-[50px] w-auto"
/>
================================================
FILE: src/icons/ChevronDown.astro
================================================
---
const { ...props } = Astro.props
---
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
class={'stroke-gray-600 transition-transform duration-300 ease-in-out'}
{...props}
>
<path d="m5.84 9.59l5.66 5.66l5.66-5.66l-.71-.7l-4.95 4.95l-4.95-4.95z" />
</svg>
================================================
FILE: src/icons/CloseIcon.astro
================================================
<svg width="14" height="14" fill="none"
><path
fill="#89898A"
fill-rule="evenodd"
d="m8.47 6.95 4.79-4.8A1 1 0 0 0 11.85.74l-4.8 4.79L2.26.74A1 1 0 0 0 .85 2.15l4.79 4.8-4.79 4.79a1 1 0 1 0 1.41 1.41l4.79-4.79 4.8 4.79a1 1 0 0 0 1.41-1.41L8.47 6.95Z"
clip-rule="evenodd"></path></svg
>
================================================
FILE: src/icons/Facebook.astro
================================================
<svg width="22" height="22" fill="none">
<path
fill="currentColor"
fill-rule="evenodd"
d="M11 .75C5.34.75.75 5.34.75 11S5.34 21.25 11 21.25 21.25 16.66 21.25 11C21.244 5.341 16.659.756 11 .75ZM19.75 11A8.75 8.75 0 1 1 11 2.25 8.76 8.76 0 0 1 19.75 11Zm-8.106 5.75v-5.474h1.77l.264-2.134h-2.034V7.78c0-.617.166-1.038 1.019-1.038h1.087V4.832a14.299 14.299 0 0 0-1.585-.083c-1.568 0-2.642.993-2.642 2.82v1.572H7.75v2.134h1.773v5.474h2.121Z"
clip-rule="evenodd"
>
</path>
</svg>
================================================
FILE: src/icons/GooglePlay.astro
================================================
<img
src="/footer-google-play.webp"
alt="Logo Google Play"
class="h-[50px] w-auto"
/>
================================================
FILE: src/icons/LeftArrow.astro
================================================
<svg width="32" height="32" fill="none" viewBox="0 0 32 32" {...Astro.props}>
<path
fill="currentColor"
fill-rule="evenodd"
d="M19.334 24.667a1.333 1.333 0 0 1-.947-.387l-5.44-5.44a4 4 0 0 1 0-5.653l5.454-5.467a1.333 1.333 0 0 1 1.88 1.88l-5.454 5.467a1.333 1.333 0 0 0 0 1.88l5.44 5.44a1.333 1.333 0 0 1-.947 2.28h.014Z"
clip-rule="evenodd"></path>
</svg>
================================================
FILE: src/icons/Play.astro
================================================
<svg width="16" height="18" viewBox="0 0 16 18" fill="none" {...Astro.props}>
<path
d="M15 7.26795C16.3333 8.03775 16.3333 9.96225 15 10.7321L3 17.6603C1.66667 18.4301 2.92002e-06 17.4678 2.98732e-06 15.9282L3.593e-06 2.0718C3.6603e-06 0.532196 1.66667 -0.430054 3 0.339746L15 7.26795Z"
fill="currentColor"></path>
</svg>
================================================
FILE: src/icons/RightArrow.astro
================================================
<svg width="32" height="32" fill="none" viewBox="0 0 32 32" {...Astro.props}>
<path
fill="currentColor"
fill-rule="evenodd"
d="M13.105 24.667c.354.002.695-.137.946-.387l5.44-5.44a4 4 0 0 0 0-5.653L14.038 7.72a1.333 1.333 0 0 0-1.88 1.88l5.453 5.467c.517.52.517 1.36 0 1.88l-5.44 5.44a1.333 1.333 0 0 0 .947 2.28h-.013Z"
clip-rule="evenodd"></path>
</svg>
================================================
FILE: src/icons/Search.astro
================================================
<svg width="16" height="19" fill="none" viewBox="0 0 19 19" {...Astro.props}>
<path
fill="currentColor"
fill-rule="evenodd"
d="m18.21 16.79-5.11-5.11a7 7 0 1 0-1.41 1.41l5.11 5.11a1 1 0 0 0 1.41-1.41ZM2.5 7.5a5 5 0 1 1 10 0 5 5 0 0 1-10 0Z"
clip-rule="evenodd"
>
</path>
</svg>
================================================
FILE: src/icons/Spinner.astro
================================================
<svg
{...Astro.props}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
width="24" height="24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4">
</circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" >
</path>
</svg>
================================================
FILE: src/icons/TikTok.astro
================================================
<svg width="24" height="24" fill="none">
<path
fill="currentColor"
fill-rule="evenodd"
d="M22.25 12C22.244 6.341 17.659 1.756 12 1.75 6.34 1.75 1.75 6.34 1.75 12S6.34 22.25 12 22.25 22.25 17.66 22.25 12Zm-19 0A8.75 8.75 0 0 1 12 3.25 8.76 8.76 0 0 1 20.75 12a8.75 8.75 0 1 1-17.5 0Z"
clip-rule="evenodd"
>
</path>
<path
fill="currentColor"
stroke="currentColor"
stroke-width=".35"
d="M17.79 9.172A3.199 3.199 0 0 1 14.587 6h-2.069v5.651l-.002 3.096a1.872 1.872 0 0 1-1.981 1.868 1.864 1.864 0 0 1-.856-.263 1.872 1.872 0 0 1-.915-1.577 1.874 1.874 0 0 1 2.466-1.805v-2.099a4.07 4.07 0 0 0-.597-.045 3.966 3.966 0 0 0-2.98 1.333 3.88 3.88 0 0 0-.978 2.34 3.875 3.875 0 0 0 1.154 3.02 3.966 3.966 0 0 0 2.805 1.148 3.953 3.953 0 0 0 2.805-1.148 3.872 3.872 0 0 0 1.16-2.756l-.01-4.623a5.225 5.225 0 0 0 3.207 1.09V9.171h-.006Z"
>
</path>
</svg>
================================================
FILE: src/icons/Twitter.astro
================================================
<svg width="22" height="22" fill="none">
<path
fill="currentColor"
fill-rule="evenodd"
d="m15.573 6-3.642 4.234L15.892 16H12.98l-2.668-3.883L6.97 16h-.864l3.82-4.44L6.108 6h2.914l2.526 3.677L14.71 6h.863Zm-2.188 9.38h1.326L8.608 6.65H7.282l6.103 8.73Z"
clip-rule="evenodd"
>
</path>
<path
fill="currentColor"
fill-rule="evenodd"
d="M21.25 11C21.244 5.341 16.659.756 11 .75 5.34.75.75 5.34.75 11S5.34 21.25 11 21.25 21.25 16.66 21.25 11Zm-19 0A8.75 8.75 0 0 1 11 2.25 8.76 8.76 0 0 1 19.75 11a8.75 8.75 0 1 1-17.5 0Z"
clip-rule="evenodd"
>
</path>
</svg>
================================================
FILE: src/icons/YouTube.astro
================================================
<svg width="22" height="22" fill="none">
<path
fill="currentColor"
fill-rule="evenodd"
d="M11 .75C5.34.75.75 5.34.75 11S5.34 21.25 11 21.25 21.25 16.66 21.25 11C21.244 5.341 16.659.756 11 .75ZM19.75 11A8.75 8.75 0 1 1 11 2.25 8.76 8.76 0 0 1 19.75 11Zm-3-3a1.469 1.469 0 0 0-1.062-1.011C14.752 6.75 11 6.75 11 6.75s-3.753 0-4.688.239a1.469 1.469 0 0 0-1.061 1.01C5 8.89 5 10.75 5 10.75s0 1.86.25 2.75c.139.492.545.88 1.062 1.011.935.239 4.688.239 4.688.239s3.752 0 4.688-.239a1.469 1.469 0 0 0 1.061-1.01C17 12.61 17 10.75 17 10.75s0-1.86-.25-2.75Zm-7 4.75 3-2-3-2v4Z"
clip-rule="evenodd"
>
</path>
</svg>
================================================
FILE: src/layouts/Layout.astro
================================================
---
import Header from "../components/Header.astro"
import Footer from "../components/Footer.astro"
interface Props {
title: string
}
const { title } = Astro.props
---
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<title>{title}</title>
<meta
name="description"
content="Usa InfoJobs e impulsa tu carrera trabajando en una empresa líder"
/>
<meta name="viewport" content="width=device-width" />
<meta
property="twitter:image"
content="https://primer-trabajo.infojobs.net/og.jpg"
/>
<meta property="twitter:card" content="summary_large_image" />
<meta
property="twitter:title"
content="Encuentra tu Primer Trabajo en InfoJobs"
/>
<meta
property="twitter:description"
content="Usa InfoJobs e impulsa tu carrera trabajando en una empresa líder"
/>
<meta
property="og:image"
content="https://primer-trabajo.infojobs.net/og.jpg"
/>
<meta property="og:site_name" content="InfoJobs.net" />
<meta property="og:title" content={title} />
<meta
property="og:description"
content="Usa InfoJobs e impulsa tu carrera trabajando en una empresa líder"
/>
<meta property="og:url" content="https://primer-trabajo.infojobs.net/" />
<link rel="preload" as="image" href="/hero-pattern.webp" />
<script is:inline>
!(function () {
try {
!(function () {
var a = "__tcfapi",
t = a + "Locator"
if (!window.frames[t]) {
var n = window,
e = n.document,
r = 999,
c = setInterval(function () {
if (e.body || 0 === r) {
clearInterval(c)
var a = e.createElement("iframe")
;(a.style.display = "none"),
(a.name = t),
e.body.appendChild(a)
}
r -= 1
}, 5)
n.addEventListener(
"message",
function (t) {
if (null != t && t.data) {
var e
try {
e =
"string" == typeof t.data ? JSON.parse(t.data) : t.data
} catch (a) {}
var r = e && e.__tcfapiCall
r &&
n[a](
r.command,
r.version,
function (a, n) {
var e,
c =
(((e = {}).__tcfapiReturn = {
returnValue: a,
success: n,
callId: r.callId,
}),
e)
t.source.postMessage(c, "*")
},
r.parameter
)
}
},
!1
),
(n[a] = function (t, e, r, c) {
"ping" === t && r
? r({ gdprApplies: !0, cmpLoaded: !1, cmpStatus: "stub" })
: n[a].Q.push(function () {
return n[a](t, e, r, c)
})
}),
(n[a].Q = []),
(n.__borosTcf = {
push: function (t) {
return n[a](t)
},
})
}
})()
} catch (a) {}
})()
</script>
<script is:inline>
!(function () {
var analytics = (window.analytics = window.analytics || [])
if (!analytics.initialize)
if (analytics.invoked)
window.console &&
console.error &&
console.error("Segment snippet included twice.")
else {
analytics.invoked = !0
analytics.methods = [
"trackSubmit",
"trackClick",
"trackLink",
"trackForm",
"pageview",
"identify",
"reset",
"group",
"track",
"ready",
"alias",
"debug",
"page",
"once",
"off",
"on",
"addSourceMiddleware",
"addIntegrationMiddleware",
"setAnonymousId",
"addDestinationMiddleware",
]
analytics.factory = function (e) {
return function () {
var t = Array.prototype.slice.call(arguments)
t.unshift(e)
analytics.push(t)
return analytics
}
}
for (var e = 0; e < analytics.methods.length; e++) {
var key = analytics.methods[e]
analytics[key] = analytics.factory(key)
}
analytics.load = function (key, e) {
var t = document.createElement("script")
t.type = "text/javascript"
t.async = !0
t.src =
"https://cdn.segment.com/analytics.js/v1/" +
key +
"/analytics.min.js"
var n = document.getElementsByTagName("script")[0]
n.parentNode.insertBefore(t, n)
analytics._loadOptions = e
}
analytics.SNIPPET_VERSION = "4.15.3"
analytics.load("I1QsIrOJ5GuAZZFGRCfN8USp89gdFD2u")
}
})()
</script>
<script
is:inline
defer
src="https://components.infojobs.com/widgets/downloader.js"></script>
<script
defer
is:inline
src="https://unpkg.com/@s-ui/ij-segment-wrapper@2.6.0/umd/index.js"
></script>
<script is:inline defer>
analytics.track("Landing Page Viewed", {
page_name: "landing_midudev_proyecto_jovenes",
site: "infojobs.net",
section: "candidate",
channel: "landing_captacion_ontarget",
platform: "web",
vertical: "jobs",
page_type: "landing",
})
</script>
</head>
</html>
<body>
<Header />
<main class="flex flex-col gap-20 md:gap-24">
<slot />
</main>
<div id="cmpContainer"></div>
<Footer />
</body>
<script is:inline defer>
const DEFAULT_TRACKING = {
channel: "landing_captacion_ontarget",
page_detail: "landing_midudev_proyecto_jovenes",
platform: "web",
section: "candidate",
site: "infojobs.net",
vertical: "jobs",
}
const elementsToTrack = document.querySelectorAll("[data-track]")
elementsToTrack.forEach((el) => {
el.addEventListener("click", (event) => {
const { track } = event.currentTarget.dataset
const trackingData = { ...DEFAULT_TRACKING, label: track }
window.analytics.track("Button Clicked", trackingData)
})
})
</script>
<style is:global>
input:focus-visible,
select:focus-visible {
outline: 0;
}
html {
scroll-behavior: smooth;
}
</style>
================================================
FILE: src/lib/generate-infojobs-keywords-url.ts
================================================
const DEFAULT_SEARCH_URL = "https://ms-autocomplete.spain.advgo.net/v1/search"
const MAX_RESULTS = "5"
export const generateInfoJobsKeywordsURL = ({ prefix }: { prefix: string }) => {
const searchURL = new URL(DEFAULT_SEARCH_URL)
if (!prefix) return
searchURL.searchParams.set("prefix", prefix)
searchURL.searchParams.set("max_results", MAX_RESULTS)
return searchURL.toString()
}
================================================
FILE: src/lib/generate-infojobs-url.ts
================================================
const DEFAULT_SEARCH_URL = "https://www.infojobs.net/jobsearch/search-results/list.xhtml"
const SPAIN_COUNTRY_ID = "17"
const FIRST_PAGE = "1"
const SORT_BY_RELEVANCE = "RELEVANCE"
export const generateInfoJobsURL = ({
keyword,
level,
provinceIds
}: {
keyword: string,
level: string,
provinceIds: string
}) => {
const searchURL = new URL(DEFAULT_SEARCH_URL)
if (keyword) searchURL.searchParams.set("keyword", keyword)
if (level) searchURL.searchParams.set("educationIds", level)
if (provinceIds) searchURL.searchParams.set("provinceIds", provinceIds)
else searchURL.searchParams.set("countryIds", SPAIN_COUNTRY_ID)
searchURL.searchParams.set("page", FIRST_PAGE)
searchURL.searchParams.set("sortBy", SORT_BY_RELEVANCE)
return searchURL.toString()
}
================================================
FILE: src/lib/get-ij-studies.ts
================================================
import { getDictionary } from '@lib/query'
export const getStudies = () => {
return getDictionary('study')
}
================================================
FILE: src/lib/list-provinces-ids.ts
================================================
const listProvincesIds = {
'Otro país': 1,
'Álava/Araba': 2,
Albacete: 3,
'Alicante/Alacant': 4,
Almería: 5,
Asturias: 6,
Ávila: 7,
Badajoz: 8,
Barcelona: 9,
Burgos: 10,
Cáceres: 11,
Cádiz: 12,
Cantabria: 13,
'Castellón/Castelló': 14,
Ceuta: 15,
'Ciudad Real': 16,
Córdoba: 17,
Cuenca: 18,
Girona: 19,
'Las Palmas': 20,
Granada: 21,
Guadalajara: 22,
'Guipúzcoa/Gipuzkoa': 23,
Huelva: 24,
Huesca: 25,
'Islas Baleares/Illes Balears': 26,
Jaén: 27,
'A Coruña': 28,
'La Rioja': 29,
León: 30,
Lleida: 31,
Lugo: 32,
Madrid: 33,
Málaga: 34,
Melilla: 35,
Murcia: 36,
Navarra: 37,
Ourense: 38,
Palencia: 39,
Pontevedra: 40,
Salamanca: 41,
Segovia: 42,
Sevilla: 43,
Soria: 44,
Tarragona: 45,
'Santa Cruz de Tenerife': 46,
Teruel: 47,
Toledo: 48,
'Valencia/València': 49,
Valladolid: 50,
'Vizcaya/Bizkaia': 51,
Zamora: 52,
Zaragoza: 53,
}
export function getProvinceId(input: HTMLInputElement): void {
const provinceId = listProvincesIds[input.value as keyof typeof listProvincesIds]
input.setAttribute("value", provinceId !== undefined ? provinceId.toString() : "0")
if (!provinceId) input.setAttribute("value", "0")
}
================================================
FILE: src/lib/mocks.ts
================================================
export const dictionaries: Record<string, { id: number, value: string, order: number, key: string }[]> = {
'study': [
{ id: 0, value: '(Indicar Nivel)', order: 0, key: 'indicar-nivel' },
{ id: 10, value: 'Sin estudios', order: 5, key: 'sin-estudios' },
{
id: 20,
value: 'Educación Secundaria Obligatoria',
order: 10,
key: 'educacion-secundaria-obligatoria'
},
{ id: 50, value: 'Bachillerato', order: 20, key: 'bachillerato' },
{
id: 35,
value: 'Ciclo Formativo Grado Medio',
order: 30,
key: 'ciclo-formativo-grado-medio'
},
{
id: 60,
value: 'Ciclo Formativo Grado Superior',
order: 40,
key: 'ciclo-formativo-grado-superior'
},
{
id: 23,
value: 'Enseñanzas artísticas (regladas)',
order: 50,
key: 'ensenanzas-artisticas-regladas'
},
{
id: 27,
value: 'Enseñanzas deportivas (regladas)',
order: 60,
key: 'ensenanzas-deportivas-regladas'
},
{ id: 125, value: 'Grado', order: 70, key: 'grado' },
{ id: 140, value: 'Licenciatura', order: 80, key: 'licenciado' },
{ id: 110, value: 'Diplomatura', order: 90, key: 'diplomado' },
{
id: 120,
value: 'Ingeniería Técnica',
order: 100,
key: 'ingeniero-tecnico'
},
{
id: 130,
value: 'Ingeniería Superior',
order: 110,
key: 'ingeniero-superior'
},
{ id: 143, value: 'Postgrado', order: 120, key: 'postgrado' },
{ id: 147, value: 'Máster', order: 130, key: 'master' },
{ id: 150, value: 'Doctorado', order: 140, key: 'doctorado' },
{
id: 160,
value: 'Otros títulos, certificaciones y carnés',
order: 150,
key: 'otros-titulos-certificaciones-y-carnes'
},
{
id: 170,
value: 'Otros cursos y formación no reglada',
order: 160,
key: 'otros-cursos-y-formacion-no-reglada'
},
{
id: 30,
value: 'Formación Profesional Grado Medio',
order: 170,
key: 'formacion-profesional-grado-medio'
},
{
id: 55,
value: 'Formación Profesional Grado Superior',
order: 180,
key: 'formacion-profesional-grado-superior'
}
]
}
================================================
FILE: src/lib/query.ts
================================================
import type { DictionaryId } from "../types.ts"
import { dictionaries } from "./mocks.ts"
const INFOJOBS_API_ENDPOINT = "https://api.infojobs.net/api/1/"
const TOKEN = import.meta.env.API_INFOJOBS_TOKEN
export const query = async (path: string) => {
const url = `${INFOJOBS_API_ENDPOINT}${path}`
return fetch(url, {
headers: {
Authorization: `Basic ${TOKEN}`,
'Content-Type': 'application/json',
}
}).then(res => res.json())
}
export const getDictionary = (dictionaryId: DictionaryId): Promise<Array<{ id: number, value: string, order: number, key: string }>> => {
if (!TOKEN) return Promise.resolve(dictionaries[dictionaryId])
return query(`/dictionary/${dictionaryId}`)
}
================================================
FILE: src/lib/queryKeywords.ts
================================================
import { generateInfoJobsKeywordsURL } from "./generate-infojobs-keywords-url";
export const queryKeywords = async (prefix: string) => {
const url = generateInfoJobsKeywordsURL({ prefix });
if (!url) return;
return fetch(url, {
headers: {
"Content-Type": "application/json",
},
}).then((res) => res.json());
};
================================================
FILE: src/pages/index.astro
================================================
---
import BentoInfo from "@components/BentoInfo.astro";
import CoolJobs from "@components/CoolJobs.astro";
import HeroSearch from "@components/HeroSearch.astro";
import SocialBest from "@components/SocialBest.astro";
import PreFooter from "@components/PreFooter.astro";
import InfographicModal from "@components/InfographicModal.astro";
import StandsInterviews from "@components/StandsInterviews.astro";
import Layout from "@layouts/Layout.astro";
import EmergentPositions from "@components/EmergentPositions.astro";
import YourNextJob from "@components/YourNextJob.astro";
---
<Layout title="InfoJobs - ¡Encuentra tu primer empleo!">
<HeroSearch />
<BentoInfo />
<SocialBest />
<CoolJobs />
<PreFooter />
<InfographicModal />
<!-- Infografías -->
<StandsInterviews infographicId="no-experience" />
<EmergentPositions infographicId="emergent-positions" />
<YourNextJob infographicId="selection-process" />
</Layout>
================================================
FILE: src/types.ts
================================================
import { DICTIONARY_TYPE } from "./const"
export type DictionaryId = (typeof DICTIONARY_TYPE)[keyof typeof DICTIONARY_TYPE]
export type CoolJobs = { image: string; title: string; brand: string; section_id: string; }
================================================
FILE: tailwind.config.mjs
================================================
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
theme: {
extend: {
colors: {
primary: "#167DB7",
accent: "#FE5230",
"ij-black": "#212121",
"ij-red": "#FF421C",
"ij-green": "#00A550",
"ij-blue": "#E8F2F8",
"ij-yellow": "#EFA500",
},
backgroundImage: {
"hero-pattern": "url(/hero-pattern.webp)",
},
borderRadius: {
"4xl": "3rem",
},
backgroundSize: {
"auto-height": "auto 100%",
},
letterSpacing: {
separated: "0.35px",
},
padding: {
15: "60px",
},
transitionDuration: {
DEFAULT: "500ms",
},
screens: {
xs: "480px",
},
},
},
plugins: [
({ addUtilities }) => {
const newUtilities = {
".no-scrollbar::-webkit-scrollbar": {
display: "none",
},
".no-scrollbar": {
"-ms-overflow-style": "none",
"scrollbar-width": "none",
},
};
addUtilities(newUtilities);
},
],
};
================================================
FILE: tsconfig.json
================================================
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@components/*": [
"src/components/*"
],
"@ui/*": [
"src/components/ui/*"
],
"@layouts/*": [
"src/layouts/*"
],
"@icons/*": [
"src/icons/*"
],
"@lib/*": [
"src/lib/*"
],
"@types": [
"src/types.ts"
]
}
}
}
gitextract_csw6vtjg/ ├── .github/ │ └── workflows/ │ └── contributors.yaml ├── .gitignore ├── .vercel/ │ └── project.json ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── README.md ├── astro.config.mjs ├── package.json ├── src/ │ ├── components/ │ │ ├── AdevintaInfo.astro │ │ ├── BentoInfo.astro │ │ ├── CoolJobBackground.astro │ │ ├── CoolJobBrand.astro │ │ ├── CoolJobCard.astro │ │ ├── CoolJobs.astro │ │ ├── EmergentPositions.astro │ │ ├── FollowUs.astro │ │ ├── FollowUsLink.astro │ │ ├── Footer.astro │ │ ├── Header.astro │ │ ├── HeroSearch.astro │ │ ├── InfographicBody.astro │ │ ├── InfographicHeader.astro │ │ ├── InfographicModal.astro │ │ ├── InfographicTips.astro │ │ ├── KingsLeagueInfo.astro │ │ ├── LinksColumns.astro │ │ ├── LiteYoutube.astro │ │ ├── OportunidadesTikTok.astro │ │ ├── PreFooter.astro │ │ ├── SectionContainer.astro │ │ ├── SocialBest.astro │ │ ├── StandsInterviews.astro │ │ ├── Subtitle.astro │ │ ├── TiktokVideo.astro │ │ ├── YourNextJob.astro │ │ └── ui/ │ │ ├── Button.astro │ │ └── Icon.astro │ ├── const.ts │ ├── env.d.ts │ ├── icons/ │ │ ├── AppStore.astro │ │ ├── ChevronDown.astro │ │ ├── CloseIcon.astro │ │ ├── Facebook.astro │ │ ├── GooglePlay.astro │ │ ├── LeftArrow.astro │ │ ├── Play.astro │ │ ├── RightArrow.astro │ │ ├── Search.astro │ │ ├── Spinner.astro │ │ ├── TikTok.astro │ │ ├── Twitter.astro │ │ └── YouTube.astro │ ├── layouts/ │ │ └── Layout.astro │ ├── lib/ │ │ ├── generate-infojobs-keywords-url.ts │ │ ├── generate-infojobs-url.ts │ │ ├── get-ij-studies.ts │ │ ├── list-provinces-ids.ts │ │ ├── mocks.ts │ │ ├── query.ts │ │ └── queryKeywords.ts │ ├── pages/ │ │ └── index.astro │ └── types.ts ├── tailwind.config.mjs └── tsconfig.json
SYMBOL INDEX (12 symbols across 6 files)
FILE: src/const.ts
constant DICTIONARY_TYPE (line 1) | const DICTIONARY_TYPE = {
FILE: src/lib/generate-infojobs-keywords-url.ts
constant DEFAULT_SEARCH_URL (line 1) | const DEFAULT_SEARCH_URL = "https://ms-autocomplete.spain.advgo.net/v1/s...
constant MAX_RESULTS (line 2) | const MAX_RESULTS = "5"
FILE: src/lib/generate-infojobs-url.ts
constant DEFAULT_SEARCH_URL (line 1) | const DEFAULT_SEARCH_URL = "https://www.infojobs.net/jobsearch/search-re...
constant SPAIN_COUNTRY_ID (line 2) | const SPAIN_COUNTRY_ID = "17"
constant FIRST_PAGE (line 3) | const FIRST_PAGE = "1"
constant SORT_BY_RELEVANCE (line 4) | const SORT_BY_RELEVANCE = "RELEVANCE"
FILE: src/lib/list-provinces-ids.ts
function getProvinceId (line 57) | function getProvinceId(input: HTMLInputElement): void {
FILE: src/lib/query.ts
constant INFOJOBS_API_ENDPOINT (line 4) | const INFOJOBS_API_ENDPOINT = "https://api.infojobs.net/api/1/"
constant TOKEN (line 6) | const TOKEN = import.meta.env.API_INFOJOBS_TOKEN
FILE: src/types.ts
type DictionaryId (line 3) | type DictionaryId = (typeof DICTIONARY_TYPE)[keyof typeof DICTIONARY_TYPE]
type CoolJobs (line 5) | type CoolJobs = { image: string; title: string; brand: string; section_i...
Condensed preview — 67 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (152K chars).
[
{
"path": ".github/workflows/contributors.yaml",
"chars": 501,
"preview": "name: Add contributors\non:\n schedule:\n - cron: '20 20 * * *'\n\njobs:\n add-contributors:\n runs-on: ubuntu-latest\n"
},
{
"path": ".gitignore",
"chars": 326,
"preview": "# build output\ndist/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyar"
},
{
"path": ".vercel/project.json",
"chars": 88,
"preview": "{\"projectId\":\"prj_SsanVSwAoWqNeWqovW1OdwWn8CrZ\",\"orgId\":\"team_pzEis5cENnPrEa0YhyEfJ7Ek\"}"
},
{
"path": ".vscode/extensions.json",
"chars": 87,
"preview": "{\n \"recommendations\": [\"astro-build.astro-vscode\"],\n \"unwantedRecommendations\": []\n}\n"
},
{
"path": ".vscode/launch.json",
"chars": 207,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"command\": \"./node_modules/.bin/astro dev\",\n \"name\": \"Dev"
},
{
"path": ".vscode/settings.json",
"chars": 69,
"preview": "{\n \"cSpell.words\": [\"Coméntalo\", \"infojobs\", \"jovenes\", \"Twitea\"]\n}\n"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 5992,
"preview": "# Código de Conducta para Contribuyentes\n\n## Nuestro compromiso\n\nNosotros, como miembros, contribuyentes y administrador"
},
{
"path": "CONTRIBUTING.md",
"chars": 11148,
"preview": "# Contribuir a Landing de InfoJobs\n\nPrimero que nada, ¡gracias por tomarte el tiempo para contribuir! ❤️\n\nSe anima y val"
},
{
"path": "README.md",
"chars": 9817,
"preview": "\n# Landing de InfoJobs\n\nEste proyecto es una landing page para InfoJobs, diseñada para proporcionar una presentación mod"
},
{
"path": "astro.config.mjs",
"chars": 242,
"preview": "// @ts-check\nimport { defineConfig } from 'astro/config';\n\nimport tailwind from '@astrojs/tailwind';\n\n// https://astro.b"
},
{
"path": "package.json",
"chars": 418,
"preview": "{\n \"name\": \"landing-jovenes-infojobs\",\n \"type\": \"module\",\n \"version\": \"0.0.1\",\n \"scripts\": {\n \"dev\": \"astro dev\","
},
{
"path": "src/components/AdevintaInfo.astro",
"chars": 1205,
"preview": "---\nconst LINKS = [\n {\n label: \"JobisJob\",\n href: \"https://www.jobisjob.es/\",\n },\n {\n label: \"Fotocasa\",\n "
},
{
"path": "src/components/BentoInfo.astro",
"chars": 6448,
"preview": "---\nimport SectionContainer from \"@components/SectionContainer.astro\"\nimport Subtitle from \"@components/Subtitle.astro\"\n"
},
{
"path": "src/components/CoolJobBackground.astro",
"chars": 140,
"preview": "<img\n src=\"/img/cool-jobs/pattern.webp\"\n alt=\"Cool Jobs Background Pattern\"\n class=\"hidden lg:block lg:absolute lg:to"
},
{
"path": "src/components/CoolJobBrand.astro",
"chars": 661,
"preview": "<a\n href=\"https://cooljobs.infojobs.net/\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n data-track=\"cool_jobs\"\n clas"
},
{
"path": "src/components/CoolJobCard.astro",
"chars": 669,
"preview": "---\nconst { image, brand, title, id } = Astro.props\n---\n\n<a\n href=`https://cooljobs.infojobs.net/#${id}`\n target=\"_bla"
},
{
"path": "src/components/CoolJobs.astro",
"chars": 2330,
"preview": "---\nimport CoolJobCard from \"@components/CoolJobCard.astro\"\nimport type { CoolJobs } from \"@types\"\nimport CoolJobBackgro"
},
{
"path": "src/components/EmergentPositions.astro",
"chars": 8689,
"preview": "---\nimport InfographicBody from \"./InfographicBody.astro\";\nimport InfographicHeader from \"./InfographicHeader.astro\";\n\nc"
},
{
"path": "src/components/FollowUs.astro",
"chars": 2052,
"preview": "---\nimport FollowUsLink from \"@components/FollowUsLink.astro\";\nimport Icon from \"./ui/Icon.astro\";\n\nconst hoverSocialNet"
},
{
"path": "src/components/FollowUsLink.astro",
"chars": 194,
"preview": "---\nconst { href, socialPlatform } = Astro.props;\n---\n\n<a\n href={href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n "
},
{
"path": "src/components/Footer.astro",
"chars": 357,
"preview": "---\nimport LinksColumns from \"@components/LinksColumns.astro\"\nimport FollowUs from \"@components/FollowUs.astro\"\nimport A"
},
{
"path": "src/components/Header.astro",
"chars": 585,
"preview": "---\nimport Button from \"./ui/Button.astro\"\n---\n\n<header\n id=\"header\"\n class=\"py-4 px-8 flex justify-between items-cent"
},
{
"path": "src/components/HeroSearch.astro",
"chars": 17074,
"preview": "---\nimport SearchIcon from \"@icons/Search.astro\"\nimport { getStudies } from \"@lib/get-ij-studies\"\nimport SectionContaine"
},
{
"path": "src/components/InfographicBody.astro",
"chars": 5034,
"preview": "---\ntype Props = {\n bentoInfoModalCards?: readonly BentoInfoModalCard[]; // Ahora es opcional\n bentoInfoModalCardsBig?"
},
{
"path": "src/components/InfographicHeader.astro",
"chars": 548,
"preview": "---\nconst { badgeText, title, subtitle } = Astro.props\n---\n\n<div class=\"flex flex-col gap-[10px] md:gap-[35px]\">\n <span"
},
{
"path": "src/components/InfographicModal.astro",
"chars": 604,
"preview": "---\nimport CloseIcon from \"@icons/CloseIcon.astro\";\n---\n\n<section\n id=\"infographic-modal\"\n class=\"transition-all fixed"
},
{
"path": "src/components/InfographicTips.astro",
"chars": 3342,
"preview": "---\nconst TIPS = [\n {\n title: \"Favoritos\",\n description: \"Marca como favoritas las empresas que te molen\",\n im"
},
{
"path": "src/components/KingsLeagueInfo.astro",
"chars": 1106,
"preview": "<div\n class=\"w-full aspect-[9/16] sm:aspect-[16/9] sm:col-span-2 rounded-3xl overflow-hidden\"\n>\n <div class=\"w-full h-"
},
{
"path": "src/components/LinksColumns.astro",
"chars": 3539,
"preview": "---\nconst linksInfo = [\n {\n title: \"Nosotros\",\n links: [\n {\n label: \"Ayuda\",\n href: \"https://a"
},
{
"path": "src/components/LiteYoutube.astro",
"chars": 12174,
"preview": "---\nimport Icon from \"./ui/Icon.astro\"\n\ninterface Props {\n\tvideoId: string\n\ttitle?: string\n}\n\nconst { videoId, title } ="
},
{
"path": "src/components/OportunidadesTikTok.astro",
"chars": 1709,
"preview": "---\nconst cardsOportunidades = [\n {\n title1: \"Oferta de empleo\",\n title2: \"Miles de oportunidades\",\n img: \"./i"
},
{
"path": "src/components/PreFooter.astro",
"chars": 460,
"preview": "---\nimport KingsLeagueInfo from \"@components/KingsLeagueInfo.astro\"\nimport SectionContainer from \"@components/SectionCon"
},
{
"path": "src/components/SectionContainer.astro",
"chars": 140,
"preview": "<section class=\"mx-auto max-w-7xl w-full\">\n <div class:list={[\"mx-auto w-full px-4\", Astro.props.class]}>\n <slot />\n"
},
{
"path": "src/components/SocialBest.astro",
"chars": 5043,
"preview": "---\nimport SectionContainer from \"./SectionContainer.astro\"\nimport TiktokVideo from \"./TiktokVideo.astro\"\nimport Icon fr"
},
{
"path": "src/components/StandsInterviews.astro",
"chars": 2561,
"preview": "---\nimport InfographicBody from \"./InfographicBody.astro\"\nimport InfographicHeader from \"./InfographicHeader.astro\"\n\ncon"
},
{
"path": "src/components/Subtitle.astro",
"chars": 127,
"preview": "<h2 class:list={[\"font-semibold text-xl pb-4 sm:pb-6 sm:text-2xl md:text-4xl md:pb-12\", Astro.props.class]}>\n <slot /"
},
{
"path": "src/components/TiktokVideo.astro",
"chars": 2698,
"preview": "---\nimport Icon from \"./ui/Icon.astro\"\n\ninterface Props {\n videoId: string\n thumbnailUrl: string\n title: string\n}\n\nco"
},
{
"path": "src/components/YourNextJob.astro",
"chars": 2043,
"preview": "---\nimport InfographicBody from \"./InfographicBody.astro\"\nimport InfographicHeader from \"./InfographicHeader.astro\"\nimpo"
},
{
"path": "src/components/ui/Button.astro",
"chars": 6293,
"preview": "---\nimport Icon from \"@ui/Icon.astro\"\n\ninterface ButtonProps {\n variant?: \"solid\" | \"bordered\" | \"light\" | \"ghost\" | \"f"
},
{
"path": "src/components/ui/Icon.astro",
"chars": 675,
"preview": "---\nconst icons = import.meta.glob(\"@icons/*.astro\", { eager: true })\n\ninterface IconMap {\n [key: string]: (_props: Rec"
},
{
"path": "src/const.ts",
"chars": 1456,
"preview": "export const DICTIONARY_TYPE = {\n AVAILABILITY: 'availability',\n CANDIDATE_EXPERIENCE: 'candidate-experience',\n CANDI"
},
{
"path": "src/env.d.ts",
"chars": 45,
"preview": "/// <reference path=\"../.astro/types.d.ts\" />"
},
{
"path": "src/icons/AppStore.astro",
"chars": 88,
"preview": "<img\n src=\"/footer-app-store.webp\"\n alt=\"Logo App Store\"\n class=\"h-[50px] w-auto\"\n/>\n"
},
{
"path": "src/icons/ChevronDown.astro",
"chars": 290,
"preview": "---\nconst { ...props } = Astro.props\n---\n\n<svg \n xmlns=\"http://www.w3.org/2000/svg\" \n viewBox=\"0 0 24 24\" \n cla"
},
{
"path": "src/icons/CloseIcon.astro",
"chars": 304,
"preview": "<svg width=\"14\" height=\"14\" fill=\"none\"\n ><path\n fill=\"#89898A\"\n fill-rule=\"evenodd\"\n d=\"m8.47 6.95 4.79-4.8A1"
},
{
"path": "src/icons/Facebook.astro",
"chars": 497,
"preview": "<svg width=\"22\" height=\"22\" fill=\"none\">\n <path\n fill=\"currentColor\"\n fill-rule=\"evenodd\"\n d=\"M11 .75C5.34.75."
},
{
"path": "src/icons/GooglePlay.astro",
"chars": 92,
"preview": "<img\n src=\"/footer-google-play.webp\"\n alt=\"Logo Google Play\"\n class=\"h-[50px] w-auto\"\n/>\n"
},
{
"path": "src/icons/LeftArrow.astro",
"chars": 375,
"preview": "<svg width=\"32\" height=\"32\" fill=\"none\" viewBox=\"0 0 32 32\" {...Astro.props}>\n <path\n fill=\"currentColor\"\n fill-r"
},
{
"path": "src/icons/Play.astro",
"chars": 332,
"preview": "<svg width=\"16\" height=\"18\" viewBox=\"0 0 16 18\" fill=\"none\" {...Astro.props}>\n <path\n d=\"M15 7.26795C16.3333 8.03775"
},
{
"path": "src/icons/RightArrow.astro",
"chars": 373,
"preview": "<svg width=\"32\" height=\"32\" fill=\"none\" viewBox=\"0 0 32 32\" {...Astro.props}>\n <path\n fill=\"currentColor\"\n fill-r"
},
{
"path": "src/icons/Search.astro",
"chars": 300,
"preview": "<svg width=\"16\" height=\"19\" fill=\"none\" viewBox=\"0 0 19 19\" {...Astro.props}>\n <path\n fill=\"currentColor\"\n fill-r"
},
{
"path": "src/icons/Spinner.astro",
"chars": 490,
"preview": "<svg\n {...Astro.props}\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n width=\"24\" height=\"24"
},
{
"path": "src/icons/TikTok.astro",
"chars": 887,
"preview": "<svg width=\"24\" height=\"24\" fill=\"none\">\n <path\n fill=\"currentColor\"\n fill-rule=\"evenodd\"\n d=\"M22.25 12C22.244"
},
{
"path": "src/icons/Twitter.astro",
"chars": 598,
"preview": "<svg width=\"22\" height=\"22\" fill=\"none\">\n <path\n fill=\"currentColor\"\n fill-rule=\"evenodd\"\n d=\"m15.573 6-3.642 "
},
{
"path": "src/icons/YouTube.astro",
"chars": 628,
"preview": "<svg width=\"22\" height=\"22\" fill=\"none\">\n <path\n fill=\"currentColor\"\n fill-rule=\"evenodd\"\n d=\"M11 .75C5.34.75."
},
{
"path": "src/layouts/Layout.astro",
"chars": 7170,
"preview": "---\nimport Header from \"../components/Header.astro\"\nimport Footer from \"../components/Footer.astro\"\n\ninterface Props {\n "
},
{
"path": "src/lib/generate-infojobs-keywords-url.ts",
"chars": 403,
"preview": "const DEFAULT_SEARCH_URL = \"https://ms-autocomplete.spain.advgo.net/v1/search\"\nconst MAX_RESULTS = \"5\"\n\nexport const gen"
},
{
"path": "src/lib/generate-infojobs-url.ts",
"chars": 786,
"preview": "const DEFAULT_SEARCH_URL = \"https://www.infojobs.net/jobsearch/search-results/list.xhtml\"\nconst SPAIN_COUNTRY_ID = \"17\"\n"
},
{
"path": "src/lib/get-ij-studies.ts",
"chars": 112,
"preview": "import { getDictionary } from '@lib/query'\n \nexport const getStudies = () => {\n return getDictionary('study')\n}"
},
{
"path": "src/lib/list-provinces-ids.ts",
"chars": 1222,
"preview": "const listProvincesIds = {\n 'Otro país': 1,\n 'Álava/Araba': 2,\n Albacete: 3,\n 'Alicante/Alacant': 4,\n Almería: 5,\n "
},
{
"path": "src/lib/mocks.ts",
"chars": 2223,
"preview": "export const dictionaries: Record<string, { id: number, value: string, order: number, key: string }[]> = {\n 'study': [\n"
},
{
"path": "src/lib/query.ts",
"chars": 708,
"preview": "import type { DictionaryId } from \"../types.ts\"\nimport { dictionaries } from \"./mocks.ts\"\n\nconst INFOJOBS_API_ENDPOINT ="
},
{
"path": "src/lib/queryKeywords.ts",
"chars": 336,
"preview": "import { generateInfoJobsKeywordsURL } from \"./generate-infojobs-keywords-url\";\n\nexport const queryKeywords = async (pre"
},
{
"path": "src/pages/index.astro",
"chars": 946,
"preview": "---\nimport BentoInfo from \"@components/BentoInfo.astro\";\nimport CoolJobs from \"@components/CoolJobs.astro\";\nimport HeroS"
},
{
"path": "src/types.ts",
"chars": 218,
"preview": "import { DICTIONARY_TYPE } from \"./const\"\n\nexport type DictionaryId = (typeof DICTIONARY_TYPE)[keyof typeof DICTIONARY_T"
},
{
"path": "tailwind.config.mjs",
"chars": 1156,
"preview": "/** @type {import('tailwindcss').Config} */\nexport default {\n content: [\"./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts"
},
{
"path": "tsconfig.json",
"chars": 429,
"preview": "{\n \"extends\": \"astro/tsconfigs/strict\",\n \"compilerOptions\": {\n \"baseUrl\": \".\",\n \"paths\": {\n \"@components/*\""
}
]
About this extraction
This page contains the full source code of the midudev/landing-infojobs GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 67 files (136.6 KB), approximately 40.3k tokens, and a symbol index with 12 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.