├── .github ├── labeler.yml ├── pull_request_template.md └── workflows │ ├── check_folder_structure.yml │ └── labels.yml ├── .gitignore ├── LICENSE ├── README.md ├── live-coding ├── 01-node-callbacks-promises │ ├── README.md │ ├── already-solved │ │ ├── dotenv.js │ │ ├── index.js │ │ └── server.js │ ├── archivo1.txt │ ├── archivo2.txt │ ├── archivo3.txt │ ├── input.txt │ ├── package.json │ ├── solutions │ │ ├── dotenv.js │ │ ├── index.js │ │ └── server.js │ └── test │ │ ├── dotenv.test.js │ │ ├── index.test.js │ │ └── server.test.js └── 02-add-items-react │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ └── vite.svg │ ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── components │ │ └── Item.tsx │ ├── hooks │ │ ├── useItems.ts │ │ └── useSEO.ts │ ├── index.css │ ├── main.tsx │ └── vite-env.d.ts │ ├── tests │ ├── App.test.tsx │ └── useItems.test.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── package.json ├── pruebas ├── 01-reading-list │ ├── README.md │ └── books.json └── 02-bazar-universal │ ├── README.md │ └── products.json └── web ├── .gitignore ├── README.md ├── astro.config.mjs ├── bun.lockb ├── package.json ├── public ├── fontawesome │ ├── css │ │ ├── all.css │ │ ├── all.min.css │ │ ├── brands.bareminimum.css │ │ ├── brands.css │ │ ├── brands.min.css │ │ ├── fontawesome.bareminimum.css │ │ ├── fontawesome.css │ │ ├── fontawesome.min.css │ │ ├── regular.css │ │ ├── regular.min.css │ │ ├── solid.css │ │ ├── solid.min.css │ │ ├── svg-with-js.css │ │ ├── svg-with-js.min.css │ │ ├── v4-font-face.css │ │ ├── v4-font-face.min.css │ │ ├── v4-shims.css │ │ ├── v4-shims.min.css │ │ ├── v5-font-face.css │ │ └── v5-font-face.min.css │ └── webfonts │ │ ├── fa-brands-400.ttf │ │ ├── fa-brands-400.woff2 │ │ ├── fa-regular-400.ttf │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff2 │ │ ├── fa-v4compatibility.ttf │ │ └── fa-v4compatibility.woff2 ├── fonts │ ├── space-grotesk-v13-latin-700.woff │ ├── space-grotesk-v13-latin-700.woff2 │ ├── space-grotesk-v13-latin-regular.woff │ └── space-grotesk-v13-latin-regular.woff2 └── images │ ├── 1.webp │ ├── 2.webp │ └── og.png ├── src ├── components │ ├── Badge.astro │ ├── CodeBlock.astro │ ├── CodePenEmbed.astro │ ├── DarkModeToggle.astro │ ├── Footer.astro │ ├── GitHubGistEmbed.astro │ ├── Header.astro │ ├── HeaderLink.astro │ ├── HeaderSocialLink.astro │ ├── Heading.astro │ ├── Intro.astro │ ├── Nav.astro │ ├── PageMeta.astro │ ├── Projects.astro │ ├── Renderer.astro │ ├── TweetEmbed.astro │ └── YouTubeEmbed.astro ├── config.ts ├── env.d.ts ├── layouts │ ├── ContentLayout.astro │ ├── Favicon.astro │ ├── FontAwesome.astro │ ├── GoogleFont.astro │ ├── PageLayout.astro │ └── ThemeScript.astro ├── lib │ ├── markdoc │ │ ├── frontmatter.schema.ts │ │ ├── markdoc.config.ts │ │ └── read.ts │ └── seo.ts ├── pages │ ├── index.astro │ └── rss.xml.ts └── styles │ └── global.css ├── tailwind.config.cjs └── tsconfig.json /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | 'pruebas': 2 | - pruebas/** 3 | 4 | 'prueba-1': 5 | - pruebas/01-reading-list/** 6 | 7 | 'prueba-2': 8 | - pruebas/02-bazar-universal/** 9 | 10 | 'web': 11 | - web/** 12 | 13 | 'chore': 14 | - .github/** 15 | - .gitignore 16 | - LICENSE 17 | - README.md 18 | - package.json 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 🔗 URL: https://rellena-aqui-tu-url-cuando-este-disponible.com 2 | 3 | ## Checklist antes de enviar la PR 4 | - [ ] He creado una carpeta con mi nombre de usuario en `pruebas//` 5 | - [ ] No he modificado ficheros fuera de mi carpeta 6 | - [ ] Mi proyecto contiene un fichero `README.md` -------------------------------------------------------------------------------- /.github/workflows/check_folder_structure.yml: -------------------------------------------------------------------------------- 1 | name: Check Folder Structure 2 | 3 | on: 4 | pull_request: 5 | types: opened 6 | 7 | jobs: 8 | check-folder-structure: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | comment: ${{ steps.check.outputs.comment }} 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Get changed files 15 | id: changed_files 16 | uses: tj-actions/changed-files@v37.5.0 17 | - name: Check if folder structure is correct 18 | id: check 19 | run: | 20 | PR_USERNAME=$(echo "${{ github.event.pull_request.user.login }}") 21 | FILES="${{ steps.changed_files.outputs.all_modified_files }}" 22 | IFS=' ' read -r -a FILES_ARR <<< "$FILES" 23 | 24 | for FILE in "${FILES_ARR[@]}"; do 25 | if [[ ${FILE} == pruebas/* ]]; then 26 | TEST_FOLDER=$(echo ${FILE} | cut -d'/' -f2) 27 | if [[ ${FILE} != pruebas/${TEST_FOLDER}/${PR_USERNAME}/* ]]; then 28 | echo "Incorrect folder structure in file: ${FILE}. Requesting changes." 29 | echo "Asegúrese de que los cambios se realicen en 'pruebas/${TEST_FOLDER}/${PR_USERNAME}/'. :file_folder:" > message.txt 30 | echo "::set-output name=comment::$(cat message.txt)" 31 | break 32 | fi 33 | fi 34 | done 35 | - name: Comment PR 36 | if: steps.check.outputs.comment != '' 37 | uses: actions/github-script@v6 38 | with: 39 | github-token: ${{secrets.GITHUB_TOKEN}} 40 | script: | 41 | github.rest.issues.createComment({ 42 | issue_number: context.issue.number, 43 | owner: context.repo.owner, 44 | repo: context.repo.repo, 45 | body: "${{ steps.check.outputs.comment }}" 46 | }) 47 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request_target 4 | - workflow_dispatch 5 | 6 | jobs: 7 | triage: 8 | permissions: 9 | contents: read 10 | pull-requests: write 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/labeler@v4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pruebas Técnicas de Programación 2 | 3 | Pruebas técnicas de programación para desarrolladores frontend y backend. 4 | 5 | ## Lista de pruebas técnicas 6 | 7 | - [01 - Reading List (FrontEnd - Nivel: Junior)](./pruebas/01-reading-list/README.md) 8 | - [02 - Bazar Universal (FrontEnd - Nivel: Junior)](./pruebas/02-bazar-universal/README.md) 9 | 10 | ## ¿Cómo participar? 11 | 12 | 1. Haz un fork de este repositorio 13 | 2. Crea una carpeta con **tu nombre de usuario de GitHub** dentro de la carpeta `pruebas/[nombre-de-la-prueba]`, por ejemplo: `pruebas/01-reading-list/midudev`. 14 | 3. Siempre **sólo modifica los ficheros y carpetas dentro de tu carpeta**, de otra manera, tu pull request será rechazada. Nunca formatees o modifiques el código de otros participantes. 15 | 16 | - Recurso: [Cómo crear una Pull Request a un proyecto](https://www.youtube.com/watch?v=BPns9r76vSI) 17 | 18 | ## Sígueme en las redes sociales 19 | 20 | - [Twitter](https://twitter.com/midudev) 21 | - [Instagram](https://instagram.com/midu.dev) 22 | - [Twitch](https://twitch.tv/midudev) 23 | - [YouTube](https://youtube.com/midudev) 24 | - [TikTok](https://tiktok.com/@midudev) 25 | - [LinkedIn](https://linkedin.com/in/midudev) 26 | - [Web](https://midu.dev) 27 | -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/README.md: -------------------------------------------------------------------------------- 1 | # Prueba Técnica JavaScript + Node.js 2 | 3 | Escribe las soluciones en el archivo `solutions/index.js` manteniendo el nombre de las funciones y sus `export`. Usa `ESModules` en tu proyecto de Node.js 4 | 5 | 1 - Arregla esta función para que el código posterior funcione como se espera: 6 | 7 | ```javascript 8 | import net from 'node:net' 9 | 10 | export const ping = (ip) => { 11 | const startTime = process.hrtime() 12 | 13 | const client = net.connect({ port: 80, host: ip }, () => { 14 | client.end() 15 | return { time: process.hrtime(startTime), ip } 16 | }) 17 | 18 | client.on('error', (err) => { 19 | throw err 20 | client.end() 21 | }) 22 | } 23 | 24 | ping('midu.dev', (err, info) => { 25 | if (err) console.error(err) 26 | console.log(info) 27 | }) 28 | ``` 29 | 30 | 2 - Transforma la siguiente función para que funcione con promesas en lugar de callbacks: 31 | 32 | ```javascript 33 | export function obtenerDatosPromise(callback) { 34 | setTimeout(() => { 35 | callback(null, { data: 'datos importantes' }); 36 | }, 2000); 37 | } 38 | ``` 39 | 40 | 3 - Explica qué hace la funcion. Identifica y corrige los errores en el siguiente código. Si ves algo innecesario, elimínalo. Luego mejoralo para que siga funcionando con callback y luego haz lo que consideres para mejorar su legibilidad. 41 | 42 | ```javascript 43 | export function procesarArchivo() { 44 | fs.readFile('input.txt', 'utf8', (error, contenido) => { 45 | if (error) { 46 | console.error('Error leyendo archivo:', error.message); 47 | return false; 48 | } 49 | 50 | setTimeout(() => { 51 | const textoProcesado = contenido.toUpperCase(); 52 | 53 | fs.writeFile('output.txt', textoProcesado, error => { 54 | if (error) { 55 | console.error('Error guardando archivo:', error.message); 56 | return false; 57 | } 58 | 59 | console.log('Archivo procesado y guardado con éxito'); 60 | return true 61 | }); 62 | 63 | }, 1000); 64 | }); 65 | } 66 | ``` 67 | 68 | 4 - ¿Cómo mejorarías el siguiente código y por qué? Arregla los tests si es necesario: 69 | 70 | ```javascript 71 | import fs from 'node:fs'; 72 | 73 | export function leerArchivos() { 74 | const archivo1 = fs.readSync('archivo1.txt', 'utf8'); 75 | const archivo2 = fs.readSync('archivo2.txt', 'utf8'); 76 | const archivo3 = fs.readSync('archivo3.txt', 'utf8'); 77 | 78 | return `${archivo1} ${archivo2} ${archivo3}` 79 | } 80 | 81 | leerArchivos(); 82 | ``` 83 | 84 | 5 - Escribe una funcion `delay` que retorne una promesa que se resuelva después de `n` milisegundos. Por ejemplo: 85 | 86 | ```javascript 87 | export async function delay () { 88 | // ... 89 | } 90 | 91 | delay(3000).then(() => console.log('Hola mundo')); 92 | // o.. 93 | await delay(3000) 94 | console.log('Hola mundo') 95 | ``` 96 | 97 | 6. Vamos a crear nuestra propia utilidad `dotenv` en el archivo `dotenv.js`. 98 | 99 | - La utilidad debe devolver un método `config` que lee el archivo `.env` y añade las variables de entorno que haya en el archivo al objeto `process.env`. 100 | 101 | - Por ejemplo si tu archivo `.env` contiene: 102 | 103 | ```sh 104 | PORT=8080 105 | TOKEN="123abc" 106 | ``` 107 | 108 | Entonces podríamos hacer esto: 109 | 110 | ```javascript 111 | const dotenv = require("./dotenv.js"); 112 | dotenv.config() 113 | 114 | console.log(process.env.PORT) // "8008" 115 | console.log(process.env.TOKEN) // "123abc" 116 | ``` 117 | 118 | También se le puede pasar el path del archivo `.env` como parámetro: 119 | 120 | ```javascript 121 | const dotenv = require("./dotenv.js"); 122 | dotenv.config("./config/.env.local") 123 | ``` 124 | 125 | Cosas a tener en cuenta: 126 | 127 | - Sólo se permite usar el módulo `fs` para leer el archivo. 128 | - Si el archivo no existe, no debe dar error, simplemente no hace nada. 129 | - Si el archivo existe, pero no tiene ninguna variable de entorno, no debe hacer nada. 130 | - Sólo debe soportar el archivo `.env` o el que se le pasa como parametro, no hace falta que soporte `.env.local`, `.env.development` y similares de forma automática. 131 | - Las variables de entorno siempre son strings, por lo que si en el archivo `.env` hay un número, por ejemplo `PORT=8080`, al leerlo con `fs` y añadirlo a `process.env` debe ser un string, no un número. 132 | - `process.env` es un objeto y, por lo tanto, es mutable. Esto significa que podemos añadir propiedades nuevas sin problemas. 133 | 134 | 135 | 7 - Diseña una API REST utilizando Express que permite a los usuarios crear, leer, modificar, actualizar y eliminar elementos de una lista. 136 | 137 | La lista tendrá objetos que tienen la siguiente forma: 138 | 139 | ```javascript 140 | { 141 | id: 1, 142 | content: 'Item 1' 143 | } 144 | ``` 145 | 146 | Haz la solución en el archivo `solutions/server.js` y exporta el `app` y `server` creado. 147 | Instala Express con `npm install express`. No te preocupes por CORS. 148 | -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/already-solved/dotenv.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs' 2 | 3 | function parseEnv(env) { 4 | const lines = env.split('\n') 5 | 6 | lines.forEach(line => { 7 | const [key, ...value] = line.split('=') 8 | const valueString = value.join('') 9 | const hasQuotes = valueString.startsWith('"') && valueString.endsWith('"') 10 | const valueToStore = hasQuotes ? valueString.slice(1, -1) : valueString 11 | process.env[key] = valueToStore 12 | }) 13 | 14 | } 15 | 16 | export function config({ path = '.env' } = {}) { 17 | try { 18 | const env = readFileSync(path, 'utf8') 19 | parseEnv(env) 20 | } catch (e) { 21 | console.error(e) 22 | } 23 | } 24 | 25 | const dotenv = { 26 | config 27 | } 28 | 29 | export default dotenv -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/already-solved/index.js: -------------------------------------------------------------------------------- 1 | import net from 'node:net' 2 | import fs from 'node:fs' 3 | 4 | export const ping = (ip, callback) => { 5 | const startTime = process.hrtime() 6 | 7 | const client = net.connect({ port: 80, host: ip }, () => { 8 | client.end() 9 | callback(null, { time: process.hrtime(startTime), ip }) 10 | }) 11 | 12 | client.on('error', (err) => { 13 | client.end() 14 | callback(err) 15 | }) 16 | } 17 | 18 | // ping('midu.dev', (err, info) => { 19 | // if (err) console.error(err) 20 | // console.log(info) 21 | // }) 22 | 23 | export function obtenerDatosPromise() { 24 | return new Promise((resolve) => { 25 | setTimeout(() => { 26 | resolve({ data: 'datos importantes' }); 27 | }, 2000); 28 | }) 29 | } 30 | 31 | export function procesarArchivo(callback) { 32 | const handleReadFile = (error, contenido) => { 33 | if (error) { 34 | console.error('Error leyendo archivo:', error.message); 35 | callback(error) 36 | } 37 | 38 | const textoProcesado = contenido.toUpperCase(); 39 | 40 | fs.writeFile('output.txt', textoProcesado, handleWriteFile); 41 | } 42 | 43 | const handleWriteFile = error => { 44 | if (error) { 45 | console.error('Error guardando archivo:', error.message); 46 | callback(error) 47 | } 48 | 49 | console.log('Archivo procesado y guardado con éxito'); 50 | callback(null) 51 | } 52 | 53 | fs.readFile('input.txt', 'utf8', handleReadFile); 54 | } 55 | 56 | export async function procesarArchivoPromise() { 57 | try { 58 | const file = await fs.promises.readFile('input.txt', 'utf8') 59 | const textoProcesado = file.toUpperCase() 60 | await fs.promises.writeFile('output.txt', textoProcesado) 61 | } catch (error) { 62 | console.error('Error procesando archivo:', error.message) 63 | throw error 64 | } 65 | } 66 | 67 | // export function leerArchivos() { 68 | // const archivo1 = fs.readSync('archivo1.txt', 'utf8'); 69 | // const archivo2 = fs.readSync('archivo2.txt', 'utf8'); 70 | // const archivo3 = fs.readSync('archivo3.txt', 'utf8'); 71 | 72 | // return `${archivo1} ${archivo2} ${archivo3}` 73 | // } 74 | 75 | // leerArchivos(); 76 | 77 | export async function leerArchivos() { 78 | console.time('leerArchivos') 79 | const [a, b, c] = await Promise.all([ 80 | fs.promises.readFile('archivo1.txt', 'utf8'), 81 | fs.promises.readFile('archivo2.txt', 'utf8'), 82 | fs.promises.readFile('archivo3.txt', 'utf8') 83 | ]) 84 | console.timeEnd('leerArchivos') 85 | 86 | return `${a} ${b} ${c}` 87 | } 88 | 89 | 90 | export async function delay (ms) { 91 | return new Promise((resolve) => { 92 | setTimeout(() => { 93 | resolve() 94 | }, ms) 95 | }) 96 | } -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/already-solved/server.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | 3 | export const app = express() 4 | app.use(express.json()) 5 | 6 | const items = [{ 7 | id: 1, 8 | content: 'Item 1' 9 | }] 10 | 11 | // GET /items 12 | // Retorna todos los items 13 | app.get('/items', (req, res) => { 14 | return res.json(items) 15 | }) 16 | 17 | // GET /items/:id 18 | // Retorna un item por su id 19 | app.get('/items/:id', (req, res) => { 20 | const { id } = req.params 21 | const item = items.find(item => item.id === +id) 22 | return res.json(item) 23 | }) 24 | 25 | // POST /items 26 | app.post('/items', (req, res) => { 27 | const { content } = req.body 28 | const newId = items.length + 1 29 | const newItem = { id: newId, content } 30 | items.push(newItem) 31 | return res.json(newItem) 32 | }) 33 | 34 | // PUT /items/:id 35 | app.put('/items/:id', (req, res) => { 36 | const { id } = req.params 37 | const { content } = req.body 38 | const item = items.find(item => item.id === +id) 39 | item.content = content 40 | return res.json(item) 41 | }) 42 | 43 | // DELETE /items/:id 44 | app.delete('/items/:id', (req, res) => { 45 | const { id } = req.params 46 | const itemIndex = items.findIndex(item => item.id === +id) 47 | items.splice(itemIndex, 1) 48 | return res.status(200).json() 49 | }) 50 | 51 | export const server = app.listen(3000) -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/archivo1.txt: -------------------------------------------------------------------------------- 1 | hola -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/archivo2.txt: -------------------------------------------------------------------------------- 1 | qué -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/archivo3.txt: -------------------------------------------------------------------------------- 1 | tal -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/input.txt: -------------------------------------------------------------------------------- 1 | gogogo -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prueba-tecnica-javascript-node", 3 | "module": "index.ts", 4 | "type": "module", 5 | "scripts": { 6 | "test": "node --test" 7 | }, 8 | "devDependencies": { 9 | "supertest": "^6.3.3" 10 | }, 11 | "dependencies": { 12 | "express": "4.18.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/solutions/dotenv.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/live-coding/01-node-callbacks-promises/solutions/dotenv.js -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/solutions/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/live-coding/01-node-callbacks-promises/solutions/index.js -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/solutions/server.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/live-coding/01-node-callbacks-promises/solutions/server.js -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/test/dotenv.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, afterEach } from 'node:test' 2 | import { equal } from 'node:assert/strict' 3 | import { unlinkSync, writeFileSync } from 'node:fs' 4 | import { createRequire } from 'node:module' 5 | import { config } from '../solutions/dotenv.js' 6 | 7 | describe('1. dotenv', () => { 8 | beforeEach(() => { 9 | // clean process.env 10 | for (const key of Object.keys(process.env)) { 11 | delete process.env[key] 12 | } 13 | }) 14 | 15 | afterEach(() => { 16 | try { 17 | unlinkSync('.env') 18 | } catch {} 19 | 20 | try { 21 | unlinkSync('./test/.env.local') 22 | } catch {} 23 | }) 24 | 25 | it('1.1. load .env file', () => { 26 | // create .env file in root directory 27 | writeFileSync('.env', 'PORT=3000\nTOKEN="123abc"') 28 | config() 29 | 30 | equal(process.env.PORT, '3000') 31 | equal(process.env.TOKEN, '123abc') 32 | }) 33 | 34 | it('1.2. load .env file from custom path', () => { 35 | // create .env file in root directory 36 | writeFileSync('./test/.env.local', 'PORT=3000\nTOKEN="123abc"') 37 | config({ path: './test/.env.local' }) 38 | 39 | equal(process.env.PORT, '3000') 40 | equal(process.env.TOKEN, '123abc') 41 | }) 42 | 43 | it('1.3 it works even without .env file', () => { 44 | config() 45 | equal(process.env.TOKEN, undefined) 46 | }) 47 | 48 | it('1.4 dont use dotenv dependency', () => { 49 | // check that dotenv dependency is not installed 50 | try { 51 | const require = createRequire(import.meta.url) 52 | require('dotenv') 53 | } catch (error) { 54 | equal(error.code, 'MODULE_NOT_FOUND') 55 | } 56 | }) 57 | }) -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/test/index.test.js: -------------------------------------------------------------------------------- 1 | import { ping, obtenerDatosPromise, procesarArchivoPromise, procesarArchivo, leerArchivos } from "../solutions/index.js"; 2 | 3 | import { describe, it, beforeEach, afterEach } from 'node:test' 4 | import { equal, ifError } from 'node:assert/strict' 5 | import { unlinkSync, writeFileSync } from 'node:fs' 6 | import { readFile } from 'node:fs/promises' 7 | import { createRequire } from 'node:module' 8 | 9 | describe('1. ping', () => { 10 | it('1.1. ping midu.dev', (_, done) => { 11 | ping('midu.dev', (err, info) => { 12 | ifError(err) 13 | equal(info.ip, 'midu.dev') 14 | done() 15 | }) 16 | }) 17 | }) 18 | 19 | describe('2. obtenerDatosPromise', () => { 20 | it('2.1. obtenerDatosPromise', async () => { 21 | const { data } = await obtenerDatosPromise({ time: 1 }) 22 | equal(data, 'datos importantes') 23 | }) 24 | }) 25 | 26 | describe('3. procesarArchivoPromise', () => { 27 | afterEach(() => { 28 | try { 29 | unlinkSync('output.txt') 30 | } catch {} 31 | }) 32 | 33 | it('3.1. procesarArchivo', (t, done) => { 34 | writeFileSync('input.txt', 'gogogo') 35 | procesarArchivo((err) => { 36 | ifError(err) 37 | readFile('output.txt', 'utf8') 38 | .then((contenido) => { 39 | equal(contenido, 'GOGOGO') 40 | done() 41 | }) 42 | }) 43 | }) 44 | 45 | // it('3.1. procesarArchivoPromise', async () => { 46 | // writeFileSync('input.txt', 'hola') 47 | // await procesarArchivoPromise() 48 | // const contenido = await readFile('output.txt', 'utf8') 49 | // equal(contenido, 'HOLA') 50 | // }) 51 | }) 52 | 53 | describe('4. leerArchivos', () => { 54 | // it('4.1. leerArchivos', () => { 55 | // const mensaje = leerArchivos() 56 | // equal(mensaje, 'hola qué tal') 57 | // }) 58 | 59 | it('4.1. leerArchivos', async () => { 60 | const mensaje = await leerArchivos() 61 | equal(mensaje, 'hola qué tal') 62 | }) 63 | }) -------------------------------------------------------------------------------- /live-coding/01-node-callbacks-promises/test/server.test.js: -------------------------------------------------------------------------------- 1 | import { describe, it, after } from 'node:test' 2 | import { equal, deepStrictEqual } from 'node:assert/strict' 3 | import request from 'supertest' 4 | 5 | import { app, server } from '../solutions/server.js' 6 | 7 | describe('Items Routes', () => { 8 | let itemId = null 9 | 10 | after(() => { 11 | server.close() 12 | }) 13 | 14 | it('should fetch all tasks', async () => { 15 | const response = await request(app).get('/items') 16 | 17 | equal(response.statusCode, 200) 18 | equal(Array.isArray(response.body), true) 19 | equal(response.body.length, 1) 20 | equal(response.body[0].content, 'Item 1') 21 | }) 22 | 23 | it('should add a new item', async () => { 24 | const response = await request(app) 25 | .post('/items') 26 | .send({ 27 | content: 'Test item' 28 | }) 29 | 30 | equal(response.statusCode, 200) 31 | equal(response.body.content, 'Test item') 32 | itemId = response.body.id 33 | 34 | const { statusCode, body } = await request(app).get(`/items/${itemId}`) 35 | equal(statusCode, 200) 36 | equal(body.content, 'Test item') 37 | equal(body.id, itemId) 38 | }) 39 | 40 | it('should delete a task', async () => { 41 | const { statusCode } = await request(app).delete(`/items/${itemId}`) 42 | equal(statusCode, 200) 43 | }) 44 | 45 | it('should have no tasks after deletion', async () => { 46 | const response = await request(app).get('/items') 47 | 48 | equal(response.statusCode, 200) 49 | deepStrictEqual(response.body, [{ 50 | id: 1, 51 | content: 'Item 1' 52 | }]) 53 | }) 54 | }) -------------------------------------------------------------------------------- /live-coding/02-add-items-react/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/README.md: -------------------------------------------------------------------------------- 1 | https://gist.github.com/brandovidal/153d30bb6f639ad26e1796bb010af8c8#ejercicio-1-a%C3%B1adir-y-eliminar-elementos-de-una-lista-react 2 | 3 | Ejercicio 1. Añadir y eliminar elementos de una lista (React) 4 | Requisitos: Tener instalado Nodejs (v16.x.x o superior). Tener instalado npm. 5 | 6 | - Duración máxima: 40 minutos 7 | 8 | - Enunciado: 9 | 10 | Crear una app en React que implemente un campo de texto y botón para añadir un elemento. 11 | 12 | `````` 13 | Cuando se hace click en el botón, el texto en el campo de entrada debe agregarse a continuación en una lista de elementos. 14 | 15 | Además, cada vez que se hace click en cualquier elemento de la lista, debe eliminarse de la lista. 16 | ``` 17 | 18 | - [] Dar importancia a la funcionalidad y usabilidad, más que al diseño visual. 19 | [] El código debe ser enteramente desarrollado en Typescript. -------------------------------------------------------------------------------- /live-coding/02-add-items-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prueba técnica de React 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-add-items-react", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "test": "vitest", 11 | "preview": "vite preview" 12 | }, 13 | "dependencies": { 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0" 16 | }, 17 | "devDependencies": { 18 | "@testing-library/react": "^14.0.0", 19 | "@testing-library/user-event": "^14.5.1", 20 | "@types/react": "^18.2.15", 21 | "@types/react-dom": "^18.2.7", 22 | "@typescript-eslint/eslint-plugin": "^6.0.0", 23 | "@typescript-eslint/parser": "^6.0.0", 24 | "@vitejs/plugin-react-swc": "^3.3.2", 25 | "eslint": "^8.45.0", 26 | "eslint-plugin-react-hooks": "^4.6.0", 27 | "eslint-plugin-react-refresh": "^0.4.3", 28 | "happy-dom": "^12.9.1", 29 | "typescript": "^5.0.2", 30 | "vite": "^4.4.5", 31 | "vitest": "^0.34.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | width: 100%; 6 | } 7 | 8 | main { 9 | display: grid; 10 | grid-template-columns: 450px 1fr; 11 | gap: 64px; 12 | } 13 | 14 | h1 { 15 | font-size: 2em; 16 | line-height: 1.1; 17 | margin: 0; 18 | } 19 | 20 | h2 { 21 | font-weight: 500; 22 | font-size: 1.1em; 23 | line-height: 1.1; 24 | margin: 0; 25 | opacity: .8; 26 | } 27 | 28 | form { 29 | margin-top: 64px; 30 | display: flex; 31 | flex-direction: column; 32 | gap: 4px; 33 | } 34 | 35 | label { 36 | font-size: .8em; 37 | } -------------------------------------------------------------------------------- /live-coding/02-add-items-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | import { Item } from './components/Item' 3 | import { useItems } from './hooks/useItems' 4 | import { useSEO } from './hooks/useSEO' 5 | 6 | export type ItemId = `${string}-${string}-${string}-${string}-${string}` 7 | export interface Item { 8 | id: ItemId 9 | timestamp: number 10 | text: string 11 | } 12 | 13 | function App() { 14 | const { items, addItem, removeItem } = useItems() 15 | 16 | useSEO({ 17 | title: `[${items.length}] Prueba técnica de React`, 18 | description: 'Añadir y eliminar elementos de una lista' 19 | }) 20 | 21 | const handleSubmit = (event: React.FormEvent) => { 22 | event.preventDefault() 23 | 24 | // e.target.value -> para escuchar el onChange de un INPUT 25 | 26 | const { elements } = event.currentTarget 27 | 28 | // estrategia 1, trampa de TypeScript 29 | // no os lo recomiendo: 30 | // const input = elements.namedItem('item') as HTMLInputElement 31 | 32 | // estrategia 2, es asegurarse que realmente es lo que es 33 | const input = elements.namedItem('item') 34 | const isInput = input instanceof HTMLInputElement // JavaScript puro 35 | if (!isInput || input == null) return 36 | 37 | addItem(input.value) 38 | 39 | input.value = '' 40 | } 41 | 42 | const createHandleRemoveItem = (id: ItemId) => () => { 43 | removeItem(id) 44 | } 45 | 46 | return ( 47 |
48 | 65 | 66 |
67 |

Lista de elementos

68 | { 69 | items.length === 0 ? ( 70 |

71 | No hay elementos en la lista. 72 |

73 | ) : ( 74 |
    75 | { 76 | items.map((item) => { 77 | return ( 78 | 82 | ) 83 | }) 84 | } 85 |
86 | ) 87 | } 88 |
89 |
90 | ) 91 | } 92 | 93 | export default App 94 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/src/components/Item.tsx: -------------------------------------------------------------------------------- 1 | export function Item ( 2 | { text, handleClick }: 3 | { text: string, handleClick: () => void 4 | }) { 5 | return ( 6 |
  • 7 | {text} 8 | 11 |
  • 12 | ) 13 | } -------------------------------------------------------------------------------- /live-coding/02-add-items-react/src/hooks/useItems.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { type Item } from "../App" 3 | 4 | export const useItems = () => { 5 | const [items, setItems] = useState([]) 6 | 7 | const addItem = (text: string) => { 8 | const newItem: Item = { 9 | id: crypto.randomUUID(), 10 | text, 11 | timestamp: Date.now() 12 | } 13 | 14 | setItems((prevItems) => { 15 | return [...prevItems, newItem] 16 | }) 17 | } 18 | 19 | const removeItem = (id: string) => { 20 | setItems((prevItems) => { 21 | return prevItems.filter((item) => item.id !== id) 22 | }) 23 | } 24 | 25 | return { 26 | items, 27 | addItem, 28 | removeItem 29 | } 30 | } -------------------------------------------------------------------------------- /live-coding/02-add-items-react/src/hooks/useSEO.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | 3 | export function useSEO ( 4 | { title, description }: 5 | { title: string, description: string } 6 | ) { 7 | 8 | useEffect(() => { 9 | document.title = title; 10 | document 11 | .querySelector('meta[name="description"]') 12 | ?.setAttribute("content", description); 13 | }, [title, description]) 14 | 15 | } -------------------------------------------------------------------------------- /live-coding/02-add-items-react/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | -webkit-text-size-adjust: 100%; 15 | } 16 | 17 | a { 18 | font-weight: 500; 19 | color: #646cff; 20 | text-decoration: inherit; 21 | } 22 | 23 | a:hover { 24 | color: #535bf2; 25 | } 26 | 27 | body { 28 | margin: 0; 29 | display: flex; 30 | place-items: center; 31 | min-width: 320px; 32 | min-height: 100vh; 33 | } 34 | 35 | h1 { 36 | font-size: 3.2em; 37 | line-height: 1.1; 38 | } 39 | 40 | button, input { 41 | border-radius: 8px; 42 | border: 1px solid transparent; 43 | padding: 0.6em 1.2em; 44 | font-size: 1em; 45 | font-weight: 500; 46 | font-family: inherit; 47 | background-color: #1a1a1a; 48 | cursor: pointer; 49 | transition: border-color 0.25s; 50 | } 51 | button:hover { 52 | border-color: #646cff; 53 | } 54 | button:focus, 55 | button:focus-visible { 56 | outline: 4px auto -webkit-focus-ring-color; 57 | } 58 | 59 | input { 60 | background: #222; 61 | border-radius: 4px; 62 | cursor: default; 63 | border: 1px solid #555; 64 | display: block; 65 | width: 50%; 66 | margin-bottom: 32px; 67 | } 68 | 69 | @media (prefers-color-scheme: light) { 70 | :root { 71 | color: #213547; 72 | background-color: #ffffff; 73 | } 74 | a:hover { 75 | color: #747bff; 76 | } 77 | button { 78 | background-color: #f9f9f9; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.tsx' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')!).render( 7 | 8 | 9 | , 10 | ) 11 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/tests/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import userEvent from '@testing-library/user-event' 3 | import { describe, test, expect } from 'vitest' 4 | import { render, screen } from '@testing-library/react' 5 | import App from '../src/App' 6 | 7 | describe('', () => { 8 | // test('should work', () => { 9 | // const { getByText } = render() 10 | 11 | // expect( 12 | // getByText(/Angular/i) 13 | // ).toBeDefined() 14 | // }) 15 | 16 | test('should add items and remove them', async () => { 17 | const user = userEvent.setup() 18 | 19 | render() 20 | 21 | // buscar el input 22 | const input = screen.getByRole('textbox') 23 | expect(input).toBeDefined() 24 | 25 | // buscar el form 26 | const form = screen.getByRole('form') 27 | expect(form).toBeDefined() 28 | 29 | const button = form.querySelector('button') 30 | expect(button).toBeDefined() 31 | 32 | const randomText = crypto.randomUUID() 33 | await user.type(input, randomText) 34 | await user.click(button!) 35 | 36 | // asegurar que el elemento se ha agregado 37 | const list = screen.getByRole('list') 38 | expect(list).toBeDefined() 39 | expect(list.childNodes.length).toBe(1) 40 | 41 | // asegurarnos que lo podemos borrar 42 | const item = screen.getByText(randomText) 43 | const removeButton = item.querySelector('button') 44 | expect(removeButton).toBeDefined() 45 | 46 | await user.click(removeButton!) 47 | 48 | const noResults = screen.getByText('No hay elementos en la lista.') 49 | expect(noResults).toBeDefined() 50 | }) 51 | }) -------------------------------------------------------------------------------- /live-coding/02-add-items-react/tests/useItems.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "vitest"; 2 | import { renderHook, act } from "@testing-library/react"; 3 | import { useItems } from "../src/hooks/useItems"; 4 | 5 | describe('useItems hook', () => { 6 | test('should add and remove items', () => { 7 | const { result } = renderHook(() => useItems()) 8 | 9 | expect(result.current.items.length).toBe(0) 10 | 11 | act(() => { 12 | result.current.addItem('Jugar a videojuegos') 13 | result.current.addItem('Ir a correr') 14 | }) 15 | 16 | expect(result.current.items.length).toBe(2) 17 | 18 | act(() => { 19 | result.current.removeItem(result.current.items[0].id) 20 | }) 21 | 22 | expect(result.current.items.length).toBe(1) 23 | }) 24 | }) -------------------------------------------------------------------------------- /live-coding/02-add-items-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /live-coding/02-add-items-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import { defineConfig } from 'vite' 4 | import react from '@vitejs/plugin-react-swc' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [react()], 9 | test: { 10 | environment: 'happy-dom' 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pruebas-tecnicas", 3 | "version": "1.0.0", 4 | "description": "Pruebas técnicas de programación para desarrolladores frontend y backend.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "cd web && npm run build", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/midudev/pruebas-tecnicas.git" 13 | }, 14 | "keywords": [], 15 | "author": "", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/midudev/pruebas-tecnicas/issues" 19 | }, 20 | "homepage": "https://github.com/midudev/pruebas-tecnicas#readme" 21 | } 22 | -------------------------------------------------------------------------------- /pruebas/01-reading-list/README.md: -------------------------------------------------------------------------------- 1 | # 01 - Desarrollo de una Aplicación de Lista de Libros 2 | 3 | El objetivo de esta prueba es diseñar e implementar una pequeña aplicación web de lista de libros utilizando las herramientas de tu elección. 4 | 5 | - [¿Cómo puedo participar?](https://github.com/midudev/pruebas-tecnicas#c%C3%B3mo-participar) 6 | - **La prueba está abierta a revisión hasta el 27 de julio de 2023** 7 | - Prueba basada en [esta prueba real para Juniors](https://discord.com/channels/741237973663612969/848944161448132628/1127729621744500806). 8 | 9 | Este proyecto busca probar tus habilidades en el manejo de interacciones con el usuario, gestión del estado, filtrado de datos y la estructuración del código. 10 | ![Sin título-2023-03-24-0943 (1)](https://github.com/midudev/pruebas-tecnicas/assets/1561955/a829323d-07e6-4937-91c6-5498481148c5) 11 | 12 | ## Contexto 13 | 14 | Somos un sello editorial de libros multinacional. Queremos ofrecer a nuestro público una forma de ver nuestro catálogo y poder guardar los libros que les interesan en una lista de lectura. 15 | 16 | Para ello, queremos desarrollar una aplicación web que permita a los usuarios ver los libros disponibles y crear una lista de lectura. Ten en cuenta que: 17 | 18 | - No sabemos si el framework que utilicemos ahora será el definitivo, pero querremos reutilizar el máximo de código posible. 19 | - La aplicación debe ser fácil de usar y agradable a la vista. 20 | - Tenemos un 80% de usuarios que vienen de navegadores de escritorio. 21 | 22 | Usa el archivo `books.json` para obtener los datos de los libros. Puedes añadir más libros si lo deseas, siempre y cuando siga la misma estructura. 23 | 24 | ## Requisitos 25 | 26 | ### Funcionalidad 27 | 28 | 1. **Visualización de Libros Disponibles**: La aplicación debe mostrar una lista de libros disponibles que el usuario pueda revisar. 29 | 30 | 2. **Creación de Lista de Lectura**: El usuario debe ser capaz de crear una lista de lectura a partir de los libros disponibles. En la UI debe quedar claro qué libros están en la lista de lectura y cuáles no. También debe ser posible mover un libro de la lista de lectura a la lista de disponibles. 31 | 32 | 3. **Filtrado de Libros por Género**: Los usuarios deben poder filtrar la lista de libros disponibles por género, y se mostrará un contador con el número de libros disponibles, el número de libros en la lista de lectura y el número de libros disponibles en el género seleccionado. 33 | 34 | 4. **Sincronización de Estado**: Debe haber una sincronización del estado global que refleje el número de libros en la lista de lectura y el número de libros todavía disponibles. Si un libro se mueve de la lista de disponibles a la lista de lectura, el recuento de ambos debe actualizarse en consecuencia. 35 | 36 | 5. **Persistencia de Datos**: La aplicación debe persistir los datos de la lista de lectura en el almacenamiento local del navegador. Al recargar la página, la lista de lectura debe mantenerse. 37 | 38 | 6. **Sincronización entre pestañas**: Si el usuario abre la aplicación en dos pestañas diferentes, los cambios realizados en una pestaña deben reflejarse en la otra. Sin necesidad de usar Backend. 39 | 40 | 7. **Despliegue**: La aplicación debe estar desplegada en algún servicio de hosting gratuito (Netlify, Vercel, Firebase, etc) y debe ser accesible a través de una URL pública. Indica la URL en el README. 41 | 42 | 8. **Test**: La aplicación debe tener AL MENOS un test. Haz el test que consideres más importante para tu aplicación. 43 | 44 | ## Consejos sobre el código 45 | 46 | 1. **Estructura del código**: El código debe estar bien organizado y fácil de leer. 47 | 48 | 2. **Semántica HTML**: El HTML debe ser semántico y accesible. 49 | 50 | 3. **Pensando en equipo**: Prepara tu proyecto pensando que cualquier persona de tu equipo puede tener que trabajar en él en el futuro. (scripts en el package.json, mínima documentación en el README, comentarios en el código si es necesario, etc) 51 | 52 | 4. **Formatea tu código**: Asegúrate de que tu código está formateado de forma consistente. Puedes usar Prettier o cualquier otra herramienta que te guste. 53 | 54 | 5. **Preparado para producción**: Asegúrate de que tu aplicación está lista para producción. Minimiza el código, optimiza las imágenes, etc. 55 | 56 | ## Desafíos adicionales 57 | 58 | **¿Quieres ir más allá?** Estos son algunos desafíos adicionales que puedes intentar: 59 | 60 | - Implementar una funcionalidad de búsqueda en la lista de libros disponibles. 61 | - Añade un nuevo filtro para filtrar los libros por número de páginas. 62 | - Permitir la reorganización de los libros en la lista de lectura por prioridad. 63 | - Haz que tu diseño sea responsive. 64 | 65 | ## Entrevista 66 | 67 | Si pasas a la siguiente fase, te pediremos que hagas una entrevista con nosotros. Durante la entrevista, te pediremos que expliques tu código y que hagas algunos cambios en el mismo. 68 | 69 | - Nos tendrás que explicar el código que has escrito y las decisiones que has tomado. 70 | - Haremos cambios en el JSON y tendrás que adaptar el código en vivo. 71 | - Añadiremos un nuevo filtro a la aplicación y tendrás que implementarlo. 72 | 73 | Buena suerte y ¡diviértete programando! 74 | 75 | ## Referencias 76 | 77 | - Diseño de Josh W. Comeau para una aplicación de libros pendientes de leer: https://twitter.com/JoshWComeau/status/1678893330480898049 78 | 79 | - Dribbble con rediseño de Goodreads: https://dribbble.com/shots/2523654-Books-listing-page-goodreads 80 | 81 | - Concepto de uso de arrastrar libros: https://dribbble.com/shots/19351938-Mybooks-Page-Board 82 | 83 | - Concepto de landing para una aplicación de libros: https://dribbble.com/shots/16279204-Book-Web-Store-Concept 84 | -------------------------------------------------------------------------------- /pruebas/01-reading-list/books.json: -------------------------------------------------------------------------------- 1 | { 2 | "library": [ 3 | { 4 | "book": { 5 | "title": "El Señor de los Anillos", 6 | "pages": 1200, 7 | "genre": "Fantasía", 8 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1566425108i/33.jpg", 9 | "synopsis": "Una aventura épica en un mundo de fantasía llamado la Tierra Media.", 10 | "year": 1954, 11 | "ISBN": "978-0618640157", 12 | "author": { 13 | "name": "J.R.R. Tolkien", 14 | "otherBooks": [ 15 | "El Hobbit", 16 | "El Silmarillion" 17 | ] 18 | } 19 | } 20 | }, 21 | { 22 | "book": { 23 | "title": "Juego de Tronos", 24 | "pages": 694, 25 | "genre": "Fantasía", 26 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1273763400i/8189620.jpg", 27 | "synopsis": "En un reino donde las estaciones duran años, una batalla épica por el trono se desarrolla.", 28 | "year": 1996, 29 | "ISBN": "978-0553103540", 30 | "author": { 31 | "name": "George R. R. Martin", 32 | "otherBooks": [ 33 | "Choque de Reyes", 34 | "Tormenta de Espadas", 35 | "Festín de Cuervos" 36 | ] 37 | } 38 | } 39 | }, 40 | { 41 | "book": { 42 | "title": "Harry Potter y la piedra filosofal", 43 | "pages": 223, 44 | "genre": "Fantasía", 45 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1550337333i/15868.jpg", 46 | "synopsis": "Un niño descubre que es un mago y comienza una aventura en una escuela de magia.", 47 | "year": 1997, 48 | "ISBN": "978-0747532699", 49 | "author": { 50 | "name": "J.K. Rowling", 51 | "otherBooks": [ 52 | "Harry Potter y la cámara secreta", 53 | "Harry Potter y el prisionero de Azkaban", 54 | "Harry Potter y el cáliz de fuego" 55 | ] 56 | } 57 | } 58 | }, 59 | { 60 | "book": { 61 | "title": "1984", 62 | "pages": 328, 63 | "genre": "Ciencia ficción", 64 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1657781256i/61439040.jpg", 65 | "synopsis": "Una inquietante visión de un futuro distópico y totalitario.", 66 | "year": 1949, 67 | "ISBN": "978-0451524935", 68 | "author": { 69 | "name": "George Orwell", 70 | "otherBooks": [ 71 | "Rebelión en la granja" 72 | ] 73 | } 74 | } 75 | }, 76 | { 77 | "book": { 78 | "title": "Apocalipsis Zombie", 79 | "pages": 444, 80 | "genre": "Zombies", 81 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1422626176i/24762432.jpg", 82 | "synopsis": "Un gallego se queda en casa en pleno apocalipsis zombie y acaba casi salvando el mundo", 83 | "year": 2001, 84 | "ISBN": "978-4444532611", 85 | "author": { 86 | "name": "Manel Loreiro", 87 | "otherBooks": [] 88 | } 89 | } 90 | }, 91 | { 92 | "book": { 93 | "title": "Dune", 94 | "pages": 412, 95 | "genre": "Ciencia ficción", 96 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1555447414i/44767458.jpg", 97 | "synopsis": "En el inhóspito planeta desértico de Arrakis, una gran intriga política y familiar se desarrolla.", 98 | "year": 1965, 99 | "ISBN": "978-0441172719", 100 | "author": { 101 | "name": "Frank Herbert", 102 | "otherBooks": [ 103 | "El mesías de Dune", 104 | "Hijos de Dune" 105 | ] 106 | } 107 | } 108 | }, 109 | { 110 | "book": { 111 | "title": "La Guía del Autoestopista Galáctico", 112 | "pages": 216, 113 | "genre": "Ciencia ficción", 114 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1653311117i/6691227.jpg", 115 | "synopsis": "Un viaje absurdo y cómico por el espacio, con toallas.", 116 | "year": 1979, 117 | "ISBN": "978-0345391803", 118 | "author": { 119 | "name": "Douglas Adams", 120 | "otherBooks": [ 121 | "El restaurante del fin del mundo", 122 | "La vida, el universo y todo lo demás" 123 | ] 124 | } 125 | } 126 | }, 127 | { 128 | "book": { 129 | "title": "Neuromante", 130 | "pages": 271, 131 | "genre": "Ciencia ficción", 132 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1554437249i/6088007.jpg", 133 | "synopsis": "Una visión profética de la ciber-realidad y la inteligencia artificial.", 134 | "year": 1984, 135 | "ISBN": "978-0441569595", 136 | "author": { 137 | "name": "William Gibson", 138 | "otherBooks": [ 139 | "Conde Cero", 140 | "Mona Lisa Acelerada" 141 | ] 142 | } 143 | } 144 | }, 145 | { 146 | "book": { 147 | "title": "Fahrenheit 451", 148 | "pages": 249, 149 | "genre": "Ciencia ficción", 150 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1383718290i/13079982.jpg", 151 | "synopsis": "Una sociedad futura donde los libros están prohibidos y 'bomberos' queman cualquier libro que encuentren.", 152 | "year": 1953, 153 | "ISBN": "978-1451673319", 154 | "author": { 155 | "name": "Ray Bradbury", 156 | "otherBooks": [ 157 | "Crónicas marcianas", 158 | "El hombre ilustrado" 159 | ] 160 | } 161 | } 162 | }, 163 | { 164 | "book": { 165 | "title": "El resplandor", 166 | "pages": 688, 167 | "genre": "Terror", 168 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1641398308i/60038757.jpg", 169 | "synopsis": "Una familia se muda a un hotel aislado para el invierno donde una presencia siniestra influye en el padre hacia la violencia.", 170 | "year": 1977, 171 | "ISBN": "978-0307743657", 172 | "author": { 173 | "name": "Stephen King", 174 | "otherBooks": [ 175 | "Carrie", 176 | "It" 177 | ] 178 | } 179 | } 180 | }, 181 | { 182 | "book": { 183 | "title": "Drácula", 184 | "pages": 418, 185 | "genre": "Terror", 186 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1387151694i/17245.jpg", 187 | "synopsis": "La historia del infame conde Drácula y su intento de mudarse de Transilvania a Inglaterra.", 188 | "year": 1897, 189 | "ISBN": "978-0486411095", 190 | "author": { 191 | "name": "Bram Stoker", 192 | "otherBooks": [ 193 | "La joya de las siete estrellas", 194 | "La madriguera del gusano blanco" 195 | ] 196 | } 197 | } 198 | }, 199 | { 200 | "book": { 201 | "title": "Frankenstein", 202 | "pages": 280, 203 | "genre": "Terror", 204 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1669159060i/63631742.jpg", 205 | "synopsis": "Un científico obsesionado crea una criatura viva a partir de partes de cuerpos robadas, con consecuencias desastrosas.", 206 | "year": 1818, 207 | "ISBN": "978-0486282114", 208 | "author": { 209 | "name": "Mary Shelley", 210 | "otherBooks": [ 211 | "El último hombre", 212 | "Valperga" 213 | ] 214 | } 215 | } 216 | }, 217 | { 218 | "book": { 219 | "title": "La llamada de Cthulhu", 220 | "pages": 43, 221 | "genre": "Terror", 222 | "cover": "https://images-na.ssl-images-amazon.com/images/S/compressed.photo.goodreads.com/books/1485924654i/34094154.jpg", 223 | "synopsis": "La historia de un monstruo ancestral que amenaza con revivir y dominar el mundo.", 224 | "year": 1928, 225 | "ISBN": "978-1542461690", 226 | "author": { 227 | "name": "H.P. Lovecraft", 228 | "otherBooks": [ 229 | "El horror de Dunwich", 230 | "En las montañas de la locura" 231 | ] 232 | } 233 | } 234 | } 235 | ]} 236 | -------------------------------------------------------------------------------- /pruebas/02-bazar-universal/README.md: -------------------------------------------------------------------------------- 1 | # 02 - Desarrollo Full Stack de Bazar 2 | 3 | 4 | 5 | Info: 6 | - [¿Cómo puedo participar?](https://github.com/midudev/pruebas-tecnicas#c%C3%B3mo-participar) 7 | - Basada en la prueba real usada en **Mercado Libre.** 8 | 9 | 10 | 11 | ![Bazar Online](https://github.com/midudev/pruebas-tecnicas/assets/1561955/d5f5872c-246d-464e-b09a-1278ab5bfbb3) 12 | 13 | Debes crear una aplicación que consta de **3 componentes principales**: 14 | 15 | - Una caja de búsqueda 16 | - La lista de resultados 17 | - Descripción del detalle del producto 18 | 19 | La aplicación debe constar de servidor y cliente. El servidor debe exponer un API RESTful y el cliente consumirlo. 20 | 21 | ## Contexto 22 | 23 | Somos un bazar con todo tipo de productos. Queremos crear nuestra app web. Por ahora **nuestro mercado va a ser el móvil**. 24 | 25 | Esta primera versión los usuarios podrán buscar el nombre del producto, le mostraremos una lista de productos y podrán hacer clic en cada uno para ver el detalle. 26 | 27 | Ten en cuenta: 28 | - No sabemos si el framework que utilicemos ahora será el definitivo, pero querremos reutilizar el máximo de código posible. 29 | 30 | - La aplicación debe ser fácil de usar y **agradable a la vista**. No importa si copias el diseño o usas un catálogo de componentes. 31 | 32 | - **Es MUY importante el SEO de la aplicación**. Así que el robot de Google debe poder ver bien nuestra página, navegarla sin problemas y el rendimiento debe ser el adecuado. 33 | 34 | - Queremos también que los usuarios puedan compartir los productos en redes sociales. 35 | 36 | ## Requisitos 37 | 38 | ### Funcionalidad 39 | 40 | 1. **Crea las 3 páginas**: Inicio con caja de búsqueda, resultados de búsqueda y detalle. 41 | 42 | 2. **Las rutas de las páginas serán**: 43 | - Home con caja de búsqueda 44 | - Ruta: `/` 45 | - Descripción: Simplemente muestra una caja de búsqueda para poder hacer la búsqueda de productos. Al realizar la búsqueda navegar a la vista de Resultados de búsqueda. 46 | 47 | - Resultados de búsqueda: 48 | - Ruta: `/items?search=`, por ejemplo: `/items/?search=laptop` 49 | - Descripción: Muestra justo debajo de la caja de búsqueda, el número de resultados y también los resultados que muestra para cada categoría. En cada tarjeta de los resultados muestra: título, descripción, precio, categoría, imagen y puntuación. 50 | 51 | - Detalle de producto: "/items/:id" 52 | - Ruta: `/items/:id` 53 | - Descripción: Muestra la descripción completa del producto, incluyendo todos los detalles que tengas: precio, descripción, marca, stock, categoría, etc. Muestra todas las imágenes. También un botón para poder realizar la compra (aunque no funcione) 54 | 55 | 3. **API**: Debes crear dos endpoints, debes basarte en el contenido del archivo `products.json` que tienes en este repositorio pero no tienes por qué seguir ese esquema. Los endpoints a crear son: 56 | - `/api/items?q=:query` donde `:query` es la búsqueda que hace el usuario. Debe devolver un JSON con los datos a mostrar en la lista de items. 57 | - `/api/items/:id`, donde `:id` es el id del producto seleccionado. Debe devolver un JSON con los datos del item seleccionado. 58 | 59 | 4. **Despliegue**: La aplicación debe estar desplegada en algún servicio de hosting gratuito (Netlify, Vercel, Firebase, etc) y debe ser accesible a través de una URL pública. **Indica la URL al hacer la Pull Request.** 60 | 61 | 5. **Test**: La aplicación debe tener AL MENOS un test. Haz el test que consideres más importante para tu aplicación. 62 | 63 | ## Consejos sobre el código 64 | 65 | 1. **Estructura del código**: El código debe estar bien organizado y fácil de leer. 66 | 67 | 2. **Semántica HTML**: El HTML debe ser semántico y accesible. 68 | 69 | 3. **Pensando en equipo**: Prepara tu proyecto pensando que cualquier persona de tu equipo puede tener que trabajar en él en el futuro. (scripts en el package.json, mínima documentación en el README, comentarios en el código si es necesario, etc) 70 | 71 | 4. **Formatea tu código**: Asegúrate de que tu código está formateado de forma consistente. Puedes usar Prettier o cualquier otra herramienta que te guste. 72 | 73 | 5. **Preparado para producción**: Asegúrate de que tu aplicación está lista para producción. Minimiza el código, optimiza las imágenes, etc. 74 | 75 | ## Desafíos adicionales 76 | 77 | **¿Quieres ir más allá?** Estos son algunos desafíos adicionales que puedes intentar: 78 | 79 | - Implementa la funcionalidad de carrito de la compra. 80 | - Haz que el diseño sea responsive. 81 | - Integra la paginación tanto en la API como en la web. 82 | 83 | ## Entrevista 84 | 85 | Si pasas a la siguiente fase, te pediremos que hagas una entrevista con nosotros. Durante la entrevista, te pediremos que expliques tu código y que hagas algunos cambios en el mismo. 86 | 87 | - Nos tendrás que explicar el código que has escrito y las decisiones que has tomado. 88 | - Haremos cambios en el JSON y tendrás que adaptar el código en vivo. 89 | 90 | Buena suerte y ¡diviértete programando! 91 | -------------------------------------------------------------------------------- /pruebas/02-bazar-universal/products.json: -------------------------------------------------------------------------------- 1 | { 2 | "products":[ 3 | { 4 | "id":1, 5 | "title":"iPhone 9", 6 | "description":"An apple mobile which is nothing like apple", 7 | "price":549, 8 | "discountPercentage":12.96, 9 | "rating":4.69, 10 | "stock":94, 11 | "brand":"Apple", 12 | "category":"smartphones", 13 | "thumbnail":"https://i.dummyjson.com/data/products/1/thumbnail.jpg", 14 | "images":[ 15 | "https://i.dummyjson.com/data/products/1/1.jpg", 16 | "https://i.dummyjson.com/data/products/1/2.jpg", 17 | "https://i.dummyjson.com/data/products/1/3.jpg", 18 | "https://i.dummyjson.com/data/products/1/4.jpg", 19 | "https://i.dummyjson.com/data/products/1/thumbnail.jpg" 20 | ] 21 | }, 22 | { 23 | "id":2, 24 | "title":"iPhone X", 25 | "description":"SIM-Free, Model A19211 6.5-inch Super Retina HD display with OLED technology A12 Bionic chip with ...", 26 | "price":899, 27 | "discountPercentage":17.94, 28 | "rating":4.44, 29 | "stock":34, 30 | "brand":"Apple", 31 | "category":"smartphones", 32 | "thumbnail":"https://i.dummyjson.com/data/products/2/thumbnail.jpg", 33 | "images":[ 34 | "https://i.dummyjson.com/data/products/2/1.jpg", 35 | "https://i.dummyjson.com/data/products/2/2.jpg", 36 | "https://i.dummyjson.com/data/products/2/3.jpg", 37 | "https://i.dummyjson.com/data/products/2/thumbnail.jpg" 38 | ] 39 | }, 40 | { 41 | "id":3, 42 | "title":"Samsung Universe 9", 43 | "description":"Samsung's new variant which goes beyond Galaxy to the Universe", 44 | "price":1249, 45 | "discountPercentage":15.46, 46 | "rating":4.09, 47 | "stock":36, 48 | "brand":"Samsung", 49 | "category":"smartphones", 50 | "thumbnail":"https://i.dummyjson.com/data/products/3/thumbnail.jpg", 51 | "images":[ 52 | "https://i.dummyjson.com/data/products/3/1.jpg" 53 | ] 54 | }, 55 | { 56 | "id":4, 57 | "title":"OPPOF19", 58 | "description":"OPPO F19 is officially announced on April 2021.", 59 | "price":280, 60 | "discountPercentage":17.91, 61 | "rating":4.3, 62 | "stock":123, 63 | "brand":"OPPO", 64 | "category":"smartphones", 65 | "thumbnail":"https://i.dummyjson.com/data/products/4/thumbnail.jpg", 66 | "images":[ 67 | "https://i.dummyjson.com/data/products/4/1.jpg", 68 | "https://i.dummyjson.com/data/products/4/2.jpg", 69 | "https://i.dummyjson.com/data/products/4/3.jpg", 70 | "https://i.dummyjson.com/data/products/4/4.jpg", 71 | "https://i.dummyjson.com/data/products/4/thumbnail.jpg" 72 | ] 73 | }, 74 | { 75 | "id":5, 76 | "title":"Huawei P30", 77 | "description":"Huawei’s re-badged P30 Pro New Edition was officially unveiled yesterday in Germany and now the device has made its way to the UK.", 78 | "price":499, 79 | "discountPercentage":10.58, 80 | "rating":4.09, 81 | "stock":32, 82 | "brand":"Huawei", 83 | "category":"smartphones", 84 | "thumbnail":"https://i.dummyjson.com/data/products/5/thumbnail.jpg", 85 | "images":[ 86 | "https://i.dummyjson.com/data/products/5/1.jpg", 87 | "https://i.dummyjson.com/data/products/5/2.jpg", 88 | "https://i.dummyjson.com/data/products/5/3.jpg" 89 | ] 90 | }, 91 | { 92 | "id":6, 93 | "title":"MacBook Pro", 94 | "description":"MacBook Pro 2021 with mini-LED display may launch between September, November", 95 | "price":1749, 96 | "discountPercentage":11.02, 97 | "rating":4.57, 98 | "stock":83, 99 | "brand":"Apple", 100 | "category":"laptops", 101 | "thumbnail":"https://i.dummyjson.com/data/products/6/thumbnail.png", 102 | "images":[ 103 | "https://i.dummyjson.com/data/products/6/1.png", 104 | "https://i.dummyjson.com/data/products/6/2.jpg", 105 | "https://i.dummyjson.com/data/products/6/3.png", 106 | "https://i.dummyjson.com/data/products/6/4.jpg" 107 | ] 108 | }, 109 | { 110 | "id":7, 111 | "title":"Samsung Galaxy Book", 112 | "description":"Samsung Galaxy Book S (2020) Laptop With Intel Lakefield Chip, 8GB of RAM Launched", 113 | "price":1499, 114 | "discountPercentage":4.15, 115 | "rating":4.25, 116 | "stock":50, 117 | "brand":"Samsung", 118 | "category":"laptops", 119 | "thumbnail":"https://i.dummyjson.com/data/products/7/thumbnail.jpg", 120 | "images":[ 121 | "https://i.dummyjson.com/data/products/7/1.jpg", 122 | "https://i.dummyjson.com/data/products/7/2.jpg", 123 | "https://i.dummyjson.com/data/products/7/3.jpg", 124 | "https://i.dummyjson.com/data/products/7/thumbnail.jpg" 125 | ] 126 | }, 127 | { 128 | "id":8, 129 | "title":"Microsoft Surface Laptop 4", 130 | "description":"Style and speed. Stand out on HD video calls backed by Studio Mics. Capture ideas on the vibrant touchscreen.", 131 | "price":1499, 132 | "discountPercentage":10.23, 133 | "rating":4.43, 134 | "stock":68, 135 | "brand":"Microsoft Surface", 136 | "category":"laptops", 137 | "thumbnail":"https://i.dummyjson.com/data/products/8/thumbnail.jpg", 138 | "images":[ 139 | "https://i.dummyjson.com/data/products/8/1.jpg", 140 | "https://i.dummyjson.com/data/products/8/2.jpg", 141 | "https://i.dummyjson.com/data/products/8/3.jpg", 142 | "https://i.dummyjson.com/data/products/8/4.jpg", 143 | "https://i.dummyjson.com/data/products/8/thumbnail.jpg" 144 | ] 145 | }, 146 | { 147 | "id":9, 148 | "title":"Infinix INBOOK", 149 | "description":"Infinix Inbook X1 Ci3 10th 8GB 256GB 14 Win10 Grey – 1 Year Warranty", 150 | "price":1099, 151 | "discountPercentage":11.83, 152 | "rating":4.54, 153 | "stock":96, 154 | "brand":"Infinix", 155 | "category":"laptops", 156 | "thumbnail":"https://i.dummyjson.com/data/products/9/thumbnail.jpg", 157 | "images":[ 158 | "https://i.dummyjson.com/data/products/9/1.jpg", 159 | "https://i.dummyjson.com/data/products/9/2.png", 160 | "https://i.dummyjson.com/data/products/9/3.png", 161 | "https://i.dummyjson.com/data/products/9/4.jpg", 162 | "https://i.dummyjson.com/data/products/9/thumbnail.jpg" 163 | ] 164 | }, 165 | { 166 | "id":10, 167 | "title":"HP Pavilion 15-DK1056WM", 168 | "description":"HP Pavilion 15-DK1056WM Gaming Laptop 10th Gen Core i5, 8GB, 256GB SSD, GTX 1650 4GB, Windows 10", 169 | "price":1099, 170 | "discountPercentage":6.18, 171 | "rating":4.43, 172 | "stock":89, 173 | "brand":"HP Pavilion", 174 | "category":"laptops", 175 | "thumbnail":"https://i.dummyjson.com/data/products/10/thumbnail.jpeg", 176 | "images":[ 177 | "https://i.dummyjson.com/data/products/10/1.jpg", 178 | "https://i.dummyjson.com/data/products/10/2.jpg", 179 | "https://i.dummyjson.com/data/products/10/3.jpg", 180 | "https://i.dummyjson.com/data/products/10/thumbnail.jpeg" 181 | ] 182 | }, 183 | { 184 | "id":11, 185 | "title":"perfume Oil", 186 | "description":"Mega Discount, Impression of Acqua Di Gio by GiorgioArmani concentrated attar perfume Oil", 187 | "price":13, 188 | "discountPercentage":8.4, 189 | "rating":4.26, 190 | "stock":65, 191 | "brand":"Impression of Acqua Di Gio", 192 | "category":"fragrances", 193 | "thumbnail":"https://i.dummyjson.com/data/products/11/thumbnail.jpg", 194 | "images":[ 195 | "https://i.dummyjson.com/data/products/11/1.jpg", 196 | "https://i.dummyjson.com/data/products/11/2.jpg", 197 | "https://i.dummyjson.com/data/products/11/3.jpg", 198 | "https://i.dummyjson.com/data/products/11/thumbnail.jpg" 199 | ] 200 | }, 201 | { 202 | "id":12, 203 | "title":"Brown Perfume", 204 | "description":"Royal_Mirage Sport Brown Perfume for Men & Women - 120ml", 205 | "price":40, 206 | "discountPercentage":15.66, 207 | "rating":4, 208 | "stock":52, 209 | "brand":"Royal_Mirage", 210 | "category":"fragrances", 211 | "thumbnail":"https://i.dummyjson.com/data/products/12/thumbnail.jpg", 212 | "images":[ 213 | "https://i.dummyjson.com/data/products/12/1.jpg", 214 | "https://i.dummyjson.com/data/products/12/2.jpg", 215 | "https://i.dummyjson.com/data/products/12/3.png", 216 | "https://i.dummyjson.com/data/products/12/4.jpg", 217 | "https://i.dummyjson.com/data/products/12/thumbnail.jpg" 218 | ] 219 | }, 220 | { 221 | "id":13, 222 | "title":"Fog Scent Xpressio Perfume", 223 | "description":"Product details of Best Fog Scent Xpressio Perfume 100ml For Men cool long lasting perfumes for Men", 224 | "price":13, 225 | "discountPercentage":8.14, 226 | "rating":4.59, 227 | "stock":61, 228 | "brand":"Fog Scent Xpressio", 229 | "category":"fragrances", 230 | "thumbnail":"https://i.dummyjson.com/data/products/13/thumbnail.webp", 231 | "images":[ 232 | "https://i.dummyjson.com/data/products/13/1.jpg", 233 | "https://i.dummyjson.com/data/products/13/2.png", 234 | "https://i.dummyjson.com/data/products/13/3.jpg", 235 | "https://i.dummyjson.com/data/products/13/4.jpg", 236 | "https://i.dummyjson.com/data/products/13/thumbnail.webp" 237 | ] 238 | }, 239 | { 240 | "id":14, 241 | "title":"Non-Alcoholic Concentrated Perfume Oil", 242 | "description":"Original Al Munakh® by Mahal Al Musk | Our Impression of Climate | 6ml Non-Alcoholic Concentrated Perfume Oil", 243 | "price":120, 244 | "discountPercentage":15.6, 245 | "rating":4.21, 246 | "stock":114, 247 | "brand":"Al Munakh", 248 | "category":"fragrances", 249 | "thumbnail":"https://i.dummyjson.com/data/products/14/thumbnail.jpg", 250 | "images":[ 251 | "https://i.dummyjson.com/data/products/14/1.jpg", 252 | "https://i.dummyjson.com/data/products/14/2.jpg", 253 | "https://i.dummyjson.com/data/products/14/3.jpg", 254 | "https://i.dummyjson.com/data/products/14/thumbnail.jpg" 255 | ] 256 | }, 257 | { 258 | "id":15, 259 | "title":"Eau De Perfume Spray", 260 | "description":"Genuine Al-Rehab spray perfume from UAE/Saudi Arabia/Yemen High Quality", 261 | "price":30, 262 | "discountPercentage":10.99, 263 | "rating":4.7, 264 | "stock":105, 265 | "brand":"Lord - Al-Rehab", 266 | "category":"fragrances", 267 | "thumbnail":"https://i.dummyjson.com/data/products/15/thumbnail.jpg", 268 | "images":[ 269 | "https://i.dummyjson.com/data/products/15/1.jpg", 270 | "https://i.dummyjson.com/data/products/15/2.jpg", 271 | "https://i.dummyjson.com/data/products/15/3.jpg", 272 | "https://i.dummyjson.com/data/products/15/4.jpg", 273 | "https://i.dummyjson.com/data/products/15/thumbnail.jpg" 274 | ] 275 | }, 276 | { 277 | "id":16, 278 | "title":"Hyaluronic Acid Serum", 279 | "description":"L'Oréal Paris introduces Hyaluron Expert Replumping Serum formulated with 1.5% Hyaluronic Acid", 280 | "price":19, 281 | "discountPercentage":13.31, 282 | "rating":4.83, 283 | "stock":110, 284 | "brand":"L'Oreal Paris", 285 | "category":"skincare", 286 | "thumbnail":"https://i.dummyjson.com/data/products/16/thumbnail.jpg", 287 | "images":[ 288 | "https://i.dummyjson.com/data/products/16/1.png", 289 | "https://i.dummyjson.com/data/products/16/2.webp", 290 | "https://i.dummyjson.com/data/products/16/3.jpg", 291 | "https://i.dummyjson.com/data/products/16/4.jpg", 292 | "https://i.dummyjson.com/data/products/16/thumbnail.jpg" 293 | ] 294 | }, 295 | { 296 | "id":17, 297 | "title":"Tree Oil 30ml", 298 | "description":"Tea tree oil contains a number of compounds, including terpinen-4-ol, that have been shown to kill certain bacteria,", 299 | "price":12, 300 | "discountPercentage":4.09, 301 | "rating":4.52, 302 | "stock":78, 303 | "brand":"Hemani Tea", 304 | "category":"skincare", 305 | "thumbnail":"https://i.dummyjson.com/data/products/17/thumbnail.jpg", 306 | "images":[ 307 | "https://i.dummyjson.com/data/products/17/1.jpg", 308 | "https://i.dummyjson.com/data/products/17/2.jpg", 309 | "https://i.dummyjson.com/data/products/17/3.jpg", 310 | "https://i.dummyjson.com/data/products/17/thumbnail.jpg" 311 | ] 312 | }, 313 | { 314 | "id":18, 315 | "title":"Oil Free Moisturizer 100ml", 316 | "description":"Dermive Oil Free Moisturizer with SPF 20 is specifically formulated with ceramides, hyaluronic acid & sunscreen.", 317 | "price":40, 318 | "discountPercentage":13.1, 319 | "rating":4.56, 320 | "stock":88, 321 | "brand":"Dermive", 322 | "category":"skincare", 323 | "thumbnail":"https://i.dummyjson.com/data/products/18/thumbnail.jpg", 324 | "images":[ 325 | "https://i.dummyjson.com/data/products/18/1.jpg", 326 | "https://i.dummyjson.com/data/products/18/2.jpg", 327 | "https://i.dummyjson.com/data/products/18/3.jpg", 328 | "https://i.dummyjson.com/data/products/18/4.jpg", 329 | "https://i.dummyjson.com/data/products/18/thumbnail.jpg" 330 | ] 331 | }, 332 | { 333 | "id":19, 334 | "title":"Skin Beauty Serum.", 335 | "description":"Product name: rorec collagen hyaluronic acid white face serum riceNet weight: 15 m", 336 | "price":46, 337 | "discountPercentage":10.68, 338 | "rating":4.42, 339 | "stock":54, 340 | "brand":"ROREC White Rice", 341 | "category":"skincare", 342 | "thumbnail":"https://i.dummyjson.com/data/products/19/thumbnail.jpg", 343 | "images":[ 344 | "https://i.dummyjson.com/data/products/19/1.jpg", 345 | "https://i.dummyjson.com/data/products/19/2.jpg", 346 | "https://i.dummyjson.com/data/products/19/3.png", 347 | "https://i.dummyjson.com/data/products/19/thumbnail.jpg" 348 | ] 349 | }, 350 | { 351 | "id":20, 352 | "title":"Freckle Treatment Cream- 15gm", 353 | "description":"Fair & Clear is Pakistan's only pure Freckle cream which helpsfade Freckles, Darkspots and pigments. Mercury level is 0%, so there are no side effects.", 354 | "price":70, 355 | "discountPercentage":16.99, 356 | "rating":4.06, 357 | "stock":140, 358 | "brand":"Fair & Clear", 359 | "category":"skincare", 360 | "thumbnail":"https://i.dummyjson.com/data/products/20/thumbnail.jpg", 361 | "images":[ 362 | "https://i.dummyjson.com/data/products/20/1.jpg", 363 | "https://i.dummyjson.com/data/products/20/2.jpg", 364 | "https://i.dummyjson.com/data/products/20/3.jpg", 365 | "https://i.dummyjson.com/data/products/20/4.jpg", 366 | "https://i.dummyjson.com/data/products/20/thumbnail.jpg" 367 | ] 368 | }, 369 | { 370 | "id":21, 371 | "title":"- Daal Masoor 500 grams", 372 | "description":"Fine quality Branded Product Keep in a cool and dry place", 373 | "price":20, 374 | "discountPercentage":4.81, 375 | "rating":4.44, 376 | "stock":133, 377 | "brand":"Saaf & Khaas", 378 | "category":"groceries", 379 | "thumbnail":"https://i.dummyjson.com/data/products/21/thumbnail.png", 380 | "images":[ 381 | "https://i.dummyjson.com/data/products/21/1.png", 382 | "https://i.dummyjson.com/data/products/21/2.jpg", 383 | "https://i.dummyjson.com/data/products/21/3.jpg" 384 | ] 385 | }, 386 | { 387 | "id":22, 388 | "title":"Elbow Macaroni - 400 gm", 389 | "description":"Product details of Bake Parlor Big Elbow Macaroni - 400 gm", 390 | "price":14, 391 | "discountPercentage":15.58, 392 | "rating":4.57, 393 | "stock":146, 394 | "brand":"Bake Parlor Big", 395 | "category":"groceries", 396 | "thumbnail":"https://i.dummyjson.com/data/products/22/thumbnail.jpg", 397 | "images":[ 398 | "https://i.dummyjson.com/data/products/22/1.jpg", 399 | "https://i.dummyjson.com/data/products/22/2.jpg", 400 | "https://i.dummyjson.com/data/products/22/3.jpg" 401 | ] 402 | }, 403 | { 404 | "id":23, 405 | "title":"Orange Essence Food Flavou", 406 | "description":"Specifications of Orange Essence Food Flavour For Cakes and Baking Food Item", 407 | "price":14, 408 | "discountPercentage":8.04, 409 | "rating":4.85, 410 | "stock":26, 411 | "brand":"Baking Food Items", 412 | "category":"groceries", 413 | "thumbnail":"https://i.dummyjson.com/data/products/23/thumbnail.jpg", 414 | "images":[ 415 | "https://i.dummyjson.com/data/products/23/1.jpg", 416 | "https://i.dummyjson.com/data/products/23/2.jpg", 417 | "https://i.dummyjson.com/data/products/23/3.jpg", 418 | "https://i.dummyjson.com/data/products/23/4.jpg", 419 | "https://i.dummyjson.com/data/products/23/thumbnail.jpg" 420 | ] 421 | }, 422 | { 423 | "id":24, 424 | "title":"cereals muesli fruit nuts", 425 | "description":"original fauji cereal muesli 250gm box pack original fauji cereals muesli fruit nuts flakes breakfast cereal break fast faujicereals cerels cerel foji fouji", 426 | "price":46, 427 | "discountPercentage":16.8, 428 | "rating":4.94, 429 | "stock":113, 430 | "brand":"fauji", 431 | "category":"groceries", 432 | "thumbnail":"https://i.dummyjson.com/data/products/24/thumbnail.jpg", 433 | "images":[ 434 | "https://i.dummyjson.com/data/products/24/1.jpg", 435 | "https://i.dummyjson.com/data/products/24/2.jpg", 436 | "https://i.dummyjson.com/data/products/24/3.jpg", 437 | "https://i.dummyjson.com/data/products/24/4.jpg", 438 | "https://i.dummyjson.com/data/products/24/thumbnail.jpg" 439 | ] 440 | }, 441 | { 442 | "id":25, 443 | "title":"Gulab Powder 50 Gram", 444 | "description":"Dry Rose Flower Powder Gulab Powder 50 Gram • Treats Wounds", 445 | "price":70, 446 | "discountPercentage":13.58, 447 | "rating":4.87, 448 | "stock":47, 449 | "brand":"Dry Rose", 450 | "category":"groceries", 451 | "thumbnail":"https://i.dummyjson.com/data/products/25/thumbnail.jpg", 452 | "images":[ 453 | "https://i.dummyjson.com/data/products/25/1.png", 454 | "https://i.dummyjson.com/data/products/25/2.jpg", 455 | "https://i.dummyjson.com/data/products/25/3.png", 456 | "https://i.dummyjson.com/data/products/25/4.jpg", 457 | "https://i.dummyjson.com/data/products/25/thumbnail.jpg" 458 | ] 459 | }, 460 | { 461 | "id":26, 462 | "title":"Plant Hanger For Home", 463 | "description":"Boho Decor Plant Hanger For Home Wall Decoration Macrame Wall Hanging Shelf", 464 | "price":41, 465 | "discountPercentage":17.86, 466 | "rating":4.08, 467 | "stock":131, 468 | "brand":"Boho Decor", 469 | "category":"home-decoration", 470 | "thumbnail":"https://i.dummyjson.com/data/products/26/thumbnail.jpg", 471 | "images":[ 472 | "https://i.dummyjson.com/data/products/26/1.jpg", 473 | "https://i.dummyjson.com/data/products/26/2.jpg", 474 | "https://i.dummyjson.com/data/products/26/3.jpg", 475 | "https://i.dummyjson.com/data/products/26/4.jpg", 476 | "https://i.dummyjson.com/data/products/26/5.jpg", 477 | "https://i.dummyjson.com/data/products/26/thumbnail.jpg" 478 | ] 479 | }, 480 | { 481 | "id":27, 482 | "title":"Flying Wooden Bird", 483 | "description":"Package Include 6 Birds with Adhesive Tape Shape: 3D Shaped Wooden Birds Material: Wooden MDF, Laminated 3.5mm", 484 | "price":51, 485 | "discountPercentage":15.58, 486 | "rating":4.41, 487 | "stock":17, 488 | "brand":"Flying Wooden", 489 | "category":"home-decoration", 490 | "thumbnail":"https://i.dummyjson.com/data/products/27/thumbnail.webp", 491 | "images":[ 492 | "https://i.dummyjson.com/data/products/27/1.jpg", 493 | "https://i.dummyjson.com/data/products/27/2.jpg", 494 | "https://i.dummyjson.com/data/products/27/3.jpg", 495 | "https://i.dummyjson.com/data/products/27/4.jpg", 496 | "https://i.dummyjson.com/data/products/27/thumbnail.webp" 497 | ] 498 | }, 499 | { 500 | "id":28, 501 | "title":"3D Embellishment Art Lamp", 502 | "description":"3D led lamp sticker Wall sticker 3d wall art light on/off button cell operated (included)", 503 | "price":20, 504 | "discountPercentage":16.49, 505 | "rating":4.82, 506 | "stock":54, 507 | "brand":"LED Lights", 508 | "category":"home-decoration", 509 | "thumbnail":"https://i.dummyjson.com/data/products/28/thumbnail.jpg", 510 | "images":[ 511 | "https://i.dummyjson.com/data/products/28/1.jpg", 512 | "https://i.dummyjson.com/data/products/28/2.jpg", 513 | "https://i.dummyjson.com/data/products/28/3.png", 514 | "https://i.dummyjson.com/data/products/28/4.jpg", 515 | "https://i.dummyjson.com/data/products/28/thumbnail.jpg" 516 | ] 517 | }, 518 | { 519 | "id":29, 520 | "title":"Handcraft Chinese style", 521 | "description":"Handcraft Chinese style art luxury palace hotel villa mansion home decor ceramic vase with brass fruit plate", 522 | "price":60, 523 | "discountPercentage":15.34, 524 | "rating":4.44, 525 | "stock":7, 526 | "brand":"luxury palace", 527 | "category":"home-decoration", 528 | "thumbnail":"https://i.dummyjson.com/data/products/29/thumbnail.webp", 529 | "images":[ 530 | "https://i.dummyjson.com/data/products/29/1.jpg", 531 | "https://i.dummyjson.com/data/products/29/2.jpg", 532 | "https://i.dummyjson.com/data/products/29/3.webp", 533 | "https://i.dummyjson.com/data/products/29/4.webp", 534 | "https://i.dummyjson.com/data/products/29/thumbnail.webp" 535 | ] 536 | }, 537 | { 538 | "id":30, 539 | "title":"Key Holder", 540 | "description":"Attractive DesignMetallic materialFour key hooksReliable & DurablePremium Quality", 541 | "price":30, 542 | "discountPercentage":2.92, 543 | "rating":4.92, 544 | "stock":54, 545 | "brand":"Golden", 546 | "category":"home-decoration", 547 | "thumbnail":"https://i.dummyjson.com/data/products/30/thumbnail.jpg", 548 | "images":[ 549 | "https://i.dummyjson.com/data/products/30/1.jpg", 550 | "https://i.dummyjson.com/data/products/30/2.jpg", 551 | "https://i.dummyjson.com/data/products/30/3.jpg", 552 | "https://i.dummyjson.com/data/products/30/thumbnail.jpg" 553 | ] 554 | } 555 | ], 556 | "total":100, 557 | "skip":0, 558 | "limit":30 559 | } -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | pnpm-debug.log* 12 | 13 | 14 | # environment variables 15 | .env 16 | .env.production 17 | 18 | # macOS-specific files 19 | .DS_Store 20 | 21 | # yarn 22 | .pnp.* 23 | .yarn/* 24 | !.yarn/patches 25 | !.yarn/plugins 26 | !.yarn/releases 27 | !.yarn/sdks 28 | !.yarn/versions 29 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Web de Pruebas Técnicas 2 | 3 | -------------------------------------------------------------------------------- /web/astro.config.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable turbo/no-undeclared-env-vars */ 2 | import { defineConfig } from "astro/config"; 3 | import sitemap from "@astrojs/sitemap"; 4 | import tailwind from "@astrojs/tailwind"; 5 | 6 | /* 7 | We are doing some URL mumbo jumbo here to tell Astro what the URL of your website will be. 8 | In local development, your SEO meta tags will have localhost URL. 9 | In built production websites, your SEO meta tags should have your website URL. 10 | So we give our website URL here and the template will know what URL to use 11 | for meta tags during build. 12 | If you don't know your website URL yet, don't worry about this 13 | and leave it empty or use localhost URL. It won't break anything. 14 | */ 15 | 16 | const SERVER_PORT = 3000; 17 | // the url to access your blog during local development 18 | const LOCALHOST_URL = `http://localhost:${SERVER_PORT}`; 19 | // the url to access your blog after deploying it somewhere (Eg. Netlify) 20 | const LIVE_URL = "https://pruebastecnicas.com"; 21 | // this is the astro command your npm script runs 22 | const SCRIPT = process.env.npm_lifecycle_script || ""; 23 | const isBuild = SCRIPT.includes("astro build"); 24 | let BASE_URL = LOCALHOST_URL; 25 | // When you're building your site in local or in CI, you could just set your URL manually 26 | if (isBuild) { 27 | BASE_URL = LIVE_URL; 28 | } 29 | 30 | export default defineConfig({ 31 | server: { port: SERVER_PORT }, 32 | site: BASE_URL, 33 | integrations: [ 34 | sitemap(), 35 | tailwind({ 36 | config: { applyBaseStyles: false }, 37 | }), 38 | ], 39 | }); 40 | -------------------------------------------------------------------------------- /web/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/bun.lockb -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pruebas-tecnicas", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro", 11 | "clean": "rm -rf dist" 12 | }, 13 | "dependencies": { 14 | "@astrojs/prism": "3.0.0", 15 | "@astrojs/rss": "3.0.0", 16 | "@astrojs/sitemap": "3.0.1", 17 | "@astrojs/tailwind": "5.0.1", 18 | "@markdoc/markdoc": "0.3.2", 19 | "@tailwindcss/typography": "0.5.10", 20 | "astro": "3.2.3", 21 | "astro-markdoc-renderer": "0.0.1-alpha.1", 22 | "globby": "13.2.2", 23 | "gray-matter": "4.0.3", 24 | "slugify": "1.6.6", 25 | "tailwindcss": "3.3.3", 26 | "zod": "3.22.4" 27 | }, 28 | "devDependencies": { 29 | "prettier-plugin-astro": "0.11.1", 30 | "typescript": "5.2.2" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /web/public/fontawesome/css/brands.bareminimum.css: -------------------------------------------------------------------------------- 1 | /* 2 | This is a customised bare minimum styles that include only the brands used in this template. 3 | If you need more brands, either use brands.min.css file or copy the brands needed to this file. 4 | */ 5 | :root, 6 | :host { 7 | --fa-style-family-brands: 'Font Awesome 6 Brands'; 8 | --fa-font-brands: normal 400 1em/1 'Font Awesome 6 Brands'; 9 | } 10 | 11 | @font-face { 12 | font-family: 'Font Awesome 6 Brands'; 13 | font-style: normal; 14 | font-weight: 400; 15 | font-display: block; 16 | src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); 17 | } 18 | 19 | .fab, 20 | .fa-brands { 21 | font-weight: 400; 22 | } 23 | 24 | 25 | .fa-github:before { 26 | content: "\f09b"; 27 | } 28 | 29 | 30 | .fa-github-alt:before { 31 | content: "\f113"; 32 | } 33 | 34 | .fa-square-github:before { 35 | content: "\f092"; 36 | } 37 | 38 | .fa-github-square:before { 39 | content: "\f092"; 40 | } 41 | 42 | .fa-twitch:before { 43 | content: "\f1e8"; 44 | } 45 | 46 | .fa-discord:before { 47 | content: "\f392"; 48 | } 49 | 50 | .fa-twitter:before { 51 | content: "\f099"; 52 | } 53 | 54 | .fa-square-twitter:before { 55 | content: "\f081"; 56 | } 57 | 58 | .fa-twitter-square:before { 59 | content: "\f081"; 60 | } -------------------------------------------------------------------------------- /web/public/fontawesome/css/brands.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | :host,:root{--fa-style-family-brands:"Font Awesome 6 Brands";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}@font-face{font-family:"Font Awesome 6 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}.fa-brands,.fab{font-weight:400}.fa-monero:before{content:"\f3d0"}.fa-hooli:before{content:"\f427"}.fa-yelp:before{content:"\f1e9"}.fa-cc-visa:before{content:"\f1f0"}.fa-lastfm:before{content:"\f202"}.fa-shopware:before{content:"\f5b5"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-aws:before{content:"\f375"}.fa-redhat:before{content:"\f7bc"}.fa-yoast:before{content:"\f2b1"}.fa-cloudflare:before{content:"\e07d"}.fa-ups:before{content:"\f7e0"}.fa-wpexplorer:before{content:"\f2de"}.fa-dyalog:before{content:"\f399"}.fa-bity:before{content:"\f37a"}.fa-stackpath:before{content:"\f842"}.fa-buysellads:before{content:"\f20d"}.fa-first-order:before{content:"\f2b0"}.fa-modx:before{content:"\f285"}.fa-guilded:before{content:"\e07e"}.fa-vnv:before{content:"\f40b"}.fa-js-square:before,.fa-square-js:before{content:"\f3b9"}.fa-microsoft:before{content:"\f3ca"}.fa-qq:before{content:"\f1d6"}.fa-orcid:before{content:"\f8d2"}.fa-java:before{content:"\f4e4"}.fa-invision:before{content:"\f7b0"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-centercode:before{content:"\f380"}.fa-glide-g:before{content:"\f2a6"}.fa-drupal:before{content:"\f1a9"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-unity:before{content:"\e049"}.fa-whmcs:before{content:"\f40d"}.fa-rocketchat:before{content:"\f3e8"}.fa-vk:before{content:"\f189"}.fa-untappd:before{content:"\f405"}.fa-mailchimp:before{content:"\f59e"}.fa-css3-alt:before{content:"\f38b"}.fa-reddit-square:before,.fa-square-reddit:before{content:"\f1a2"}.fa-vimeo-v:before{content:"\f27d"}.fa-contao:before{content:"\f26d"}.fa-square-font-awesome:before{content:"\e5ad"}.fa-deskpro:before{content:"\f38f"}.fa-sistrix:before{content:"\f3ee"}.fa-instagram-square:before,.fa-square-instagram:before{content:"\e055"}.fa-battle-net:before{content:"\f835"}.fa-the-red-yeti:before{content:"\f69d"}.fa-hacker-news-square:before,.fa-square-hacker-news:before{content:"\f3af"}.fa-edge:before{content:"\f282"}.fa-napster:before{content:"\f3d2"}.fa-snapchat-square:before,.fa-square-snapchat:before{content:"\f2ad"}.fa-google-plus-g:before{content:"\f0d5"}.fa-artstation:before{content:"\f77a"}.fa-markdown:before{content:"\f60f"}.fa-sourcetree:before{content:"\f7d3"}.fa-google-plus:before{content:"\f2b3"}.fa-diaspora:before{content:"\f791"}.fa-foursquare:before{content:"\f180"}.fa-stack-overflow:before{content:"\f16c"}.fa-github-alt:before{content:"\f113"}.fa-phoenix-squadron:before{content:"\f511"}.fa-pagelines:before{content:"\f18c"}.fa-algolia:before{content:"\f36c"}.fa-red-river:before{content:"\f3e3"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-safari:before{content:"\f267"}.fa-google:before{content:"\f1a0"}.fa-font-awesome-alt:before,.fa-square-font-awesome-stroke:before{content:"\f35c"}.fa-atlassian:before{content:"\f77b"}.fa-linkedin-in:before{content:"\f0e1"}.fa-digital-ocean:before{content:"\f391"}.fa-nimblr:before{content:"\f5a8"}.fa-chromecast:before{content:"\f838"}.fa-evernote:before{content:"\f839"}.fa-hacker-news:before{content:"\f1d4"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-adversal:before{content:"\f36a"}.fa-creative-commons:before{content:"\f25e"}.fa-watchman-monitoring:before{content:"\e087"}.fa-fonticons:before{content:"\f280"}.fa-weixin:before{content:"\f1d7"}.fa-shirtsinbulk:before{content:"\f214"}.fa-codepen:before{content:"\f1cb"}.fa-git-alt:before{content:"\f841"}.fa-lyft:before{content:"\f3c3"}.fa-rev:before{content:"\f5b2"}.fa-windows:before{content:"\f17a"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-square-viadeo:before,.fa-viadeo-square:before{content:"\f2aa"}.fa-meetup:before{content:"\f2e0"}.fa-centos:before{content:"\f789"}.fa-adn:before{content:"\f170"}.fa-cloudsmith:before{content:"\f384"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-dribbble-square:before,.fa-square-dribbble:before{content:"\f397"}.fa-codiepie:before{content:"\f284"}.fa-node:before{content:"\f419"}.fa-mix:before{content:"\f3cb"}.fa-steam:before{content:"\f1b6"}.fa-cc-apple-pay:before{content:"\f416"}.fa-scribd:before{content:"\f28a"}.fa-openid:before{content:"\f19b"}.fa-instalod:before{content:"\e081"}.fa-expeditedssl:before{content:"\f23e"}.fa-sellcast:before{content:"\f2da"}.fa-square-twitter:before,.fa-twitter-square:before{content:"\f081"}.fa-r-project:before{content:"\f4f7"}.fa-delicious:before{content:"\f1a5"}.fa-freebsd:before{content:"\f3a4"}.fa-vuejs:before{content:"\f41f"}.fa-accusoft:before{content:"\f369"}.fa-ioxhost:before{content:"\f208"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-app-store:before{content:"\f36f"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-itunes-note:before{content:"\f3b5"}.fa-golang:before{content:"\e40f"}.fa-kickstarter:before{content:"\f3bb"}.fa-grav:before{content:"\f2d6"}.fa-weibo:before{content:"\f18a"}.fa-uncharted:before{content:"\e084"}.fa-firstdraft:before{content:"\f3a1"}.fa-square-youtube:before,.fa-youtube-square:before{content:"\f431"}.fa-wikipedia-w:before{content:"\f266"}.fa-rendact:before,.fa-wpressr:before{content:"\f3e4"}.fa-angellist:before{content:"\f209"}.fa-galactic-republic:before{content:"\f50c"}.fa-nfc-directional:before{content:"\e530"}.fa-skype:before{content:"\f17e"}.fa-joget:before{content:"\f3b7"}.fa-fedora:before{content:"\f798"}.fa-stripe-s:before{content:"\f42a"}.fa-meta:before{content:"\e49b"}.fa-laravel:before{content:"\f3bd"}.fa-hotjar:before{content:"\f3b1"}.fa-bluetooth-b:before{content:"\f294"}.fa-sticker-mule:before{content:"\f3f7"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-hips:before{content:"\f452"}.fa-behance:before{content:"\f1b4"}.fa-reddit:before{content:"\f1a1"}.fa-discord:before{content:"\f392"}.fa-chrome:before{content:"\f268"}.fa-app-store-ios:before{content:"\f370"}.fa-cc-discover:before{content:"\f1f2"}.fa-wpbeginner:before{content:"\f297"}.fa-confluence:before{content:"\f78d"}.fa-mdb:before{content:"\f8ca"}.fa-dochub:before{content:"\f394"}.fa-accessible-icon:before{content:"\f368"}.fa-ebay:before{content:"\f4f4"}.fa-amazon:before{content:"\f270"}.fa-unsplash:before{content:"\e07c"}.fa-yarn:before{content:"\f7e3"}.fa-square-steam:before,.fa-steam-square:before{content:"\f1b7"}.fa-500px:before{content:"\f26e"}.fa-square-vimeo:before,.fa-vimeo-square:before{content:"\f194"}.fa-asymmetrik:before{content:"\f372"}.fa-font-awesome-flag:before,.fa-font-awesome-logo-full:before,.fa-font-awesome:before{content:"\f2b4"}.fa-gratipay:before{content:"\f184"}.fa-apple:before{content:"\f179"}.fa-hive:before{content:"\e07f"}.fa-gitkraken:before{content:"\f3a6"}.fa-keybase:before{content:"\f4f5"}.fa-apple-pay:before{content:"\f415"}.fa-padlet:before{content:"\e4a0"}.fa-amazon-pay:before{content:"\f42c"}.fa-github-square:before,.fa-square-github:before{content:"\f092"}.fa-stumbleupon:before{content:"\f1a4"}.fa-fedex:before{content:"\f797"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-shopify:before{content:"\e057"}.fa-neos:before{content:"\f612"}.fa-hackerrank:before{content:"\f5f7"}.fa-researchgate:before{content:"\f4f8"}.fa-swift:before{content:"\f8e1"}.fa-angular:before{content:"\f420"}.fa-speakap:before{content:"\f3f3"}.fa-angrycreative:before{content:"\f36e"}.fa-y-combinator:before{content:"\f23b"}.fa-empire:before{content:"\f1d1"}.fa-envira:before{content:"\f299"}.fa-gitlab-square:before,.fa-square-gitlab:before{content:"\e5ae"}.fa-studiovinari:before{content:"\f3f8"}.fa-pied-piper:before{content:"\f2ae"}.fa-wordpress:before{content:"\f19a"}.fa-product-hunt:before{content:"\f288"}.fa-firefox:before{content:"\f269"}.fa-linode:before{content:"\f2b8"}.fa-goodreads:before{content:"\f3a8"}.fa-odnoklassniki-square:before,.fa-square-odnoklassniki:before{content:"\f264"}.fa-jsfiddle:before{content:"\f1cc"}.fa-sith:before{content:"\f512"}.fa-themeisle:before{content:"\f2b2"}.fa-page4:before{content:"\f3d7"}.fa-hashnode:before{content:"\e499"}.fa-react:before{content:"\f41b"}.fa-cc-paypal:before{content:"\f1f4"}.fa-squarespace:before{content:"\f5be"}.fa-cc-stripe:before{content:"\f1f5"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-bitcoin:before{content:"\f379"}.fa-keycdn:before{content:"\f3ba"}.fa-opera:before{content:"\f26a"}.fa-itch-io:before{content:"\f83a"}.fa-umbraco:before{content:"\f8e8"}.fa-galactic-senate:before{content:"\f50d"}.fa-ubuntu:before{content:"\f7df"}.fa-draft2digital:before{content:"\f396"}.fa-stripe:before{content:"\f429"}.fa-houzz:before{content:"\f27c"}.fa-gg:before{content:"\f260"}.fa-dhl:before{content:"\f790"}.fa-pinterest-square:before,.fa-square-pinterest:before{content:"\f0d3"}.fa-xing:before{content:"\f168"}.fa-blackberry:before{content:"\f37b"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-playstation:before{content:"\f3df"}.fa-quinscape:before{content:"\f459"}.fa-less:before{content:"\f41d"}.fa-blogger-b:before{content:"\f37d"}.fa-opencart:before{content:"\f23d"}.fa-vine:before{content:"\f1ca"}.fa-paypal:before{content:"\f1ed"}.fa-gitlab:before{content:"\f296"}.fa-typo3:before{content:"\f42b"}.fa-reddit-alien:before{content:"\f281"}.fa-yahoo:before{content:"\f19e"}.fa-dailymotion:before{content:"\e052"}.fa-affiliatetheme:before{content:"\f36b"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-bootstrap:before{content:"\f836"}.fa-odnoklassniki:before{content:"\f263"}.fa-nfc-symbol:before{content:"\e531"}.fa-ethereum:before{content:"\f42e"}.fa-speaker-deck:before{content:"\f83c"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-patreon:before{content:"\f3d9"}.fa-avianex:before{content:"\f374"}.fa-ello:before{content:"\f5f1"}.fa-gofore:before{content:"\f3a7"}.fa-bimobject:before{content:"\f378"}.fa-facebook-f:before{content:"\f39e"}.fa-google-plus-square:before,.fa-square-google-plus:before{content:"\f0d4"}.fa-mandalorian:before{content:"\f50f"}.fa-first-order-alt:before{content:"\f50a"}.fa-osi:before{content:"\f41a"}.fa-google-wallet:before{content:"\f1ee"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-periscope:before{content:"\f3da"}.fa-fulcrum:before{content:"\f50b"}.fa-cloudscale:before{content:"\f383"}.fa-forumbee:before{content:"\f211"}.fa-mizuni:before{content:"\f3cc"}.fa-schlix:before{content:"\f3ea"}.fa-square-xing:before,.fa-xing-square:before{content:"\f169"}.fa-bandcamp:before{content:"\f2d5"}.fa-wpforms:before{content:"\f298"}.fa-cloudversify:before{content:"\f385"}.fa-usps:before{content:"\f7e1"}.fa-megaport:before{content:"\f5a3"}.fa-magento:before{content:"\f3c4"}.fa-spotify:before{content:"\f1bc"}.fa-optin-monster:before{content:"\f23c"}.fa-fly:before{content:"\f417"}.fa-aviato:before{content:"\f421"}.fa-itunes:before{content:"\f3b4"}.fa-cuttlefish:before{content:"\f38c"}.fa-blogger:before{content:"\f37c"}.fa-flickr:before{content:"\f16e"}.fa-viber:before{content:"\f409"}.fa-soundcloud:before{content:"\f1be"}.fa-digg:before{content:"\f1a6"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-symfony:before{content:"\f83d"}.fa-maxcdn:before{content:"\f136"}.fa-etsy:before{content:"\f2d7"}.fa-facebook-messenger:before{content:"\f39f"}.fa-audible:before{content:"\f373"}.fa-think-peaks:before{content:"\f731"}.fa-bilibili:before{content:"\e3d9"}.fa-erlang:before{content:"\f39d"}.fa-cotton-bureau:before{content:"\f89e"}.fa-dashcube:before{content:"\f210"}.fa-42-group:before,.fa-innosoft:before{content:"\e080"}.fa-stack-exchange:before{content:"\f18d"}.fa-elementor:before{content:"\f430"}.fa-pied-piper-square:before,.fa-square-pied-piper:before{content:"\e01e"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-palfed:before{content:"\f3d8"}.fa-superpowers:before{content:"\f2dd"}.fa-resolving:before{content:"\f3e7"}.fa-xbox:before{content:"\f412"}.fa-searchengin:before{content:"\f3eb"}.fa-tiktok:before{content:"\e07b"}.fa-facebook-square:before,.fa-square-facebook:before{content:"\f082"}.fa-renren:before{content:"\f18b"}.fa-linux:before{content:"\f17c"}.fa-glide:before{content:"\f2a5"}.fa-linkedin:before{content:"\f08c"}.fa-hubspot:before{content:"\f3b2"}.fa-deploydog:before{content:"\f38e"}.fa-twitch:before{content:"\f1e8"}.fa-ravelry:before{content:"\f2d9"}.fa-mixer:before{content:"\e056"}.fa-lastfm-square:before,.fa-square-lastfm:before{content:"\f203"}.fa-vimeo:before{content:"\f40a"}.fa-mendeley:before{content:"\f7b3"}.fa-uniregistry:before{content:"\f404"}.fa-figma:before{content:"\f799"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-dropbox:before{content:"\f16b"}.fa-instagram:before{content:"\f16d"}.fa-cmplid:before{content:"\e360"}.fa-facebook:before{content:"\f09a"}.fa-gripfire:before{content:"\f3ac"}.fa-jedi-order:before{content:"\f50e"}.fa-uikit:before{content:"\f403"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-phabricator:before{content:"\f3db"}.fa-ussunnah:before{content:"\f407"}.fa-earlybirds:before{content:"\f39a"}.fa-trade-federation:before{content:"\f513"}.fa-autoprefixer:before{content:"\f41c"}.fa-whatsapp:before{content:"\f232"}.fa-slideshare:before{content:"\f1e7"}.fa-google-play:before{content:"\f3ab"}.fa-viadeo:before{content:"\f2a9"}.fa-line:before{content:"\f3c0"}.fa-google-drive:before{content:"\f3aa"}.fa-servicestack:before{content:"\f3ec"}.fa-simplybuilt:before{content:"\f215"}.fa-bitbucket:before{content:"\f171"}.fa-imdb:before{content:"\f2d8"}.fa-deezer:before{content:"\e077"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-jira:before{content:"\f7b1"}.fa-docker:before{content:"\f395"}.fa-screenpal:before{content:"\e570"}.fa-bluetooth:before{content:"\f293"}.fa-gitter:before{content:"\f426"}.fa-d-and-d:before{content:"\f38d"}.fa-microblog:before{content:"\e01a"}.fa-cc-diners-club:before{content:"\f24c"}.fa-gg-circle:before{content:"\f261"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-yandex:before{content:"\f413"}.fa-readme:before{content:"\f4d5"}.fa-html5:before{content:"\f13b"}.fa-sellsy:before{content:"\f213"}.fa-sass:before{content:"\f41e"}.fa-wirsindhandwerk:before,.fa-wsh:before{content:"\e2d0"}.fa-buromobelexperte:before{content:"\f37f"}.fa-salesforce:before{content:"\f83b"}.fa-octopus-deploy:before{content:"\e082"}.fa-medapps:before{content:"\f3c6"}.fa-ns8:before{content:"\f3d5"}.fa-pinterest-p:before{content:"\f231"}.fa-apper:before{content:"\f371"}.fa-fort-awesome:before{content:"\f286"}.fa-waze:before{content:"\f83f"}.fa-cc-jcb:before{content:"\f24b"}.fa-snapchat-ghost:before,.fa-snapchat:before{content:"\f2ab"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-rust:before{content:"\e07a"}.fa-wix:before{content:"\f5cf"}.fa-behance-square:before,.fa-square-behance:before{content:"\f1b5"}.fa-supple:before{content:"\f3f9"}.fa-rebel:before{content:"\f1d0"}.fa-css3:before{content:"\f13c"}.fa-staylinked:before{content:"\f3f5"}.fa-kaggle:before{content:"\f5fa"}.fa-space-awesome:before{content:"\e5ac"}.fa-deviantart:before{content:"\f1bd"}.fa-cpanel:before{content:"\f388"}.fa-goodreads-g:before{content:"\f3a9"}.fa-git-square:before,.fa-square-git:before{content:"\f1d2"}.fa-square-tumblr:before,.fa-tumblr-square:before{content:"\f174"}.fa-trello:before{content:"\f181"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-get-pocket:before{content:"\f265"}.fa-perbyte:before{content:"\e083"}.fa-grunt:before{content:"\f3ad"}.fa-weebly:before{content:"\f5cc"}.fa-connectdevelop:before{content:"\f20e"}.fa-leanpub:before{content:"\f212"}.fa-black-tie:before{content:"\f27e"}.fa-themeco:before{content:"\f5c6"}.fa-python:before{content:"\f3e2"}.fa-android:before{content:"\f17b"}.fa-bots:before{content:"\e340"}.fa-free-code-camp:before{content:"\f2c5"}.fa-hornbill:before{content:"\f592"}.fa-js:before{content:"\f3b8"}.fa-ideal:before{content:"\e013"}.fa-git:before{content:"\f1d3"}.fa-dev:before{content:"\f6cc"}.fa-sketch:before{content:"\f7c6"}.fa-yandex-international:before{content:"\f414"}.fa-cc-amex:before{content:"\f1f3"}.fa-uber:before{content:"\f402"}.fa-github:before{content:"\f09b"}.fa-php:before{content:"\f457"}.fa-alipay:before{content:"\f642"}.fa-youtube:before{content:"\f167"}.fa-skyatlas:before{content:"\f216"}.fa-firefox-browser:before{content:"\e007"}.fa-replyd:before{content:"\f3e6"}.fa-suse:before{content:"\f7d6"}.fa-jenkins:before{content:"\f3b6"}.fa-twitter:before{content:"\f099"}.fa-rockrms:before{content:"\f3e9"}.fa-pinterest:before{content:"\f0d2"}.fa-buffer:before{content:"\f837"}.fa-npm:before{content:"\f3d4"}.fa-yammer:before{content:"\f840"}.fa-btc:before{content:"\f15a"}.fa-dribbble:before{content:"\f17d"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-internet-explorer:before{content:"\f26b"}.fa-telegram-plane:before,.fa-telegram:before{content:"\f2c6"}.fa-old-republic:before{content:"\f510"}.fa-square-whatsapp:before,.fa-whatsapp-square:before{content:"\f40c"}.fa-node-js:before{content:"\f3d3"}.fa-edge-legacy:before{content:"\e078"}.fa-slack-hash:before,.fa-slack:before{content:"\f198"}.fa-medrt:before{content:"\f3c8"}.fa-usb:before{content:"\f287"}.fa-tumblr:before{content:"\f173"}.fa-vaadin:before{content:"\f408"}.fa-quora:before{content:"\f2c4"}.fa-reacteurope:before{content:"\f75d"}.fa-medium-m:before,.fa-medium:before{content:"\f23a"}.fa-amilia:before{content:"\f36d"}.fa-mixcloud:before{content:"\f289"}.fa-flipboard:before{content:"\f44d"}.fa-viacoin:before{content:"\f237"}.fa-critical-role:before{content:"\f6c9"}.fa-sitrox:before{content:"\e44a"}.fa-discourse:before{content:"\f393"}.fa-joomla:before{content:"\f1aa"}.fa-mastodon:before{content:"\f4f6"}.fa-airbnb:before{content:"\f834"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-buy-n-large:before{content:"\f8a6"}.fa-gulp:before{content:"\f3ae"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-strava:before{content:"\f428"}.fa-ember:before{content:"\f423"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-teamspeak:before{content:"\f4f9"}.fa-pushed:before{content:"\f3e1"}.fa-wordpress-simple:before{content:"\f411"}.fa-nutritionix:before{content:"\f3d6"}.fa-wodu:before{content:"\e088"}.fa-google-pay:before{content:"\e079"}.fa-intercom:before{content:"\f7af"}.fa-zhihu:before{content:"\f63f"}.fa-korvue:before{content:"\f42f"}.fa-pix:before{content:"\e43a"}.fa-steam-symbol:before{content:"\f3f6"} -------------------------------------------------------------------------------- /web/public/fontawesome/css/fontawesome.bareminimum.css: -------------------------------------------------------------------------------- 1 | /* 2 | This is a customised bare minimum styles needed to use the brand icons used in this template. 3 | If you need more icons, you probably might want to use fontawesome.min.css file 4 | instead of this file. 5 | */ 6 | .fa { 7 | font-family: var(--fa-style-family, "Font Awesome 6 Free"); 8 | font-weight: var(--fa-style, 900); 9 | } 10 | 11 | .fa, 12 | .fa-classic, 13 | .fa-sharp, 14 | .fas, 15 | .fa-solid, 16 | .far, 17 | .fa-regular, 18 | .fab, 19 | .fa-brands { 20 | -moz-osx-font-smoothing: grayscale; 21 | -webkit-font-smoothing: antialiased; 22 | display: var(--fa-display, inline-block); 23 | font-style: normal; 24 | font-variant: normal; 25 | line-height: 1; 26 | text-rendering: auto; 27 | } 28 | 29 | .fas, 30 | .fa-classic, 31 | .fa-solid, 32 | .far, 33 | .fa-regular { 34 | font-family: 'Font Awesome 6 Free'; 35 | } 36 | 37 | .fab, 38 | .fa-brands { 39 | font-family: 'Font Awesome 6 Brands'; 40 | } 41 | 42 | .fa-1x { 43 | font-size: 1em; 44 | } 45 | 46 | .fa-2x { 47 | font-size: 2em; 48 | } 49 | 50 | .fa-3x { 51 | font-size: 3em; 52 | } 53 | 54 | .fa-4x { 55 | font-size: 4em; 56 | } 57 | 58 | .fa-5x { 59 | font-size: 5em; 60 | } 61 | 62 | .fa-6x { 63 | font-size: 6em; 64 | } 65 | 66 | .fa-7x { 67 | font-size: 7em; 68 | } 69 | 70 | .fa-8x { 71 | font-size: 8em; 72 | } 73 | 74 | .fa-9x { 75 | font-size: 9em; 76 | } 77 | 78 | .fa-10x { 79 | font-size: 10em; 80 | } 81 | 82 | .fa-2xs { 83 | font-size: 0.625em; 84 | line-height: 0.1em; 85 | vertical-align: 0.225em; 86 | } 87 | 88 | .fa-xs { 89 | font-size: 0.75em; 90 | line-height: 0.08333em; 91 | vertical-align: 0.125em; 92 | } 93 | 94 | .fa-sm { 95 | font-size: 0.875em; 96 | line-height: 0.07143em; 97 | vertical-align: 0.05357em; 98 | } 99 | 100 | .fa-lg { 101 | font-size: 1.25em; 102 | line-height: 0.05em; 103 | vertical-align: -0.075em; 104 | } 105 | 106 | .fa-xl { 107 | font-size: 1.5em; 108 | line-height: 0.04167em; 109 | vertical-align: -0.125em; 110 | } 111 | 112 | .fa-2xl { 113 | font-size: 2em; 114 | line-height: 0.03125em; 115 | vertical-align: -0.1875em; 116 | } 117 | 118 | .fa-fw { 119 | text-align: center; 120 | width: 1.25em; 121 | } 122 | 123 | .fa-ul { 124 | list-style-type: none; 125 | margin-left: var(--fa-li-margin, 2.5em); 126 | padding-left: 0; 127 | } 128 | 129 | .fa-ul>li { 130 | position: relative; 131 | } 132 | 133 | .fa-li { 134 | left: calc(var(--fa-li-width, 2em) * -1); 135 | position: absolute; 136 | text-align: center; 137 | width: var(--fa-li-width, 2em); 138 | line-height: inherit; 139 | } 140 | 141 | .fa-border { 142 | border-color: var(--fa-border-color, #eee); 143 | border-radius: var(--fa-border-radius, 0.1em); 144 | border-style: var(--fa-border-style, solid); 145 | border-width: var(--fa-border-width, 0.08em); 146 | padding: var(--fa-border-padding, 0.2em 0.25em 0.15em); 147 | } 148 | 149 | .fa-pull-left { 150 | float: left; 151 | margin-right: var(--fa-pull-margin, 0.3em); 152 | } 153 | 154 | .fa-pull-right { 155 | float: right; 156 | margin-left: var(--fa-pull-margin, 0.3em); 157 | } 158 | 159 | .fa-up-right-from-square::before { 160 | content: "\f35d"; 161 | } 162 | 163 | .fa-link::before { 164 | content: "\f0c1"; 165 | } 166 | 167 | .fa-hashtag::before { 168 | content: "\23"; 169 | } 170 | 171 | .fa-bars::before { 172 | content: "\f0c9"; 173 | } 174 | 175 | .fa-xmark::before { 176 | content: "\f00d"; 177 | } 178 | 179 | .fa-rss::before { 180 | content: "\f09e"; 181 | } 182 | 183 | .sr-only, 184 | .fa-sr-only { 185 | position: absolute; 186 | width: 1px; 187 | height: 1px; 188 | padding: 0; 189 | margin: -1px; 190 | overflow: hidden; 191 | clip: rect(0, 0, 0, 0); 192 | white-space: nowrap; 193 | border-width: 0; 194 | } 195 | 196 | .sr-only-focusable:not(:focus), 197 | .fa-sr-only-focusable:not(:focus) { 198 | position: absolute; 199 | width: 1px; 200 | height: 1px; 201 | padding: 0; 202 | margin: -1px; 203 | overflow: hidden; 204 | clip: rect(0, 0, 0, 0); 205 | white-space: nowrap; 206 | border-width: 0; 207 | } -------------------------------------------------------------------------------- /web/public/fontawesome/css/regular.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | :root, :host { 7 | --fa-style-family-classic: 'Font Awesome 6 Free'; 8 | --fa-font-regular: normal 400 1em/1 'Font Awesome 6 Free'; } 9 | 10 | @font-face { 11 | font-family: 'Font Awesome 6 Free'; 12 | font-style: normal; 13 | font-weight: 400; 14 | font-display: block; 15 | src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); } 16 | 17 | .far, 18 | .fa-regular { 19 | font-weight: 400; } 20 | -------------------------------------------------------------------------------- /web/public/fontawesome/css/regular.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | :host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")}.fa-regular,.far{font-weight:400} -------------------------------------------------------------------------------- /web/public/fontawesome/css/solid.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | :root, 7 | :host { 8 | --fa-style-family-classic: 'Font Awesome 6 Free'; 9 | --fa-font-solid: normal 900 1em/1 'Font Awesome 6 Free'; 10 | } 11 | 12 | @font-face { 13 | font-family: 'Font Awesome 6 Free'; 14 | font-style: normal; 15 | font-weight: 900; 16 | font-display: block; 17 | src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); 18 | } 19 | 20 | .fas, 21 | .fa-solid { 22 | font-weight: 900; 23 | } -------------------------------------------------------------------------------- /web/public/fontawesome/css/solid.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | :host,:root{--fa-style-family-classic:"Font Awesome 6 Free";--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Free"}@font-face{font-family:"Font Awesome 6 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}.fa-solid,.fas{font-weight:900} -------------------------------------------------------------------------------- /web/public/fontawesome/css/svg-with-js.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | :host,:root{--fa-font-solid:normal 900 1em/1 "Font Awesome 6 Solid";--fa-font-regular:normal 400 1em/1 "Font Awesome 6 Regular";--fa-font-light:normal 300 1em/1 "Font Awesome 6 Light";--fa-font-thin:normal 100 1em/1 "Font Awesome 6 Thin";--fa-font-duotone:normal 900 1em/1 "Font Awesome 6 Duotone";--fa-font-sharp-solid:normal 900 1em/1 "Font Awesome 6 Sharp";--fa-font-brands:normal 400 1em/1 "Font Awesome 6 Brands"}svg:not(:host).svg-inline--fa,svg:not(:root).svg-inline--fa{overflow:visible;box-sizing:content-box}.svg-inline--fa{display:var(--fa-display,inline-block);height:1em;overflow:visible;vertical-align:-.125em}.svg-inline--fa.fa-2xs{vertical-align:.1em}.svg-inline--fa.fa-xs{vertical-align:0}.svg-inline--fa.fa-sm{vertical-align:-.07143em}.svg-inline--fa.fa-lg{vertical-align:-.2em}.svg-inline--fa.fa-xl{vertical-align:-.25em}.svg-inline--fa.fa-2xl{vertical-align:-.3125em}.svg-inline--fa.fa-pull-left{margin-right:var(--fa-pull-margin,.3em);width:auto}.svg-inline--fa.fa-pull-right{margin-left:var(--fa-pull-margin,.3em);width:auto}.svg-inline--fa.fa-li{width:var(--fa-li-width,2em);top:.25em}.svg-inline--fa.fa-fw{width:var(--fa-fw-width,1.25em)}.fa-layers svg.svg-inline--fa{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0}.fa-layers-counter,.fa-layers-text{display:inline-block;position:absolute;text-align:center}.fa-layers{display:inline-block;height:1em;position:relative;text-align:center;vertical-align:-.125em;width:1em}.fa-layers svg.svg-inline--fa{-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-text{left:50%;top:50%;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);-webkit-transform-origin:center center;transform-origin:center center}.fa-layers-counter{background-color:var(--fa-counter-background-color,#ff253a);border-radius:var(--fa-counter-border-radius,1em);box-sizing:border-box;color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);max-width:var(--fa-counter-max-width,5em);min-width:var(--fa-counter-min-width,1.5em);overflow:hidden;padding:var(--fa-counter-padding,.25em .5em);right:var(--fa-right,0);text-overflow:ellipsis;top:var(--fa-top,0);-webkit-transform:scale(var(--fa-counter-scale,.25));transform:scale(var(--fa-counter-scale,.25));-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-bottom-right{bottom:var(--fa-bottom,0);right:var(--fa-right,0);top:auto;-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));-webkit-transform-origin:bottom right;transform-origin:bottom right}.fa-layers-bottom-left{bottom:var(--fa-bottom,0);left:var(--fa-left,0);right:auto;top:auto;-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));-webkit-transform-origin:bottom left;transform-origin:bottom left}.fa-layers-top-right{top:var(--fa-top,0);right:var(--fa-right,0);-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));-webkit-transform-origin:top right;transform-origin:top right}.fa-layers-top-left{left:var(--fa-left,0);right:auto;top:var(--fa-top,0);-webkit-transform:scale(var(--fa-layers-scale,.25));transform:scale(var(--fa-layers-scale,.25));-webkit-transform-origin:top left;transform-origin:top left}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{font-size:.625em;line-height:.1em;vertical-align:.225em}.fa-xs{font-size:.75em;line-height:.08333em;vertical-align:.125em}.fa-sm{font-size:.875em;line-height:.07143em;vertical-align:.05357em}.fa-lg{font-size:1.25em;line-height:.05em;vertical-align:-.075em}.fa-xl{font-size:1.5em;line-height:.04167em;vertical-align:-.125em}.fa-2xl{font-size:2em;line-height:.03125em;vertical-align:-.1875em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:var(--fa-li-margin,2.5em);padding-left:0}.fa-ul>li{position:relative}.fa-li{left:calc(var(--fa-li-width, 2em)*-1);position:absolute;text-align:center;width:var(--fa-li-width,2em);line-height:inherit}.fa-border{border-radius:var(--fa-border-radius,.1em);border:var(--fa-border-width,.08em) var(--fa-border-style,solid) var(--fa-border-color,#eee);padding:var(--fa-border-padding,.2em .25em .15em)}.fa-pull-left{float:left;margin-right:var(--fa-pull-margin,.3em)}.fa-pull-right{float:right;margin-left:var(--fa-pull-margin,.3em)}.fa-beat{-webkit-animation-name:fa-beat;animation-name:fa-beat;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{-webkit-animation-name:fa-bounce;animation-name:fa-bounce;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28,.84,.42,1))}.fa-fade{-webkit-animation-name:fa-fade;animation-name:fa-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-beat-fade,.fa-fade{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s)}.fa-beat-fade{-webkit-animation-name:fa-beat-fade;animation-name:fa-beat-fade;-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1));animation-timing-function:var(--fa-animation-timing,cubic-bezier(.4,0,.6,1))}.fa-flip{-webkit-animation-name:fa-flip;animation-name:fa-flip;-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,ease-in-out);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{-webkit-animation-name:fa-shake;animation-name:fa-shake;-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-shake,.fa-spin{-webkit-animation-delay:var(--fa-animation-delay,0s);animation-delay:var(--fa-animation-delay,0s);-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal)}.fa-spin{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-duration:var(--fa-animation-duration,2s);animation-duration:var(--fa-animation-duration,2s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,linear);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{-webkit-animation-name:fa-spin;animation-name:fa-spin;-webkit-animation-direction:var(--fa-animation-direction,normal);animation-direction:var(--fa-animation-direction,normal);-webkit-animation-duration:var(--fa-animation-duration,1s);animation-duration:var(--fa-animation-duration,1s);-webkit-animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-iteration-count:var(--fa-animation-iteration-count,infinite);-webkit-animation-timing-function:var(--fa-animation-timing,steps(8));animation-timing-function:var(--fa-animation-timing,steps(8))}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-beat-fade,.fa-bounce,.fa-fade,.fa-flip,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse{-webkit-animation-delay:-1ms;animation-delay:-1ms;-webkit-animation-duration:1ms;animation-duration:1ms;-webkit-animation-iteration-count:1;animation-iteration-count:1;transition-delay:0s;transition-duration:0s}}@-webkit-keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@keyframes fa-beat{0%,90%{-webkit-transform:scale(1);transform:scale(1)}45%{-webkit-transform:scale(var(--fa-beat-scale,1.25));transform:scale(var(--fa-beat-scale,1.25))}}@-webkit-keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@keyframes fa-bounce{0%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}10%{-webkit-transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0);transform:scale(var(--fa-bounce-start-scale-x,1.1),var(--fa-bounce-start-scale-y,.9)) translateY(0)}30%{-webkit-transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em));transform:scale(var(--fa-bounce-jump-scale-x,.9),var(--fa-bounce-jump-scale-y,1.1)) translateY(var(--fa-bounce-height,-.5em))}50%{-webkit-transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0);transform:scale(var(--fa-bounce-land-scale-x,1.05),var(--fa-bounce-land-scale-y,.95)) translateY(0)}57%{-webkit-transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em));transform:scale(1) translateY(var(--fa-bounce-rebound,-.125em))}64%{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}to{-webkit-transform:scale(1) translateY(0);transform:scale(1) translateY(0)}}@-webkit-keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@keyframes fa-fade{50%{opacity:var(--fa-fade-opacity,.4)}}@-webkit-keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@keyframes fa-beat-fade{0%,to{opacity:var(--fa-beat-fade-opacity,.4);-webkit-transform:scale(1);transform:scale(1)}50%{opacity:1;-webkit-transform:scale(var(--fa-beat-fade-scale,1.125));transform:scale(var(--fa-beat-fade-scale,1.125))}}@-webkit-keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@keyframes fa-flip{50%{-webkit-transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg));transform:rotate3d(var(--fa-flip-x,0),var(--fa-flip-y,1),var(--fa-flip-z,0),var(--fa-flip-angle,-180deg))}}@-webkit-keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes fa-shake{0%{-webkit-transform:rotate(-15deg);transform:rotate(-15deg)}4%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}8%,24%{-webkit-transform:rotate(-18deg);transform:rotate(-18deg)}12%,28%{-webkit-transform:rotate(18deg);transform:rotate(18deg)}16%{-webkit-transform:rotate(-22deg);transform:rotate(-22deg)}20%{-webkit-transform:rotate(22deg);transform:rotate(22deg)}32%{-webkit-transform:rotate(-12deg);transform:rotate(-12deg)}36%{-webkit-transform:rotate(12deg);transform:rotate(12deg)}40%,to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}.fa-rotate-by{-webkit-transform:rotate(var(--fa-rotate-angle,none));transform:rotate(var(--fa-rotate-angle,none))}.fa-stack{display:inline-block;vertical-align:middle;height:2em;position:relative;width:2.5em}.fa-stack-1x,.fa-stack-2x{bottom:0;left:0;margin:auto;position:absolute;right:0;top:0;z-index:var(--fa-stack-z-index,auto)}.svg-inline--fa.fa-stack-1x{height:1em;width:1.25em}.svg-inline--fa.fa-stack-2x{height:2em;width:2.5em}.fa-inverse{color:var(--fa-inverse,#fff)}.fa-sr-only,.fa-sr-only-focusable:not(:focus),.sr-only,.sr-only-focusable:not(:focus){position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.svg-inline--fa .fa-primary{fill:var(--fa-primary-color,currentColor);opacity:var(--fa-primary-opacity,1)}.svg-inline--fa .fa-secondary{fill:var(--fa-secondary-color,currentColor)}.svg-inline--fa .fa-secondary,.svg-inline--fa.fa-swap-opacity .fa-primary{opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-secondary{opacity:var(--fa-primary-opacity,1)}.svg-inline--fa mask .fa-primary,.svg-inline--fa mask .fa-secondary{fill:#000}.fa-duotone.fa-inverse,.fad.fa-inverse{color:var(--fa-inverse,#fff)} -------------------------------------------------------------------------------- /web/public/fontawesome/css/v4-font-face.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | @font-face { 7 | font-family: 'FontAwesome'; 8 | font-display: block; 9 | src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } 10 | 11 | @font-face { 12 | font-family: 'FontAwesome'; 13 | font-display: block; 14 | src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } 15 | 16 | @font-face { 17 | font-family: 'FontAwesome'; 18 | font-display: block; 19 | src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); 20 | unicode-range: U+F003,U+F006,U+F014,U+F016-F017,U+F01A-F01B,U+F01D,U+F022,U+F03E,U+F044,U+F046,U+F05C-F05D,U+F06E,U+F070,U+F087-F088,U+F08A,U+F094,U+F096-F097,U+F09D,U+F0A0,U+F0A2,U+F0A4-F0A7,U+F0C5,U+F0C7,U+F0E5-F0E6,U+F0EB,U+F0F6-F0F8,U+F10C,U+F114-F115,U+F118-F11A,U+F11C-F11D,U+F133,U+F147,U+F14E,U+F150-F152,U+F185-F186,U+F18E,U+F190-F192,U+F196,U+F1C1-F1C9,U+F1D9,U+F1DB,U+F1E3,U+F1EA,U+F1F7,U+F1F9,U+F20A,U+F247-F248,U+F24A,U+F24D,U+F255-F25B,U+F25D,U+F271-F274,U+F278,U+F27B,U+F28C,U+F28E,U+F29C,U+F2B5,U+F2B7,U+F2BA,U+F2BC,U+F2BE,U+F2C0-F2C1,U+F2C3,U+F2D0,U+F2D2,U+F2D4,U+F2DC; } 21 | 22 | @font-face { 23 | font-family: 'FontAwesome'; 24 | font-display: block; 25 | src: url("../webfonts/fa-v4compatibility.woff2") format("woff2"), url("../webfonts/fa-v4compatibility.ttf") format("truetype"); 26 | unicode-range: U+F041,U+F047,U+F065-F066,U+F07D-F07E,U+F080,U+F08B,U+F08E,U+F090,U+F09A,U+F0AC,U+F0AE,U+F0B2,U+F0D0,U+F0D6,U+F0E4,U+F0EC,U+F10A-F10B,U+F123,U+F13E,U+F148-F149,U+F14C,U+F156,U+F15E,U+F160-F161,U+F163,U+F175-F178,U+F195,U+F1F8,U+F219,U+F27A; } 27 | -------------------------------------------------------------------------------- /web/public/fontawesome/css/v4-font-face.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | @font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype");unicode-range:u+f003,u+f006,u+f014,u+f016-f017,u+f01a-f01b,u+f01d,u+f022,u+f03e,u+f044,u+f046,u+f05c-f05d,u+f06e,u+f070,u+f087-f088,u+f08a,u+f094,u+f096-f097,u+f09d,u+f0a0,u+f0a2,u+f0a4-f0a7,u+f0c5,u+f0c7,u+f0e5-f0e6,u+f0eb,u+f0f6-f0f8,u+f10c,u+f114-f115,u+f118-f11a,u+f11c-f11d,u+f133,u+f147,u+f14e,u+f150-f152,u+f185-f186,u+f18e,u+f190-f192,u+f196,u+f1c1-f1c9,u+f1d9,u+f1db,u+f1e3,u+f1ea,u+f1f7,u+f1f9,u+f20a,u+f247-f248,u+f24a,u+f24d,u+f255-f25b,u+f25d,u+f271-f274,u+f278,u+f27b,u+f28c,u+f28e,u+f29c,u+f2b5,u+f2b7,u+f2ba,u+f2bc,u+f2be,u+f2c0-f2c1,u+f2c3,u+f2d0,u+f2d2,u+f2d4,u+f2dc}@font-face{font-family:"FontAwesome";font-display:block;src:url(../webfonts/fa-v4compatibility.woff2) format("woff2"),url(../webfonts/fa-v4compatibility.ttf) format("truetype");unicode-range:u+f041,u+f047,u+f065-f066,u+f07d-f07e,u+f080,u+f08b,u+f08e,u+f090,u+f09a,u+f0ac,u+f0ae,u+f0b2,u+f0d0,u+f0d6,u+f0e4,u+f0ec,u+f10a-f10b,u+f123,u+f13e,u+f148-f149,u+f14c,u+f156,u+f15e,u+f160-f161,u+f163,u+f175-f178,u+f195,u+f1f8,u+f219,u+f27a} -------------------------------------------------------------------------------- /web/public/fontawesome/css/v5-font-face.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | @font-face { 7 | font-family: 'Font Awesome 5 Brands'; 8 | font-display: block; 9 | font-weight: 400; 10 | src: url("../webfonts/fa-brands-400.woff2") format("woff2"), url("../webfonts/fa-brands-400.ttf") format("truetype"); } 11 | 12 | @font-face { 13 | font-family: 'Font Awesome 5 Free'; 14 | font-display: block; 15 | font-weight: 900; 16 | src: url("../webfonts/fa-solid-900.woff2") format("woff2"), url("../webfonts/fa-solid-900.ttf") format("truetype"); } 17 | 18 | @font-face { 19 | font-family: 'Font Awesome 5 Free'; 20 | font-display: block; 21 | font-weight: 400; 22 | src: url("../webfonts/fa-regular-400.woff2") format("woff2"), url("../webfonts/fa-regular-400.ttf") format("truetype"); } 23 | -------------------------------------------------------------------------------- /web/public/fontawesome/css/v5-font-face.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 6.2.1 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | * Copyright 2022 Fonticons, Inc. 5 | */ 6 | @font-face{font-family:"Font Awesome 5 Brands";font-display:block;font-weight:400;src:url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:900;src:url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.ttf) format("truetype")}@font-face{font-family:"Font Awesome 5 Free";font-display:block;font-weight:400;src:url(../webfonts/fa-regular-400.woff2) format("woff2"),url(../webfonts/fa-regular-400.ttf) format("truetype")} -------------------------------------------------------------------------------- /web/public/fontawesome/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fontawesome/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /web/public/fontawesome/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fontawesome/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /web/public/fontawesome/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fontawesome/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /web/public/fontawesome/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fontawesome/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /web/public/fontawesome/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fontawesome/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /web/public/fontawesome/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fontawesome/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /web/public/fontawesome/webfonts/fa-v4compatibility.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fontawesome/webfonts/fa-v4compatibility.ttf -------------------------------------------------------------------------------- /web/public/fontawesome/webfonts/fa-v4compatibility.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fontawesome/webfonts/fa-v4compatibility.woff2 -------------------------------------------------------------------------------- /web/public/fonts/space-grotesk-v13-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fonts/space-grotesk-v13-latin-700.woff -------------------------------------------------------------------------------- /web/public/fonts/space-grotesk-v13-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fonts/space-grotesk-v13-latin-700.woff2 -------------------------------------------------------------------------------- /web/public/fonts/space-grotesk-v13-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fonts/space-grotesk-v13-latin-regular.woff -------------------------------------------------------------------------------- /web/public/fonts/space-grotesk-v13-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/fonts/space-grotesk-v13-latin-regular.woff2 -------------------------------------------------------------------------------- /web/public/images/1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/images/1.webp -------------------------------------------------------------------------------- /web/public/images/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/images/2.webp -------------------------------------------------------------------------------- /web/public/images/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midudev/pruebas-tecnicas/c9d87e01ba4883268359584a00d45fc8a3beaa09/web/public/images/og.png -------------------------------------------------------------------------------- /web/src/components/Badge.astro: -------------------------------------------------------------------------------- 1 | 4 | 15 | ¡Abierto hasta el 27 de julio! 16 | 17 | -------------------------------------------------------------------------------- /web/src/components/CodeBlock.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Prism } from "@astrojs/prism"; 3 | 4 | const { language, content } = Astro.props; 5 | --- 6 | 7 | 8 | 9 | 331 | -------------------------------------------------------------------------------- /web/src/components/CodePenEmbed.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = { 3 | url: string; 4 | title: string; 5 | }; 6 | 7 | const { url, title } = Astro.props; 8 | --- 9 | 10 |

    18 | {title} 19 |

    20 | 22 | -------------------------------------------------------------------------------- /web/src/components/DarkModeToggle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 20 | 21 | 22 | 31 | 32 | 110 | 111 | 121 | -------------------------------------------------------------------------------- /web/src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | 15 | 16 | 47 | -------------------------------------------------------------------------------- /web/src/components/GitHubGistEmbed.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = { 3 | id: string; 4 | }; 5 | 6 | const { id } = Astro.props; 7 | --- 8 | 9 | 15 | -------------------------------------------------------------------------------- /web/src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import HeaderSocialLink from "./HeaderSocialLink.astro" 3 | import DarkModeToggle from "./DarkModeToggle.astro" 4 | --- 5 | 6 |
    7 | 11 | Saltar al contenido 12 | 13 | 14 |
    17 | 21 | 23 | Twitch 24 | 25 | 26 | 30 | 34 | GitHub 35 | 36 | 40 | 44 | Twitter 45 | 46 | 50 | 54 | Discord 55 | 56 |
    57 | 58 |
    59 | 70 | -------------------------------------------------------------------------------- /web/src/components/HeaderLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props extends astroHTML.JSX.AnchorHTMLAttributes {} 3 | 4 | const { href, class: className, ...props } = Astro.props; 5 | const path = Astro.url.pathname.replace(/\/$/, ""); 6 | 7 | const isHome = href === '/' && path === ''; 8 | const isOtherPages = typeof href === "string" && href.length > 1 9 | ? path.substring(1).startsWith(href.substring(1)) 10 | : false; 11 | const isActive = isHome || isOtherPages 12 | 13 | --- 14 | 15 | 24 | -------------------------------------------------------------------------------- /web/src/components/HeaderSocialLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | class?: string 4 | href: string 5 | } 6 | 7 | const { href, class: className, ...props } = Astro.props 8 | --- 9 | 10 | 20 | -------------------------------------------------------------------------------- /web/src/components/Heading.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import slugify from 'slugify'; 3 | import type { HTMLAttributes, HTMLTag } from 'astro/types' 4 | 5 | interface Props extends HTMLAttributes<'h1'> { 6 | level: 1 | 2 | 3 | 4 | 5 | 6; 7 | children: any; 8 | } 9 | 10 | const { level, children } = Astro.props; 11 | const headingText = children.length && typeof children[0] === 'string' ? children[0] : ''; 12 | const id = slugify(headingText.toLowerCase()); 13 | 14 | let Tag: HTMLTag = "h1"; 15 | if (level === 2) { 16 | Tag = "h2"; 17 | } else if (level === 3) { 18 | Tag = "h3"; 19 | } else if (level === 4) { 20 | Tag = "h4"; 21 | } else if (level === 5) { 22 | Tag = "h5"; 23 | } else if (level === 6) { 24 | Tag = "h6"; 25 | } 26 | const shouldAddBeforePseudoStyle = ["h1", "h2"].includes(Tag) 27 | --- 28 | 29 | 30 | { 31 | shouldAddBeforePseudoStyle? 32 | 36 | {children} 37 | 38 | : 39 | 40 | {children} 41 | 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /web/src/components/Intro.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Projects from "./Projects.astro"; 3 | --- 4 | 5 |
    6 |
    7 |

    10 | 17 | Pruebas técnicas de Programación 18 | 19 |

    20 |
    21 |
    22 |

    23 | Practica tus habilidades de programación con pruebas técnicas de empresas reales. Usa el stack que prefieras, entrega tus ejercicios y recibe el feedback de la comunidad. 24 |

    25 |
    26 |
    27 | 28 | 29 | -------------------------------------------------------------------------------- /web/src/components/Nav.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import HeaderLink from "./HeaderLink.astro"; 3 | --- 4 | 5 | 6 | 32 | 33 | 34 | 50 | 51 | 59 | -------------------------------------------------------------------------------- /web/src/components/PageMeta.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getPageMeta } from "../lib/seo" 3 | import { 4 | SITE_TITLE, 5 | SITE_DESCRIPTION, 6 | SITE_URL, 7 | TWITTER_HANDLE, 8 | } from "../config" 9 | 10 | export interface Props { 11 | title?: string 12 | description?: string 13 | } 14 | 15 | const { title, description } = Astro.props 16 | 17 | const { meta, og, twitter } = getPageMeta({ 18 | title: title || SITE_TITLE, 19 | description: description || SITE_DESCRIPTION, 20 | baseUrl: SITE_URL, 21 | ogImageAbsoluteUrl: `${SITE_URL}/images/og.png`, 22 | ogImageAltText: "Pruebas técnicas de frontend y backend", 23 | ogImageWidth: 1200, 24 | ogImageHeight: 630, 25 | siteOwnerTwitterHandle: TWITTER_HANDLE, 26 | contentAuthorTwitterHandle: TWITTER_HANDLE, 27 | }) 28 | --- 29 | 30 | 31 | {meta.title} 32 | 33 | {meta.description && } 34 | 35 | 36 | {og.title && } 37 | {og.description && } 38 | {og.type && } 39 | {og.url && } 40 | {og.image && } 41 | {og.imageAlt && } 42 | {og.imageWidth && } 43 | {og.imageHeight && } 44 | 45 | 46 | {twitter.title && } 47 | { 48 | twitter.description && ( 49 | 50 | ) 51 | } 52 | {twitter.site && } 53 | { 54 | twitter.creator && ( 55 | 56 | ) 57 | } 58 | 59 | {twitter.image && } 60 | { 61 | twitter.imageAlt && ( 62 | 63 | ) 64 | } 65 | 66 | -------------------------------------------------------------------------------- /web/src/components/Projects.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Badge from "./Badge.astro" 3 | 4 | const PROJECTS = [ 5 | { 6 | title: "Lista de lectura", 7 | description: 8 | "Implementar una pequeña aplicación de lista de libros utilizando el framework de frontend de tu elección (React, Angular, Vue, Svelte, Qwik, etc).", 9 | image: "/images/1.webp", 10 | alt: "Una persona en una escalera rodeado de libros", 11 | url: "https://github.com/midudev/pruebas-tecnicas/tree/main/pruebas/01-reading-list", 12 | }, 13 | { 14 | title: "Tienda y resultados", 15 | description: 16 | "Crear una aplicación que consta de 3 componentes principales: Una caja de búsqueda, lista de resultados y descripción del detalle del producto. Usa el framework de frontend de tu elección.", 17 | image: "/images/2.webp", 18 | alt: "Una persona en una escalera rodeado de libros", 19 | url: "https://github.com/midudev/pruebas-tecnicas/tree/main/pruebas/02-bazar-universal", 20 | }, 21 | ] 22 | --- 23 | 24 | 56 | -------------------------------------------------------------------------------- /web/src/components/Renderer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { MarkdocRenderer } from "astro-markdoc-renderer"; 3 | import type { Content } from "astro-markdoc-renderer"; 4 | import YouTubeEmbed from "./YouTubeEmbed.astro"; 5 | import TweetEmbed from "./TweetEmbed.astro"; 6 | import CodePenEmbed from "./CodePenEmbed.astro"; 7 | import GitHubGistEmbed from "./GitHubGistEmbed.astro"; 8 | import CodeBlock from "./CodeBlock.astro"; 9 | import Heading from "./Heading.astro"; 10 | 11 | type Props = { 12 | content: Content; 13 | }; 14 | 15 | const { content } = Astro.props; 16 | 17 | const components = { 18 | Heading: { 19 | Component: Heading, 20 | props: {}, 21 | }, 22 | CodeBlock: { 23 | Component: CodeBlock, 24 | props: {}, 25 | }, 26 | YouTubeEmbed: { 27 | Component: YouTubeEmbed, 28 | props: {}, 29 | }, 30 | TweetEmbed: { 31 | Component: TweetEmbed, 32 | props: {}, 33 | }, 34 | CodePenEmbed: { 35 | Component: CodePenEmbed, 36 | props: {}, 37 | }, 38 | GitHubGistEmbed: { 39 | Component: GitHubGistEmbed, 40 | props: {}, 41 | }, 42 | }; 43 | --- 44 | 45 | 46 | -------------------------------------------------------------------------------- /web/src/components/TweetEmbed.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = { 3 | url: string; 4 | }; 5 | 6 | const { url } = Astro.props; 7 | --- 8 | 9 |
    10 | 19 |
    20 | 26 | -------------------------------------------------------------------------------- /web/src/components/YouTubeEmbed.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Props = { 3 | url: string; 4 | label: string; 5 | }; 6 | 7 | const { url, label } = Astro.props; 8 | --- 9 | 10 |
    11 | 20 |
    21 | -------------------------------------------------------------------------------- /web/src/config.ts: -------------------------------------------------------------------------------- 1 | export const SITE_TITLE = "Pruebas Técnicas de Programación"; 2 | export const SITE_DESCRIPTION = 3 | "Practica tus habilidades en programación con pruebas técnicas de empresas reales"; 4 | export const TWITTER_HANDLE = "@midudev"; 5 | export const MY_NAME = "Miguel Ángel Durán"; 6 | 7 | // setup in astro.config.mjs 8 | const BASE_URL = new URL(import.meta.env.SITE); 9 | export const SITE_URL = BASE_URL.origin; 10 | -------------------------------------------------------------------------------- /web/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/src/layouts/ContentLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | /* 3 | This layout is used in pages that render markdoc content 4 | - pages/blog/[slug].astro 5 | */ 6 | 7 | // Import the global.css file here so that it is included on 8 | import "../styles/global.css"; 9 | 10 | import GoogleFont from "./GoogleFont.astro"; 11 | import FontAwesome from "./FontAwesome.astro"; 12 | import ThemeScript from "./ThemeScript.astro"; 13 | import Favicon from "./Favicon.astro"; 14 | import Header from "../components/Header.astro"; 15 | import Footer from "../components/Footer.astro"; 16 | 17 | export interface Props { 18 | title: string; 19 | date: Date; 20 | } 21 | 22 | const { title, date } = Astro.props; 23 | const formattedDate = new Date(date).toLocaleDateString("en-us", { 24 | year: "numeric", 25 | month: "short", 26 | day: "numeric", 27 | }); 28 | --- 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 47 |
    48 |
    49 |
    50 |

    51 | 55 | {title} 56 | 57 |

    58 | 59 | 60 |
    61 |
    62 |