Repository: midudev/aprendiendo-react Branch: master Commit: 2de1877d7b63 Files: 269 Total size: 260.5 KB Directory structure: gitextract_1gd43b_x/ ├── .eslintignore ├── .gitignore ├── README.md ├── package.json ├── pnpm-workspace.yaml ├── projects/ │ ├── 01-twitter-follow-card/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.jsx │ │ │ ├── TwitterFollowCard.jsx │ │ │ ├── index.css │ │ │ └── main.jsx │ │ └── vite.config.js │ ├── 02-tic-tac-toe/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.jsx │ │ │ ├── components/ │ │ │ │ ├── Square.jsx │ │ │ │ └── WinnerModal.jsx │ │ │ ├── constants.js │ │ │ ├── index.css │ │ │ ├── logic/ │ │ │ │ ├── board.js │ │ │ │ └── storage/ │ │ │ │ └── index.js │ │ │ └── main.jsx │ │ └── vite.config.js │ ├── 03-mouse-follower/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.jsx │ │ │ ├── index.css │ │ │ └── main.jsx │ │ └── vite.config.js │ ├── 04-react-prueba-tecnica/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── counter.js │ │ ├── index.html │ │ ├── main.jsx │ │ ├── package.json │ │ ├── playwright.config.cjs │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.jsx │ │ │ ├── Components/ │ │ │ │ └── Otro.jsx │ │ │ ├── hooks/ │ │ │ │ ├── useCatFact.js │ │ │ │ └── useCatImage.js │ │ │ └── services/ │ │ │ └── facts.js │ │ ├── style.css │ │ ├── tests/ │ │ │ └── example.spec.js │ │ └── vite.config.js │ ├── 05-react-buscador-peliculas/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.jsx │ │ │ ├── components/ │ │ │ │ └── Movies.jsx │ │ │ ├── hooks/ │ │ │ │ └── useMovies.js │ │ │ ├── index.css │ │ │ ├── main.jsx │ │ │ ├── mocks/ │ │ │ │ ├── no-results.json │ │ │ │ └── with-results.json │ │ │ └── services/ │ │ │ └── movies.js │ │ └── vite.config.js │ ├── 06-shopping-cart/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.jsx │ │ │ ├── components/ │ │ │ │ ├── Cart.css │ │ │ │ ├── Cart.jsx │ │ │ │ ├── Filters.css │ │ │ │ ├── Filters.jsx │ │ │ │ ├── Footer.css │ │ │ │ ├── Footer.jsx │ │ │ │ ├── Header.jsx │ │ │ │ ├── Icons.jsx │ │ │ │ ├── Products.css │ │ │ │ └── Products.jsx │ │ │ ├── config.js │ │ │ ├── context/ │ │ │ │ ├── cart.jsx │ │ │ │ └── filters.jsx │ │ │ ├── hooks/ │ │ │ │ ├── useCart.js │ │ │ │ └── useFilters.js │ │ │ ├── index.css │ │ │ ├── main.jsx │ │ │ ├── mocks/ │ │ │ │ └── products.json │ │ │ └── reducers/ │ │ │ └── cart.js │ │ └── vite.config.js │ ├── 07-midu-router/ │ │ ├── .npmignore │ │ ├── .swcrc │ │ ├── README.md │ │ ├── index.html │ │ ├── lib/ │ │ │ ├── Link.js │ │ │ ├── Route.js │ │ │ ├── Router.js │ │ │ └── index.js │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.jsx │ │ │ ├── Router.test.jsx │ │ │ ├── components/ │ │ │ │ ├── Link.jsx │ │ │ │ ├── Route.jsx │ │ │ │ └── Router.jsx │ │ │ ├── index.css │ │ │ ├── index.jsx │ │ │ ├── main.jsx │ │ │ ├── pages/ │ │ │ │ ├── 404.jsx │ │ │ │ ├── About.jsx │ │ │ │ ├── Home.jsx │ │ │ │ └── Search.jsx │ │ │ └── utils/ │ │ │ ├── consts.js │ │ │ └── getCurrentPath.js │ │ └── vite.config.js │ ├── 08-todo-app-typescript/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.tsx │ │ │ ├── components/ │ │ │ │ ├── Copyright.css │ │ │ │ ├── Copyright.tsx │ │ │ │ ├── CreateTodo.tsx │ │ │ │ ├── Filters.tsx │ │ │ │ ├── Footer.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── Todo.tsx │ │ │ │ └── Todos.tsx │ │ │ ├── consts.ts │ │ │ ├── hooks/ │ │ │ │ ├── useTodoFirst.ts │ │ │ │ └── useTodos.ts │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── mocks/ │ │ │ │ └── todos.ts │ │ │ ├── services/ │ │ │ │ └── todos.ts │ │ │ ├── types.d.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── 09-google-translate-clone/ │ │ ├── .eslintrc.cjs │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.test.tsx │ │ │ ├── App.tsx │ │ │ ├── components/ │ │ │ │ ├── Icons.tsx │ │ │ │ ├── LanguageSelector.tsx │ │ │ │ └── TextArea.tsx │ │ │ ├── constants.ts │ │ │ ├── hooks/ │ │ │ │ ├── useDebounce.ts │ │ │ │ └── useStore.ts │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── services/ │ │ │ │ └── translate.ts │ │ │ ├── types.d.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── 10-crud-redux/ │ │ ├── .gitignore │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── rome.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── components/ │ │ │ │ ├── CreateNewUser.tsx │ │ │ │ └── ListOfUsers.tsx │ │ │ ├── hooks/ │ │ │ │ ├── store.ts │ │ │ │ └── useUserActions.ts │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── store/ │ │ │ │ ├── index.ts │ │ │ │ └── users/ │ │ │ │ └── slice.ts │ │ │ └── vite-env.d.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── 11-typescript-prueba-tecnica/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── components/ │ │ │ │ └── UsersList.tsx │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── types.d.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── 11b-typescript-prueba-tecnica-with-react-query/ │ │ ├── README.md │ │ ├── index.html │ │ ├── package.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── components/ │ │ │ │ ├── Results.tsx │ │ │ │ └── UsersList.tsx │ │ │ ├── hooks/ │ │ │ │ └── useUsers.ts │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── services/ │ │ │ │ └── users.ts │ │ │ ├── types.d.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── 12-comments-react-query/ │ │ ├── index.html │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── components/ │ │ │ │ ├── Form.tsx │ │ │ │ └── Results.tsx │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── service/ │ │ │ │ └── comments.ts │ │ │ └── vite-env.d.ts │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ ├── 13-javascript-quiz-con-zustand/ │ │ ├── .eslintrc.cjs │ │ ├── index.html │ │ ├── package.json │ │ ├── public/ │ │ │ └── data.json │ │ ├── src/ │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Game.tsx │ │ │ ├── JavaScriptLogo.tsx │ │ │ ├── Results.tsx │ │ │ ├── Start.tsx │ │ │ ├── hooks/ │ │ │ │ └── useQuestionsData.ts │ │ │ ├── index.css │ │ │ ├── main.tsx │ │ │ ├── services/ │ │ │ │ └── questions.ts │ │ │ ├── store/ │ │ │ │ └── questions.ts │ │ │ ├── types.d.ts │ │ │ └── vite-env.d.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.node.json │ │ └── vite.config.ts │ └── 14-hacker-news-prueba-tecnica/ │ ├── .gitignore │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src/ │ │ ├── App.css │ │ ├── App.tsx │ │ ├── components/ │ │ │ ├── CommentLoader.tsx │ │ │ ├── Header.css.ts │ │ │ ├── Header.tsx │ │ │ ├── ListOfComments.tsx │ │ │ ├── Story.css.ts │ │ │ ├── Story.tsx │ │ │ └── StoryLoader.tsx │ │ ├── index.css │ │ ├── main.tsx │ │ ├── pages/ │ │ │ ├── Detail.tsx │ │ │ └── TopStories.tsx │ │ ├── services/ │ │ │ └── hacker-news.ts │ │ ├── utils/ │ │ │ └── getRelativeTime.ts │ │ └── vite-env.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ node_modules dist ================================================ FILE: .gitignore ================================================ package-lock.json node_modules .DS_Store projects/**/.DS_Store projects/**/dist ================================================ FILE: README.md ================================================
Curso de React js desde cero y con proyectos # Aprendiendo React ⚛️ Curso para aprender **React** basado en proyectos. **[Todos los miércoles a las 18PM 🇪🇸 en Twitch](https://twitch.tv/midudev)**
## 📹 Videos con las clases - 01: [Introducción a React](https://www.youtube.com/watch?v=7iobxzd_2wY) - 02: [React Hooks: useState y useEffect](https://www.youtube.com/watch?v=qkzcjwnueLA&feature=youtu.be) - 03: [Prueba técnica con lo aprendido](https://www.youtube.com/watch?v=XYpadB4VadY&feature=youtu.be) - 04: [Fetching de datos y Custom Hooks](https://youtu.be/x-LcbVw99o8) - 05: [React Hooks: useRef, useMemo, useCallback](https://youtu.be/GOEiMwDJ3lc) - 06: [React Hooks: useContext, useReducer, useId](https://www.youtube.com/watch?v=B9tDYAZZxcE) - 07: [React Router + Lazy Loading](https://www.youtube.com/watch?v=K2NcGYajvY4) - 08: [React + TypeScript (Día 01): props y state](https://www.youtube.com/watch?v=4lAYfsq-2TE) - 09: [React + TypeScript + ChatGPT - Clon de Google Translate](https://www.youtube.com/watch?v=kZhabulNCUc) - 10: [React Redux Toolkit + Rome Tools](https://www.youtube.com/watch?v=bEEjuwujbbU) - 11: [Prueba técnica de React con TypeScript](https://www.youtube.com/watch?v=mNJOWXc83Y4) - 12: [React Query + Paginación + Infinite Scroll](https://www.youtube.com/watch?v=WKfVjQUa6nE) - 13: [JavaScript Quiz con Zustand + TypeScript desde cero](https://www.youtube.com/watch?v=p2wF2wRjcN0) - 14: Hacker News con TypeScript + SWR - Pendiente de subir ## ⌨️ Proyectos de React con código | Número | Proyecto | Código | Web | | --- | --- | --- | --- | | `01` | Twitter Follow Card | [Ver](projects/01-twitter-follow-card/) | [Visitar](https://midu-react-01.surge.sh) | | `02` | Tic Tac Toe | [Ver](projects/02-tic-tac-toe/) | [Visitar](https://midu-react-02.surge.sh) | | `03` | Mouse Follower | [Ver](projects/03-mouse-follower) | [Visitar](https://midu-react-03.surge.sh) | | `04` | Prueba técnica con Promesas, fetching y testing E2E | [Ver](projects/04-react-prueba-tecnica) | [Visitar](https://midu-react-04.surge.sh) | | `05` | Prueba técnica con formularios, buscador utilizando una API | [Ver](projects/05-react-buscador-peliculas) | [Visitar](https://midu-react-05.surge.sh) | | `06` | Creación de un ecommerce con carrito de compras | [Ver](projects/06-shopping-cart) | [Visitar](https://midu-react-06.surge.sh) | | `07` | Creación de un React Router desde cero | [Ver](projects/07-midu-router) | [Visitar](https://midu-react-07.surge.sh) | | `08` | Todo App con TypeScript y animaciones | [Ver](projects/08-todo-app-typescript) | [Visitar](https://midu-react-08.surge.sh) | | `09` | Crear un Google Translate con ChatGPT y TypeScript | [Ver](projects/09-google-translate-clone/) | [Visitar](https://midu-react-09.surge.sh) | | `10` | Crear un CRUD con Redux Toolkit y TypeScript | [Ver](projects/10-crud-redux/) | [Visitar](https://midu-react-10.surge.sh) | | `11` | Prueba Técnica con TypeScript y React | [Ver](projects/11-typescript-prueba-tecnica/) | [Visitar](https://midu-react-11.surge.sh) | | `11b` | Prueba Técnica con TypeScript y React con React Query | [Ver](projects/11b-typescript-prueba-tecnica-with-react-query/) | [Visitar](https://midu-react-11.surge.sh) | | `12` | Sistema de comentarios con React Query | [Ver](projects/12-comments-react-query) | [Visitar](https://midu-react-12.surge.sh) | | `13` | JavaScript Quiz con Zustand y TypeScript | [Ver](projects/13-javascript-quiz-con-zustand/) | [Visitar](https://midu-react-13.surge.sh) | | `14` | Hacker News con TypeScript y SWR | [Ver](projects/14-hacker-news-prueba-tecnica) | [Visitar](https://midu-react-14.surge.sh) | ================================================ FILE: package.json ================================================ { "name": "aprendiendo-react", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "workspaces": [ "projects/*" ], "repository": { "type": "git", "url": "git+https://github.com/midudev/aprendiendo-react.git" }, "keywords": [], "author": "", "license": "ISC", "bugs": { "url": "https://github.com/midudev/aprendiendo-react/issues" }, "homepage": "https://github.com/midudev/aprendiendo-react#readme", "devDependencies": { "standard": "17.0.0" } } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: # todos los proyectos dentro de projects son paquetes - 'projects/**' ================================================ FILE: projects/01-twitter-follow-card/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? package-lock.json ================================================ FILE: projects/01-twitter-follow-card/index.html ================================================ Vite + React
================================================ FILE: projects/01-twitter-follow-card/package.json ================================================ { "name": "01-twitter-follow-card", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react-swc": "^3.0.0", "vite": "^4.0.0" } } ================================================ FILE: projects/01-twitter-follow-card/src/App.css ================================================ .tw-followCard { display: flex; align-items: center; color: #fff; font-size: .8rem; justify-content: space-between; } .tw-followCard-header { display: flex; align-items: center; gap: 4px } .tw-followCard-info { display: flex; flex-direction: column; } .tw-followCard-infoUserName { opacity: .6; } .tw-followCard-avatar { width: 48px; height: 48px; border-radius: 1000px; } .tw-followCard-button { cursor: pointer; margin-left: 16px; border: 0; border-radius: 999px; padding: 6px 16px; font-weight: bold; border: 1px solid #000; transition: .3s ease background-color; } .tw-followCard-button:hover { opacity: .8; } .tw-followCard-text { display: block; } .tw-followCard-button.is-following { border: 1px solid #bbb; background: transparent; color: #fff; width: 140px; } .tw-followCard-button.is-following:hover { background-color: rgba(255, 0, 0, 0.178); color: red; border: 1px solid red; transition: .3s ease all; opacity: 1; } .tw-followCard-button.is-following:hover .tw-followCard-text { display: none; } .tw-followCard-button.is-following:hover .tw-followCard-stopFollow { display: block; } .tw-followCard-stopFollow { display: none; } ================================================ FILE: projects/01-twitter-follow-card/src/App.jsx ================================================ import './App.css' import { TwitterFollowCard } from './TwitterFollowCard.jsx' const users = [ { userName: 'midudev', name: 'Miguel Ángel Durán', isFollowing: true }, { userName: 'pheralb', name: 'Pablo H.', isFollowing: false }, { userName: 'PacoHdezs', name: 'Paco Hdez', isFollowing: true }, { userName: 'TMChein', name: 'Tomas', isFollowing: false } ] export function App () { return (
{ users.map(({ userName, name, isFollowing }) => ( {name} )) }
) } ================================================ FILE: projects/01-twitter-follow-card/src/TwitterFollowCard.jsx ================================================ import { useState } from 'react' export function TwitterFollowCard ({ children, userName, initialIsFollowing }) { const [isFollowing, setIsFollowing] = useState(initialIsFollowing) console.log('[TwitterFollowCard] render with userName: ', userName) const text = isFollowing ? 'Siguiendo' : 'Seguir' const buttonClassName = isFollowing ? 'tw-followCard-button is-following' : 'tw-followCard-button' const handleClick = () => { setIsFollowing(!isFollowing) } return (
El avatar de midudev
{children} @{userName}
) } ================================================ FILE: projects/01-twitter-follow-card/src/index.css ================================================ body { margin: 0; background: #222; font-family: system-ui; display: grid; place-content: center; min-height: 100vh; } .App { display: flex; flex-direction: column; gap: 8px; } ================================================ FILE: projects/01-twitter-follow-card/src/main.jsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import { App } from './App.jsx' import './index.css' const root = ReactDOM.createRoot(document.getElementById('root')) root.render( ) ================================================ FILE: projects/01-twitter-follow-card/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()] }) ================================================ FILE: projects/02-tic-tac-toe/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: projects/02-tic-tac-toe/index.html ================================================ Vite + React
================================================ FILE: projects/02-tic-tac-toe/package.json ================================================ { "name": "02-tic-tac-toe", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "canvas-confetti": "1.6.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react-swc": "^3.0.0", "vite": "^4.0.0" } } ================================================ FILE: projects/02-tic-tac-toe/src/App.css ================================================ #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } ================================================ FILE: projects/02-tic-tac-toe/src/App.jsx ================================================ import { useState } from 'react' import confetti from 'canvas-confetti' import { Square } from './components/Square.jsx' import { TURNS } from './constants.js' import { checkWinnerFrom, checkEndGame } from './logic/board.js' import { WinnerModal } from './components/WinnerModal.jsx' import { saveGameToStorage, resetGameStorage } from './logic/storage/index.js' function App () { const [board, setBoard] = useState(() => { const boardFromStorage = window.localStorage.getItem('board') if (boardFromStorage) return JSON.parse(boardFromStorage) return Array(9).fill(null) }) const [turn, setTurn] = useState(() => { const turnFromStorage = window.localStorage.getItem('turn') return turnFromStorage ?? TURNS.X }) // null es que no hay ganador, false es que hay un empate const [winner, setWinner] = useState(null) const resetGame = () => { setBoard(Array(9).fill(null)) setTurn(TURNS.X) setWinner(null) resetGameStorage() } const updateBoard = (index) => { // no actualizamos esta posición // si ya tiene algo if (board[index] || winner) return // actualizar el tablero const newBoard = [...board] newBoard[index] = turn setBoard(newBoard) // cambiar el turno const newTurn = turn === TURNS.X ? TURNS.O : TURNS.X setTurn(newTurn) // guardar aqui partida saveGameToStorage({ board: newBoard, turn: newTurn }) // revisar si hay ganador const newWinner = checkWinnerFrom(newBoard) if (newWinner) { confetti() setWinner(newWinner) } else if (checkEndGame(newBoard)) { setWinner(false) // empate } } return (

Tic tac toe

{ board.map((square, index) => { return ( {square} ) }) }
{TURNS.X} {TURNS.O}
) } export default App ================================================ FILE: projects/02-tic-tac-toe/src/components/Square.jsx ================================================ export const Square = ({ children, isSelected, updateBoard, index }) => { const className = `square ${isSelected ? 'is-selected' : ''}` const handleClick = () => { updateBoard(index) } return (
{children}
) } ================================================ FILE: projects/02-tic-tac-toe/src/components/WinnerModal.jsx ================================================ import { Square } from './Square.jsx' export function WinnerModal ({ winner, resetGame }) { if (winner === null) return null const winnerText = winner === false ? 'Empate' : 'Ganó:' return (

{winnerText}

{winner && {winner}}
) } ================================================ FILE: projects/02-tic-tac-toe/src/constants.js ================================================ export const TURNS = { // turnos X: '❌', O: '⚪' } export const WINNER_COMBOS = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6] ] ================================================ FILE: projects/02-tic-tac-toe/src/index.css ================================================ :root { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } body { margin: 0; display: flex; justify-content: center; min-width: 320px; min-height: 100vh; } * { padding: 0; margin: 0; box-sizing: border-box; } .board { width: fit-content; margin: 40px auto; text-align: center; } .board h1 { color: #eee; margin-bottom: 16px; } .board button { padding: 8px 12px; margin: 25px; background: transparent; border: 2px solid #eee; color: #eee; width: 100px; border-radius: 5px; transition: 0.2s; font-weight: bold; cursor: pointer; } .board button:hover { background: #eee; color: #222; } .board .game { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; } .turn { display: flex; justify-content: center; margin: 15px auto; width: fit-content; position: relative; border-radius: 10px; } .turn .square, .winner .square { width: 70px; height: 70px; pointer-events: none; border-color: transparent; } .square.is-selected { color: #fff; background: #09f; } .winner { position: absolute; width: 100vw; height: 100vh; top: 0; left: 0; display: grid; place-items: center; background-color: rgba(0, 0, 0, 0.7); } .winner .text { background: #111; height: 300px; width: 320px; border: 2px solid #eee; border-radius: 10px; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 20px; } .winner .win { margin: 0 auto; width: fit-content; border-radius: 10px; display: flex; gap: 15px; } .square { width: 100px; height: 100px; border: 2px solid #eee; border-radius: 5px; display: grid; place-items: center; cursor: pointer; font-size: 48px; } ================================================ FILE: projects/02-tic-tac-toe/src/logic/board.js ================================================ import { WINNER_COMBOS } from '../constants.js' export const checkWinnerFrom = (boardToCheck) => { // revisamos todas las combinaciones ganadoras // para ver si X u O ganó for (const combo of WINNER_COMBOS) { const [a, b, c] = combo if ( boardToCheck[a] && boardToCheck[a] === boardToCheck[b] && boardToCheck[a] === boardToCheck[c] ) { return boardToCheck[a] } } // si no hay ganador return null } export const checkEndGame = (newBoard) => { // revisamos si hay un empate // si no hay más espacios vacíos // en el tablero return newBoard.every((square) => square !== null) } ================================================ FILE: projects/02-tic-tac-toe/src/logic/storage/index.js ================================================ export const saveGameToStorage = ({ board, turn }) => { // guardar aqui partida window.localStorage.setItem('board', JSON.stringify(board)) window.localStorage.setItem('turn', turn) } export const resetGameStorage = () => { window.localStorage.removeItem('board') window.localStorage.removeItem('turn') } ================================================ FILE: projects/02-tic-tac-toe/src/main.jsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( ) ================================================ FILE: projects/02-tic-tac-toe/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()] }) ================================================ FILE: projects/03-mouse-follower/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: projects/03-mouse-follower/index.html ================================================ Vite + React
================================================ FILE: projects/03-mouse-follower/package.json ================================================ { "name": "03-mouse-follower", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.0.26", "@types/react-dom": "^18.0.9", "@vitejs/plugin-react-swc": "^3.0.0", "vite": "^4.0.0" } } ================================================ FILE: projects/03-mouse-follower/src/App.css ================================================ #root { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.react:hover { filter: drop-shadow(0 0 2em #61dafbaa); } @keyframes logo-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @media (prefers-reduced-motion: no-preference) { a:nth-of-type(2) .logo { animation: logo-spin infinite 20s linear; } } .card { padding: 2em; } .read-the-docs { color: #888; } ================================================ FILE: projects/03-mouse-follower/src/App.jsx ================================================ import { useEffect, useState } from 'react' const FollowMouse = () => { const [enabled, setEnabled] = useState(false) const [position, setPosition] = useState({ x: 0, y: 0 }) // pointer move useEffect(() => { console.log('effect ', { enabled }) const handleMove = (event) => { const { clientX, clientY } = event setPosition({ x: clientX, y: clientY }) } if (enabled) { window.addEventListener('pointermove', handleMove) } // cleanup: // -> cuando el componente se desmonta // -> cuando cambian las dependencias, antes de ejecutar // el efecto de nuevo return () => { // cleanup method console.log('cleanup') window.removeEventListener('pointermove', handleMove) } }, [enabled]) // [] -> solo se ejecuta una vez cuando se monta el componente // [enabled] -> se ejecuta cuando cambia enabled y cuando se monta el componente // undefined -> se ejecuta cada vez que se renderiza el componente // change body className useEffect(() => { document.body.classList.toggle('no-cursor', enabled) return () => { document.body.classList.remove('no-cursor') } }, [enabled]) return ( <>
) } function App () { return (
) } export default App ================================================ FILE: projects/03-mouse-follower/src/index.css ================================================ :root { font-family: Inter, Avenir, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: grid; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } body.no-cursor { cursor: none; } ================================================ FILE: projects/03-mouse-follower/src/main.jsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( ) ================================================ FILE: projects/03-mouse-follower/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()] }) ================================================ FILE: projects/04-react-prueba-tecnica/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? /test-results/ /playwright-report/ /playwright/.cache/ ================================================ FILE: projects/04-react-prueba-tecnica/README.md ================================================ # Prueba técnica para Juniors y Trainees de React en Live Coding. APIs: - Facts Random: https://catfact.ninja/fact - Imagen random: https://cataas.com/cat/says/hello - Recupera un hecho aleatorio de gatos de la primera API - Recuperar la primera palabra del hecho - Muestra una imagen de un gato con la primera palabra. ================================================ FILE: projects/04-react-prueba-tecnica/counter.js ================================================ export function setupCounter (element) { let counter = 0 const setCounter = (count) => { counter = count element.innerHTML = `count is ${counter}` } element.addEventListener('click', () => setCounter(counter + 1)) setCounter(0) } ================================================ FILE: projects/04-react-prueba-tecnica/index.html ================================================ Vite App
================================================ FILE: projects/04-react-prueba-tecnica/main.jsx ================================================ import { createRoot } from 'react-dom/client' import { App } from './src/App.jsx' const root = createRoot(document.getElementById('app')) root.render() ================================================ FILE: projects/04-react-prueba-tecnica/package.json ================================================ { "name": "react-prueba-tecnica", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "devDependencies": { "@playwright/test": "^1.30.0", "standard": "^17.0.0", "vite": "^4.0.0" }, "dependencies": { "@vitejs/plugin-react": "3.0.1", "react": "18.2.0", "react-dom": "18.2.0" }, "eslintConfig": { "extends": "./node_modules/standard/eslintrc.json" } } ================================================ FILE: projects/04-react-prueba-tecnica/playwright.config.cjs ================================================ // @ts-check const { devices } = require('@playwright/test') /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ // require('dotenv').config(); /** * @see https://playwright.dev/docs/test-configuration * @type {import('@playwright/test').PlaywrightTestConfig} */ const config = { testDir: './tests', /* Maximum time one test can run for. */ timeout: 30 * 1000, expect: { /** * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ timeout: 5000 }, /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ actionTimeout: 0, /* Base URL to use in actions like `await page.goto('/')`. */ // baseURL: 'http://localhost:3000', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry' }, /* Configure projects for major browsers */ projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] } }, { name: 'webkit', use: { ...devices['Desktop Safari'] } } /* Test against mobile viewports. */ // { // name: 'Mobile Chrome', // use: { // ...devices['Pixel 5'], // }, // }, // { // name: 'Mobile Safari', // use: { // ...devices['iPhone 12'], // }, // }, /* Test against branded browsers. */ // { // name: 'Microsoft Edge', // use: { // channel: 'msedge', // }, // }, // { // name: 'Google Chrome', // use: { // channel: 'chrome', // }, // }, ] /* Folder for test artifacts such as screenshots, videos, traces, etc. */ // outputDir: 'test-results/', /* Run your local dev server before starting the tests */ // webServer: { // command: 'npm run start', // port: 3000, // }, } module.exports = config ================================================ FILE: projects/04-react-prueba-tecnica/src/App.css ================================================ main { display: flex; flex-direction: column; place-items: center; max-width: 800px; margin: 0 auto; font-family: system-ui; } ================================================ FILE: projects/04-react-prueba-tecnica/src/App.jsx ================================================ import './App.css' import { useCatImage } from './hooks/useCatImage.js' import { useCatFact } from './hooks/useCatFact.js' export function App () { const { fact, refreshFact } = useCatFact() const { imageUrl } = useCatImage({ fact }) const handleClick = async () => { refreshFact() } return (

App de gatitos

{fact &&

{fact}

} {imageUrl && {`Image}
) } ================================================ FILE: projects/04-react-prueba-tecnica/src/Components/Otro.jsx ================================================ import { useCatImage } from '../hooks/useCatImage.js' export function Otro () { const { imageUrl } = useCatImage({ fact: 'cat' }) console.log(imageUrl) return ( <> {imageUrl && } ) } ================================================ FILE: projects/04-react-prueba-tecnica/src/hooks/useCatFact.js ================================================ import { useState, useEffect } from 'react' import { getRandomFact } from '../services/facts.js' export function useCatFact () { const [fact, setFact] = useState() const refreshFact = () => { getRandomFact().then(newFact => setFact(newFact)) } // para recuperar la cita al cargar la página useEffect(refreshFact, []) return { fact, refreshFact } } ================================================ FILE: projects/04-react-prueba-tecnica/src/hooks/useCatImage.js ================================================ import { useEffect, useState } from 'react' const CAT_PREFIX_IMAGE_URL = 'https://cataas.com' export function useCatImage ({ fact }) { const [imageUrl, setImageUrl] = useState() // para recuperar la imagen cada vez que tenemos una cita nueva useEffect(() => { if (!fact) return const threeFirstWords = fact.split(' ', 3).join(' ') fetch(`https://cataas.com/cat/says/${threeFirstWords}?size=50&color=red&json=true`) .then(res => res.json()) .then(response => { const { _id } = response const url = `/cat/${_id}/says/${threeFirstWords}` setImageUrl(url) }) }, [fact]) return { imageUrl: `${CAT_PREFIX_IMAGE_URL}${imageUrl}` } } ================================================ FILE: projects/04-react-prueba-tecnica/src/services/facts.js ================================================ const CAT_ENDPOINT_RANDOM_FACT = 'https://catfact.ninja/fact' export const getRandomFact = async () => { const res = await fetch(CAT_ENDPOINT_RANDOM_FACT) const data = await res.json() const { fact } = data return fact } ================================================ FILE: projects/04-react-prueba-tecnica/style.css ================================================ :root { font-family: Inter, Avenir, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 24px; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; -webkit-text-size-adjust: 100%; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } #app { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.vanilla:hover { filter: drop-shadow(0 0 2em #f7df1eaa); } .card { padding: 2em; } .read-the-docs { color: #888; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ================================================ FILE: projects/04-react-prueba-tecnica/tests/example.spec.js ================================================ // @ts-check import { test, expect } from '@playwright/test' const CAT_PREFIX_IMAGE_URL = 'https://cataas.com' const LOCALHOST_URL = 'http://localhost:5173/' test('app shows random fact and image', async ({ page }) => { await page.goto(LOCALHOST_URL) const text = await page.getByRole('paragraph') const image = await page.getByRole('img') const textContent = await text.textContent() const imageSrc = await image.getAttribute('src') await expect(textContent?.length).toBeGreaterThan(0) await expect(imageSrc?.startsWith(CAT_PREFIX_IMAGE_URL)).toBeTruthy() }) ================================================ FILE: projects/04-react-prueba-tecnica/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()] }) ================================================ FILE: projects/05-react-buscador-peliculas/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: projects/05-react-buscador-peliculas/README.md ================================================ ## Enunciado Crea una aplicación para buscar películas API a usar: - https://www.omdbapi.com/ Consigue la API Key en la propia página web registrando tu email. Requerimientos: ✅ Necesita mostrar un input para buscar la película y un botón para buscar. ✅ Lista las películas y muestra el título, año y poster. ✅ Que el formulario funcione ✅ Haz que las películas se muestren en un grid responsive. ✅ Hacer el fetching de datos a la API Primera iteración: ✅ Evitar que se haga la misma búsqueda dos veces seguidas. ✅ Haz que la búsqueda se haga automáticamente al escribir. ✅ Evita que se haga la búsqueda continuamente al escribir (debounce) ================================================ FILE: projects/05-react-buscador-peliculas/index.html ================================================ Vite + React
================================================ FILE: projects/05-react-buscador-peliculas/package.json ================================================ { "name": "05-react-buscador-peliculas", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "just-debounce-it": "3.2.0", "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "@vitejs/plugin-react-swc": "^3.0.0", "vite": "^4.1.0" } } ================================================ FILE: projects/05-react-buscador-peliculas/src/App.css ================================================ .page { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 100%; max-width: 800px; } main { display: flex; justify-content: center; width: 100%; } form { align-items: center; display: flex; justify-content: center; } .movies { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); width: 100%; gap: 32px; } .movie { text-align: center; } .movie h3, .movie p { margin: 0; } .movie img { border-radius: 8px; margin-top: 16px; } ================================================ FILE: projects/05-react-buscador-peliculas/src/App.jsx ================================================ import './App.css' import { useMovies } from './hooks/useMovies.js' import { Movies } from './components/Movies.jsx' import { useState, useEffect, useRef, useCallback } from 'react' import debounce from 'just-debounce-it' function useSearch () { const [search, updateSearch] = useState('') const [error, setError] = useState(null) const isFirstInput = useRef(true) useEffect(() => { if (isFirstInput.current) { isFirstInput.current = search === '' return } if (search === '') { setError('No se puede buscar una película vacía') return } if (search.match(/^\d+$/)) { setError('No se puede buscar una película con un número') return } if (search.length < 3) { setError('La búsqueda debe tener al menos 3 caracteres') return } setError(null) }, [search]) return { search, updateSearch, error } } function App () { const [sort, setSort] = useState(false) const { search, updateSearch, error } = useSearch() const { movies, loading, getMovies } = useMovies({ search, sort }) const debouncedGetMovies = useCallback( debounce(search => { console.log('search', search) getMovies({ search }) }, 300) , [getMovies] ) const handleSubmit = (event) => { event.preventDefault() getMovies({ search }) } const handleSort = () => { setSort(!sort) } const handleChange = (event) => { const newSearch = event.target.value updateSearch(newSearch) debouncedGetMovies(newSearch) } return (

Buscador de películas

{error &&

{error}

}
{ loading ?

Cargando...

: }
) } export default App ================================================ FILE: projects/05-react-buscador-peliculas/src/components/Movies.jsx ================================================ function ListOfMovies ({ movies }) { return (
    { movies.map(movie => (
  • {movie.title}

    {movie.year}

    {movie.title}
  • )) }
) } function NoMoviesResults () { return (

No se encontraron películas para esta búsqueda

) } export function Movies ({ movies }) { const hasMovies = movies?.length > 0 return ( hasMovies ? : ) } ================================================ FILE: projects/05-react-buscador-peliculas/src/hooks/useMovies.js ================================================ import { useRef, useState, useMemo, useCallback } from 'react' import { searchMovies } from '../services/movies.js' export function useMovies ({ search, sort }) { const [movies, setMovies] = useState([]) const [loading, setLoading] = useState(false) // el error no se usa pero puedes implementarlo // si quieres: const [, setError] = useState(null) const previousSearch = useRef(search) const getMovies = useCallback(async ({ search }) => { if (search === previousSearch.current) return // search es '' try { setLoading(true) setError(null) previousSearch.current = search const newMovies = await searchMovies({ search }) setMovies(newMovies) } catch (e) { setError(e.message) } finally { // tanto en el try como en el catch setLoading(false) } }, []) const sortedMovies = useMemo(() => { if (!movies) return; return sort ? [...movies].sort((a, b) => a.title.localeCompare(b.title)) : movies }, [sort, movies]) return { movies: sortedMovies, getMovies, loading } } ================================================ FILE: projects/05-react-buscador-peliculas/src/index.css ================================================ /** * Automatic version: * Uses light theme by default but switches to dark theme * if a system-wide theme preference is set on the user's device. */ :root { --background-body: #fff; --background: #efefef; --background-alt: #f7f7f7; --selection: #9e9e9e; --text-main: #363636; --text-bright: #000; --text-muted: #70777f; --links: #0076d1; --focus: #0096bfab; --border: #dbdbdb; --code: #000; --animation-duration: 0.1s; --button-base: #d0cfcf; --button-hover: #9b9b9b; --scrollbar-thumb: rgb(170, 170, 170); --scrollbar-thumb-hover: var(--button-hover); --form-placeholder: #949494; --form-text: #1d1d1d; --variable: #39a33c; --highlight: #ff0; --select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23161f27'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E"); } @media (prefers-color-scheme: dark) { :root { --background-body: #202b38; --background: #161f27; --background-alt: #1a242f; --selection: #1c76c5; --text-main: #dbdbdb; --text-bright: #fff; --text-muted: #a9b1ba; --links: #41adff; --focus: #0096bfab; --border: #526980; --code: #ffbe85; --animation-duration: 0.1s; --button-base: #0c151c; --button-hover: #040a0f; --scrollbar-thumb: var(--button-hover); --scrollbar-thumb-hover: rgb(0, 0, 0); --form-placeholder: #a9a9a9; --form-text: #fff; --variable: #d941e2; --highlight: #efdb43; --select-arrow: url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E"); } } html { scrollbar-color: rgb(170, 170, 170) #fff; scrollbar-color: var(--scrollbar-thumb) var(--background-body); scrollbar-width: thin; } @media (prefers-color-scheme: dark) { html { scrollbar-color: #040a0f #202b38; scrollbar-color: var(--scrollbar-thumb) var(--background-body); } } @media (prefers-color-scheme: dark) { html { scrollbar-color: #040a0f #202b38; scrollbar-color: var(--scrollbar-thumb) var(--background-body); } } @media (prefers-color-scheme: dark) { html { scrollbar-color: #040a0f #202b38; scrollbar-color: var(--scrollbar-thumb) var(--background-body); } } @media (prefers-color-scheme: dark) { html { scrollbar-color: #040a0f #202b38; scrollbar-color: var(--scrollbar-thumb) var(--background-body); } } @media (prefers-color-scheme: dark) { html { scrollbar-color: #040a0f #202b38; scrollbar-color: var(--scrollbar-thumb) var(--background-body); } } @media (prefers-color-scheme: dark) { html { scrollbar-color: #040a0f #202b38; scrollbar-color: var(--scrollbar-thumb) var(--background-body); } } body { font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', sans-serif; line-height: 1.4; max-width: 800px; margin: 20px auto; padding: 0 10px; word-wrap: break-word; color: #363636; color: var(--text-main); background: #fff; background: var(--background-body); text-rendering: optimizeLegibility; } @media (prefers-color-scheme: dark) { body { background: #202b38; background: var(--background-body); } } @media (prefers-color-scheme: dark) { body { color: #dbdbdb; color: var(--text-main); } } button { transition: background-color 0.1s linear, border-color 0.1s linear, color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease; transition: background-color var(--animation-duration) linear, border-color var(--animation-duration) linear, color var(--animation-duration) linear, box-shadow var(--animation-duration) linear, transform var(--animation-duration) ease; } @media (prefers-color-scheme: dark) { button { transition: background-color 0.1s linear, border-color 0.1s linear, color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease; transition: background-color var(--animation-duration) linear, border-color var(--animation-duration) linear, color var(--animation-duration) linear, box-shadow var(--animation-duration) linear, transform var(--animation-duration) ease; } } input { transition: background-color 0.1s linear, border-color 0.1s linear, color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease; transition: background-color var(--animation-duration) linear, border-color var(--animation-duration) linear, color var(--animation-duration) linear, box-shadow var(--animation-duration) linear, transform var(--animation-duration) ease; } @media (prefers-color-scheme: dark) { input { transition: background-color 0.1s linear, border-color 0.1s linear, color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease; transition: background-color var(--animation-duration) linear, border-color var(--animation-duration) linear, color var(--animation-duration) linear, box-shadow var(--animation-duration) linear, transform var(--animation-duration) ease; } } textarea { transition: background-color 0.1s linear, border-color 0.1s linear, color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease; transition: background-color var(--animation-duration) linear, border-color var(--animation-duration) linear, color var(--animation-duration) linear, box-shadow var(--animation-duration) linear, transform var(--animation-duration) ease; } @media (prefers-color-scheme: dark) { textarea { transition: background-color 0.1s linear, border-color 0.1s linear, color 0.1s linear, box-shadow 0.1s linear, transform 0.1s ease; transition: background-color var(--animation-duration) linear, border-color var(--animation-duration) linear, color var(--animation-duration) linear, box-shadow var(--animation-duration) linear, transform var(--animation-duration) ease; } } h1 { font-size: 2.2em; margin-top: 0; } h1, h2, h3, h4, h5, h6 { margin-bottom: 12px; margin-top: 24px; } h1 { color: #000; color: var(--text-bright); } @media (prefers-color-scheme: dark) { h1 { color: #fff; color: var(--text-bright); } } h2 { color: #000; color: var(--text-bright); } @media (prefers-color-scheme: dark) { h2 { color: #fff; color: var(--text-bright); } } h3 { color: #000; color: var(--text-bright); } @media (prefers-color-scheme: dark) { h3 { color: #fff; color: var(--text-bright); } } h4 { color: #000; color: var(--text-bright); } @media (prefers-color-scheme: dark) { h4 { color: #fff; color: var(--text-bright); } } h5 { color: #000; color: var(--text-bright); } @media (prefers-color-scheme: dark) { h5 { color: #fff; color: var(--text-bright); } } h6 { color: #000; color: var(--text-bright); } @media (prefers-color-scheme: dark) { h6 { color: #fff; color: var(--text-bright); } } strong { color: #000; color: var(--text-bright); } @media (prefers-color-scheme: dark) { strong { color: #fff; color: var(--text-bright); } } h1, h2, h3, h4, h5, h6, b, strong, th { font-weight: 600; } q::before { content: none; } q::after { content: none; } blockquote { border-left: 4px solid #0096bfab; border-left: 4px solid var(--focus); margin: 1.5em 0; padding: 0.5em 1em; font-style: italic; } @media (prefers-color-scheme: dark) { blockquote { border-left: 4px solid #0096bfab; border-left: 4px solid var(--focus); } } q { border-left: 4px solid #0096bfab; border-left: 4px solid var(--focus); margin: 1.5em 0; padding: 0.5em 1em; font-style: italic; } @media (prefers-color-scheme: dark) { q { border-left: 4px solid #0096bfab; border-left: 4px solid var(--focus); } } blockquote > footer { font-style: normal; border: 0; } blockquote cite { font-style: normal; } address { font-style: normal; } a[href^='mailto\:']::before { content: '📧 '; } a[href^='tel\:']::before { content: '📞 '; } a[href^='sms\:']::before { content: '💬 '; } mark { background-color: #ff0; background-color: var(--highlight); border-radius: 2px; padding: 0 2px 0 2px; color: #000; } @media (prefers-color-scheme: dark) { mark { background-color: #efdb43; background-color: var(--highlight); } } a > code, a > strong { color: inherit; } button, select, input[type='submit'], input[type='reset'], input[type='button'], input[type='checkbox'], input[type='range'], input[type='radio'] { cursor: pointer; } input, select { display: block; } [type='checkbox'], [type='radio'] { display: initial; } input { color: #1d1d1d; color: var(--form-text); background-color: #efefef; background-color: var(--background); font-family: inherit; font-size: inherit; margin-right: 6px; margin-bottom: 6px; padding: 10px; border: none; border-radius: 6px; outline: none; } @media (prefers-color-scheme: dark) { input { background-color: #161f27; background-color: var(--background); } } @media (prefers-color-scheme: dark) { input { color: #fff; color: var(--form-text); } } button { color: #1d1d1d; color: var(--form-text); background-color: #efefef; background-color: var(--background); font-family: inherit; font-size: inherit; margin-right: 6px; margin-bottom: 6px; padding: 10px; border: none; border-radius: 6px; outline: none; } @media (prefers-color-scheme: dark) { button { background-color: #161f27; background-color: var(--background); } } @media (prefers-color-scheme: dark) { button { color: #fff; color: var(--form-text); } } textarea { color: #1d1d1d; color: var(--form-text); background-color: #efefef; background-color: var(--background); font-family: inherit; font-size: inherit; margin-right: 6px; margin-bottom: 6px; padding: 10px; border: none; border-radius: 6px; outline: none; } @media (prefers-color-scheme: dark) { textarea { background-color: #161f27; background-color: var(--background); } } @media (prefers-color-scheme: dark) { textarea { color: #fff; color: var(--form-text); } } select { color: #1d1d1d; color: var(--form-text); background-color: #efefef; background-color: var(--background); font-family: inherit; font-size: inherit; margin-right: 6px; margin-bottom: 6px; padding: 10px; border: none; border-radius: 6px; outline: none; } @media (prefers-color-scheme: dark) { select { background-color: #161f27; background-color: var(--background); } } @media (prefers-color-scheme: dark) { select { color: #fff; color: var(--form-text); } } button { background-color: #d0cfcf; background-color: var(--button-base); padding-right: 30px; padding-left: 30px; } @media (prefers-color-scheme: dark) { button { background-color: #0c151c; background-color: var(--button-base); } } input[type='submit'] { background-color: #d0cfcf; background-color: var(--button-base); padding-right: 30px; padding-left: 30px; } @media (prefers-color-scheme: dark) { input[type='submit'] { background-color: #0c151c; background-color: var(--button-base); } } input[type='reset'] { background-color: #d0cfcf; background-color: var(--button-base); padding-right: 30px; padding-left: 30px; } @media (prefers-color-scheme: dark) { input[type='reset'] { background-color: #0c151c; background-color: var(--button-base); } } input[type='button'] { background-color: #d0cfcf; background-color: var(--button-base); padding-right: 30px; padding-left: 30px; } @media (prefers-color-scheme: dark) { input[type='button'] { background-color: #0c151c; background-color: var(--button-base); } } button:hover { background: #9b9b9b; background: var(--button-hover); } @media (prefers-color-scheme: dark) { button:hover { background: #040a0f; background: var(--button-hover); } } input[type='submit']:hover { background: #9b9b9b; background: var(--button-hover); } @media (prefers-color-scheme: dark) { input[type='submit']:hover { background: #040a0f; background: var(--button-hover); } } input[type='reset']:hover { background: #9b9b9b; background: var(--button-hover); } @media (prefers-color-scheme: dark) { input[type='reset']:hover { background: #040a0f; background: var(--button-hover); } } input[type='button']:hover { background: #9b9b9b; background: var(--button-hover); } @media (prefers-color-scheme: dark) { input[type='button']:hover { background: #040a0f; background: var(--button-hover); } } input[type='color'] { min-height: 2rem; padding: 8px; cursor: pointer; } input[type='checkbox'], input[type='radio'] { height: 1em; width: 1em; } input[type='radio'] { border-radius: 100%; } input { vertical-align: top; } label { vertical-align: middle; margin-bottom: 4px; display: inline-block; } input:not([type='checkbox']):not([type='radio']), input[type='range'], select, button, textarea { -webkit-appearance: none; } textarea { display: block; margin-right: 0; box-sizing: border-box; resize: vertical; } textarea:not([cols]) { width: 100%; } textarea:not([rows]) { min-height: 40px; height: 140px; } select { background: #efefef url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23161f27'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; padding-right: 35px; } @media (prefers-color-scheme: dark) { select { background: #161f27 url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; } } @media (prefers-color-scheme: dark) { select { background: #161f27 url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; } } @media (prefers-color-scheme: dark) { select { background: #161f27 url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; } } @media (prefers-color-scheme: dark) { select { background: #161f27 url("data:image/svg+xml;charset=utf-8,%3C?xml version='1.0' encoding='utf-8'?%3E %3Csvg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' height='62.5' width='116.9' fill='%23efefef'%3E %3Cpath d='M115.3,1.6 C113.7,0 111.1,0 109.5,1.6 L58.5,52.7 L7.4,1.6 C5.8,0 3.2,0 1.6,1.6 C0,3.2 0,5.8 1.6,7.4 L55.5,61.3 C56.3,62.1 57.3,62.5 58.4,62.5 C59.4,62.5 60.5,62.1 61.3,61.3 L115.2,7.4 C116.9,5.8 116.9,3.2 115.3,1.6Z'/%3E %3C/svg%3E") calc(100% - 12px) 50% / 12px no-repeat; background: var(--background) var(--select-arrow) calc(100% - 12px) 50% / 12px no-repeat; } } select::-ms-expand { display: none; } select[multiple] { padding-right: 10px; background-image: none; overflow-y: auto; } input:focus { box-shadow: 0 0 0 2px #0096bfab; box-shadow: 0 0 0 2px var(--focus); } @media (prefers-color-scheme: dark) { input:focus { box-shadow: 0 0 0 2px #0096bfab; box-shadow: 0 0 0 2px var(--focus); } } select:focus { box-shadow: 0 0 0 2px #0096bfab; box-shadow: 0 0 0 2px var(--focus); } @media (prefers-color-scheme: dark) { select:focus { box-shadow: 0 0 0 2px #0096bfab; box-shadow: 0 0 0 2px var(--focus); } } button:focus { box-shadow: 0 0 0 2px #0096bfab; box-shadow: 0 0 0 2px var(--focus); } @media (prefers-color-scheme: dark) { button:focus { box-shadow: 0 0 0 2px #0096bfab; box-shadow: 0 0 0 2px var(--focus); } } textarea:focus { box-shadow: 0 0 0 2px #0096bfab; box-shadow: 0 0 0 2px var(--focus); } @media (prefers-color-scheme: dark) { textarea:focus { box-shadow: 0 0 0 2px #0096bfab; box-shadow: 0 0 0 2px var(--focus); } } input[type='checkbox']:active, input[type='radio']:active, input[type='submit']:active, input[type='reset']:active, input[type='button']:active, input[type='range']:active, button:active { transform: translateY(2px); } input:disabled, select:disabled, button:disabled, textarea:disabled { cursor: not-allowed; opacity: 0.5; } ::-moz-placeholder { color: #949494; color: var(--form-placeholder); } :-ms-input-placeholder { color: #949494; color: var(--form-placeholder); } ::-ms-input-placeholder { color: #949494; color: var(--form-placeholder); } ::placeholder { color: #949494; color: var(--form-placeholder); } @media (prefers-color-scheme: dark) { ::-moz-placeholder { color: #a9a9a9; color: var(--form-placeholder); } :-ms-input-placeholder { color: #a9a9a9; color: var(--form-placeholder); } ::-ms-input-placeholder { color: #a9a9a9; color: var(--form-placeholder); } ::placeholder { color: #a9a9a9; color: var(--form-placeholder); } } fieldset { border: 1px #0096bfab solid; border: 1px var(--focus) solid; border-radius: 6px; margin: 0; margin-bottom: 12px; padding: 10px; } @media (prefers-color-scheme: dark) { fieldset { border: 1px #0096bfab solid; border: 1px var(--focus) solid; } } legend { font-size: 0.9em; font-weight: 600; } input[type='range'] { margin: 10px 0; padding: 10px 0; background: transparent; } input[type='range']:focus { outline: none; } input[type='range']::-webkit-slider-runnable-track { width: 100%; height: 9.5px; -webkit-transition: 0.2s; transition: 0.2s; background: #efefef; background: var(--background); border-radius: 3px; } @media (prefers-color-scheme: dark) { input[type='range']::-webkit-slider-runnable-track { background: #161f27; background: var(--background); } } input[type='range']::-webkit-slider-thumb { box-shadow: 0 1px 1px #000, 0 0 1px #0d0d0d; height: 20px; width: 20px; border-radius: 50%; background: #dbdbdb; background: var(--border); -webkit-appearance: none; margin-top: -7px; } @media (prefers-color-scheme: dark) { input[type='range']::-webkit-slider-thumb { background: #526980; background: var(--border); } } input[type='range']:focus::-webkit-slider-runnable-track { background: #efefef; background: var(--background); } @media (prefers-color-scheme: dark) { input[type='range']:focus::-webkit-slider-runnable-track { background: #161f27; background: var(--background); } } input[type='range']::-moz-range-track { width: 100%; height: 9.5px; -moz-transition: 0.2s; transition: 0.2s; background: #efefef; background: var(--background); border-radius: 3px; } @media (prefers-color-scheme: dark) { input[type='range']::-moz-range-track { background: #161f27; background: var(--background); } } input[type='range']::-moz-range-thumb { box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; height: 20px; width: 20px; border-radius: 50%; background: #dbdbdb; background: var(--border); } @media (prefers-color-scheme: dark) { input[type='range']::-moz-range-thumb { background: #526980; background: var(--border); } } input[type='range']::-ms-track { width: 100%; height: 9.5px; background: transparent; border-color: transparent; border-width: 16px 0; color: transparent; } input[type='range']::-ms-fill-lower { background: #efefef; background: var(--background); border: 0.2px solid #010101; border-radius: 3px; box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; } @media (prefers-color-scheme: dark) { input[type='range']::-ms-fill-lower { background: #161f27; background: var(--background); } } input[type='range']::-ms-fill-upper { background: #efefef; background: var(--background); border: 0.2px solid #010101; border-radius: 3px; box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; } @media (prefers-color-scheme: dark) { input[type='range']::-ms-fill-upper { background: #161f27; background: var(--background); } } input[type='range']::-ms-thumb { box-shadow: 1px 1px 1px #000, 0 0 1px #0d0d0d; border: 1px solid #000; height: 20px; width: 20px; border-radius: 50%; background: #dbdbdb; background: var(--border); } @media (prefers-color-scheme: dark) { input[type='range']::-ms-thumb { background: #526980; background: var(--border); } } input[type='range']:focus::-ms-fill-lower { background: #efefef; background: var(--background); } @media (prefers-color-scheme: dark) { input[type='range']:focus::-ms-fill-lower { background: #161f27; background: var(--background); } } input[type='range']:focus::-ms-fill-upper { background: #efefef; background: var(--background); } @media (prefers-color-scheme: dark) { input[type='range']:focus::-ms-fill-upper { background: #161f27; background: var(--background); } } a { text-decoration: none; color: #0076d1; color: var(--links); } @media (prefers-color-scheme: dark) { a { color: #41adff; color: var(--links); } } a:hover { text-decoration: underline; } code { background: #efefef; background: var(--background); color: #000; color: var(--code); padding: 2.5px 5px; border-radius: 6px; font-size: 1em; } @media (prefers-color-scheme: dark) { code { color: #ffbe85; color: var(--code); } } @media (prefers-color-scheme: dark) { code { background: #161f27; background: var(--background); } } samp { background: #efefef; background: var(--background); color: #000; color: var(--code); padding: 2.5px 5px; border-radius: 6px; font-size: 1em; } @media (prefers-color-scheme: dark) { samp { color: #ffbe85; color: var(--code); } } @media (prefers-color-scheme: dark) { samp { background: #161f27; background: var(--background); } } time { background: #efefef; background: var(--background); color: #000; color: var(--code); padding: 2.5px 5px; border-radius: 6px; font-size: 1em; } @media (prefers-color-scheme: dark) { time { color: #ffbe85; color: var(--code); } } @media (prefers-color-scheme: dark) { time { background: #161f27; background: var(--background); } } pre > code { padding: 10px; display: block; overflow-x: auto; } var { color: #39a33c; color: var(--variable); font-style: normal; font-family: monospace; } @media (prefers-color-scheme: dark) { var { color: #d941e2; color: var(--variable); } } kbd { background: #efefef; background: var(--background); border: 1px solid #dbdbdb; border: 1px solid var(--border); border-radius: 2px; color: #363636; color: var(--text-main); padding: 2px 4px 2px 4px; } @media (prefers-color-scheme: dark) { kbd { color: #dbdbdb; color: var(--text-main); } } @media (prefers-color-scheme: dark) { kbd { border: 1px solid #526980; border: 1px solid var(--border); } } @media (prefers-color-scheme: dark) { kbd { background: #161f27; background: var(--background); } } img, video { max-width: 100%; height: auto; } hr { border: none; border-top: 1px solid #dbdbdb; border-top: 1px solid var(--border); } @media (prefers-color-scheme: dark) { hr { border-top: 1px solid #526980; border-top: 1px solid var(--border); } } table { border-collapse: collapse; margin-bottom: 10px; width: 100%; table-layout: fixed; } table caption { text-align: left; } td, th { padding: 6px; text-align: left; vertical-align: top; word-wrap: break-word; } thead { border-bottom: 1px solid #dbdbdb; border-bottom: 1px solid var(--border); } @media (prefers-color-scheme: dark) { thead { border-bottom: 1px solid #526980; border-bottom: 1px solid var(--border); } } tfoot { border-top: 1px solid #dbdbdb; border-top: 1px solid var(--border); } @media (prefers-color-scheme: dark) { tfoot { border-top: 1px solid #526980; border-top: 1px solid var(--border); } } tbody tr:nth-child(even) { background-color: #efefef; background-color: var(--background); } @media (prefers-color-scheme: dark) { tbody tr:nth-child(even) { background-color: #161f27; background-color: var(--background); } } tbody tr:nth-child(even) button { background-color: #f7f7f7; background-color: var(--background-alt); } @media (prefers-color-scheme: dark) { tbody tr:nth-child(even) button { background-color: #1a242f; background-color: var(--background-alt); } } tbody tr:nth-child(even) button:hover { background-color: #fff; background-color: var(--background-body); } @media (prefers-color-scheme: dark) { tbody tr:nth-child(even) button:hover { background-color: #202b38; background-color: var(--background-body); } } ::-webkit-scrollbar { height: 10px; width: 10px; } ::-webkit-scrollbar-track { background: #efefef; background: var(--background); border-radius: 6px; } @media (prefers-color-scheme: dark) { ::-webkit-scrollbar-track { background: #161f27; background: var(--background); } } ::-webkit-scrollbar-thumb { background: rgb(170, 170, 170); background: var(--scrollbar-thumb); border-radius: 6px; } @media (prefers-color-scheme: dark) { ::-webkit-scrollbar-thumb { background: #040a0f; background: var(--scrollbar-thumb); } } @media (prefers-color-scheme: dark) { ::-webkit-scrollbar-thumb { background: #040a0f; background: var(--scrollbar-thumb); } } ::-webkit-scrollbar-thumb:hover { background: #9b9b9b; background: var(--scrollbar-thumb-hover); } @media (prefers-color-scheme: dark) { ::-webkit-scrollbar-thumb:hover { background: rgb(0, 0, 0); background: var(--scrollbar-thumb-hover); } } @media (prefers-color-scheme: dark) { ::-webkit-scrollbar-thumb:hover { background: rgb(0, 0, 0); background: var(--scrollbar-thumb-hover); } } ::-moz-selection { background-color: #9e9e9e; background-color: var(--selection); color: #000; color: var(--text-bright); } ::selection { background-color: #9e9e9e; background-color: var(--selection); color: #000; color: var(--text-bright); } @media (prefers-color-scheme: dark) { ::-moz-selection { color: #fff; color: var(--text-bright); } ::selection { color: #fff; color: var(--text-bright); } } @media (prefers-color-scheme: dark) { ::-moz-selection { background-color: #1c76c5; background-color: var(--selection); } ::selection { background-color: #1c76c5; background-color: var(--selection); } } details { display: flex; flex-direction: column; align-items: flex-start; background-color: #f7f7f7; background-color: var(--background-alt); padding: 10px 10px 0; margin: 1em 0; border-radius: 6px; overflow: hidden; } @media (prefers-color-scheme: dark) { details { background-color: #1a242f; background-color: var(--background-alt); } } details[open] { padding: 10px; } details > :last-child { margin-bottom: 0; } details[open] summary { margin-bottom: 10px; } summary { display: list-item; background-color: #efefef; background-color: var(--background); padding: 10px; margin: -10px -10px 0; cursor: pointer; outline: none; } @media (prefers-color-scheme: dark) { summary { background-color: #161f27; background-color: var(--background); } } summary:hover, summary:focus { text-decoration: underline; } details > :not(summary) { margin-top: 0; } summary::-webkit-details-marker { color: #363636; color: var(--text-main); } @media (prefers-color-scheme: dark) { summary::-webkit-details-marker { color: #dbdbdb; color: var(--text-main); } } dialog { background-color: #f7f7f7; background-color: var(--background-alt); color: #363636; color: var(--text-main); border: none; border-radius: 6px; border-color: #dbdbdb; border-color: var(--border); padding: 10px 30px; } @media (prefers-color-scheme: dark) { dialog { border-color: #526980; border-color: var(--border); } } @media (prefers-color-scheme: dark) { dialog { color: #dbdbdb; color: var(--text-main); } } @media (prefers-color-scheme: dark) { dialog { background-color: #1a242f; background-color: var(--background-alt); } } dialog > header:first-child { background-color: #efefef; background-color: var(--background); border-radius: 6px 6px 0 0; margin: -10px -30px 10px; padding: 10px; text-align: center; } @media (prefers-color-scheme: dark) { dialog > header:first-child { background-color: #161f27; background-color: var(--background); } } dialog::-webkit-backdrop { background: #0000009c; -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); } dialog::backdrop { background: #0000009c; -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); } footer { border-top: 1px solid #dbdbdb; border-top: 1px solid var(--border); padding-top: 10px; color: #70777f; color: var(--text-muted); } @media (prefers-color-scheme: dark) { footer { color: #a9b1ba; color: var(--text-muted); } } @media (prefers-color-scheme: dark) { footer { border-top: 1px solid #526980; border-top: 1px solid var(--border); } } body > footer { margin-top: 40px; } @media print { body, pre, code, summary, details, button, input, textarea { background-color: #fff; } button, input, textarea { border: 1px solid #000; } body, h1, h2, h3, h4, h5, h6, pre, code, button, input, textarea, footer, summary, strong { color: #000; } summary::marker { color: #000; } summary::-webkit-details-marker { color: #000; } tbody tr:nth-child(even) { background-color: #f2f2f2; } a { color: #00f; text-decoration: underline; } } ================================================ FILE: projects/05-react-buscador-peliculas/src/main.jsx ================================================ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' import './index.css' ReactDOM.createRoot(document.getElementById('root')).render( ) ================================================ FILE: projects/05-react-buscador-peliculas/src/mocks/no-results.json ================================================ { "Response": "False", "Error": "Movie not found!" } ================================================ FILE: projects/05-react-buscador-peliculas/src/mocks/with-results.json ================================================ {"Search":[{"Title":"The Avengers","Year":"2012","imdbID":"tt0848228","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BNDYxNjQyMjAtNTdiOS00NGYwLWFmNTAtNThmYjU5ZGI2YTI1XkEyXkFqcGdeQXVyMTMxODk2OTU@._V1_SX300.jpg"},{"Title":"Avengers: Endgame","Year":"2019","imdbID":"tt4154796","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTc5MDE2ODcwNV5BMl5BanBnXkFtZTgwMzI2NzQ2NzM@._V1_SX300.jpg"},{"Title":"Avengers: Infinity War","Year":"2018","imdbID":"tt4154756","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMjMxNjY2MDU1OV5BMl5BanBnXkFtZTgwNzY1MTUwNTM@._V1_SX300.jpg"},{"Title":"Avengers: Age of Ultron","Year":"2015","imdbID":"tt2395427","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTM4OGJmNWMtOTM4Ni00NTE3LTg3MDItZmQxYjc4N2JhNmUxXkEyXkFqcGdeQXVyNTgzMDMzMTg@._V1_SX300.jpg"},{"Title":"The Avengers","Year":"1998","imdbID":"tt0118661","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BYWE1NTdjOWQtYTQ2Ny00Nzc5LWExYzMtNmRlOThmOTE2N2I4XkEyXkFqcGdeQXVyNjUwNzk3NDc@._V1_SX300.jpg"},{"Title":"The Avengers: Earth's Mightiest Heroes","Year":"2010–2012","imdbID":"tt1626038","Type":"series","Poster":"https://m.media-amazon.com/images/M/MV5BYzA4ZjVhYzctZmI0NC00ZmIxLWFmYTgtOGIxMDYxODhmMGQ2XkEyXkFqcGdeQXVyNjExODE1MDc@._V1_SX300.jpg"},{"Title":"Ultimate Avengers: The Movie","Year":"2006","imdbID":"tt0491703","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BMTYyMjk0NTMwMl5BMl5BanBnXkFtZTgwNzY0NjAwNzE@._V1_SX300.jpg"},{"Title":"Ultimate Avengers II","Year":"2006","imdbID":"tt0803093","Type":"movie","Poster":"https://m.media-amazon.com/images/M/MV5BZjI3MTI5ZTYtZmNmNy00OGZmLTlhNWMtNjZiYmYzNDhlOGRkL2ltYWdlL2ltYWdlXkEyXkFqcGdeQXVyNTAyODkwOQ@@._V1_SX300.jpg"},{"Title":"The Avengers","Year":"1961–1969","imdbID":"tt0054518","Type":"series","Poster":"https://m.media-amazon.com/images/M/MV5BZWQwZTdjMDUtNTY1YS00MDI0LWFkNjYtZDA4MDdmZjdlMDRlXkEyXkFqcGdeQXVyNjUwNzk3NDc@._V1_SX300.jpg"},{"Title":"Avengers Assemble","Year":"2012–2019","imdbID":"tt2455546","Type":"series","Poster":"https://m.media-amazon.com/images/M/MV5BMTY0NTUyMDQwOV5BMl5BanBnXkFtZTgwNjAwMTA0MDE@._V1_SX300.jpg"}],"totalResults":"144","Response":"True"} ================================================ FILE: projects/05-react-buscador-peliculas/src/services/movies.js ================================================ const API_KEY = '4287ad07' export const searchMovies = async ({ search }) => { if (search === '') return null try { const response = await fetch(`https://www.omdbapi.com/?apikey=${API_KEY}&s=${search}`) const json = await response.json() const movies = json.Search return movies?.map(movie => ({ id: movie.imdbID, title: movie.Title, year: movie.Year, image: movie.Poster })) } catch (e) { throw new Error('Error searching movies') } } ================================================ FILE: projects/05-react-buscador-peliculas/vite.config.js ================================================ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()] }) ================================================ FILE: projects/06-shopping-cart/.gitignore ================================================ # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* lerna-debug.log* node_modules dist dist-ssr *.local # Editor directories and files .vscode/* !.vscode/extensions.json .idea .DS_Store *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: projects/06-shopping-cart/README.md ================================================ # Enunciado Ecommerce - [x] Muestra una lista de productos que vienen de un JSON - [x] Añade un filtro por categoría - [x] Añade un filtro por precio Haz uso de useContext para evitar pasar props innecesarias. Carrito: - [x] Haz que se puedan añadir los productos a un carrito. - [x] Haz que se puedan eliminar los productos del carrito. - [x] Haz que se puedan modificar la cantidad de productos del carrito. - [x] Sincroniza los cambios del carrito con la lista de productos. - [x] Guarda en un localStorage el carrito para que se recupere al recargar la página. (da puntos) ================================================ FILE: projects/06-shopping-cart/index.html ================================================ Vite + React
================================================ FILE: projects/06-shopping-cart/package.json ================================================ { "name": "06-shopping-cart", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" }, "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0" }, "devDependencies": { "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "@vitejs/plugin-react-swc": "^3.0.0", "vite": "^4.1.0" } } ================================================ FILE: projects/06-shopping-cart/src/App.jsx ================================================ import { products as initialProducts } from './mocks/products.json' import { Products } from './components/Products.jsx' import { Header } from './components/Header.jsx' import { Footer } from './components/Footer.jsx' import { IS_DEVELOPMENT } from './config.js' import { useFilters } from './hooks/useFilters.js' import { Cart } from './components/Cart.jsx' import { CartProvider } from './context/cart.jsx' function App () { const { filterProducts } = useFilters() const filteredProducts = filterProducts(initialProducts) return (
{IS_DEVELOPMENT &&