{post.username}
8 |{post.likes}
19 |{post.caption}
21 |├── .gitignore ├── README.md ├── nodegirls-ig.gif ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── img │ ├── camera.svg │ ├── heart.svg │ ├── home.svg │ ├── left-arrow.svg │ ├── nodegirls.svg │ ├── right-arrow.svg │ └── share.svg ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── components ├── Body.jsx ├── CardFilters.jsx ├── CardPosts.jsx ├── Footer.jsx └── Header.jsx ├── containers └── Home.jsx ├── data ├── filters.js └── userImage.js ├── index.css ├── index.js ├── logo.svg ├── serviceWorker.js └── setupTests.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Taller IG con React NodeGirlsMadrid 29F 2 | Bienvenidas!!!! 3 | 4 | > 👉 Las slides de la primera parte [las puedes ver aquí](https://slides.com/yunevk/taller-react-nodegirls/live#/). 5 | 6 | ## ¿Qué vamos a hacer? 7 | Vamos a hacer una app basada (siempre basada, nunca copiada...) en instagram. Así podremos ver las fotos de nuestras compis de taller y presumir de las cosas molonas que estamos haciendo. 8 | 9 | La pinta que esperamos que tenga es esta: 10 | 11 |
12 |
13 |
{filter.name}
6 |{post.username}
8 |{post.likes}
19 |{post.caption}
21 |147 | 148 | > 149 | ) 150 | } 151 | 152 | export default Home; 153 | ``` 154 | A su vez, este tendremos que llamarlo desde App para que sea visible: 155 | 156 | 157 | ```js 158 | import React from 'react'; 159 | import Home from './containers/Home'; 160 | import './App.css'; 161 | 162 | function App() { 163 | return ( 164 |
167 | );
168 | }
169 |
170 | export default App;
171 | ```
172 |
173 | Ahora sí, ahora levantaremos nuestra aplicación y podremos ver esas __preciosidades__ de componentes en pantalla.
174 |
175 | ## Dando contenido a nuestros _dummy components_
176 | Vamos a ver qué va a hacer cada uno de nuestros componentes y a añadirles el código que necesitan.
177 |
178 | ### Header
179 | Este componente debe permitirnos navegar entre pantallas cuando estemos cargando la imagen y deberá permitirnos cancelar el post. Para ello vamos a incluir condicionalmente cuatro botones que estarán o no estarán dependiendo del paso en el que estemos.
180 |
181 | Y nuestro componente `Header` quedaría así:
182 |
183 | ```js
184 | import React from 'react';
185 |
186 | const Header = ({ step}) => {
187 | return (
188 | Body in step {step}
239 |
269 |
272 | >
273 | )
274 | }
275 |
276 | export default Home;
277 | ```
278 |
279 | ## Funciones como ciudadanos de primera: pasando lógica entre componentes
280 | Hasta el momento, nuestros componentes `Header` y `Footer`, contienen unos botones estupendísimos y preciosísimos que no hacen ná de ná. Necesitamos darles un poco de vida, pero, sobre todo de lógica.
281 |
282 | > :hand: One minute!!!!! ¿No habíais dicho que `Header`, `Footer` y `Body` eran componentes UI si ninguna lógica? Bingoooo!!!!! :tada: Así es, premio para tí, pequeña padawan por estar atenta. Entonces... ¿Cómo hago para darles ese soplo de vida y espíritu y que esos botones e input sirvan para algo más que para mostrar una interfaz bonita?
283 |
284 | Para esos menesteres, vamos a hacer uso de una de las características más molonas de JavaScript que es que las funciones son ciudadanos de primera categoría, oiga, nada que envidiarles a sus primos los objetos, strings, numbers ni ningún otro. Y si estos últimos, pueden venir como parámetros de una función otra función no va a ser menos. Así, nuestros _dummy components_ quedarían:
285 |
286 | ```js
287 | const Header = ({ step, handleGoHome, handleShare, handleNext }) => {
288 | return (
289 |
353 |
358 | >
359 | )
360 | }
361 |
362 | export default Home;
363 | ```
364 |
365 | Hasta el momento, tenemos una app que nos permite navegar entre pantallas, y cambiar la variable de estado `step`. Hemos aprendido varias cositas interesantes, hemos hecho nuestros primero pinitos con _jsx_, con el _state_ de un componente, con los hooks, con sus propiedades... Hemos hecho un montón de cosas pero sinceramente, esa app, hasta aquí no es muy divertida. _Stay with us_, ahora vamos a entrar en la parte con más enjundia del taller!!! :mag:
366 |
367 | ## Carga inicial de los posts
368 |
369 | > :warning: **Warning!!!!** La carga inicial de los posts es un poco compleja!!!! Keep your eyes :eyes: and ears :ear: open!!!
370 |
371 | El componente `Body` será el que nos muestre el contenido de los post de nuestro IG. Por ello, como medida inicial lo primero que haremos, será la carga de los mismos.
372 |
373 | Posts es una variable que pasaremos como propiedad al componente Body. Puesto que nos interesa que cada vez que `posts` varíe su valor, `Body` se actualice, hemos de establecerla como parte del estado de` Home`. Esto lo hacemos de manera análoga a como hacíamos con `step`.
374 | ```js
375 | const [posts, setPosts] = useState([]);
376 | ```
377 |
378 | Por otra parte hemos de incluir la petición a back. Vamos a separar este proceso en varios pasos:
379 | 1. **instalación del módulo de node axios** que nos va a facilitar realizar y procesar las peticiones:
380 | ``` npm i -S axios ```
381 | 2. **Importaremos el módulo** axios en `Home`:
382 | ``` import axios from 'axios' ```
383 | 3. Queremos que la petición se realice la primera vez que se "monta" nuestro componente, para ello usaremos el _hook_ `useEffect`, al que le pasaremos como dependencia un array vacío. El hecho de que no tenga dependencias, evita que entremos en un bucle infinito:
384 | ```js
385 | const getPosts = async () => {
386 | const res = await axios.get('http://localhost:3000/api/posts');
387 | setPosts(res.data);
388 | }
389 | useEffect(() => {
390 | getPosts();
391 | }, []);
392 | ```
393 | Una vez obtenidos los datos, estos se pasarán a `Body` (componente encargado de mostrarlos) como propiedades.
394 |
395 | Este es el código de `Home` y de `Body` en este punto del taller:
396 |
397 | * **Home**:
398 |
399 | ```js
400 | import React, { useState } from 'react';
401 | import axios from 'axios';
402 | import Header from '../components/Header';
403 | import Body from '../components/Body';
404 | import Footer from '../components/Footer';
405 |
406 | const Home = () => {
407 | const [step, setStep] = useState(1);
408 | const [posts, setPosts] = useState([]);
409 | const handleGoHome = () => setState(1);
410 | const handleNext = () => setState(step + 1);
411 | const handleShare = () => {};
412 | const handleUploadImage = () => {};
413 | const getPosts = async () => {
414 | const res = await axios.get('http://localhost:3000/api/posts');
415 | setPosts(res.data);
416 | }
417 | useEffect(() => {
418 | getPosts();
419 | }, []);
420 | return (
421 | <>
422 |
432 |
437 | >
438 | )
439 | }
440 |
441 | export default Home;
442 | ```
443 | * **Body**:
444 |
445 | ```js
446 | import React from 'react';
447 |
448 | const Body = ({ step, posts }) => {
449 | return (
450 | {post.username} {post.likes} {post.caption}Body in step {step}
452 |
483 |
488 |
} 508 | ``` 509 | No olvidéis que: 510 | 1. `Body` debe importar `CardPost` o no podrá utilizarlo. 511 | 2. El _array_ de posts, le tiene que ser pasado a `Body` como _prop_. 512 | 513 | ## Subida del post: recogiendo la info 514 | A continuación vamos a darle duro a la subida del post. Elegiremos una foto, un filtro, escribiremos un comentario inspiracional y lo guardaremos en la BBDD para la posteridad o hasta que reiniciemos back ;P. 515 | 516 | ### Subida de la imagen 517 | Recordemos (que con todo lo que hemos hecho hasta el momento, igual ya ni nos acordamos de qué había en el footer), que en caso de estar en el step 1, habíamos habilitado un input de tipo file. Vamos a manejar la subida de archivos, enlazando el método `handleUpload` (en `Home`) con el evento `onChange` del `input`. Este `hanldeUpload` será el encargado de leer el archivo de la imagen, seter `image` como variable de estado y navegar al siguiente step. 518 | 519 | ```js 520 | const handleUpload = (ev) => { 521 | const files = ev.target.files 522 | if (files.length){ 523 | const reader = new FileReader(); 524 | reader.readAsDataURL(files[0]); 525 | reader.onload = (ev) => { 526 | setImage(ev.target.result); 527 | setStep(2); 528 | } 529 | } 530 | } 531 | ``` 532 | 533 | No olvidéis que este método hay que enlazarlo con `Footer` como una propiedad del mismo. 534 | 535 | ### Eliginedo el mejor filtro: CardFilter y setFilter 536 | Vamos a tener una serie de filtros disponibles, para que nuestras fotos sean lo más aparentes posibles y el resto de la humanidad se muera de envidia con esa foto tan original de nuestro pie frente al mar (sí, vamos necesitando y soñando con unas merecidas vacatas ;P). 537 | 538 | Además de `components` y `containers`, dentro de `src`, crearemos una carpeta `data`que incluya algo de info necesaria. El primer archivo que incluiremos dentro de las misma será una lista de los filtros que tenemos disponibles. Es el archivo `filter.js` y los filtros son: 539 | 540 | ```js 541 | export default [ 542 | { name: 'normal' }, 543 | { name: 'clarendon' }, 544 | { name: 'gingham' }, 545 | { name: 'moon' }, 546 | { name: 'lark' }, 547 | { name: 'reyes' }, 548 | { name: 'juno' }, 549 | { name: 'slumber' }, 550 | { name: 'aden' }, 551 | { name: 'perpetua' }, 552 | { name: 'mayfair' }, 553 | { name: 'rise' }, 554 | { name: 'hudson' }, 555 | { name: 'valencia' }, 556 | { name: 'xpro2' }, 557 | { name: 'willow' }, 558 | { name: 'lofi' }, 559 | { name: 'inkwell' }, 560 | { name: 'nashville' } 561 | ] 562 | ``` 563 | 564 | En el step 2, mostraremos un listado de estos, aplicados sobre nuestra imagen. Vamos a crear un componente específico que nos permita hacer esto, se llamará ``CardFilter` y lo vamos a hacer, dentro de la carpeta `components`. 565 | 566 | ```js 567 | import React from 'react'; 568 | 569 | const CardFilter = ({filter, image, setFilter}) => { 570 | return ( 571 |
{filter.name}
573 |580 | ) 581 | } 582 | ``` 583 | Al igual que en los CardPosts, el renderizado de los componetes de filtros, será condicional, ya que solo lo vamos a hacer después de haber elegido una imagen (step 2) y se hará desde `Body` através de un `map` de los distintos filtros, estas líneas en nuestro `Body`, serán las responsables de dicho comportamiento: 584 | 585 | ```js 586 | import CardFilter from './CardFilter'; 587 | import filters from '../data/filters'; 588 | ``` 589 | 590 | ```js 591 | {step === 2 592 | &&
} 593 | ``` 594 | 595 | Desde `Home`, filter debe setar establecida como variable de stado, y por tanto, también debemos haber definido setFilter para poder modificar su valor. No vamos a poner aquí el código porque hemos dado ya un montón la turra con las variables de stado y los hooks, os dejamos que le deis un poco al coco... y si a estar altura tenéis fitras las neuronas, podéis encontrar cómo hacerlo, en el código. 596 | 597 | ### ¿Cómo ser Paulo Coelho y dejar comentarios filosóficos? Solo necesitas un textArea y un setCaption. 598 | 599 | Vengaaaa, chicas, que ya no nos queda ná de ná. Después de haber elegido el filtro más molón, navegaremos a la siguiente pantalla, clickando en el botón `Next` del `Header`, haciendo uso del método `handleNext` que setea el step a su valor más uno. 600 | 601 | La última pantalla antes de guardar el post, mostrará la imagen con su filtro aplicado y nos permitirá dejar un comentario filosófico sobre lo hermosa que es la vida (recordemos que esto es IG, no tuiter, así que aquí no caben los haters, somos todo amor, salud y vacatas molonas). De nuevo, vamos a echar mano de un renderizado condicional denro de `Body`: 602 | 603 | ```js 604 | { 605 | step === 3 606 | && 607 | <> 608 |
613 |
623 | > 624 | } 625 | ``` 626 | 627 | ### Guardando la info: llamada a la API. 628 | Y yaaaaaaa casiiiii lo tenemos. Solo nos falta implementar el método `handleShare` que haga la petición de guardado de los datos, vuelva al step 1 y actualice los posts: 629 | 630 | ```js 631 | const savePost = async () => { 632 | const url = 'localhost:3000/api/posts'; 633 | const post = { 634 | username: 'ngm', 635 | userImage: userImage, //imagen guardada dentro de la carpta data 636 | hasBeenLiked: false, 637 | likes: 0 638 | caption, 639 | filter, 640 | postImage: image, 641 | } 642 | const config = { 643 | method: 'post', 644 | url, 645 | data: post, 646 | } 647 | const res = await axios(config); 648 | } 649 | 650 | const handleShare = () => { 651 | savePost(); 652 | setStep(1); 653 | setTimeout(() => getPosts()); 654 | } 655 | ``` 656 | 657 | ## Likes y dislikes: interaccionando con los post de tus compis. 658 | 659 | Bueno, bueno, bueno... esto ya... mola!!!! Solo una última cosita y es manejar las interacciones con los posts de nuestras coleguis!!! La variable `hasBeenLiked` es una variable `boolean` que indica si ya le hemos dado `me gusta` a una imagen o no. Así que cuando clickamos en los likes, lo que tenemos que hacer, es comprobar si ya le habíamos dado a me gusta o no, si ya le habíamos dado a me gusta, estaremos haciendo un `dislike` y debemos restarle un like, en caso contrario, debemos sumarselo. Y por supuesto, actualizar nuestros posts en base de datos. Con estas pautas... ¿Os atrevéis a hacer este ejercicio vosotras solas? Recordad que si os atascáis en algo simepre podéis acudir al código final del repo. 660 | 661 | ## ¡Enhorabuena! ¡Has completado el taller! :tada: 662 | 663 | Esperamos que hayas aprendido mucho y te hayas quedado con ganas de seguir trasteando. :wink: ¡Eso es lo importante! 664 | 665 |
666 |
667 |
668 | 669 | Ahora tienes un mundo abierto de posibilidades: puedes tratar de mejorar tu aplicación, añadir nuevas funcionalidades, seguir estudiando, practicando, ¡lo que tú quieras! 670 | 671 | Si quieres seguir ampliando información, recuerda que tienes varios enlaces en las slides para seguir aprendiendo. ¡Pero tómatelo con calma! ¡Ahora toca celebrarlo! :beers: 672 | 673 | ## ¡Pero esto no termina aquí! 674 | 675 | ¡No ha hecho más que empezar! 676 | 677 | Si tienes cualquier duda o sugerencia, puedes dejarla en un `issue` de este repo, o incluso hacer una `pull request` encuentras algún error o quieres añadir algo. 🤗 678 | 679 |
680 |
681 |
682 | -------------------------------------------------------------------------------- /nodegirls-ig.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-girls/react-hooks-workshop/59d824a2822733c3fee7e84bc12e112dfbedc418/nodegirls-ig.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ig-ngm", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^4.2.4", 7 | "@testing-library/react": "^9.4.1", 8 | "@testing-library/user-event": "^7.2.1", 9 | "axios": "^0.19.2", 10 | "react": "^16.12.0", 11 | "react-dom": "^16.12.0", 12 | "react-scripts": "3.4.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test", 18 | "eject": "react-scripts eject" 19 | }, 20 | "eslintConfig": { 21 | "extends": "react-app" 22 | }, 23 | "browserslist": { 24 | "production": [ 25 | ">0.2%", 26 | "not dead", 27 | "not op_mini all" 28 | ], 29 | "development": [ 30 | "last 1 chrome version", 31 | "last 1 firefox version", 32 | "last 1 safari version" 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-girls/react-hooks-workshop/59d824a2822733c3fee7e84bc12e112dfbedc418/public/favicon.ico -------------------------------------------------------------------------------- /public/img/camera.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 54 | -------------------------------------------------------------------------------- /public/img/heart.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/img/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/left-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/nodegirls.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 130 | -------------------------------------------------------------------------------- /public/img/right-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/img/share.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 26 |
27 | 28 | 29 |
94 |
99 | >
100 | );
101 | };
102 |
103 | export default Home;
104 |
--------------------------------------------------------------------------------
/src/data/filters.js:
--------------------------------------------------------------------------------
1 | export default [
2 | { name: 'normal' },
3 | { name: 'clarendon' },
4 | { name: 'gingham' },
5 | { name: 'moon' },
6 | { name: 'lark' },
7 | { name: 'reyes' },
8 | { name: 'juno' },
9 | { name: 'slumber' },
10 | { name: 'aden' },
11 | { name: 'perpetua' },
12 | { name: 'mayfair' },
13 | { name: 'rise' },
14 | { name: 'hudson' },
15 | { name: 'valencia' },
16 | { name: 'xpro2' },
17 | { name: 'willow' },
18 | { name: 'lofi' },
19 | { name: 'inkwell' },
20 | { name: 'nashville' }
21 | ]
--------------------------------------------------------------------------------
/src/data/userImage.js:
--------------------------------------------------------------------------------
1 | export default
2 | { data: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxMTEBUQEBEVERUXFRcWGBYVFxUYFxoVFRUXFhgXFRkYHSggGBolGxYVITEhJSkrLjouFx8zODMvNygtLisBCgoKDg0OGxAQGy0lICYtLy0vLy0tLS01LS8tLSstLS0vNS0tLS0yLS0tLS0tLS0tLS8tLS0tLS0tLTUtLS0tLf/AABEIAOEA4QMBIgACEQEDEQH/xAAcAAEAAgIDAQAAAAAAAAAAAAAABgcEBQECCAP/xABGEAABAwIDBAUJBwIEBQUBAAABAAIDBBEFEiEGMUFhBxMiUXEjMjNyc4GRsbIUNEJSYqHBgtGSwsPwQ1NjorMWFyTS8RX/xAAZAQEBAQEBAQAAAAAAAAAAAAAABAMCBQH/xAAqEQACAgMAAQMCBQUAAAAAAAAAAQIDBBEhMRITQTJRIiMzcfAUNIGRof/aAAwDAQACEQMRAD8AvFERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAFxdcqtKDaWaCRzb9ZHnd2HHd2j5p4fJZ2WqGtmNt0a9er5LLRavB8ehqB2HWdxY7R3u7x4LDxvaqGC7W+Vk/K06A/qdw8Bqvrsjrezp2wUfVvhv7rlVNiWOzzOzPkLbG7WtJaG+FuPMrd4Ltm9tmVIMjfzjzh4j8Xz8VjHJg3onjmwctPhPkWPRVscrc8Tw8cvkRwKyFQnsrT31BERfT6ERYmIYjFC3PK8NHDvPgN5XxtLyfG0ltmWl1XmNbYySXZBeJn5vxn/6+74rWYTtBNAey7O29yxxJB8OIKneVBPRI82Clr/payLS4LtJDUWaD1b/yO4+qfxLdKiMlJbRVGaktphERfToIiIAiIgCIiAIiIAiIgCIiAKmav0j/AF3fUVcyoeavIlkDtRnf4+cfipMvwiDOW0jOBtqNFwukUocLtN1tMJwWaoPk29ni92jR7+J5BRJNvSPOjFyeka5FZOGbIQRtIkHXOIsS7QD1QN3jvWkxrYt7bvpjnH5D5w9U/i+fitpY80tlEsSyMdkZoq2SJ2eJ5YeXHkRuIU2wXbNj7MqB1bvzjzD4/l+Sgb2EEtcCCNCCLEHmCsOormt0HaP7fFc12Sg+HFV063wvNjwQCCCDqCNR7lxNK1jS57g1o3kmwHiVCeiqoc+GbMSQJBYcBdt9BwWJ0r1TmPpg06ESkt4Egx2JHfqfir3b+X69HqO78r16NjjW2oF2UwzH/mO3f0jj4lQ2qqXyOL5HF7jxJ/3YclgU9a12nmnuP8FZkMLnuDGNLnHcALn4Lz52Sm+nlWWzsf4joimeC7FE2fVG3/TafqcPkPitni+x8MgvD5FwHDzT4j+R+67WPNrZosSxx2VypJgu10sVmy3mZz88DkePv+K1GJ4VLA7LKy3c4atPgf43rXTTNaLuNvms1KUHzhlGU65c4y4sMxWKduaJ4d3jc4eI4LNVKbOYg411OG3aDK0G28gnUHlyV1r0KbHNbZ62Pa7I7YREWxuEREAREQBERAEREAREQBeeq/0sntH/AFFehV56r/Sye0f9RUuV8EWZ4Rzh58tHze0HmC4XB5K4sLx5oAZI0MA0BaOz7wN3uVO0Hpo/aM+oKwFlTJrwZ4z1tlhRvDhdpBB4jUL5z1DW79/cFEMHqHtkDWuIBvccNxW4KodvOFjs+xFek6TNFG+wBMmW43luVxsTxFwFXSsHpI+7xe1/yOVfKOb3I8676yzuiP0M/tG/QsPpf8+m9Wb5xLM6I/Qz+0b9Cw+l7z6b1ZvnEqX+h/PuUP8Atv59yvVYGwWJmKG+UOu4gn8Vha1nd3Iqv1Mdlfu/9bv4U9b1LZhj/WWhQ4hHKOwdeLToR7lkvcALk2VfseQbgkEcRvUihnc9jS4kmwVfu8PQ9zhm11Q17SzKHNO/MLg+4qiZTdxvrqVdipKTefE/NTWyb6yLJe9Nmz2V+/U/tmfNXuqI2V+/U/tmfNXut8bwzbD+l/uERFSWBERAEREAREQBERAEREAXnqv9LJ7R/wBRXoVeeq/0sntH/UVLlfBFmeEfTCGXqIWncZox8XtCtPEcCeztM8o3l5w8Rx9yq7BPvUHt4v8AyNV/LmiKkmfMSKaZBsM9K33/AElb+OIuNgLrPlw2NzxJls4cRpe4tr3rKYwAWAstfa70p9vpX3ShTZaaEk3Jm/03qtlaPS192h9t/pvVXKa5JT0jz8lasLO6I/Qz+0b9Cw+l/wA+m9Wb5xLM6I/Qz+0b9C2u3Gy7q1rHRyBj4w6wcOy7PluCRq09ka6rdRcqdIpUXLH0v50pxbrBMaEQ6t7btve43i/LiFgYnhstO/q54zG7hfcR3tO4jwWIpOpkKbgyw6eoa9uZjg4cv57lJaQ2iaToMqp2mqXxuzMcWnlx8RxWbiWOzzNDHvswC2VujTzd3nxXfucKP6ha6iZY3tpHHdlOBK/834B7/wAXu05quyVwuzGkkNaCSTYAC5J7gBvXDbZhObk+mz2V+/U/tmfNXuqy2O2HmEsdTUHqQxwe1m95I1Gbg0fv4KzVZjxaj0vxYOMXsIiKgqCIiAIiIAiIgCIiAIiIAqe2o2MqIHPlYOujLi67B2m3JPabvtzF/crhRZ2Vqa6ZW1KxaZQGB/eoPbxf+Rqv9R3FNj6eWZlQ0dVI2RryWAWdlcHWe3drbeLHxUiXNNbhvZxRU69phERbFBBOlr7tD7b/AE3quMOw6Wd/VwRukd3DcB3uO5o5lXVtHgDKxsbJXOa1j85DbXd2S21zuGqzsOw6KBgjhjbG0cBx5k7yeZU06XOe34JLMdzs2/BpNh9nX0cLmyPDnPcHENvZtha1zv8AHRaOHauWGeRj/KxiV4sfOAznzT/B/ZWCqexX7xL7WT6yub260vScZLdUY+jhZEVTS10RYQ2UcWOHabztvHiFCdoujt7LyUZMjd/VuPbHqu3O8Dr4rVULy2VhaS05m6g2O8K5F9rauX4l0+1NZEX6l1HnaWNzXFr2lrhoWuBBB7iDqF0V7Y7s5T1TbTM7VtJG6PHgeI5G4WpwDYOngdnk/wDkPB0LwMre6zdxPM35WXDxpb4ZvElvS8EC2d2OqKqzrdTF/wAx4Oo/Q3e7x0HNWfgGzFPSDyTMz7ayP1eff+EchZbkBcqiFMYFVVEYfuERFqbhERAEREAREQBERAEREAREQBERAEREAXBK1OM7Qw0+jnZn8GN1Pv4N96geM7RTVFwTkZ+Ru7+o73fJY2Xxh+5Pbkwr55ZLMa2wiiuyHyz+8eYDzP4vAfFaTDNtZWuPXgSNJ4ANc31eBHI/FRZFHLIm3vZ50sqxy3vRb2G4nFO3NE8O7xucPEbwqrxX7xL7WT6yvlTzuY4PY4scNxBsV1lkLnFzjcuJJPMm5S273Eti7I92KTXTtS+kZ6zfmFcypml9Iz1m/MK5ltieGU4HiX+Ai+FXVsjaXyPDGjif96lQvGttXOuymGUfncO1/SOHvVE7Iw8ldl0K1+JkqxXGYqdt5Xa8GjVx8B/J0UMrdtJ3SB0QbGwHzSA7N6x/tZRuWQuJc4lxO8k3J8SV0UU8iUvHDzbcuc/HEWTgu1kU1mP8lJ3E9kn9Lv4KkIVKre4LtRNBZpPWx/lcdR6ruHhuWleV8SNqc34n/ss5FrMIxyGoHk3WdxY7Rw93EcwtmrE01tHoRkpLaCIi+n0IiIAiIgCIiAIiIAiIgCiXSJjElPFH1Tsud5a4jzrBt9DwUtUB6XPQwe0d9Kztf4GY3tqt6IdFUB+oNzxvv96+ij7XEG4NlKdm2CRhe/tEOsO7cDqPevNUds8iNbk9IU1E5+o0Hef471tn7Nl0YfE654tdpex4H+Cshb3DfRN9/wAytfbSRWsaCWmV9NE5ji17S0jgdCuhKsWtoY5RlkaD3HiPA8FU2KyHrZI79lr3NA5NcQL89FnKGiaylwZmx14ErA3tdtuvDzh8Vd8psCe4Erz7Q+lj9oz6gvQM3mu8D8lVjcTLcJaUilZsefUOzTvJdwv5o5NH4V3UebuW02e7dRFC4nI5wBHG3I8FI1tkDi5M2lJSPldkiYXu7h8z3DmVLaPYW8ZM0uV5GgaLtb43873WUnwmGGNuSFoZy4nmTxWwVlePHW309CrDgluXSp8XwOanPlG3bwe3Vp/sfFaxXHWTMa0mUgN3WOt+VuKqjbV8bHtNMwxtdmuL91tWj8O9Y3UKPUye/F9HYs18lYIyDmIcNRlOt/4Vl7B4rJUUnWSnM4SOaDxsALX7zqqYJVtdFn3E+2f8mr7jclo6w+T0TFERXnphERAEREAREQBERAEREAUB6XPQwe0d9KnygPS56GD2jvpWV30MxyP02VipZsj6F3r/AOUKJqWbDYjTMJjqnFgLrtP4NwFnkajd4c1FX9R51LXr6SSjonymzG37zwHiVI4sMdGwC+a2+3jfTvW0pw3KOrtltplta3Ky+itVS0emq1o0apvFvvE3tZPrKubaLF6anbmnfZ1tGt1e7wb3czYc1SlbNnlfIBYOe5wB7nOJt+6luj6XoiyUlpHND6WP2jPqC9Azea7wPyXn6h9LH7Rn1BegZvNd4H5LTG8M7w/Ejzu1bTZf77B64+RWraszB6sRTxyuBIY4Egb7crqVeSKPkuNKvE3xx3FidwJ4f3WHhuJxTtzQvDu8bnD1hvC4xb0fvCpjJrwejGX2NVPO55zPcXHn/Hcovtj/AML+r/KpKxhJsASTwG9Zx2LE5Y6pcWtbfsN3m9t7uA04a8wji5LSPk4OcdIrPC8LmqH9XBGZDxtuHNzjoB4q49jsEdSU3UvcHuLi85b2BcBoL7929bShoY4WCOFjY2jg0W957zzKyVtVSod+TunHVffkIiLYoC6dYL5bi++19beCrbpJ2/dC40NCbznsvkbqWF2gZGOMhv7vHdG49jJaN9FVSzu+2zVcQEY1OQm8md5N3EN847tba70BeCIiAIiIAiIgCIiAKI9I2CzVMDOoaHmNxcW3sSC23Z7zyUuRcyj6lo5nFSj6WedZIy0lrgWkGxBBBB7iDuXVXpj2zdPVN8qyzrWEjdHj38RyKrHaLYqoprvaOuiH42jUD9beHiLhRTplE823HlDq6jCwDaWopD5J92cY3asPh+U8wpDjHSRK9gbTx9SSO09xDiDxDNLe8/BQVFwrJJaTM42zitJneaVz3F73F7jqXOJJJ5krosmgoZJniOFjpHHgB+5O4DmVYuzvR2xtpKwiR2/q2+YPWO937DxSFcp+D7XVKx8IXs1gM9TK0xM7LXtLnu0YLEEi/E8grvkFwR3gpDE1rQ1rQ1o0AAAAHcANy7q2upQR6NNKrWig8YwSeldknYW9zhqx3qu/jetevQ9TTMkYWSND2ne1wBB9xVf7RdHO+Sidbj1Tz9Dju8D8VPZjtdiSW4rXY9K9pqh8bg+NxY4bi02P/wCclL8M2wEgEVZ2BceVYL/4m8PEfBQ+phdG8xyDI9psWnePcvmsE3Fk8ZSgy/MIghDA+Ate0jzwQ6/v/hdsaxSOmp5KmU9iNpcbbyeDRzJsPeqSwbG56V+aCQtvvadWu9ZvHx3qVbf1VVV4VGTSviLqiMPFi67SHWflHaDcxbvHcrqrVLh6NN6nzXSHx4vimK1gFPLJGQczWxvcyKFt9C8jf4m5PAcFf0DSGtDjmcAATuubam3DVafA8JpsOpMjS2NjRmkkeQ3M7cXvcf8Ae4LaUVbHMwSwyNlY7c5hDmmxsbEc1sUH3UT6SNqfsNISw+XluyIdxt2pDyaP3IUpmlDWl7iGtaCSTuAAuSeVl59xKplxrFg1lwxzsjP0QMNy8jvIu7xICA7dHjJGTmqFDUVsv/B0tEHuJzSSSu4/HeTvVsbO7PTGoOIYi9r6jKWxxsv1UDDvDL73ni5bHEMQpsNpGmQ9XFG1rGNGrjYaNaOLtPmSsTZnbKKsk6oQzU8hj61rZmBueIm2dhBIIugJKiIgCIiAIiIAiIgCItDidW+Ora/OeqbEBIzgBJIW9byLSG3O7KXHgEBvkXBKj2GYk4zTzyyFsPUxyMafNbEHTDP4vDc3gWjggMbaPYeCou+PyEu/M0dlx/U3+RYqN4P0bSF5NU8MYDujN3O53I7I/fwU7djOUZ3080ce8yOaywb+ZzQ8vaPFunGyyKzEGxlrMrpHuBLWMF3EC1zqQGt1GriBqAsnTBvejGVEJPbQwzDIoGdXBGI28t55uO8nxWYtJUYneWCNzJIHul819hmaI5CbOY4tduva99L2WXHisby5jM7spc1zg12VpbcEF26+m4a6jvC1S0apa8GwRa52JMY2NrGvlc5gc1jRd5ZYdpxcQGjm4jXmsOqxPNNTRlskLzMbsfYZmiGbcWkteL2NgTbS4GiH03qpXpJ6QZJZXUdE8tjacj5GE5pH3sWsI1DL6ab/AA3z7pOxw0uHSOYcskhETDxBeDmcOYaHHxsoF0M7LNlkNfM27InZYgdxkA1fzyggDmT3IDCd0YzR4dLXTy9VKyJ0whAuQGNLyHuvo4gHQbisjBdnBWEfYqlkoaGdbmDmujc5t9xHbFw4AjuUm6Z9o+ppxRRu7c47dt4hG8f1HTwDlz0LYL1NG+rk0NQQRfS0UeYNJ8SXnwsuJ1xn5M7Koz8jGaWlwal+0ZRPUu7MRePx23tb+FrRqTv3C+q1XQ5JVVFXUVk8skjMmQlziWukc4OAA3DKBw3Zh3qJbcY2/EsRDYO2wOEMDfzXdbP/AFO18AO5WxWTw4LhQa2znNblaOMk7hck8r3J5BdRiorSOoxUVpEB6YtoXT1QoYjeOIjMB+Kd3A9+UEDxJX0wZszsPdFFM+moKZr31FRH6SeXV72QH8gJyg7tNb7lFtkMDkxGuDC42JMs0g3hpN3EfqcTYePJei6WhjjibAxjWxtbkDLDLlta1uK+nRQzcXnhwR0b5HkVc5EYe4kiCMDrXAng52Vnd5ym/Qps/wBXTurXjtzdll+ETTvHrOHwa1RDpCcazGW0UNg1hjpmBtrC9nPcAN1i4g+zV3RMipqcNuI4oYwLnQNZG22vuCAqDpmrTJiMFM4+TYxhI4XlfZx/wtH7qwMGwud+Iy11RGIWMjNNTx5muJjD7mV2W4GawsO7eqb6QcQkqakVrmdXHMzyAOjjFG4ta9w4Em594V7O2ghipIqmokDA+NjgNS5znNBysaLue433AEoDcouGOuAe/vXKAIiIAiIgCIiALVyRh1U9rgHA04BB3EF7wQeVltFxlF7213X5ICN9c5w//nOcTIDle6/aNKNesvvu9to7/mzkeau+LVHVSyublb5GlZdwu1jXzysL3DuaDfu0UgyC97C+6/G3chjBvcA3FjpvHceWp+KAju0MIjpZTLWSuvFIGgmJpcch0aI2Au8NVlUkgZVnOQOtghEZJ0d1ZkL2t5jO027jyNs+lwyCIkxQRRk7yxjWk+Nhqu76GIx9UYmGPdkLWllvVtZAYGL1LOupoyQXma4A1IHUy9o/lHC577LvhzQKeSwt5SpOneZpSSsyloYoxliiZGL3sxrWi/fYDevuGi1raf33oDRYLIGS5X2DpIICwnTM1jCCxveWkl1v+ovrjFSz7RSx3Bf15dYakD7POLu/LfcL71s6ikjkbkkja9v5XNDm6cjoutNRRRjLHEyMA3AY1rRe1r2A32QFYdPbndXS78uaUnuzBrLe+2b91JI8TgwnDII3dqTq2hkTfPlmfqcoHAvdqefgFIsewSCsh6ipjzsvcakEOG4tI1B1PxUJ2nw2iwildUwsLqpw6qF8r3SPa4i12Zz2Q1tzpbu4oCsHRT4higjnd5WaYMfbcwA9oN5Ma13+FW9tdVF5iwSh7D5GASubugpGgA37i4dkDnzCrzYbZHES6HEaQRN1fkdMTuc10ZkLbXIs51vDu325snsy2jY9znmeolOaad3nPd3fpYODUBT3Re2CLEZJap7Ym08UrgZCBZ4cI+O8gOdoNbqRbUU02I01TicrHRwRREUkTrhxGZpfUPbwu0G3LwubGqNlaJ832h9JC+S+YuLGkl35jwLuZ1W2cwEFpAIIsQd1u7wQFa9BdG0Uk8+md82QniGsY0gfF7j71Zi1sUFLQwPc1sVLC0l7soaxtzYEm3HQD4BcYDj1PWRmSllEjWuynRwIO+xDgCNEBT+xz427QVEtU9sfVvq35pCGjN1hZvP6XuPuW/2r2hbWsL3l8eFxvAe9oIkrJQezDCND1dwbu03X4aT2u2XoppeumpIZZNLucxpJtuzXHa960fSRhEz4KaSkiEppaiObqQB2msBFmjlpp3Xt3ICrekzFJJpoRLSfYgyHycZc0u6tx7Jc1o8n5tsqtXYTZSGGCCpeHTVDoWeUlcXmMFoOSIHSNo3aa81AI9m63FsRNTVU76WElubOHNtGzdGzMAXOOutrak8ldrGgAACwAsByCA7IiIAiIgCIiAIiIAiIgCIiAIiIAoFiu30r5nU2E0jq17DZ8u6Fru6/HxJA7rqbV8JfE9jXZXOY5od3FzSAfddQbYLHaWkpmYdVFtFURXa9ktmB7rnyjHnsvDtDe6A+X2DaGbV1VTUv6WAEj/sd81x/6Qxg6uxojwYf4IU3djdMBmNTCB39ay3xuo3jHSVRRdiBxrJT5scALrn1hp8LnkgNLiOB4pSxOnlx0NYwXJfHf3C97k9wULo8OxLGXdc5zZmwgNBl8mw63LAGDVx0vbhbXcpg3ZmvxaRs2KE0tO03ZTM88+P5Tb8R17gFZOH0McMbYYWCNjRZrW7gP98UBDafHcVgaGS4Q2RjQADTTNsABYBrDc2tzWQ3pCjb94oq6n789O4j4supkiAisXSLhp31Qj5SMkZ9TViYj0oYfGD1Uj6l3BsTHG58XABTNzAd4B8QuGxtG5oHgAgKRxZuK4zILU7oIAey192Rj9TnOAMjuYB5BWbsLso3D6cxB/WPe7NI+1gXWsA0cGj+6kiIAiIgCIiAIiIAiIgCIiAIiIAiIgCIiAIiIAsPEcLgnblqIY5h3Pa13wuNFmIgIz/7fYZe/wBhi/7rfC9luMOwengFqeCOH1GNb8SAs5EAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAEREAREQBERAf//Z'};
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css?family=Sarabun:300,500,600&display=swap');
2 |
3 | :root {
4 | --primary-background: #F9FAFB;
5 | --secondary-background: #E5E6ED;
6 | --text-high-emphasis: #171D33;
7 | --text-low-emphasis: #757F8C;
8 | }
9 |
10 | body {
11 | margin: 0;
12 | font-family: "Sarabun", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
13 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
14 | sans-serif;
15 | -webkit-font-smoothing: antialiased;
16 | -moz-osx-font-smoothing: grayscale;
17 | min-height: 100vh;
18 | color: var(--text-medium-emphasis);
19 | }
20 |
21 | code {
22 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
23 | monospace;
24 | }
25 |
26 | img {
27 | max-width: 100%;
28 | object-fit: cover;
29 | }
30 |
31 | /* Text */
32 | h1 {
33 | text-align: left;
34 | font-size: 2rem;
35 | margin-left: 1rem;
36 | }
37 |
38 | h2 {
39 | color: var(--text-low-emphasis);
40 | text-align: left;
41 | font-weight: 300;
42 | font-size: 1rem;
43 | }
44 |
45 | /* Main Layout */
46 | [id="root"] {
47 | background: var(--secondary-background);
48 | min-height: 100vh;
49 | display: flex;
50 | align-items: center;
51 | justify-content: center;
52 | }
53 |
54 | .App {
55 | background: var(--primary-background);
56 | border-radius: 20px;
57 | margin: auto;
58 | box-shadow: 0.4em 0.4em 1rem rgba(0,0,0,0.05);
59 | height: 90vh;
60 | overflow: scroll;
61 | display: flex;
62 | flex-direction: column;
63 | width: 25rem;
64 | }
65 |
66 | /* Buttons */
67 | button {
68 | background: transparent;
69 | border: none;
70 | cursor: pointer;
71 | padding: .5rem;
72 | border-radius: 15px;
73 | transition: background .2s ease;
74 | }
75 |
76 | button:hover {
77 | background: rgba(0,0,0,0.05);
78 | }
79 |
80 | button:active, button:focus {
81 | outline: none;
82 | }
83 |
84 | /* Icons */
85 | .icon {
86 | height: 2rem;
87 | }
88 |
89 | .logo {
90 | height: 4rem;
91 | }
92 |
93 | /* Header */
94 | header {
95 | display: flex;
96 | padding: .5rem;
97 | justify-content: space-between;
98 | align-items: center;
99 | }
100 |
101 | header button {
102 | margin: 0 .5rem;
103 | }
104 |
105 | /* Main */
106 | main {
107 | padding: 0 2rem;
108 | flex-grow: 1;
109 | overflow: scroll;
110 | }
111 |
112 | /* Footer */
113 | footer {
114 | display: flex;
115 | padding: .5rem;
116 | justify-content: space-evenly;
117 | box-shadow: 0px -4px 14px rgba(0, 0, 0, 0.06);
118 | background: #fff;
119 | }
120 |
121 | /* Upload field */
122 | .upload-btn-wrapper {
123 | position: relative;
124 | overflow: hidden;
125 | display: inline-block;
126 | border-radius: 15px;
127 | transition: background .2s ease;
128 | }
129 |
130 | .upload-btn-wrapper:hover {
131 | background: rgba(0,0,0,0.05);
132 | }
133 |
134 | .upload-btn-wrapper input[type=file] {
135 | font-size: 100px;
136 | position: absolute;
137 | left: 0;
138 | top: 0;
139 | opacity: 0;
140 | cursor: pointer;
141 | }
142 |
143 | /* Textarea */
144 | textarea {
145 | outline: none;
146 | width: 100%;
147 | margin: 1rem 0;
148 | border: none;
149 | border-bottom: 0.5px solid #A6AAB4;
150 | border-radius: 0;
151 | padding: .5rem;
152 | font-family: Sarabun;
153 | font-size: .8rem;
154 | line-height: 22px;
155 | letter-spacing: 0.2px;
156 | box-sizing: border-box;
157 | background: transparent;
158 | }
159 |
160 | /* Selected image */
161 | .selected-image img {
162 | width: 100%;
163 | border-radius: 6px;
164 | }
165 |
166 | /* Filters */
167 | .filter-container {
168 | padding: 1rem 0;
169 | display: grid;
170 | grid-template-columns: repeat(2, 1fr);
171 | grid-gap: .5rem;
172 | }
173 |
174 | .filter-container > div {
175 | box-shadow: 0px 4px 26px rgba(0, 0, 0, 0.06);
176 | border-radius: 6px;
177 | position: relative;
178 | border-radius: 6px;
179 | overflow: hidden;
180 | cursor: pointer;
181 | min-height: 5rem;
182 | }
183 |
184 | .filter-container > div p {
185 | position: absolute;
186 | top: 0;
187 | width: 100%;
188 | background: rgba(0,0,0,0.5);
189 | color: #fff;
190 | margin: 0;
191 | padding: .25rem 0;
192 | font-size: .9rem;
193 | text-transform: capitalize;
194 | z-index: 9;
195 | }
196 |
197 |
198 | /* Post */
199 | .post {
200 | margin-bottom: 4rem;
201 | }
202 |
203 | .post-user {
204 | display: flex;
205 | align-items: center;
206 | margin-bottom: .5rem;
207 | }
208 |
209 | .post-user img {
210 | height: 3rem;
211 | width: 3rem;
212 | border-radius: 50%;
213 | display: block;
214 | border: 2px solid #318431;
215 | }
216 |
217 | .post-user p {
218 | margin: 0;
219 | margin-left: 1rem;
220 | height: fit-content;
221 | font-size: 1.1rem;
222 | letter-spacing: 0.025rem;
223 | margin-bottom: .5em;
224 | }
225 |
226 | img:not([src]) {
227 | background: white;
228 | overflow: hidden;
229 | position: relative;
230 | }
231 |
232 | img:not([src]):after {
233 | content: '';
234 | position: absolute;
235 | top: 0; left: 0;
236 | height: 100%;
237 | width: 100%;
238 | background-image: url(/img/nodegirls.svg), linear-gradient(white, white);
239 | background-size: contain;
240 | }
241 |
242 | .post-content > div:first-child {
243 | margin-left: -2rem;
244 | margin-right: -2rem;
245 | }
246 |
247 | .post-info {
248 | text-align: left;
249 | }
250 |
251 | .post-info p {
252 | margin: 0;
253 | font-weight: 100;
254 | }
255 |
256 | .post-likes {
257 | display: flex;
258 | padding: 1rem 0;
259 | font-weight: 100;
260 | color: var(--text-low-emphasis);
261 | }
262 |
263 | .post-likes button, .post-likes p {
264 | margin: 0;
265 | padding: 0;
266 | }
267 |
268 | .post-likes button {
269 | margin-right: .75rem;
270 | display: block;
271 | }
272 |
273 | .post-likes img {
274 | height: 1.5rem;
275 | width: 1.5rem;
276 | }
277 |
278 | .post-likes img.not-liked {
279 | filter: grayscale(1);
280 | }
281 |
282 | /* Posts */
283 | .posts {
284 | display: flex;
285 | flex-direction: column-reverse;
286 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 | import * as serviceWorker from './serviceWorker';
6 |
7 | ReactDOM.render(