├── .eslintignore ├── .gitignore ├── README.md ├── package.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── projects ├── 01-twitter-follow-card │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── TwitterFollowCard.jsx │ │ ├── index.css │ │ └── main.jsx │ └── vite.config.js ├── 02-tic-tac-toe │ ├── .gitignore │ ├── index.html │ ├── package-lock.json │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── 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 │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── index.css │ │ └── main.jsx │ └── vite.config.js ├── 04-react-prueba-tecnica │ ├── .gitignore │ ├── README.md │ ├── counter.js │ ├── index.html │ ├── javascript.svg │ ├── main.jsx │ ├── package.json │ ├── playwright.config.cjs │ ├── public │ │ └── vite.svg │ ├── 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 │ ├── pnpm-lock.yaml │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.jsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── pnpm-lock.yaml │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.jsx │ │ ├── Router.test.jsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── pnpm-lock.yaml │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── .env │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── pnpm-lock.yaml │ ├── postcss.config.js │ ├── public │ │ └── vite.svg │ ├── rome.json │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── pnpm-lock.yaml │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── pnpm-lock.yaml │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── public │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── pnpm-lock.yaml │ ├── public │ │ ├── data.json │ │ └── vite.svg │ ├── src │ │ ├── App.css │ │ ├── App.tsx │ │ ├── Footer.tsx │ │ ├── Game.tsx │ │ ├── JavaScriptLogo.tsx │ │ ├── Results.tsx │ │ ├── Start.tsx │ │ ├── assets │ │ │ └── react.svg │ │ ├── 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 │ ├── pnpm-lock.yaml │ ├── public │ ├── logo.gif │ └── vite.svg │ ├── src │ ├── App.css │ ├── App.tsx │ ├── assets │ │ └── react.svg │ ├── 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 /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | package-lock.json 2 | node_modules 3 | .DS_Store 4 | projects/**/.DS_Store 5 | projects/**/dist -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aprendiendo-react", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "workspaces": [ 10 | "projects/*" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/midudev/aprendiendo-react.git" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/midudev/aprendiendo-react/issues" 21 | }, 22 | "homepage": "https://github.com/midudev/aprendiendo-react#readme", 23 | "devDependencies": { 24 | "standard": "17.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | # todos los proyectos dentro de projects son paquetes 3 | - 'projects/**' -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/.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 | 26 | 27 | package-lock.json -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "01-twitter-follow-card", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.26", 17 | "@types/react-dom": "^18.0.9", 18 | "@vitejs/plugin-react-swc": "^3.0.0", 19 | "vite": "^4.0.0" 20 | } 21 | } -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/src/App.css: -------------------------------------------------------------------------------- 1 | .tw-followCard { 2 | display: flex; 3 | align-items: center; 4 | color: #fff; 5 | font-size: .8rem; 6 | justify-content: space-between; 7 | } 8 | 9 | .tw-followCard-header { 10 | display: flex; 11 | align-items: center; 12 | gap: 4px 13 | } 14 | 15 | .tw-followCard-info { 16 | display: flex; 17 | flex-direction: column; 18 | } 19 | 20 | .tw-followCard-infoUserName { 21 | opacity: .6; 22 | } 23 | 24 | .tw-followCard-avatar { 25 | width: 48px; 26 | height: 48px; 27 | border-radius: 1000px; 28 | } 29 | 30 | .tw-followCard-button { 31 | cursor: pointer; 32 | margin-left: 16px; 33 | border: 0; 34 | border-radius: 999px; 35 | padding: 6px 16px; 36 | font-weight: bold; 37 | border: 1px solid #000; 38 | transition: .3s ease background-color; 39 | } 40 | 41 | .tw-followCard-button:hover { 42 | opacity: .8; 43 | } 44 | 45 | .tw-followCard-text { 46 | display: block; 47 | } 48 | 49 | .tw-followCard-button.is-following { 50 | border: 1px solid #bbb; 51 | background: transparent; 52 | color: #fff; 53 | width: 140px; 54 | } 55 | 56 | .tw-followCard-button.is-following:hover { 57 | background-color: rgba(255, 0, 0, 0.178); 58 | color: red; 59 | border: 1px solid red; 60 | transition: .3s ease all; 61 | opacity: 1; 62 | } 63 | 64 | .tw-followCard-button.is-following:hover .tw-followCard-text { 65 | display: none; 66 | } 67 | 68 | .tw-followCard-button.is-following:hover .tw-followCard-stopFollow { 69 | display: block; 70 | } 71 | 72 | .tw-followCard-stopFollow { 73 | display: none; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/src/App.jsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | import { TwitterFollowCard } from './TwitterFollowCard.jsx' 3 | 4 | const users = [ 5 | { 6 | userName: 'midudev', 7 | name: 'Miguel Ángel Durán', 8 | isFollowing: true 9 | }, 10 | { 11 | userName: 'pheralb', 12 | name: 'Pablo H.', 13 | isFollowing: false 14 | }, 15 | { 16 | userName: 'PacoHdezs', 17 | name: 'Paco Hdez', 18 | isFollowing: true 19 | }, 20 | { 21 | userName: 'TMChein', 22 | name: 'Tomas', 23 | isFollowing: false 24 | } 25 | ] 26 | 27 | export function App () { 28 | return ( 29 |
30 | { 31 | users.map(({ userName, name, isFollowing }) => ( 32 | 37 | {name} 38 | 39 | )) 40 | } 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/src/TwitterFollowCard.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export function TwitterFollowCard ({ children, userName, initialIsFollowing }) { 4 | const [isFollowing, setIsFollowing] = useState(initialIsFollowing) 5 | 6 | console.log('[TwitterFollowCard] render with userName: ', userName) 7 | 8 | const text = isFollowing ? 'Siguiendo' : 'Seguir' 9 | const buttonClassName = isFollowing 10 | ? 'tw-followCard-button is-following' 11 | : 'tw-followCard-button' 12 | 13 | const handleClick = () => { 14 | setIsFollowing(!isFollowing) 15 | } 16 | 17 | return ( 18 |
19 |
20 | El avatar de midudev 25 |
26 | {children} 27 | @{userName} 28 |
29 |
30 | 31 | 37 |
38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background: #222; 4 | font-family: system-ui; 5 | display: grid; 6 | place-content: center; 7 | min-height: 100vh; 8 | } 9 | 10 | .App { 11 | display: flex; 12 | flex-direction: column; 13 | gap: 8px; 14 | } -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import { App } from './App.jsx' 4 | import './index.css' 5 | 6 | const root = ReactDOM.createRoot(document.getElementById('root')) 7 | 8 | root.render( 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /projects/01-twitter-follow-card/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/.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 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "02-tic-tac-toe", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "canvas-confetti": "1.6.0", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.26", 18 | "@types/react-dom": "^18.0.9", 19 | "@vitejs/plugin-react-swc": "^3.0.0", 20 | "vite": "^4.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/src/components/Square.jsx: -------------------------------------------------------------------------------- 1 | export const Square = ({ children, isSelected, updateBoard, index }) => { 2 | const className = `square ${isSelected ? 'is-selected' : ''}` 3 | 4 | const handleClick = () => { 5 | updateBoard(index) 6 | } 7 | 8 | return ( 9 |
10 | {children} 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/src/components/WinnerModal.jsx: -------------------------------------------------------------------------------- 1 | import { Square } from './Square.jsx' 2 | 3 | export function WinnerModal ({ winner, resetGame }) { 4 | if (winner === null) return null 5 | 6 | const winnerText = winner === false ? 'Empate' : 'Ganó:' 7 | 8 | return ( 9 |
10 |
11 |

{winnerText}

12 | 13 |
14 | {winner && {winner}} 15 |
16 | 17 |
18 | 19 |
20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/src/constants.js: -------------------------------------------------------------------------------- 1 | export const TURNS = { // turnos 2 | X: '❌', 3 | O: '⚪' 4 | } 5 | 6 | export const WINNER_COMBOS = [ 7 | [0, 1, 2], 8 | [3, 4, 5], 9 | [6, 7, 8], 10 | [0, 3, 6], 11 | [1, 4, 7], 12 | [2, 5, 8], 13 | [0, 4, 8], 14 | [2, 4, 6] 15 | ] 16 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/src/logic/board.js: -------------------------------------------------------------------------------- 1 | import { WINNER_COMBOS } from '../constants.js' 2 | 3 | export const checkWinnerFrom = (boardToCheck) => { 4 | // revisamos todas las combinaciones ganadoras 5 | // para ver si X u O ganó 6 | for (const combo of WINNER_COMBOS) { 7 | const [a, b, c] = combo 8 | if ( 9 | boardToCheck[a] && 10 | boardToCheck[a] === boardToCheck[b] && 11 | boardToCheck[a] === boardToCheck[c] 12 | ) { 13 | return boardToCheck[a] 14 | } 15 | } 16 | // si no hay ganador 17 | return null 18 | } 19 | 20 | export const checkEndGame = (newBoard) => { 21 | // revisamos si hay un empate 22 | // si no hay más espacios vacíos 23 | // en el tablero 24 | return newBoard.every((square) => square !== null) 25 | } 26 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/src/logic/storage/index.js: -------------------------------------------------------------------------------- 1 | export const saveGameToStorage = ({ board, turn }) => { 2 | // guardar aqui partida 3 | window.localStorage.setItem('board', JSON.stringify(board)) 4 | window.localStorage.setItem('turn', turn) 5 | } 6 | 7 | export const resetGameStorage = () => { 8 | window.localStorage.removeItem('board') 9 | window.localStorage.removeItem('turn') 10 | } 11 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /projects/02-tic-tac-toe/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /projects/03-mouse-follower/.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 | -------------------------------------------------------------------------------- /projects/03-mouse-follower/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/03-mouse-follower/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "03-mouse-follower", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.26", 17 | "@types/react-dom": "^18.0.9", 18 | "@vitejs/plugin-react-swc": "^3.0.0", 19 | "vite": "^4.0.0" 20 | } 21 | } -------------------------------------------------------------------------------- /projects/03-mouse-follower/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/03-mouse-follower/src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | } 13 | .logo:hover { 14 | filter: drop-shadow(0 0 2em #646cffaa); 15 | } 16 | .logo.react:hover { 17 | filter: drop-shadow(0 0 2em #61dafbaa); 18 | } 19 | 20 | @keyframes logo-spin { 21 | from { 22 | transform: rotate(0deg); 23 | } 24 | to { 25 | transform: rotate(360deg); 26 | } 27 | } 28 | 29 | @media (prefers-reduced-motion: no-preference) { 30 | a:nth-of-type(2) .logo { 31 | animation: logo-spin infinite 20s linear; 32 | } 33 | } 34 | 35 | .card { 36 | padding: 2em; 37 | } 38 | 39 | .read-the-docs { 40 | color: #888; 41 | } 42 | -------------------------------------------------------------------------------- /projects/03-mouse-follower/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const FollowMouse = () => { 4 | const [enabled, setEnabled] = useState(false) 5 | const [position, setPosition] = useState({ x: 0, y: 0 }) 6 | 7 | // pointer move 8 | useEffect(() => { 9 | console.log('effect ', { enabled }) 10 | 11 | const handleMove = (event) => { 12 | const { clientX, clientY } = event 13 | setPosition({ x: clientX, y: clientY }) 14 | } 15 | 16 | if (enabled) { 17 | window.addEventListener('pointermove', handleMove) 18 | } 19 | 20 | // cleanup: 21 | // -> cuando el componente se desmonta 22 | // -> cuando cambian las dependencias, antes de ejecutar 23 | // el efecto de nuevo 24 | return () => { // cleanup method 25 | console.log('cleanup') 26 | window.removeEventListener('pointermove', handleMove) 27 | } 28 | }, [enabled]) 29 | 30 | // [] -> solo se ejecuta una vez cuando se monta el componente 31 | // [enabled] -> se ejecuta cuando cambia enabled y cuando se monta el componente 32 | // undefined -> se ejecuta cada vez que se renderiza el componente 33 | 34 | // change body className 35 | useEffect(() => { 36 | document.body.classList.toggle('no-cursor', enabled) 37 | 38 | return () => { 39 | document.body.classList.remove('no-cursor') 40 | } 41 | }, [enabled]) 42 | 43 | return ( 44 | <> 45 |
59 | 62 | 63 | ) 64 | } 65 | 66 | function App () { 67 | return ( 68 |
69 | 70 |
71 | ) 72 | } 73 | 74 | export default App 75 | -------------------------------------------------------------------------------- /projects/03-mouse-follower/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 22 | } 23 | 24 | a:hover { 25 | color: #535bf2; 26 | } 27 | 28 | body { 29 | margin: 0; 30 | display: grid; 31 | place-items: center; 32 | min-width: 320px; 33 | min-height: 100vh; 34 | } 35 | 36 | h1 { 37 | font-size: 3.2em; 38 | line-height: 1.1; 39 | } 40 | 41 | button { 42 | border-radius: 8px; 43 | border: 1px solid transparent; 44 | padding: 0.6em 1.2em; 45 | font-size: 1em; 46 | font-weight: 500; 47 | font-family: inherit; 48 | background-color: #1a1a1a; 49 | cursor: pointer; 50 | transition: border-color 0.25s; 51 | } 52 | button:hover { 53 | border-color: #646cff; 54 | } 55 | button:focus, 56 | button:focus-visible { 57 | outline: 4px auto -webkit-focus-ring-color; 58 | } 59 | 60 | @media (prefers-color-scheme: light) { 61 | :root { 62 | color: #213547; 63 | background-color: #ffffff; 64 | } 65 | a:hover { 66 | color: #747bff; 67 | } 68 | button { 69 | background-color: #f9f9f9; 70 | } 71 | } 72 | 73 | body.no-cursor { 74 | cursor: none; 75 | } 76 | -------------------------------------------------------------------------------- /projects/03-mouse-follower/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | ) 11 | -------------------------------------------------------------------------------- /projects/03-mouse-follower/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/.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 | /test-results/ 26 | /playwright-report/ 27 | /playwright/.cache/ 28 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/README.md: -------------------------------------------------------------------------------- 1 | # Prueba técnica para Juniors y Trainees de React en Live Coding. 2 | 3 | APIs: 4 | 5 | - Facts Random: https://catfact.ninja/fact 6 | - Imagen random: https://cataas.com/cat/says/hello 7 | 8 | - Recupera un hecho aleatorio de gatos de la primera API 9 | - Recuperar la primera palabra del hecho 10 | - Muestra una imagen de un gato con la primera palabra. -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/counter.js: -------------------------------------------------------------------------------- 1 | export function setupCounter (element) { 2 | let counter = 0 3 | const setCounter = (count) => { 4 | counter = count 5 | element.innerHTML = `count is ${counter}` 6 | } 7 | element.addEventListener('click', () => setCounter(counter + 1)) 8 | setCounter(0) 9 | } 10 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/javascript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/main.jsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client' 2 | import { App } from './src/App.jsx' 3 | 4 | const root = createRoot(document.getElementById('app')) 5 | 6 | root.render() 7 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-prueba-tecnica", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "@playwright/test": "^1.30.0", 13 | "standard": "^17.0.0", 14 | "vite": "^4.0.0" 15 | }, 16 | "dependencies": { 17 | "@vitejs/plugin-react": "3.0.1", 18 | "react": "18.2.0", 19 | "react-dom": "18.2.0" 20 | }, 21 | "eslintConfig": { 22 | "extends": "./node_modules/standard/eslintrc.json" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/src/App.css: -------------------------------------------------------------------------------- 1 | main { 2 | display: flex; 3 | flex-direction: column; 4 | place-items: center; 5 | max-width: 800px; 6 | margin: 0 auto; 7 | font-family: system-ui; 8 | } -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/src/App.jsx: -------------------------------------------------------------------------------- 1 | import './App.css' 2 | import { useCatImage } from './hooks/useCatImage.js' 3 | import { useCatFact } from './hooks/useCatFact.js' 4 | 5 | export function App () { 6 | const { fact, refreshFact } = useCatFact() 7 | const { imageUrl } = useCatImage({ fact }) 8 | 9 | const handleClick = async () => { 10 | refreshFact() 11 | } 12 | 13 | return ( 14 |
15 |

App de gatitos

16 | 17 | 18 | 19 | {fact &&

{fact}

} 20 | {imageUrl && {`Image} 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/src/Components/Otro.jsx: -------------------------------------------------------------------------------- 1 | import { useCatImage } from '../hooks/useCatImage.js' 2 | 3 | export function Otro () { 4 | const { imageUrl } = useCatImage({ fact: 'cat' }) 5 | console.log(imageUrl) 6 | 7 | return ( 8 | <> 9 | {imageUrl && } 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/src/hooks/useCatFact.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { getRandomFact } from '../services/facts.js' 3 | 4 | export function useCatFact () { 5 | const [fact, setFact] = useState() 6 | 7 | const refreshFact = () => { 8 | getRandomFact().then(newFact => setFact(newFact)) 9 | } 10 | 11 | // para recuperar la cita al cargar la página 12 | useEffect(refreshFact, []) 13 | 14 | return { fact, refreshFact } 15 | } 16 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/src/hooks/useCatImage.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | 3 | const CAT_PREFIX_IMAGE_URL = 'https://cataas.com' 4 | 5 | export function useCatImage ({ fact }) { 6 | const [imageUrl, setImageUrl] = useState() 7 | 8 | // para recuperar la imagen cada vez que tenemos una cita nueva 9 | useEffect(() => { 10 | if (!fact) return 11 | 12 | const threeFirstWords = fact.split(' ', 3).join(' ') 13 | 14 | fetch(`https://cataas.com/cat/says/${threeFirstWords}?size=50&color=red&json=true`) 15 | .then(res => res.json()) 16 | .then(response => { 17 | const { _id } = response 18 | const url = `/cat/${_id}/says/${threeFirstWords}` 19 | setImageUrl(url) 20 | }) 21 | }, [fact]) 22 | 23 | return { imageUrl: `${CAT_PREFIX_IMAGE_URL}${imageUrl}` } 24 | } 25 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/src/services/facts.js: -------------------------------------------------------------------------------- 1 | const CAT_ENDPOINT_RANDOM_FACT = 'https://catfact.ninja/fact' 2 | 3 | export const getRandomFact = async () => { 4 | const res = await fetch(CAT_ENDPOINT_RANDOM_FACT) 5 | const data = await res.json() 6 | const { fact } = data 7 | return fact 8 | } 9 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif; 3 | font-size: 16px; 4 | line-height: 24px; 5 | font-weight: 400; 6 | 7 | color-scheme: light dark; 8 | color: rgba(255, 255, 255, 0.87); 9 | background-color: #242424; 10 | 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | a { 19 | font-weight: 500; 20 | color: #646cff; 21 | text-decoration: inherit; 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 | #app { 41 | max-width: 1280px; 42 | margin: 0 auto; 43 | padding: 2rem; 44 | text-align: center; 45 | } 46 | 47 | .logo { 48 | height: 6em; 49 | padding: 1.5em; 50 | will-change: filter; 51 | } 52 | .logo:hover { 53 | filter: drop-shadow(0 0 2em #646cffaa); 54 | } 55 | .logo.vanilla:hover { 56 | filter: drop-shadow(0 0 2em #f7df1eaa); 57 | } 58 | 59 | .card { 60 | padding: 2em; 61 | } 62 | 63 | .read-the-docs { 64 | color: #888; 65 | } 66 | 67 | button { 68 | border-radius: 8px; 69 | border: 1px solid transparent; 70 | padding: 0.6em 1.2em; 71 | font-size: 1em; 72 | font-weight: 500; 73 | font-family: inherit; 74 | background-color: #1a1a1a; 75 | cursor: pointer; 76 | transition: border-color 0.25s; 77 | } 78 | button:hover { 79 | border-color: #646cff; 80 | } 81 | button:focus, 82 | button:focus-visible { 83 | outline: 4px auto -webkit-focus-ring-color; 84 | } 85 | 86 | @media (prefers-color-scheme: light) { 87 | :root { 88 | color: #213547; 89 | background-color: #ffffff; 90 | } 91 | a:hover { 92 | color: #747bff; 93 | } 94 | button { 95 | background-color: #f9f9f9; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/tests/example.spec.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { test, expect } from '@playwright/test' 3 | 4 | const CAT_PREFIX_IMAGE_URL = 'https://cataas.com' 5 | const LOCALHOST_URL = 'http://localhost:5173/' 6 | 7 | test('app shows random fact and image', async ({ page }) => { 8 | await page.goto(LOCALHOST_URL) 9 | 10 | const text = await page.getByRole('paragraph') 11 | const image = await page.getByRole('img') 12 | 13 | const textContent = await text.textContent() 14 | const imageSrc = await image.getAttribute('src') 15 | 16 | await expect(textContent?.length).toBeGreaterThan(0) 17 | await expect(imageSrc?.startsWith(CAT_PREFIX_IMAGE_URL)).toBeTruthy() 18 | }) 19 | -------------------------------------------------------------------------------- /projects/04-react-prueba-tecnica/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | export default defineConfig({ 5 | plugins: [react()] 6 | }) 7 | -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/.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 | -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/README.md: -------------------------------------------------------------------------------- 1 | ## Enunciado 2 | 3 | Crea una aplicación para buscar películas 4 | 5 | API a usar: - https://www.omdbapi.com/ 6 | Consigue la API Key en la propia página web registrando tu email. 7 | 8 | Requerimientos: 9 | 10 | ✅ Necesita mostrar un input para buscar la película y un botón para buscar. 11 | 12 | ✅ Lista las películas y muestra el título, año y poster. 13 | 14 | ✅ Que el formulario funcione 15 | 16 | ✅ Haz que las películas se muestren en un grid responsive. 17 | 18 | ✅ Hacer el fetching de datos a la API 19 | 20 | Primera iteración: 21 | 22 | ✅ Evitar que se haga la misma búsqueda dos veces seguidas. 23 | 24 | ✅ Haz que la búsqueda se haga automáticamente al escribir. 25 | 26 | ✅ Evita que se haga la búsqueda continuamente al escribir (debounce) 27 | -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "05-react-buscador-peliculas", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "just-debounce-it": "3.2.0", 13 | "react": "^18.2.0", 14 | "react-dom": "^18.2.0" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^18.0.27", 18 | "@types/react-dom": "^18.0.10", 19 | "@vitejs/plugin-react-swc": "^3.0.0", 20 | "vite": "^4.1.0" 21 | } 22 | } -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/src/App.css: -------------------------------------------------------------------------------- 1 | .page { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | width: 100%; 7 | max-width: 800px; 8 | } 9 | 10 | main { 11 | display: flex; 12 | justify-content: center; 13 | width: 100%; 14 | } 15 | 16 | form { 17 | align-items: center; 18 | display: flex; 19 | justify-content: center; 20 | } 21 | 22 | .movies { 23 | list-style: none; 24 | margin: 0; 25 | padding: 0; 26 | 27 | display: grid; 28 | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); 29 | 30 | width: 100%; 31 | gap: 32px; 32 | } 33 | 34 | .movie { 35 | text-align: center; 36 | } 37 | 38 | .movie h3, .movie p { 39 | margin: 0; 40 | } 41 | 42 | .movie img { 43 | border-radius: 8px; 44 | margin-top: 16px; 45 | } -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/src/components/Movies.jsx: -------------------------------------------------------------------------------- 1 | function ListOfMovies ({ movies }) { 2 | return ( 3 |
    4 | { 5 | movies.map(movie => ( 6 |
  • 7 |

    {movie.title}

    8 |

    {movie.year}

    9 | {movie.title} 10 |
  • 11 | )) 12 | } 13 |
14 | ) 15 | } 16 | 17 | function NoMoviesResults () { 18 | return ( 19 |

No se encontraron películas para esta búsqueda

20 | ) 21 | } 22 | 23 | export function Movies ({ movies }) { 24 | const hasMovies = movies?.length > 0 25 | 26 | return ( 27 | hasMovies 28 | ? 29 | : 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/src/hooks/useMovies.js: -------------------------------------------------------------------------------- 1 | import { useRef, useState, useMemo, useCallback } from 'react' 2 | import { searchMovies } from '../services/movies.js' 3 | 4 | export function useMovies ({ search, sort }) { 5 | const [movies, setMovies] = useState([]) 6 | const [loading, setLoading] = useState(false) 7 | // el error no se usa pero puedes implementarlo 8 | // si quieres: 9 | const [, setError] = useState(null) 10 | const previousSearch = useRef(search) 11 | 12 | const getMovies = useCallback(async ({ search }) => { 13 | if (search === previousSearch.current) return 14 | // search es '' 15 | 16 | try { 17 | setLoading(true) 18 | setError(null) 19 | previousSearch.current = search 20 | const newMovies = await searchMovies({ search }) 21 | setMovies(newMovies) 22 | } catch (e) { 23 | setError(e.message) 24 | } finally { 25 | // tanto en el try como en el catch 26 | setLoading(false) 27 | } 28 | }, []) 29 | 30 | const sortedMovies = useMemo(() => { 31 | if (!movies) return; 32 | return sort 33 | ? [...movies].sort((a, b) => a.title.localeCompare(b.title)) 34 | : movies 35 | }, [sort, movies]) 36 | 37 | return { movies: sortedMovies, getMovies, loading } 38 | } 39 | -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App' 4 | import './index.css' 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | ) 9 | -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/src/mocks/no-results.json: -------------------------------------------------------------------------------- 1 | { 2 | "Response": "False", 3 | "Error": "Movie not found!" 4 | } -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/src/mocks/with-results.json: -------------------------------------------------------------------------------- 1 | {"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"} -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/src/services/movies.js: -------------------------------------------------------------------------------- 1 | const API_KEY = '4287ad07' 2 | 3 | export const searchMovies = async ({ search }) => { 4 | if (search === '') return null 5 | 6 | try { 7 | const response = await fetch(`https://www.omdbapi.com/?apikey=${API_KEY}&s=${search}`) 8 | const json = await response.json() 9 | 10 | const movies = json.Search 11 | 12 | return movies?.map(movie => ({ 13 | id: movie.imdbID, 14 | title: movie.Title, 15 | year: movie.Year, 16 | image: movie.Poster 17 | })) 18 | } catch (e) { 19 | throw new Error('Error searching movies') 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /projects/05-react-buscador-peliculas/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react-swc' 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }) 8 | -------------------------------------------------------------------------------- /projects/06-shopping-cart/.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 | -------------------------------------------------------------------------------- /projects/06-shopping-cart/README.md: -------------------------------------------------------------------------------- 1 | # Enunciado 2 | 3 | Ecommerce 4 | 5 | - [x] Muestra una lista de productos que vienen de un JSON 6 | - [x] Añade un filtro por categoría 7 | - [x] Añade un filtro por precio 8 | 9 | Haz uso de useContext para evitar pasar props innecesarias. 10 | 11 | Carrito: 12 | 13 | - [x] Haz que se puedan añadir los productos a un carrito. 14 | - [x] Haz que se puedan eliminar los productos del carrito. 15 | - [x] Haz que se puedan modificar la cantidad de productos del carrito. 16 | - [x] Sincroniza los cambios del carrito con la lista de productos. 17 | - [x] Guarda en un localStorage el carrito para que se recupere al recargar la página. (da puntos) 18 | -------------------------------------------------------------------------------- /projects/06-shopping-cart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /projects/06-shopping-cart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "06-shopping-cart", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^18.0.27", 17 | "@types/react-dom": "^18.0.10", 18 | "@vitejs/plugin-react-swc": "^3.0.0", 19 | "vite": "^4.1.0" 20 | } 21 | } -------------------------------------------------------------------------------- /projects/06-shopping-cart/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /projects/06-shopping-cart/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { products as initialProducts } from './mocks/products.json' 2 | import { Products } from './components/Products.jsx' 3 | import { Header } from './components/Header.jsx' 4 | import { Footer } from './components/Footer.jsx' 5 | import { IS_DEVELOPMENT } from './config.js' 6 | import { useFilters } from './hooks/useFilters.js' 7 | import { Cart } from './components/Cart.jsx' 8 | import { CartProvider } from './context/cart.jsx' 9 | 10 | function App () { 11 | const { filterProducts } = useFilters() 12 | 13 | const filteredProducts = filterProducts(initialProducts) 14 | 15 | return ( 16 | 17 |
18 | 19 | 20 | {IS_DEVELOPMENT &&